From 0baf273a973f96f0f72955803fb942fd77e7ffd4 Mon Sep 17 00:00:00 2001 From: darshankabariya Date: Wed, 1 Oct 2025 16:35:22 +0530 Subject: [PATCH 001/155] chore: initial change of changelog --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc073792c..f033f10ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,58 @@ +## v0.37.0 (2025-10-01) + +### Notes + +- Deprecated parameters: + - `tree_path` and `rlnDB` (RLN-related storage paths) + - `--dns-discovery` (fully removed, including dns-discovery-name-server) + - `keepAlive` (deprecated, config updated accordingly) +- Legacy `store` protocol is no longer supported by default. +- Improved sharding configuration: now explicit and shard-specific metrics added. +- Mix nodes are limited to IPv4 addresses only. + +### Features + +- Waku API: create node via API ([#3580](https://github.com/waku-org/nwaku/pull/3580)) ([bc8acf76](https://github.com/waku-org/nwaku/commit/bc8acf76)) +- Waku Sync: full topic support ([#3275](https://github.com/waku-org/nwaku/pull/3275)) ([9327da5a](https://github.com/waku-org/nwaku/commit/9327da5a)) +- Mix PoC implementation ([#3284](https://github.com/waku-org/nwaku/pull/3284)) ([eb7a3d13](https://github.com/waku-org/nwaku/commit/eb7a3d13)) +- Rendezvous: add request interval option ([#3569](https://github.com/waku-org/nwaku/pull/3569)) ([cc7a6406](https://github.com/waku-org/nwaku/commit/cc7a6406)) +- Shard-specific metrics tracking ([#3520](https://github.com/waku-org/nwaku/pull/3520)) ([c3da29fd](https://github.com/waku-org/nwaku/commit/c3da29fd)) +- Libwaku: build Windows DLL for Status-go ([#3460](https://github.com/waku-org/nwaku/pull/3460)) ([5c38a53f](https://github.com/waku-org/nwaku/commit/5c38a53f)) + +### Bug Fixes + +- Prevent invalid pubsub topic subscription via Relay REST API ([#3559](https://github.com/waku-org/nwaku/pull/3559)) ([a36601ab](https://github.com/waku-org/nwaku/commit/a36601ab)) +- Fixed node crash when RLN is unregistered ([#3573](https://github.com/waku-org/nwaku/pull/3573)) ([3d0c6279](https://github.com/waku-org/nwaku/commit/3d0c6279)) +- REST: fixed sync protocol issues ([#3503](https://github.com/waku-org/nwaku/pull/3503)) ([393e3cce](https://github.com/waku-org/nwaku/commit/393e3cce)) +- Regex pattern fix for `username:password@` in URLs ([#3517](https://github.com/waku-org/nwaku/pull/3517)) ([89a3f735](https://github.com/waku-org/nwaku/commit/89a3f735)) +- Sharding: applied modulus fix ([#3530](https://github.com/waku-org/nwaku/pull/3530)) ([f68d7999](https://github.com/waku-org/nwaku/commit/f68d7999)) +- Metrics: switched to counter instead of gauge ([#3355](https://github.com/waku-org/nwaku/pull/3355)) ([a27eec90](https://github.com/waku-org/nwaku/commit/a27eec90)) +- Fixed lightpush metrics and diagnostics ([#3486](https://github.com/waku-org/nwaku/pull/3486)) ([0ed3fc80](https://github.com/waku-org/nwaku/commit/0ed3fc80)) +- Misc sync, dashboard, and CI fixes ([#3434](https://github.com/waku-org/nwaku/pull/3434), [#3508](https://github.com/waku-org/nwaku/pull/3508), [#3464](https://github.com/waku-org/nwaku/pull/3464)) + +### Changes + +- Enable peer-exchange by default ([#3557](https://github.com/waku-org/nwaku/pull/3557)) ([7df526f8](https://github.com/waku-org/nwaku/commit/7df526f8)) +- Refactor peer-exchange client and service implementations ([#3523](https://github.com/waku-org/nwaku/pull/3523)) ([4379f9ec](https://github.com/waku-org/nwaku/commit/4379f9ec)) +- Updated rendezvous to use callback-based shard/capability updates ([#3558](https://github.com/waku-org/nwaku/pull/3558)) ([028bf297](https://github.com/waku-org/nwaku/commit/028bf297)) +- Config updates and explicit sharding setup ([#3468](https://github.com/waku-org/nwaku/pull/3468)) ([994d485b](https://github.com/waku-org/nwaku/commit/994d485b)) +- Bumped libp2p to v1.13.0 ([#3574](https://github.com/waku-org/nwaku/pull/3574)) ([b1616e55](https://github.com/waku-org/nwaku/commit/b1616e55)) +- Removed legacy dependencies (e.g., libpcre in Docker builds) ([#3552](https://github.com/waku-org/nwaku/pull/3552)) ([4db4f830](https://github.com/waku-org/nwaku/commit/4db4f830)) +- Benchmarks for RLN proof generation & verification ([#3567](https://github.com/waku-org/nwaku/pull/3567)) ([794c3a85](https://github.com/waku-org/nwaku/commit/794c3a85)) +- Various CI/CD & infra updates ([#3515](https://github.com/waku-org/nwaku/pull/3515), [#3505](https://github.com/waku-org/nwaku/pull/3505)) + +### This release supports the following [libp2p protocols](https://docs.libp2p.io/concepts/protocols/): + +| Protocol | Spec status | Protocol id | +| ---: | :---: | :--- | +| [`11/WAKU2-RELAY`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/11/relay.md) | `stable` | `/vac/waku/relay/2.0.0` | +| [`12/WAKU2-FILTER`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/12/filter.md) | `draft` | `/vac/waku/filter/2.0.0-beta1`
`/vac/waku/filter-subscribe/2.0.0-beta1`
`/vac/waku/filter-push/2.0.0-beta1` | +| [`13/WAKU2-STORE`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/13/store.md) | `draft` | `/vac/waku/store/2.0.0-beta4` | +| [`19/WAKU2-LIGHTPUSH`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/19/lightpush.md) | `draft` | `/vac/waku/lightpush/2.0.0-beta1` | +| [`WAKU2-LIGHTPUSH v3`](https://github.com/waku-org/specs/blob/master/standards/core/lightpush.md) | `draft` | `/vac/waku/lightpush/3.0.0` | +| [`66/WAKU2-METADATA`](https://github.com/waku-org/specs/blob/master/standards/core/metadata.md) | `raw` | `/vac/waku/metadata/1.0.0` | +| [`WAKU-SYNC`](https://github.com/waku-org/specs/blob/master/standards/core/sync.md) | `draft` | `/vac/waku/sync/1.0.0` | + ## v0.36.0 (2025-06-20) ### Notes From 902732eb7702d9e151d65303a62b01300552065b Mon Sep 17 00:00:00 2001 From: darshankabariya Date: Wed, 8 Oct 2025 14:55:52 +0530 Subject: [PATCH 002/155] chore: edit changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f033f10ba..89fd19bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Legacy `store` protocol is no longer supported by default. - Improved sharding configuration: now explicit and shard-specific metrics added. - Mix nodes are limited to IPv4 addresses only. +- [lightpush legacy](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/19/lightpush.md) is being deprecated. Use [lightpush v3](https://github.com/waku-org/specs/blob/master/standards/core/lightpush.md) instead. ### Features From 9223fac807a39c41d2c096259162e35aeb7ccd42 Mon Sep 17 00:00:00 2001 From: darshankabariya Date: Fri, 17 Oct 2025 15:26:10 +0530 Subject: [PATCH 003/155] chore: update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89fd19bf0..61e818afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ - Rendezvous: add request interval option ([#3569](https://github.com/waku-org/nwaku/pull/3569)) ([cc7a6406](https://github.com/waku-org/nwaku/commit/cc7a6406)) - Shard-specific metrics tracking ([#3520](https://github.com/waku-org/nwaku/pull/3520)) ([c3da29fd](https://github.com/waku-org/nwaku/commit/c3da29fd)) - Libwaku: build Windows DLL for Status-go ([#3460](https://github.com/waku-org/nwaku/pull/3460)) ([5c38a53f](https://github.com/waku-org/nwaku/commit/5c38a53f)) +- RLN: add Stateless RLN support ([#3621](https://github.com/waku-org/nwaku/pull/3621)) +- LOG: Reduce log level of messages from debug to info for better visibility ([#3622](https://github.com/waku-org/nwaku/pull/3622)) ### Bug Fixes @@ -30,6 +32,7 @@ - Metrics: switched to counter instead of gauge ([#3355](https://github.com/waku-org/nwaku/pull/3355)) ([a27eec90](https://github.com/waku-org/nwaku/commit/a27eec90)) - Fixed lightpush metrics and diagnostics ([#3486](https://github.com/waku-org/nwaku/pull/3486)) ([0ed3fc80](https://github.com/waku-org/nwaku/commit/0ed3fc80)) - Misc sync, dashboard, and CI fixes ([#3434](https://github.com/waku-org/nwaku/pull/3434), [#3508](https://github.com/waku-org/nwaku/pull/3508), [#3464](https://github.com/waku-org/nwaku/pull/3464)) +- Raise log level of numerous operational messages from debug to info for better visibility ([#3622](https://github.com/waku-org/nwaku/pull/3622)) ### Changes From 9a341a68e564c9834a8ef4a6b4aa79b622af23d9 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:38:57 +0200 Subject: [PATCH 004/155] use nightly docker rust image to allow release creation (#3628) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6afb2bcdb..90fb0a9c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # BUILD NIM APP ---------------------------------------------------------------- -FROM rust:1.81.0-alpine3.19 AS nim-build +FROM rustlang/rust:nightly-alpine3.19 AS nim-build ARG NIMFLAGS ARG MAKE_TARGET=wakunode2 From 7b580dbf39d9049c757a8ff0703a7e4de1370de6 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 27 Oct 2025 14:07:06 -0300 Subject: [PATCH 005/155] chore(refactoring): replace some isErr usage with better alternatives (#3615) * Closes apply isOkOr || valueOr approach (#1969) --- apps/chat2/chat2.nim | 20 +--- apps/chat2bridge/chat2bridge.nim | 41 +++---- apps/chat2mix/chat2mix.nim | 105 ++++++++---------- .../diagnose_connections.nim | 5 +- .../liteprotocoltester/liteprotocoltester.nim | 9 +- apps/liteprotocoltester/publisher.nim | 5 +- apps/liteprotocoltester/receiver.nim | 93 ++++++++-------- .../service_peer_management.nim | 2 +- apps/liteprotocoltester/statistics.nim | 24 ++-- apps/networkmonitor/networkmonitor.nim | 81 ++++++-------- apps/wakucanary/wakucanary.nim | 17 +-- examples/filter_subscriber.nim | 28 ++--- examples/lightpush_publisher.nim | 10 +- examples/publisher.nim | 10 +- examples/subscriber.nim | 10 +- examples/wakustealthcommitments/node_spec.nim | 7 +- .../stealth_commitment_protocol.nim | 69 ++++++------ library/waku_context.nim | 15 +-- .../requests/ping_request.nim | 6 +- tests/wakunode_rest/test_rest_store.nim | 2 +- tools/confutils/cli_args.nim | 7 +- .../rln_keystore_generator.nim | 28 ++--- waku/common/databases/db_postgres/dbconn.nim | 5 +- .../databases/db_postgres/pgasyncpool.nim | 4 +- waku/common/databases/db_sqlite.nim | 32 ++---- waku/common/enr/typed_record.nim | 5 +- waku/discovery/waku_discv5.nim | 7 +- waku/factory/internal_config.nim | 27 ++--- waku/factory/node_factory.nim | 56 +++++----- waku/factory/waku.nim | 22 ++-- waku/incentivization/eligibility_manager.nim | 7 +- waku/node/api/filter.nim | 45 +++----- waku/node/api/lightpush.nim | 8 +- waku/node/api/peer_exchange.nim | 7 +- waku/node/api/relay.nim | 7 +- waku/node/api/store.nim | 52 ++++----- .../not_delivered_storage/migrations.nim | 6 +- .../peer_manager/peer_store/migrations.nim | 8 +- .../peer_store/waku_peer_storage.nim | 13 +-- waku/node/waku_node.nim | 16 +-- waku/waku_api/rest/admin/handlers.nim | 9 +- waku/waku_api/rest/debug/handlers.nim | 7 +- waku/waku_api/rest/filter/handlers.nim | 87 ++++++--------- .../rest/legacy_lightpush/handlers.nim | 14 +-- waku/waku_api/rest/legacy_store/handlers.nim | 31 +++--- waku/waku_api/rest/legacy_store/types.nim | 3 +- waku/waku_api/rest/origin_handler.nim | 3 +- waku/waku_api/rest/relay/handlers.nim | 24 ++-- waku/waku_api/rest/rest_serdes.nim | 9 +- waku/waku_api/rest/serdes.nim | 6 +- waku/waku_api/rest/server.nim | 34 +++--- waku/waku_api/rest/store/handlers.nim | 15 +-- waku/waku_archive/driver/builder.nim | 61 ++++------ .../postgres_driver/postgres_driver.nim | 65 +++++------ .../driver/queue_driver/queue_driver.nim | 18 ++- .../driver/sqlite_driver/migrations.nim | 11 +- .../driver/sqlite_driver/queries.nim | 9 +- .../driver/sqlite_driver/sqlite_driver.nim | 14 +-- .../retention_policy_time.nim | 12 +- waku/waku_archive_legacy/driver/builder.nim | 55 ++++----- .../postgres_driver/postgres_driver.nim | 14 +-- .../driver/queue_driver/queue_driver.nim | 11 +- .../driver/sqlite_driver/migrations.nim | 11 +- .../driver/sqlite_driver/queries.nim | 15 +-- .../driver/sqlite_driver/sqlite_driver.nim | 19 ++-- waku/waku_core/peers.nim | 12 +- waku/waku_core/topics/content_topic.nim | 9 +- waku/waku_core/topics/sharding.nim | 8 +- waku/waku_enr/capabilities.nim | 10 +- waku/waku_enr/multiaddr.nim | 5 +- waku/waku_enr/sharding.nim | 16 ++- waku/waku_filter_v2/client.nim | 5 +- waku/waku_filter_v2/protocol.nim | 16 +-- waku/waku_keystore/keyfile.nim | 19 +--- waku/waku_keystore/keystore.nim | 42 ++----- waku/waku_lightpush/callbacks.nim | 13 +-- waku/waku_lightpush_legacy/callbacks.nim | 15 +-- waku/waku_lightpush_legacy/client.nim | 4 +- waku/waku_metadata/protocol.nim | 19 ++-- waku/waku_peer_exchange/protocol.nim | 9 +- waku/waku_relay/protocol.nim | 24 ++-- waku/waku_rendezvous/protocol.nim | 48 ++++---- .../group_manager/on_chain/group_manager.nim | 46 ++++---- waku/waku_rln_relay/rln_relay.nim | 30 ++--- waku/waku_store/protocol.nim | 4 +- waku/waku_store/resume.nim | 8 +- waku/waku_store/self_req_handler.nim | 7 +- waku/waku_store_legacy/client.nim | 17 +-- waku/waku_store_legacy/protocol.nim | 15 +-- waku/waku_store_legacy/rpc.nim | 27 ++--- waku/waku_store_sync/reconciliation.nim | 11 +- waku/waku_store_sync/transfer.nim | 5 +- 92 files changed, 754 insertions(+), 1168 deletions(-) diff --git a/apps/chat2/chat2.nim b/apps/chat2/chat2.nim index 6b52bc919..e2a46ca1b 100644 --- a/apps/chat2/chat2.nim +++ b/apps/chat2/chat2.nim @@ -317,27 +317,19 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = if conf.logLevel != LogLevel.NONE: setLogLevel(conf.logLevel) - let natRes = setupNat( + let (extIp, extTcpPort, extUdpPort) = setupNat( conf.nat, clientId, Port(uint16(conf.tcpPort) + conf.portsShift), Port(uint16(conf.udpPort) + conf.portsShift), - ) - - if natRes.isErr(): - raise newException(ValueError, "setupNat error " & natRes.error) - - let (extIp, extTcpPort, extUdpPort) = natRes.get() + ).valueOr: + raise newException(ValueError, "setupNat error " & error) var enrBuilder = EnrBuilder.init(nodeKey) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) let node = block: var builder = WakuNodeBuilder.init() diff --git a/apps/chat2bridge/chat2bridge.nim b/apps/chat2bridge/chat2bridge.nim index 2d7b48cf8..9a22572cd 100644 --- a/apps/chat2bridge/chat2bridge.nim +++ b/apps/chat2bridge/chat2bridge.nim @@ -126,23 +126,20 @@ proc toMatterbridge( assert chat2Msg.isOk - let postRes = cmb.mbClient.postMessage( - text = string.fromBytes(chat2Msg[].payload), username = chat2Msg[].nick - ) - - if postRes.isErr() or (postRes[] == false): + if not cmb.mbClient + .postMessage(text = string.fromBytes(chat2Msg[].payload), username = chat2Msg[].nick) + .containsValue(true): chat2_mb_dropped.inc(labelValues = ["duplicate"]) error "Matterbridge host unreachable. Dropping message." proc pollMatterbridge(cmb: Chat2MatterBridge, handler: MbMessageHandler) {.async.} = while cmb.running: - if (let getRes = cmb.mbClient.getMessages(); getRes.isOk()): - for jsonNode in getRes[]: - await handler(jsonNode) - else: + let msg = cmb.mbClient.getMessages().valueOr: error "Matterbridge host unreachable. Sleeping before retrying." await sleepAsync(chronos.seconds(10)) - + continue + for jsonNode in msg: + await handler(jsonNode) await sleepAsync(cmb.pollPeriod) ############## @@ -252,25 +249,21 @@ when isMainModule: if conf.logLevel != LogLevel.NONE: setLogLevel(conf.logLevel) - let natRes = setupNat( + let (nodev2ExtIp, nodev2ExtPort, _) = setupNat( conf.nat, clientId, Port(uint16(conf.libp2pTcpPort) + conf.portsShift), Port(uint16(conf.udpPort) + conf.portsShift), - ) - if natRes.isErr(): - error "Error in setupNat", error = natRes.error + ).valueOr: + raise newException(ValueError, "setupNat error " & error) - # Load address configuration - let - (nodev2ExtIp, nodev2ExtPort, _) = natRes.get() - ## The following heuristic assumes that, in absence of manual - ## config, the external port is the same as the bind port. - extPort = - if nodev2ExtIp.isSome() and nodev2ExtPort.isNone(): - some(Port(uint16(conf.libp2pTcpPort) + conf.portsShift)) - else: - nodev2ExtPort + ## The following heuristic assumes that, in absence of manual + ## config, the external port is the same as the bind port. + let extPort = + if nodev2ExtIp.isSome() and nodev2ExtPort.isNone(): + some(Port(uint16(conf.libp2pTcpPort) + conf.portsShift)) + else: + nodev2ExtPort let bridge = Chat2Matterbridge.new( mbHostUri = "http://" & $initTAddress(conf.mbHostAddress, Port(conf.mbHostPort)), diff --git a/apps/chat2mix/chat2mix.nim b/apps/chat2mix/chat2mix.nim index 2b4e0a924..5979e2936 100644 --- a/apps/chat2mix/chat2mix.nim +++ b/apps/chat2mix/chat2mix.nim @@ -175,18 +175,16 @@ proc startMetricsServer( ): Result[MetricsHttpServerRef, string] = info "Starting metrics HTTP server", serverIp = $serverIp, serverPort = $serverPort - let metricsServerRes = MetricsHttpServerRef.new($serverIp, serverPort) - if metricsServerRes.isErr(): - return err("metrics HTTP server start failed: " & $metricsServerRes.error) + let server = MetricsHttpServerRef.new($serverIp, serverPort).valueOr: + return err("metrics HTTP server start failed: " & $error) - let server = metricsServerRes.value try: waitFor server.start() except CatchableError: return err("metrics HTTP server start failed: " & getCurrentExceptionMsg()) info "Metrics HTTP server started", serverIp = $serverIp, serverPort = $serverPort - ok(metricsServerRes.value) + ok(server) proc publish(c: Chat, line: string) {.async.} = # First create a Chat2Message protobuf with this line of text @@ -333,57 +331,56 @@ proc maintainSubscription( const maxFailedServiceNodeSwitches = 10 var noFailedSubscribes = 0 var noFailedServiceNodeSwitches = 0 + const RetryWaitMs = 2.seconds # Quick retry interval + const SubscriptionMaintenanceMs = 30.seconds # Subscription maintenance interval while true: info "maintaining subscription at", peer = constructMultiaddrStr(actualFilterPeer) # First use filter-ping to check if we have an active subscription - let pingRes = await wakuNode.wakuFilterClient.ping(actualFilterPeer) - if pingRes.isErr(): - # No subscription found. Let's subscribe. - error "ping failed.", err = pingRes.error - trace "no subscription found. Sending subscribe request" + let pingErr = (await wakuNode.wakuFilterClient.ping(actualFilterPeer)).errorOr: + await sleepAsync(SubscriptionMaintenanceMs) + info "subscription is live." + continue - let subscribeRes = await wakuNode.filterSubscribe( + # No subscription found. Let's subscribe. + error "ping failed.", error = pingErr + trace "no subscription found. Sending subscribe request" + + let subscribeErr = ( + await wakuNode.filterSubscribe( some(filterPubsubTopic), filterContentTopic, actualFilterPeer ) + ).errorOr: + await sleepAsync(SubscriptionMaintenanceMs) + if noFailedSubscribes > 0: + noFailedSubscribes -= 1 + notice "subscribe request successful." + continue - if subscribeRes.isErr(): - noFailedSubscribes += 1 - error "Subscribe request failed.", - err = subscribeRes.error, - peer = actualFilterPeer, - failCount = noFailedSubscribes + noFailedSubscribes += 1 + error "Subscribe request failed.", + error = subscribeErr, peer = actualFilterPeer, failCount = noFailedSubscribes - # TODO: disconnet from failed actualFilterPeer - # asyncSpawn(wakuNode.peerManager.switch.disconnect(p)) - # wakunode.peerManager.peerStore.delete(actualFilterPeer) + # TODO: disconnet from failed actualFilterPeer + # asyncSpawn(wakuNode.peerManager.switch.disconnect(p)) + # wakunode.peerManager.peerStore.delete(actualFilterPeer) - if noFailedSubscribes < maxFailedSubscribes: - await sleepAsync(2000) # Wait a bit before retrying - continue - elif not preventPeerSwitch: - let peerOpt = selectRandomServicePeer( - wakuNode.peerManager, some(actualFilterPeer), WakuFilterSubscribeCodec - ) - peerOpt.isOkOr: - error "Failed to find new service peer. Exiting." - noFailedServiceNodeSwitches += 1 - break + if noFailedSubscribes < maxFailedSubscribes: + await sleepAsync(RetryWaitMs) # Wait a bit before retrying + elif not preventPeerSwitch: + # try again with new peer without delay + let actualFilterPeer = selectRandomServicePeer( + wakuNode.peerManager, some(actualFilterPeer), WakuFilterSubscribeCodec + ).valueOr: + error "Failed to find new service peer. Exiting." + noFailedServiceNodeSwitches += 1 + break - actualFilterPeer = peerOpt.get() - info "Found new peer for codec", - codec = filterPubsubTopic, peer = constructMultiaddrStr(actualFilterPeer) + info "Found new peer for codec", + codec = filterPubsubTopic, peer = constructMultiaddrStr(actualFilterPeer) - noFailedSubscribes = 0 - continue # try again with new peer without delay - else: - if noFailedSubscribes > 0: - noFailedSubscribes -= 1 - - notice "subscribe request successful." + noFailedSubscribes = 0 else: - info "subscription is live." - - await sleepAsync(30000) # Subscription maintenance interval + await sleepAsync(SubscriptionMaintenanceMs) {.pop.} # @TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError @@ -401,17 +398,13 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = if conf.logLevel != LogLevel.NONE: setLogLevel(conf.logLevel) - let natRes = setupNat( + let (extIp, extTcpPort, extUdpPort) = setupNat( conf.nat, clientId, Port(uint16(conf.tcpPort) + conf.portsShift), Port(uint16(conf.udpPort) + conf.portsShift), - ) - - if natRes.isErr(): - raise newException(ValueError, "setupNat error " & natRes.error) - - let (extIp, extTcpPort, extUdpPort) = natRes.get() + ).valueOr: + raise newException(ValueError, "setupNat error " & error) var enrBuilder = EnrBuilder.init(nodeKey) @@ -421,13 +414,9 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = error "failed to add sharded topics to ENR", error = error quit(QuitFailure) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) let node = block: var builder = WakuNodeBuilder.init() diff --git a/apps/liteprotocoltester/diagnose_connections.nim b/apps/liteprotocoltester/diagnose_connections.nim index f595b4e03..15c0768f4 100644 --- a/apps/liteprotocoltester/diagnose_connections.nim +++ b/apps/liteprotocoltester/diagnose_connections.nim @@ -59,7 +59,4 @@ proc logSelfPeers*(pm: PeerManager) = {allPeers(pm)} *------------------------------------------------------------------------------------------*""".fmt() - if printable.isErr(): - echo "Error while printing statistics: " & printable.error().msg - else: - echo printable.get() + echo printable.valueOr("Error while printing statistics: " & error.msg) diff --git a/apps/liteprotocoltester/liteprotocoltester.nim b/apps/liteprotocoltester/liteprotocoltester.nim index 90c355a25..7778183d1 100644 --- a/apps/liteprotocoltester/liteprotocoltester.nim +++ b/apps/liteprotocoltester/liteprotocoltester.nim @@ -49,13 +49,10 @@ when isMainModule: const versionString = "version / git commit hash: " & waku_factory.git_version - let confRes = LiteProtocolTesterConf.load(version = versionString) - if confRes.isErr(): - error "failure while loading the configuration", error = confRes.error + let conf = LiteProtocolTesterConf.load(version = versionString).valueOr: + error "failure while loading the configuration", error = error quit(QuitFailure) - var conf = confRes.get() - ## Logging setup logging.setupLog(conf.logLevel, conf.logFormat) @@ -187,7 +184,7 @@ when isMainModule: error "Service node not found in time via PX" quit(QuitFailure) - if futForServiceNode.read().isErr(): + futForServiceNode.read().isOkOr: error "Service node for test not found via PX" quit(QuitFailure) diff --git a/apps/liteprotocoltester/publisher.nim b/apps/liteprotocoltester/publisher.nim index 1debfdf56..0df3f3e3e 100644 --- a/apps/liteprotocoltester/publisher.nim +++ b/apps/liteprotocoltester/publisher.nim @@ -89,10 +89,7 @@ proc reportSentMessages() = |{numMessagesToSend+failedToSendCount:>11} |{messagesSent:>11} |{failedToSendCount:>11} | *----------------------------------------*""".fmt() - if report.isErr: - echo "Error while printing statistics" - else: - echo report.get() + echo report.valueOr("Error while printing statistics") echo "*--------------------------------------------------------------------------------------------------*" echo "| Failure cause | count |" diff --git a/apps/liteprotocoltester/receiver.nim b/apps/liteprotocoltester/receiver.nim index 0e6638c61..9792549ca 100644 --- a/apps/liteprotocoltester/receiver.nim +++ b/apps/liteprotocoltester/receiver.nim @@ -54,64 +54,65 @@ proc maintainSubscription( var noFailedSubscribes = 0 var noFailedServiceNodeSwitches = 0 var isFirstPingOnNewPeer = true + const RetryWaitMs = 2.seconds # Quick retry interval + const SubscriptionMaintenanceMs = 30.seconds # Subscription maintenance interval while true: info "maintaining subscription at", peer = constructMultiaddrStr(actualFilterPeer) # First use filter-ping to check if we have an active subscription - let pingRes = await wakuNode.wakuFilterClient.ping(actualFilterPeer) - if pingRes.isErr(): - if isFirstPingOnNewPeer == false: - # Very first ping expected to fail as we have not yet subscribed at all - lpt_receiver_lost_subscription_count.inc() - isFirstPingOnNewPeer = false - # No subscription found. Let's subscribe. - error "ping failed.", err = pingRes.error - trace "no subscription found. Sending subscribe request" + let pingErr = (await wakuNode.wakuFilterClient.ping(actualFilterPeer)).errorOr: + await sleepAsync(SubscriptionMaintenanceMs) + info "subscription is live." + continue - let subscribeRes = await wakuNode.filterSubscribe( + if isFirstPingOnNewPeer == false: + # Very first ping expected to fail as we have not yet subscribed at all + lpt_receiver_lost_subscription_count.inc() + isFirstPingOnNewPeer = false + # No subscription found. Let's subscribe. + error "ping failed.", error = pingErr + trace "no subscription found. Sending subscribe request" + + let subscribeErr = ( + await wakuNode.filterSubscribe( some(filterPubsubTopic), filterContentTopic, actualFilterPeer ) + ).errorOr: + await sleepAsync(subscriptionMaintenanceMs) + if noFailedSubscribes > 0: + noFailedSubscribes -= 1 + notice "subscribe request successful." + continue - if subscribeRes.isErr(): - noFailedSubscribes += 1 - lpt_service_peer_failure_count.inc( - labelValues = ["receiver", actualFilterPeer.getAgent()] - ) - error "Subscribe request failed.", - err = subscribeRes.error, - peer = actualFilterPeer, - failCount = noFailedSubscribes + noFailedSubscribes += 1 + lpt_service_peer_failure_count.inc( + labelValues = ["receiver", actualFilterPeer.getAgent()] + ) + error "Subscribe request failed.", + err = subscribeErr, peer = actualFilterPeer, failCount = noFailedSubscribes - # TODO: disconnet from failed actualFilterPeer - # asyncSpawn(wakuNode.peerManager.switch.disconnect(p)) - # wakunode.peerManager.peerStore.delete(actualFilterPeer) + # TODO: disconnet from failed actualFilterPeer + # asyncSpawn(wakuNode.peerManager.switch.disconnect(p)) + # wakunode.peerManager.peerStore.delete(actualFilterPeer) - if noFailedSubscribes < maxFailedSubscribes: - await sleepAsync(2.seconds) # Wait a bit before retrying - continue - elif not preventPeerSwitch: - actualFilterPeer = selectRandomServicePeer( - wakuNode.peerManager, some(actualFilterPeer), WakuFilterSubscribeCodec - ).valueOr: - error "Failed to find new service peer. Exiting." - noFailedServiceNodeSwitches += 1 - break + if noFailedSubscribes < maxFailedSubscribes: + await sleepAsync(RetryWaitMs) # Wait a bit before retrying + elif not preventPeerSwitch: + # try again with new peer without delay + actualFilterPeer = selectRandomServicePeer( + wakuNode.peerManager, some(actualFilterPeer), WakuFilterSubscribeCodec + ).valueOr: + error "Failed to find new service peer. Exiting." + noFailedServiceNodeSwitches += 1 + break - info "Found new peer for codec", - codec = filterPubsubTopic, peer = constructMultiaddrStr(actualFilterPeer) + info "Found new peer for codec", + codec = filterPubsubTopic, peer = constructMultiaddrStr(actualFilterPeer) - noFailedSubscribes = 0 - lpt_change_service_peer_count.inc(labelValues = ["receiver"]) - isFirstPingOnNewPeer = true - continue # try again with new peer without delay - else: - if noFailedSubscribes > 0: - noFailedSubscribes -= 1 - - notice "subscribe request successful." + noFailedSubscribes = 0 + lpt_change_service_peer_count.inc(labelValues = ["receiver"]) + isFirstPingOnNewPeer = true else: - info "subscription is live." - - await sleepAsync(30.seconds) # Subscription maintenance interval + await sleepAsync(SubscriptionMaintenanceMs) proc setupAndListen*( wakuNode: WakuNode, conf: LiteProtocolTesterConf, servicePeer: RemotePeerInfo diff --git a/apps/liteprotocoltester/service_peer_management.nim b/apps/liteprotocoltester/service_peer_management.nim index 747ace86b..053445740 100644 --- a/apps/liteprotocoltester/service_peer_management.nim +++ b/apps/liteprotocoltester/service_peer_management.nim @@ -181,7 +181,7 @@ proc pxLookupServiceNode*( if not await futPeers.withTimeout(30.seconds): notice "Cannot get peers from PX", round = 5 - trialCount else: - if futPeers.value().isErr(): + futPeers.value().isOkOr: info "PeerExchange reported error", error = futPeers.read().error return err() diff --git a/apps/liteprotocoltester/statistics.nim b/apps/liteprotocoltester/statistics.nim index 8322edd8f..5ca215b2c 100644 --- a/apps/liteprotocoltester/statistics.nim +++ b/apps/liteprotocoltester/statistics.nim @@ -114,12 +114,7 @@ proc addMessage*( if not self.contains(peerId): self[peerId] = Statistics.init() - let shortSenderId = block: - let senderPeer = PeerId.init(msg.sender) - if senderPeer.isErr(): - msg.sender - else: - senderPeer.get().shortLog() + let shortSenderId = PeerId.init(msg.sender).map(p => p.shortLog()).valueOr(msg.sender) discard catch: self[peerId].addMessage(shortSenderId, msg, msgHash) @@ -220,10 +215,7 @@ proc echoStat*(self: Statistics, peerId: string) = | {self.missingIndices()} | *------------------------------------------------------------------------------------------*""".fmt() - if printable.isErr(): - echo "Error while printing statistics: " & printable.error().msg - else: - echo printable.get() + echo printable.valueOr("Error while printing statistics: " & error.msg) proc jsonStat*(self: Statistics): string = let minL, maxL, avgL = self.calcLatency() @@ -243,20 +235,18 @@ proc jsonStat*(self: Statistics): string = }}, "lostIndices": {self.missingIndices()} }}""".fmt() - if json.isErr: - return "{\"result:\": \"" & json.error.msg & "\"}" - return json.get() + return json.valueOr("{\"result:\": \"" & error.msg & "\"}") proc echoStats*(self: var PerPeerStatistics) = for peerId, stats in self.pairs: let peerLine = catch: "Receiver statistics from peer {peerId}".fmt() - if peerLine.isErr: + peerLine.isOkOr: echo "Error while printing statistics" - else: - echo peerLine.get() - stats.echoStat(peerId) + continue + echo peerLine.get() + stats.echoStat(peerId) proc jsonStats*(self: PerPeerStatistics): string = try: diff --git a/apps/networkmonitor/networkmonitor.nim b/apps/networkmonitor/networkmonitor.nim index ad7732db2..23607b118 100644 --- a/apps/networkmonitor/networkmonitor.nim +++ b/apps/networkmonitor/networkmonitor.nim @@ -443,12 +443,8 @@ proc initAndStartApp( error "failed to add sharded topics to ENR", error = error return err("failed to add sharded topics to ENR: " & $error) - let recordRes = builder.build() - let record = - if recordRes.isErr(): - return err("cannot build record: " & $recordRes.error) - else: - recordRes.get() + let record = builder.build().valueOr: + return err("cannot build record: " & $error) var nodeBuilder = WakuNodeBuilder.init() @@ -461,21 +457,15 @@ proc initAndStartApp( relayServiceRatio = "13.33:86.67", shardAware = true, ) - let res = nodeBuilder.withNetworkConfigurationDetails(bindIp, nodeTcpPort) - if res.isErr(): - return err("node building error" & $res.error) + nodeBuilder.withNetworkConfigurationDetails(bindIp, nodeTcpPort).isOkOr: + return err("node building error" & $error) - let nodeRes = nodeBuilder.build() - let node = - if nodeRes.isErr(): - return err("node building error" & $res.error) - else: - nodeRes.get() + let node = nodeBuilder.build().valueOr: + return err("node building error" & $error) - var discv5BootstrapEnrsRes = await getBootstrapFromDiscDns(conf) - if discv5BootstrapEnrsRes.isErr(): + var discv5BootstrapEnrs = (await getBootstrapFromDiscDns(conf)).valueOr: error("failed discovering peers from DNS") - var discv5BootstrapEnrs = discv5BootstrapEnrsRes.get() + quit(QuitFailure) # parse enrURIs from the configuration and add the resulting ENRs to the discv5BootstrapEnrs seq for enrUri in conf.bootstrapNodes: @@ -553,12 +543,10 @@ proc subscribeAndHandleMessages( when isMainModule: # known issue: confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError {.pop.} - let confRes = NetworkMonitorConf.loadConfig() - if confRes.isErr(): - error "could not load cli variables", err = confRes.error - quit(1) + var conf = NetworkMonitorConf.loadConfig().valueOr: + error "could not load cli variables", error = error + quit(QuitFailure) - var conf = confRes.get() info "cli flags", conf = conf if conf.clusterId == 1: @@ -586,37 +574,30 @@ when isMainModule: # start metrics server if conf.metricsServer: - let res = - startMetricsServer(conf.metricsServerAddress, Port(conf.metricsServerPort)) - if res.isErr(): - error "could not start metrics server", err = res.error - quit(1) + startMetricsServer(conf.metricsServerAddress, Port(conf.metricsServerPort)).isOkOr: + error "could not start metrics server", error = error + quit(QuitFailure) # start rest server for custom metrics - let res = startRestApiServer(conf, allPeersInfo, msgPerContentTopic) - if res.isErr(): - error "could not start rest api server", err = res.error - quit(1) + startRestApiServer(conf, allPeersInfo, msgPerContentTopic).isOkOr: + error "could not start rest api server", error = error + quit(QuitFailure) # create a rest client - let clientRest = - RestClientRef.new(url = "http://ip-api.com", connectTimeout = ctime.seconds(2)) - if clientRest.isErr(): - error "could not start rest api client", err = res.error - quit(1) - let restClient = clientRest.get() + let restClient = RestClientRef.new( + url = "http://ip-api.com", connectTimeout = ctime.seconds(2) + ).valueOr: + error "could not start rest api client", error = error + quit(QuitFailure) # start waku node - let nodeRes = waitFor initAndStartApp(conf) - if nodeRes.isErr(): - error "could not start node" - quit 1 - - let (node, discv5) = nodeRes.get() + let (node, discv5) = (waitFor initAndStartApp(conf)).valueOr: + error "could not start node", error = error + quit(QuitFailure) (waitFor node.mountRelay()).isOkOr: - error "failed to mount waku relay protocol: ", err = error - quit 1 + error "failed to mount waku relay protocol: ", error = error + quit(QuitFailure) waitFor node.mountLibp2pPing() @@ -640,12 +621,12 @@ when isMainModule: try: waitFor node.mountRlnRelay(rlnConf) except CatchableError: - error "failed to setup RLN", err = getCurrentExceptionMsg() - quit 1 + error "failed to setup RLN", error = getCurrentExceptionMsg() + quit(QuitFailure) node.mountMetadata(conf.clusterId, conf.shards).isOkOr: - error "failed to mount waku metadata protocol: ", err = error - quit 1 + error "failed to mount waku metadata protocol: ", error = error + quit(QuitFailure) for shard in conf.shards: # Subscribe the node to the shards, to count messages diff --git a/apps/wakucanary/wakucanary.nim b/apps/wakucanary/wakucanary.nim index 337896d39..bcff9653e 100644 --- a/apps/wakucanary/wakucanary.nim +++ b/apps/wakucanary/wakucanary.nim @@ -181,13 +181,10 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = protocols = conf.protocols, logLevel = conf.logLevel - let peerRes = parsePeerInfo(conf.address) - if peerRes.isErr(): - error "Couldn't parse 'conf.address'", error = peerRes.error + let peer = parsePeerInfo(conf.address).valueOr: + error "Couldn't parse 'conf.address'", error = error quit(QuitFailure) - let peer = peerRes.value - let nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[] bindIp = parseIpAddress("0.0.0.0") @@ -225,13 +222,9 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = error "could not initialize ENR with shards", error quit(QuitFailure) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) if isWss and (conf.websocketSecureKeyPath.len == 0 or conf.websocketSecureCertPath.len == 0): diff --git a/examples/filter_subscriber.nim b/examples/filter_subscriber.nim index e4e26bdb7..03a5de4eb 100644 --- a/examples/filter_subscriber.nim +++ b/examples/filter_subscriber.nim @@ -62,13 +62,9 @@ proc setupAndSubscribe(rng: ref HmacDrbgContext) {.async.} = "Building ENR with relay sharding failed" ) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) var builder = WakuNodeBuilder.init() builder.withNodeKey(nodeKey) @@ -92,20 +88,18 @@ proc setupAndSubscribe(rng: ref HmacDrbgContext) {.async.} = while true: notice "maintaining subscription" # First use filter-ping to check if we have an active subscription - let pingRes = await node.wakuFilterClient.ping(filterPeer) - if pingRes.isErr(): + if (await node.wakuFilterClient.ping(filterPeer)).isErr(): # No subscription found. Let's subscribe. notice "no subscription found. Sending subscribe request" - let subscribeRes = await node.wakuFilterClient.subscribe( - filterPeer, FilterPubsubTopic, @[FilterContentTopic] - ) - - if subscribeRes.isErr(): - notice "subscribe request failed. Quitting.", err = subscribeRes.error + ( + await node.wakuFilterClient.subscribe( + filterPeer, FilterPubsubTopic, @[FilterContentTopic] + ) + ).isOkOr: + notice "subscribe request failed. Quitting.", error = error break - else: - notice "subscribe request successful." + notice "subscribe request successful." else: notice "subscription found." diff --git a/examples/lightpush_publisher.nim b/examples/lightpush_publisher.nim index 70ebd9c53..c7eacdd30 100644 --- a/examples/lightpush_publisher.nim +++ b/examples/lightpush_publisher.nim @@ -54,13 +54,9 @@ proc setupAndPublish(rng: ref HmacDrbgContext) {.async.} = "Building ENR with relay sharding failed" ) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) var builder = WakuNodeBuilder.init() builder.withNodeKey(nodeKey) diff --git a/examples/publisher.nim b/examples/publisher.nim index 8c2d03679..6f5d34bc4 100644 --- a/examples/publisher.nim +++ b/examples/publisher.nim @@ -49,13 +49,9 @@ proc setupAndPublish(rng: ref HmacDrbgContext) {.async.} = var enrBuilder = EnrBuilder.init(nodeKey) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) var builder = WakuNodeBuilder.init() builder.withNodeKey(nodeKey) diff --git a/examples/subscriber.nim b/examples/subscriber.nim index fb040b05a..ce64bb803 100644 --- a/examples/subscriber.nim +++ b/examples/subscriber.nim @@ -47,13 +47,9 @@ proc setupAndSubscribe(rng: ref HmacDrbgContext) {.async.} = var enrBuilder = EnrBuilder.init(nodeKey) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) var builder = WakuNodeBuilder.init() builder.withNodeKey(nodeKey) diff --git a/examples/wakustealthcommitments/node_spec.nim b/examples/wakustealthcommitments/node_spec.nim index b935f9ab1..21286340e 100644 --- a/examples/wakustealthcommitments/node_spec.nim +++ b/examples/wakustealthcommitments/node_spec.nim @@ -18,13 +18,10 @@ proc setup*(): Waku = const versionString = "version / git commit hash: " & waku.git_version let rng = crypto.newRng() - let confRes = WakuNodeConf.load(version = versionString) - if confRes.isErr(): - error "failure while loading the configuration", error = $confRes.error + let conf = WakuNodeConf.load(version = versionString).valueOr: + error "failure while loading the configuration", error = $error quit(QuitFailure) - var conf = confRes.get() - let twnNetworkConf = NetworkConf.TheWakuNetworkConf() if len(conf.shards) != 0: conf.pubsubTopics = conf.shards.mapIt(twnNetworkConf.pubsubTopics[it.uint16]) diff --git a/examples/wakustealthcommitments/stealth_commitment_protocol.nim b/examples/wakustealthcommitments/stealth_commitment_protocol.nim index 9a2045a67..63311bf7b 100644 --- a/examples/wakustealthcommitments/stealth_commitment_protocol.nim +++ b/examples/wakustealthcommitments/stealth_commitment_protocol.nim @@ -95,61 +95,54 @@ proc sendResponse*( type SCPHandler* = proc(msg: WakuMessage): Future[void] {.async.} proc getSCPHandler(self: StealthCommitmentProtocol): SCPHandler = let handler = proc(msg: WakuMessage): Future[void] {.async.} = - let decodedRes = WakuStealthCommitmentMsg.decode(msg.payload) - if decodedRes.isErr(): - error "could not decode scp message" - let decoded = decodedRes.get() + let decoded = WakuStealthCommitmentMsg.decode(msg.payload).valueOr: + error "could not decode scp message", error = error + quit(QuitFailure) if decoded.request == false: # check if the generated stealth commitment belongs to the receiver # if not, continue - let ephemeralPubKeyRes = - deserialize(StealthCommitmentFFI.PublicKey, decoded.ephemeralPubKey.get()) - if ephemeralPubKeyRes.isErr(): - error "could not deserialize ephemeral public key: ", - err = ephemeralPubKeyRes.error() - let ephemeralPubKey = ephemeralPubKeyRes.get() - let stealthCommitmentPrivateKeyRes = StealthCommitmentFFI.generateStealthPrivateKey( + let ephemeralPubKey = deserialize( + StealthCommitmentFFI.PublicKey, decoded.ephemeralPubKey.get() + ).valueOr: + error "could not deserialize ephemeral public key: ", error = error + quit(QuitFailure) + let stealthCommitmentPrivateKey = StealthCommitmentFFI.generateStealthPrivateKey( ephemeralPubKey, self.spendingKeyPair.privateKey, self.viewingKeyPair.privateKey, decoded.viewTag.get(), - ) - if stealthCommitmentPrivateKeyRes.isErr(): - info "received stealth commitment does not belong to the receiver: ", - err = stealthCommitmentPrivateKeyRes.error() - - let stealthCommitmentPrivateKey = stealthCommitmentPrivateKeyRes.get() + ).valueOr: + error "received stealth commitment does not belong to the receiver: ", + error = error + quit(QuitFailure) info "received stealth commitment belongs to the receiver: ", stealthCommitmentPrivateKey, stealthCommitmentPubKey = decoded.stealthCommitment.get() return # send response # deseralize the keys - let spendingKeyRes = - deserialize(StealthCommitmentFFI.PublicKey, decoded.spendingPubKey.get()) - if spendingKeyRes.isErr(): - error "could not deserialize spending key: ", err = spendingKeyRes.error() - let spendingKey = spendingKeyRes.get() - let viewingKeyRes = - (deserialize(StealthCommitmentFFI.PublicKey, decoded.viewingPubKey.get())) - if viewingKeyRes.isErr(): - error "could not deserialize viewing key: ", err = viewingKeyRes.error() - let viewingKey = viewingKeyRes.get() + let spendingKey = deserialize( + StealthCommitmentFFI.PublicKey, decoded.spendingPubKey.get() + ).valueOr: + error "could not deserialize spending key: ", error = error + quit(QuitFailure) + let viewingKey = ( + deserialize(StealthCommitmentFFI.PublicKey, decoded.viewingPubKey.get()) + ).valueOr: + error "could not deserialize viewing key: ", error = error + quit(QuitFailure) info "received spending key", spendingKey info "received viewing key", viewingKey - let ephemeralKeyPairRes = StealthCommitmentFFI.generateKeyPair() - if ephemeralKeyPairRes.isErr(): - error "could not generate ephemeral key pair: ", err = ephemeralKeyPairRes.error() - let ephemeralKeyPair = ephemeralKeyPairRes.get() + let ephemeralKeyPair = StealthCommitmentFFI.generateKeyPair().valueOr: + error "could not generate ephemeral key pair: ", error = error + quit(QuitFailure) - let stealthCommitmentRes = StealthCommitmentFFI.generateStealthCommitment( + let stealthCommitment = StealthCommitmentFFI.generateStealthCommitment( spendingKey, viewingKey, ephemeralKeyPair.privateKey - ) - if stealthCommitmentRes.isErr(): - error "could not generate stealth commitment: ", - err = stealthCommitmentRes.error() - let stealthCommitment = stealthCommitmentRes.get() + ).valueOr: + error "could not generate stealth commitment: ", error = error + quit(QuitFailure) ( await self.sendResponse( @@ -157,7 +150,7 @@ proc getSCPHandler(self: StealthCommitmentProtocol): SCPHandler = stealthCommitment.viewTag, ) ).isOkOr: - error "could not send response: ", err = $error + error "could not send response: ", error = $error return handler diff --git a/library/waku_context.nim b/library/waku_context.nim index 64a9e3466..ab4b996af 100644 --- a/library/waku_context.nim +++ b/library/waku_context.nim @@ -96,18 +96,16 @@ proc sendRequestToWakuThread*( deallocShared(req) return err("Couldn't send a request to the waku thread: " & $req[]) - let fireSyncRes = ctx.reqSignal.fireSync() - if fireSyncRes.isErr(): + let fireSync = ctx.reqSignal.fireSync().valueOr: deallocShared(req) - return err("failed fireSync: " & $fireSyncRes.error) + return err("failed fireSync: " & $error) - if fireSyncRes.get() == false: + if not fireSync: deallocShared(req) return err("Couldn't fireSync in time") ## wait until the Waku Thread properly received the request - let res = ctx.reqReceivedSignal.waitSync(timeout) - if res.isErr(): + ctx.reqReceivedSignal.waitSync(timeout).isOkOr: deallocShared(req) return err("Couldn't receive reqReceivedSignal signal") @@ -176,9 +174,8 @@ proc wakuThreadBody(ctx: ptr WakuContext) {.thread.} = ## Handle the request asyncSpawn WakuThreadRequest.process(request, addr waku) - let fireRes = ctx.reqReceivedSignal.fireSync() - if fireRes.isErr(): - error "could not fireSync back to requester thread", error = fireRes.error + ctx.reqReceivedSignal.fireSync().isOkOr: + error "could not fireSync back to requester thread", error = error waitFor wakuRun(ctx) diff --git a/library/waku_thread_requests/requests/ping_request.nim b/library/waku_thread_requests/requests/ping_request.nim index 53d33968e..716b9ed68 100644 --- a/library/waku_thread_requests/requests/ping_request.nim +++ b/library/waku_thread_requests/requests/ping_request.nim @@ -44,13 +44,11 @@ proc process*( let pingFuture = ping() let pingRTT: Duration = if self[].timeout == chronos.milliseconds(0): # No timeout expected - (await pingFuture).valueOr: - return err(error) + ?(await pingFuture) else: let timedOut = not (await pingFuture.withTimeout(self[].timeout)) if timedOut: return err("ping timed out") - pingFuture.read().valueOr: - return err(error) + ?(pingFuture.read()) ok($(pingRTT.nanos)) diff --git a/tests/wakunode_rest/test_rest_store.nim b/tests/wakunode_rest/test_rest_store.nim index b8882328b..b86513f0d 100644 --- a/tests/wakunode_rest/test_rest_store.nim +++ b/tests/wakunode_rest/test_rest_store.nim @@ -485,7 +485,7 @@ procSuite "Waku Rest API - Store v3": $response.contentType == $MIMETYPE_TEXT response.data.messages.len == 0 response.data.statusDesc == - "Failed parsing remote peer info [MultiAddress.init [multiaddress: Invalid MultiAddress, must start with `/`]]" + "Failed parsing remote peer info: MultiAddress.init [multiaddress: Invalid MultiAddress, must start with `/`]" await restServer.stop() await restServer.closeWait() diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index fd1e3a576..fb8437299 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -725,12 +725,11 @@ proc parseCmdArg*(T: type ProtectedShard, p: string): T = raise newException( ValueError, "Invalid format for protected shard expected shard:publickey" ) - let publicKey = secp256k1.SkPublicKey.fromHex(elements[1]) - if publicKey.isErr: + let publicKey = secp256k1.SkPublicKey.fromHex(elements[1]).valueOr: raise newException(ValueError, "Invalid public key") if isNumber(elements[0]): - return ProtectedShard(shard: uint16.parseCmdArg(elements[0]), key: publicKey.get()) + return ProtectedShard(shard: uint16.parseCmdArg(elements[0]), key: publicKey) # TODO: Remove when removing protected-topic configuration let shard = RelayShard.parse(elements[0]).valueOr: @@ -738,7 +737,7 @@ proc parseCmdArg*(T: type ProtectedShard, p: string): T = ValueError, "Invalid pubsub topic. Pubsub topics must be in the format /waku/2/rs//", ) - return ProtectedShard(shard: shard.shardId, key: publicKey.get()) + return ProtectedShard(shard: shard.shardId, key: publicKey) proc completeCmdArg*(T: type ProtectedShard, val: string): seq[string] = return @[] diff --git a/tools/rln_keystore_generator/rln_keystore_generator.nim b/tools/rln_keystore_generator/rln_keystore_generator.nim index 36a3759c9..85df37982 100644 --- a/tools/rln_keystore_generator/rln_keystore_generator.nim +++ b/tools/rln_keystore_generator/rln_keystore_generator.nim @@ -31,12 +31,10 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = trace "configuration", conf = $conf # 2. generate credentials - let credentialRes = membershipKeyGen() - if credentialRes.isErr(): - error "failure while generating credentials", error = credentialRes.error - quit(1) + let credential = membershipKeyGen().valueOr: + error "failure while generating credentials", error = error + quit(QuitFailure) - let credential = credentialRes.get() info "credentials", idTrapdoor = credential.idTrapdoor.inHex(), idNullifier = credential.idNullifier.inHex(), @@ -45,7 +43,7 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = if not conf.execute: info "not executing, exiting" - quit(0) + quit(QuitSuccess) var onFatalErrorAction = proc(msg: string) {.gcsafe, closure.} = ## Action to be taken when an internal error occurs during the node run. @@ -66,12 +64,12 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = try: (waitFor groupManager.init()).isOkOr: error "failure while initializing OnchainGroupManager", error = $error - quit(1) + quit(QuitFailure) # handling the exception is required since waitFor raises an exception except Exception, CatchableError: error "failure while initializing OnchainGroupManager", error = getCurrentExceptionMsg() - quit(1) + quit(QuitFailure) # 4. register on-chain try: @@ -79,7 +77,7 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = except Exception, CatchableError: error "failure while registering credentials on-chain", error = getCurrentExceptionMsg() - quit(1) + quit(QuitFailure) info "Transaction hash", txHash = groupManager.registrationTxHash.get() @@ -99,11 +97,9 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = userMessageLimit: conf.userMessageLimit, ) - let persistRes = - addMembershipCredentials(conf.credPath, keystoreCred, conf.credPassword, RLNAppInfo) - if persistRes.isErr(): - error "failed to persist credentials", error = persistRes.error - quit(1) + addMembershipCredentials(conf.credPath, keystoreCred, conf.credPassword, RLNAppInfo).isOkOr: + error "failed to persist credentials", error = error + quit(QuitFailure) info "credentials persisted", path = conf.credPath @@ -111,5 +107,5 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = waitFor groupManager.stop() except CatchableError: error "failure while stopping OnchainGroupManager", error = getCurrentExceptionMsg() - quit(0) # 0 because we already registered on-chain - quit(0) + quit(QuitSuccess) # 0 because we already registered on-chain + quit(QuitSuccess) diff --git a/waku/common/databases/db_postgres/dbconn.nim b/waku/common/databases/db_postgres/dbconn.nim index ee758730a..a6c237ae5 100644 --- a/waku/common/databases/db_postgres/dbconn.nim +++ b/waku/common/databases/db_postgres/dbconn.nim @@ -63,9 +63,8 @@ proc openDbConn(connString: string): Result[DbConn, string] = return err("exception opening new connection: " & getCurrentExceptionMsg()) if conn.status != CONNECTION_OK: - let checkRes = conn.check() - if checkRes.isErr(): - return err("failed to connect to database: " & checkRes.error) + conn.check().isOkOr: + return err("failed to connect to database: " & error) return err("unknown reason") diff --git a/waku/common/databases/db_postgres/pgasyncpool.nim b/waku/common/databases/db_postgres/pgasyncpool.nim index 5f8bf40be..0b298084e 100644 --- a/waku/common/databases/db_postgres/pgasyncpool.nim +++ b/waku/common/databases/db_postgres/pgasyncpool.nim @@ -174,8 +174,8 @@ proc runStmt*( let len = paramValues.len discard dbConnWrapper.getDbConn().prepare(stmtName, sql(stmtDefinition), len) - if res.isErr(): - return err("failed prepare in runStmt: " & res.error.msg) + res.isOkOr: + return err("failed prepare in runStmt: " & error.msg) pool.conns[connIndex].inclPreparedStmt(stmtName) diff --git a/waku/common/databases/db_sqlite.nim b/waku/common/databases/db_sqlite.nim index a28668cde..e398ea5ac 100644 --- a/waku/common/databases/db_sqlite.nim +++ b/waku/common/databases/db_sqlite.nim @@ -265,8 +265,7 @@ proc getPageSize*(db: SqliteDatabase): DatabaseResult[int64] = proc handler(s: RawStmtPtr) = size = sqlite3_column_int64(s, 0) - let res = db.query("PRAGMA page_size;", handler) - if res.isErr(): + db.query("PRAGMA page_size;", handler).isOkOr: return err("failed to get page_size") return ok(size) @@ -277,8 +276,7 @@ proc getFreelistCount*(db: SqliteDatabase): DatabaseResult[int64] = proc handler(s: RawStmtPtr) = count = sqlite3_column_int64(s, 0) - let res = db.query("PRAGMA freelist_count;", handler) - if res.isErr(): + db.query("PRAGMA freelist_count;", handler).isOkOr: return err("failed to get freelist_count") return ok(count) @@ -289,8 +287,7 @@ proc getPageCount*(db: SqliteDatabase): DatabaseResult[int64] = proc handler(s: RawStmtPtr) = count = sqlite3_column_int64(s, 0) - let res = db.query("PRAGMA page_count;", handler) - if res.isErr(): + db.query("PRAGMA page_count;", handler).isOkOr: return err("failed to get page_count") return ok(count) @@ -319,8 +316,7 @@ proc gatherSqlitePageStats*(db: SqliteDatabase): DatabaseResult[(int64, int64, i proc vacuum*(db: SqliteDatabase): DatabaseResult[void] = ## The VACUUM command rebuilds the database file, repacking it into a minimal amount of disk space. - let res = db.query("VACUUM;", NoopRowHandler) - if res.isErr(): + db.query("VACUUM;", NoopRowHandler).isOkOr: return err("vacuum failed") return ok() @@ -339,8 +335,7 @@ proc getUserVersion*(database: SqliteDatabase): DatabaseResult[int64] = proc handler(s: ptr sqlite3_stmt) = version = sqlite3_column_int64(s, 0) - let res = database.query("PRAGMA user_version;", handler) - if res.isErr(): + database.query("PRAGMA user_version;", handler).isOkOr: return err("failed to get user_version") ok(version) @@ -354,8 +349,7 @@ proc setUserVersion*(database: SqliteDatabase, version: int64): DatabaseResult[v ## ## For more info check: https://www.sqlite.org/pragma.html#pragma_user_version let query = "PRAGMA user_version=" & $version & ";" - let res = database.query(query, NoopRowHandler) - if res.isErr(): + database.query(query, NoopRowHandler).isOkOr: return err("failed to set user_version") ok() @@ -400,11 +394,9 @@ proc filterMigrationScripts( if direction != "" and not script.toLower().endsWith("." & direction & ".sql"): return false - let scriptVersionRes = getMigrationScriptVersion(script) - if scriptVersionRes.isErr(): + let scriptVersion = getMigrationScriptVersion(script).valueOr: return false - let scriptVersion = scriptVersionRes.value return lowVersion < scriptVersion and scriptVersion <= highVersion paths.filter(filterPredicate) @@ -476,10 +468,9 @@ proc migrate*( for statement in script.breakIntoStatements(): info "executing migration statement", statement = statement - let execRes = db.query(statement, NoopRowHandler) - if execRes.isErr(): + db.query(statement, NoopRowHandler).isOkOr: error "failed to execute migration statement", - statement = statement, error = execRes.error + statement = statement, error = error return err("failed to execute migration statement") info "migration statement executed succesfully", statement = statement @@ -497,9 +488,8 @@ proc performSqliteVacuum*(db: SqliteDatabase): DatabaseResult[void] = info "starting sqlite database vacuuming" - let resVacuum = db.vacuum() - if resVacuum.isErr(): - return err("failed to execute vacuum: " & resVacuum.error) + db.vacuum().isOkOr: + return err("failed to execute vacuum: " & error) info "finished sqlite database vacuuming" ok() diff --git a/waku/common/enr/typed_record.nim b/waku/common/enr/typed_record.nim index d0b055ac4..1db357621 100644 --- a/waku/common/enr/typed_record.nim +++ b/waku/common/enr/typed_record.nim @@ -65,11 +65,10 @@ func id*(record: TypedRecord): Option[RecordId] = if fieldOpt.isNone(): return none(RecordId) - let fieldRes = toRecordId(fieldOpt.get()) - if fieldRes.isErr(): + let field = toRecordId(fieldOpt.get()).valueOr: return none(RecordId) - some(fieldRes.value) + return some(field) func secp256k1*(record: TypedRecord): Option[array[33, byte]] = record.tryGet("secp256k1", array[33, byte]) diff --git a/waku/discovery/waku_discv5.nim b/waku/discovery/waku_discv5.nim index 94fc467fa..0eb329fa4 100644 --- a/waku/discovery/waku_discv5.nim +++ b/waku/discovery/waku_discv5.nim @@ -393,12 +393,11 @@ proc addBootstrapNode*(bootstrapAddr: string, bootstrapEnrs: var seq[enr.Record] if bootstrapAddr.len == 0 or bootstrapAddr[0] == '#': return - let enrRes = parseBootstrapAddress(bootstrapAddr) - if enrRes.isErr(): - info "ignoring invalid bootstrap address", reason = enrRes.error + let enr = parseBootstrapAddress(bootstrapAddr).valueOr: + info "ignoring invalid bootstrap address", reason = error return - bootstrapEnrs.add(enrRes.value) + bootstrapEnrs.add(enr) proc setupDiscoveryV5*( myENR: enr.Record, diff --git a/waku/factory/internal_config.nim b/waku/factory/internal_config.nim index 22b101021..7aad6e615 100644 --- a/waku/factory/internal_config.nim +++ b/waku/factory/internal_config.nim @@ -29,13 +29,9 @@ proc enrConfiguration*( ).isOkOr: return err("could not initialize ENR with shards") - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create record", error = recordRes.error - return err($recordRes.error) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + return err($error) return ok(record) @@ -70,16 +66,13 @@ proc networkConfiguration*( ): Future[NetConfigResult] {.async.} = ## `udpPort` is only supplied to satisfy underlying APIs but is not ## actually a supported transport for libp2p traffic. - let natRes = setupNat( + var (extIp, extTcpPort, _) = setupNat( conf.natStrategy.string, clientId, Port(uint16(conf.p2pTcpPort) + portsShift), Port(uint16(conf.p2pTcpPort) + portsShift), - ) - if natRes.isErr(): - return err("failed to setup NAT: " & $natRes.error) - - var (extIp, extTcpPort, _) = natRes.get() + ).valueOr: + return err("failed to setup NAT: " & $error) let discv5UdpPort = @@ -101,12 +94,10 @@ proc networkConfiguration*( # Resolve and use DNS domain IP if conf.dns4DomainName.isSome() and extIp.isNone(): try: - let dnsRes = await dnsResolve(conf.dns4DomainName.get(), dnsAddrsNameServers) + let dns = (await dnsResolve(conf.dns4DomainName.get(), dnsAddrsNameServers)).valueOr: + return err($error) # Pass error down the stack - if dnsRes.isErr(): - return err($dnsRes.error) # Pass error down the stack - - extIp = some(parseIpAddress(dnsRes.get())) + extIp = some(parseIpAddress(dns)) except CatchableError: return err("Could not update extIp to resolved DNS IP: " & getCurrentExceptionMsg()) diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index a5eb4f2ca..488d07c06 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -47,11 +47,10 @@ proc setupPeerStorage(): Result[Option[WakuPeerStorage], string] = ?peer_store_sqlite_migrations.migrate(db) - let res = WakuPeerStorage.new(db) - if res.isErr(): - return err("failed to init peer store" & res.error) + let res = WakuPeerStorage.new(db).valueOr: + return err("failed to init peer store" & error) - ok(some(res.value)) + return ok(some(res)) ## Init waku node instance @@ -167,16 +166,17 @@ proc setupProtocols( if conf.storeServiceConf.isSome(): let storeServiceConf = conf.storeServiceConf.get() if storeServiceConf.supportV2: - let archiveDriverRes = await legacy_driver.ArchiveDriver.new( - storeServiceConf.dbUrl, storeServiceConf.dbVacuum, storeServiceConf.dbMigration, - storeServiceConf.maxNumDbConnections, onFatalErrorAction, - ) - if archiveDriverRes.isErr(): - return err("failed to setup legacy archive driver: " & archiveDriverRes.error) + let archiveDriver = ( + await legacy_driver.ArchiveDriver.new( + storeServiceConf.dbUrl, storeServiceConf.dbVacuum, + storeServiceConf.dbMigration, storeServiceConf.maxNumDbConnections, + onFatalErrorAction, + ) + ).valueOr: + return err("failed to setup legacy archive driver: " & error) - let mountArcRes = node.mountLegacyArchive(archiveDriverRes.get()) - if mountArcRes.isErr(): - return err("failed to mount waku legacy archive protocol: " & mountArcRes.error) + node.mountLegacyArchive(archiveDriver).isOkOr: + return err("failed to mount waku legacy archive protocol: " & error) ## For now we always mount the future archive driver but if the legacy one is mounted, ## then the legacy will be in charge of performing the archiving. @@ -189,11 +189,8 @@ proc setupProtocols( ## So for now, we need to make sure that when legacy store is enabled and we use sqlite ## that we migrate our db according to legacy store's schema to have the extra field - let engineRes = dburl.getDbEngine(storeServiceConf.dbUrl) - if engineRes.isErr(): - return err("error getting db engine in setupProtocols: " & engineRes.error) - - let engine = engineRes.get() + let engine = dburl.getDbEngine(storeServiceConf.dbUrl).valueOr: + return err("error getting db engine in setupProtocols: " & error) let migrate = if engine == "sqlite" and storeServiceConf.supportV2: @@ -201,20 +198,19 @@ proc setupProtocols( else: storeServiceConf.dbMigration - let archiveDriverRes = await driver.ArchiveDriver.new( - storeServiceConf.dbUrl, storeServiceConf.dbVacuum, migrate, - storeServiceConf.maxNumDbConnections, onFatalErrorAction, - ) - if archiveDriverRes.isErr(): - return err("failed to setup archive driver: " & archiveDriverRes.error) + let archiveDriver = ( + await driver.ArchiveDriver.new( + storeServiceConf.dbUrl, storeServiceConf.dbVacuum, migrate, + storeServiceConf.maxNumDbConnections, onFatalErrorAction, + ) + ).valueOr: + return err("failed to setup archive driver: " & error) - let retPolicyRes = policy.RetentionPolicy.new(storeServiceConf.retentionPolicy) - if retPolicyRes.isErr(): - return err("failed to create retention policy: " & retPolicyRes.error) + let retPolicy = policy.RetentionPolicy.new(storeServiceConf.retentionPolicy).valueOr: + return err("failed to create retention policy: " & error) - let mountArcRes = node.mountArchive(archiveDriverRes.get(), retPolicyRes.get()) - if mountArcRes.isErr(): - return err("failed to mount waku archive protocol: " & mountArcRes.error) + node.mountArchive(archiveDriver, retPolicy).isOkOr: + return err("failed to mount waku archive protocol: " & error) if storeServiceConf.supportV2: # Store legacy setup diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 15d73d64d..0adebd44e 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -205,13 +205,11 @@ proc new*( if wakuConf.remoteStoreNode.isNone(): return err("A storenode should be set when reliability mode is on") - let deliveryMonitorRes = DeliveryMonitor.new( + let deliveryMonitor = DeliveryMonitor.new( node.wakuStoreClient, node.wakuRelay, node.wakuLightpushClient, node.wakuFilterClient, - ) - if deliveryMonitorRes.isErr(): - return err("could not create delivery monitor: " & $deliveryMonitorRes.error) - deliveryMonitor = deliveryMonitorRes.get() + ).valueOr: + return err("could not create delivery monitor: " & $error) var waku = Waku( version: git_version, @@ -328,16 +326,14 @@ proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} = await sleepAsync(30.seconds) if waku.conf.dnsDiscoveryConf.isSome(): let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get() - let dynamicBootstrapNodesRes = await waku_dnsdisc.retrieveDynamicBootstrapNodes( - dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers - ) - if dynamicBootstrapNodesRes.isErr(): - error "Retrieving dynamic bootstrap nodes failed", - error = dynamicBootstrapNodesRes.error + waku[].dynamicBootstrapNodes = ( + await waku_dnsdisc.retrieveDynamicBootstrapNodes( + dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers + ) + ).valueOr: + error "Retrieving dynamic bootstrap nodes failed", error = error continue - waku[].dynamicBootstrapNodes = dynamicBootstrapNodesRes.get() - if not waku[].wakuDiscv5.isNil(): let dynamicBootstrapEnrs = waku[].dynamicBootstrapNodes .filterIt(it.hasUdpPort()) diff --git a/waku/incentivization/eligibility_manager.nim b/waku/incentivization/eligibility_manager.nim index b10b293e1..29443536a 100644 --- a/waku/incentivization/eligibility_manager.nim +++ b/waku/incentivization/eligibility_manager.nim @@ -42,10 +42,9 @@ proc getTxAndTxReceipt( let receiptFuture = eligibilityManager.getMinedTransactionReceipt(txHash) await allFutures(txFuture, receiptFuture) let tx = txFuture.read() - let txReceipt = receiptFuture.read() - if txReceipt.isErr(): - return err("Cannot get tx receipt: " & txReceipt.error) - return ok((tx, txReceipt.get())) + let txReceipt = receiptFuture.read().valueOr: + return err("Cannot get tx receipt: " & error) + return ok((tx, txReceipt)) proc isEligibleTxId*( eligibilityManager: EligibilityManager, diff --git a/waku/node/api/filter.nim b/waku/node/api/filter.nim index 242640a44..948035f14 100644 --- a/waku/node/api/filter.nim +++ b/waku/node/api/filter.nim @@ -108,13 +108,10 @@ proc filterSubscribe*( error = "waku filter client is not set up" return err(FilterSubscribeError.serviceUnavailable()) - let remotePeerRes = parsePeerInfo(peer) - if remotePeerRes.isErr(): - error "Couldn't parse the peer info properly", error = remotePeerRes.error + let remotePeer = parsePeerInfo(peer).valueOr: + error "Couldn't parse the peer info properly", error = error return err(FilterSubscribeError.serviceUnavailable("No peers available")) - let remotePeer = remotePeerRes.value - if pubsubTopic.isSome(): info "registering filter subscription to content", pubsubTopic = pubsubTopic.get(), @@ -143,15 +140,11 @@ proc filterSubscribe*( else: # No pubsub topic, autosharding is used to deduce it # but content topics must be well-formed for this - let topicMapRes = - node.wakuAutoSharding.get().getShardsFromContentTopics(contentTopics) - - let topicMap = - if topicMapRes.isErr(): - error "can't get shard", error = topicMapRes.error + let topicMap = node.wakuAutoSharding + .get() + .getShardsFromContentTopics(contentTopics).valueOr: + error "can't get shard", error = error return err(FilterSubscribeError.badResponse("can't get shard")) - else: - topicMapRes.get() var futures = collect(newSeq): for shard, topics in topicMap.pairs: @@ -195,13 +188,10 @@ proc filterUnsubscribe*( ): Future[FilterSubscribeResult] {.async: (raises: []).} = ## Unsubscribe from a content filter V2". - let remotePeerRes = parsePeerInfo(peer) - if remotePeerRes.isErr(): - error "couldn't parse remotePeerInfo", error = remotePeerRes.error + let remotePeer = parsePeerInfo(peer).valueOr: + error "couldn't parse remotePeerInfo", error = error return err(FilterSubscribeError.serviceUnavailable("No peers available")) - let remotePeer = remotePeerRes.value - if pubsubTopic.isSome(): info "deregistering filter subscription to content", pubsubTopic = pubsubTopic.get(), @@ -226,15 +216,11 @@ proc filterUnsubscribe*( error "Failed filter un-subscription, pubsub topic must be specified with static sharding" waku_node_errors.inc(labelValues = ["unsubscribe_filter_failure"]) else: # pubsubTopic.isNone - let topicMapRes = - node.wakuAutoSharding.get().getShardsFromContentTopics(contentTopics) - - let topicMap = - if topicMapRes.isErr(): - error "can't get shard", error = topicMapRes.error + let topicMap = node.wakuAutoSharding + .get() + .getShardsFromContentTopics(contentTopics).valueOr: + error "can't get shard", error = error return err(FilterSubscribeError.badResponse("can't get shard")) - else: - topicMapRes.get() var futures = collect(newSeq): for shard, topics in topicMap.pairs: @@ -275,13 +261,10 @@ proc filterUnsubscribeAll*( ): Future[FilterSubscribeResult] {.async: (raises: []).} = ## Unsubscribe from a content filter V2". - let remotePeerRes = parsePeerInfo(peer) - if remotePeerRes.isErr(): - error "couldn't parse remotePeerInfo", error = remotePeerRes.error + let remotePeer = parsePeerInfo(peer).valueOr: + error "couldn't parse remotePeerInfo", error = error return err(FilterSubscribeError.serviceUnavailable("No peers available")) - let remotePeer = remotePeerRes.value - info "deregistering all filter subscription to content", peer = remotePeer.peerId let unsubRes = await node.wakuFilterClient.unsubscribeAll(remotePeer) diff --git a/waku/node/api/lightpush.nim b/waku/node/api/lightpush.nim index 550c5bd9f..f42cb146e 100644 --- a/waku/node/api/lightpush.nim +++ b/waku/node/api/lightpush.nim @@ -114,14 +114,8 @@ proc legacyLightpushPublish*( if node.wakuAutoSharding.isNone(): return err("Pubsub topic must be specified when static sharding is enabled") - let topicMapRes = - node.wakuAutoSharding.get().getShardsFromContentTopics(message.contentTopic) - let topicMap = - if topicMapRes.isErr(): - return err(topicMapRes.error) - else: - topicMapRes.get() + ?node.wakuAutoSharding.get().getShardsFromContentTopics(message.contentTopic) for pubsub, _ in topicMap.pairs: # There's only one pair anyway return await internalPublish(node, $pubsub, message, peer) diff --git a/waku/node/api/peer_exchange.nim b/waku/node/api/peer_exchange.nim index d2e0f5575..a4bec727b 100644 --- a/waku/node/api/peer_exchange.nim +++ b/waku/node/api/peer_exchange.nim @@ -111,10 +111,9 @@ proc setPeerExchangePeer*( info "Set peer-exchange peer", peer = peer - let remotePeerRes = parsePeerInfo(peer) - if remotePeerRes.isErr(): - error "could not parse peer info", error = remotePeerRes.error + let remotePeer = parsePeerInfo(peer).valueOr: + error "could not parse peer info", error = error return - node.peerManager.addPeer(remotePeerRes.value, PeerExchange) + node.peerManager.addPeer(remotePeer, PeerExchange) waku_px_peers.inc() diff --git a/waku/node/api/relay.nim b/waku/node/api/relay.nim index 1e38c5535..827cc1e5f 100644 --- a/waku/node/api/relay.nim +++ b/waku/node/api/relay.nim @@ -240,11 +240,8 @@ proc mountRlnRelay*( CatchableError, "WakuRelay protocol is not mounted, cannot mount WakuRlnRelay" ) - let rlnRelayRes = await WakuRlnRelay.new(rlnConf, registrationHandler) - if rlnRelayRes.isErr(): - raise - newException(CatchableError, "failed to mount WakuRlnRelay: " & rlnRelayRes.error) - let rlnRelay = rlnRelayRes.get() + let rlnRelay = (await WakuRlnRelay.new(rlnConf, registrationHandler)).valueOr: + raise newException(CatchableError, "failed to mount WakuRlnRelay: " & error) if (rlnConf.userMessageLimit > rlnRelay.groupManager.rlnRelayMaxMessageLimit): error "rln-relay-user-message-limit can't exceed the MAX_MESSAGE_LIMIT in the rln contract" let validator = generateRlnValidator(rlnRelay, spamHandler) diff --git a/waku/node/api/store.nim b/waku/node/api/store.nim index ddac5fbfd..7edae7966 100644 --- a/waku/node/api/store.nim +++ b/waku/node/api/store.nim @@ -87,30 +87,27 @@ proc toArchiveQuery( proc toHistoryResult*( res: waku_archive_legacy.ArchiveResult ): legacy_store_common.HistoryResult = - if res.isErr(): - let error = res.error - case res.error.kind + let response = res.valueOr: + case error.kind of waku_archive_legacy.ArchiveErrorKind.DRIVER_ERROR, waku_archive_legacy.ArchiveErrorKind.INVALID_QUERY: - err(HistoryError(kind: HistoryErrorKind.BAD_REQUEST, cause: res.error.cause)) + return err(HistoryError(kind: HistoryErrorKind.BAD_REQUEST, cause: error.cause)) else: - err(HistoryError(kind: HistoryErrorKind.UNKNOWN)) - else: - let response = res.get() - ok( - HistoryResponse( - messages: response.messages, - cursor: response.cursor.map( - proc(cursor: waku_archive_legacy.ArchiveCursor): HistoryCursor = - HistoryCursor( - pubsubTopic: cursor.pubsubTopic, - senderTime: cursor.senderTime, - storeTime: cursor.storeTime, - digest: cursor.digest, - ) - ), - ) + return err(HistoryError(kind: HistoryErrorKind.UNKNOWN)) + return ok( + HistoryResponse( + messages: response.messages, + cursor: response.cursor.map( + proc(cursor: waku_archive_legacy.ArchiveCursor): HistoryCursor = + HistoryCursor( + pubsubTopic: cursor.pubsubTopic, + senderTime: cursor.senderTime, + storeTime: cursor.storeTime, + digest: cursor.digest, + ) + ), ) + ) proc mountLegacyStore*( node: WakuNode, rateLimit: RateLimitSetting = DefaultGlobalNonRelayRateLimit @@ -126,8 +123,7 @@ proc mountLegacyStore*( request: HistoryQuery ): Future[legacy_store_common.HistoryResult] {.async.} = if request.cursor.isSome(): - request.cursor.get().checkHistCursor().isOkOr: - return err(error) + ?request.cursor.get().checkHistCursor() let request = request.toArchiveQuery() let response = await node.wakuLegacyArchive.findMessagesV2(request) @@ -160,11 +156,8 @@ proc query*( if node.wakuLegacyStoreClient.isNil(): return err("waku legacy store client is nil") - let queryRes = await node.wakuLegacyStoreClient.query(query, peer) - if queryRes.isErr(): - return err("legacy store client query error: " & $queryRes.error) - - let response = queryRes.get() + let response = (await node.wakuLegacyStoreClient.query(query, peer)).valueOr: + return err("legacy store client query error: " & $error) return ok(response) @@ -201,9 +194,8 @@ when defined(waku_exp_store_resume): if node.wakuLegacyStoreClient.isNil(): return - let retrievedMessages = await node.wakuLegacyStoreClient.resume(peerList) - if retrievedMessages.isErr(): - error "failed to resume store", error = retrievedMessages.error + let retrievedMessages = (await node.wakuLegacyStoreClient.resume(peerList)).valueOr: + error "failed to resume store", error = error return info "the number of retrieved messages since the last online time: ", diff --git a/waku/node/delivery_monitor/not_delivered_storage/migrations.nim b/waku/node/delivery_monitor/not_delivered_storage/migrations.nim index 6f0b3265d..8175aea62 100644 --- a/waku/node/delivery_monitor/not_delivered_storage/migrations.nim +++ b/waku/node/delivery_monitor/not_delivered_storage/migrations.nim @@ -17,10 +17,8 @@ const PeerStoreMigrationPath: string = projectRoot / "migrations" / "sent_msgs" proc migrate*(db: SqliteDatabase): DatabaseResult[void] = info "starting peer store's sqlite database migration for sent messages" - let migrationRes = - migrate(db, TargetSchemaVersion, migrationsScriptsDir = PeerStoreMigrationPath) - if migrationRes.isErr(): - return err("failed to execute migration scripts: " & migrationRes.error) + migrate(db, TargetSchemaVersion, migrationsScriptsDir = PeerStoreMigrationPath).isOkOr: + return err("failed to execute migration scripts: " & error) info "finished peer store's sqlite database migration for sent messages" ok() diff --git a/waku/node/peer_manager/peer_store/migrations.nim b/waku/node/peer_manager/peer_store/migrations.nim index 61b416ed8..97961d25a 100644 --- a/waku/node/peer_manager/peer_store/migrations.nim +++ b/waku/node/peer_manager/peer_store/migrations.nim @@ -18,16 +18,14 @@ proc migrate*(db: SqliteDatabase, targetVersion = SchemaVersion): DatabaseResult ## it runs migration scripts if the `user_version` is outdated. The `migrationScriptsDir` path ## points to the directory holding the migrations scripts once the db is updated, it sets the ## `user_version` to the `tragetVersion`. - ## + ## ## If not `targetVersion` is provided, it defaults to `SchemaVersion`. ## ## NOTE: Down migration it is not currently supported info "starting peer store's sqlite database migration" - let migrationRes = - migrate(db, targetVersion, migrationsScriptsDir = PeerStoreMigrationPath) - if migrationRes.isErr(): - return err("failed to execute migration scripts: " & migrationRes.error) + migrate(db, targetVersion, migrationsScriptsDir = PeerStoreMigrationPath).isOkOr: + return err("failed to execute migration scripts: " & error) info "finished peer store's sqlite database migration" ok() diff --git a/waku/node/peer_manager/peer_store/waku_peer_storage.nim b/waku/node/peer_manager/peer_store/waku_peer_storage.nim index 876e8e258..dc1452618 100644 --- a/waku/node/peer_manager/peer_store/waku_peer_storage.nim +++ b/waku/node/peer_manager/peer_store/waku_peer_storage.nim @@ -67,7 +67,7 @@ proc encode*(remotePeerInfo: RemotePeerInfo): PeerStorageResult[ProtoBuffer] = let catchRes = catch: pb.write(4, remotePeerInfo.publicKey) - if catchRes.isErr(): + catchRes.isOkOr: return err("Enncoding public key failed: " & catchRes.error.msg) pb.write(5, uint32(ord(remotePeerInfo.connectedness))) @@ -154,14 +154,11 @@ method getAll*( let catchRes = catch: db.database.query("SELECT peerId, storedInfo FROM Peer", peer) - let queryRes = - if catchRes.isErr(): - return err("failed to extract peer from query result: " & catchRes.error.msg) - else: - catchRes.get() + let queryRes = catchRes.valueOr: + return err("failed to extract peer from query result: " & catchRes.error.msg) - if queryRes.isErr(): - return err("peer storage query failed: " & queryRes.error) + queryRes.isOkOr: + return err("peer storage query failed: " & error) return ok() diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index f34a47a01..114775951 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -225,8 +225,8 @@ proc mountMetadata*( let catchRes = catch: node.switch.mount(node.wakuMetadata, protocolMatcher(WakuMetadataCodec)) - if catchRes.isErr(): - return err(catchRes.error.msg) + catchRes.isOkOr: + return err(error.msg) return ok() @@ -266,8 +266,8 @@ proc mountMix*( node.wakuMix.registerDestReadBehavior(WakuLightPushCodec, readLp(int(-1))) let catchRes = catch: node.switch.mount(node.wakuMix) - if catchRes.isErr(): - return err(catchRes.error.msg) + catchRes.isOkOr: + return err(error.msg) return ok() ## Waku Sync @@ -300,8 +300,8 @@ proc mountStoreSync*( node.switch.mount( node.wakuStoreReconciliation, protocolMatcher(WakuReconciliationCodec) ) - if reconMountRes.isErr(): - return err(reconMountRes.error.msg) + reconMountRes.isOkOr: + return err(error.msg) let transfer = SyncTransfer.new( node.peerManager, node.wakuArchive, idsChannel, wantsChannel, needsChannel @@ -311,8 +311,8 @@ proc mountStoreSync*( let transMountRes = catch: node.switch.mount(node.wakuStoreTransfer, protocolMatcher(WakuTransferCodec)) - if transMountRes.isErr(): - return err(transMountRes.error.msg) + transMountRes.isOkOr: + return err(error.msg) return ok() diff --git a/waku/waku_api/rest/admin/handlers.nim b/waku/waku_api/rest/admin/handlers.nim index 172172376..1efbf7d04 100644 --- a/waku/waku_api/rest/admin/handlers.nim +++ b/waku/waku_api/rest/admin/handlers.nim @@ -426,14 +426,13 @@ proc installAdminV1GetFilterSubsHandler(router: var RestRouter, node: WakuNode) FilterSubscription(peerId: $peerId, filterCriteria: filterCriteria) ) - let resp = RestApiResponse.jsonResponse(subscriptions, status = Http200) - if resp.isErr(): - error "An error ocurred while building the json respose: ", error = resp.error + let resp = RestApiResponse.jsonResponse(subscriptions, status = Http200).valueOr: + error "An error ocurred while building the json respose", error = error return RestApiResponse.internalServerError( - fmt("An error ocurred while building the json respose: {resp.error}") + fmt("An error ocurred while building the json respose: {error}") ) - return resp.get() + return resp proc installAdminV1PostLogLevelHandler(router: var RestRouter, node: WakuNode) = router.api(MethodPost, ROUTE_ADMIN_V1_POST_LOG_LEVEL) do( diff --git a/waku/waku_api/rest/debug/handlers.nim b/waku/waku_api/rest/debug/handlers.nim index eb1529759..43b6fbbf1 100644 --- a/waku/waku_api/rest/debug/handlers.nim +++ b/waku/waku_api/rest/debug/handlers.nim @@ -15,12 +15,11 @@ const ROUTE_DEBUG_INFOV1 = "/debug/v1/info" proc installDebugInfoV1Handler(router: var RestRouter, node: WakuNode) = let getInfo = proc(): RestApiResponse = let info = node.info().toDebugWakuInfo() - let resp = RestApiResponse.jsonResponse(info, status = Http200) - if resp.isErr(): - info "An error occurred while building the json respose", error = resp.error + let resp = RestApiResponse.jsonResponse(info, status = Http200).valueOr: + info "An error occurred while building the json respose", error = error return RestApiResponse.internalServerError() - return resp.get() + return resp # /debug route is deprecated, will be removed router.api(MethodGet, ROUTE_DEBUG_INFOV1) do() -> RestApiResponse: diff --git a/waku/waku_api/rest/filter/handlers.nim b/waku/waku_api/rest/filter/handlers.nim index f3f6e4837..61d7eb96f 100644 --- a/waku/waku_api/rest/filter/handlers.nim +++ b/waku/waku_api/rest/filter/handlers.nim @@ -49,15 +49,12 @@ func decodeRequestBody[T]( let reqBodyData = contentBody.get().data - let requestResult = decodeFromJsonBytes(T, reqBodyData) - if requestResult.isErr(): + let requestResult = decodeFromJsonBytes(T, reqBodyData).valueOr: return err( - RestApiResponse.badRequest( - "Invalid content body, could not decode. " & $requestResult.error - ) + RestApiResponse.badRequest("Invalid content body, could not decode. " & $error) ) - return ok(requestResult.get()) + return ok(requestResult) proc getStatusDesc( protocolClientRes: filter_protocol_type.FilterSubscribeResult @@ -129,16 +126,15 @@ proc makeRestResponse( httpStatus = convertErrorKindToHttpStatus(protocolClientRes.error().kind) # TODO: convert status codes! - let resp = - RestApiResponse.jsonResponse(filterSubscriptionResponse, status = httpStatus) - - if resp.isErr(): - error "An error ocurred while building the json respose: ", error = resp.error + let resp = RestApiResponse.jsonResponse( + filterSubscriptionResponse, status = httpStatus + ).valueOr: + error "An error ocurred while building the json respose: ", error = error return RestApiResponse.internalServerError( - fmt("An error ocurred while building the json respose: {resp.error}") + fmt("An error ocurred while building the json respose: {error}") ) - return resp.get() + return resp proc makeRestResponse( requestId: string, protocolClientRes: filter_protocol_type.FilterSubscribeError @@ -149,16 +145,15 @@ proc makeRestResponse( let httpStatus = convertErrorKindToHttpStatus(protocolClientRes.kind) # TODO: convert status codes! - let resp = - RestApiResponse.jsonResponse(filterSubscriptionResponse, status = httpStatus) - - if resp.isErr(): - error "An error ocurred while building the json respose: ", error = resp.error + let resp = RestApiResponse.jsonResponse( + filterSubscriptionResponse, status = httpStatus + ).valueOr: + error "An error ocurred while building the json respose: ", error = error return RestApiResponse.internalServerError( - fmt("An error ocurred while building the json respose: {resp.error}") + fmt("An error ocurred while building the json respose: {error}") ) - return resp.get() + return resp const NoPeerNoDiscoError = FilterSubscribeError.serviceUnavailable( "No suitable service peer & no discovery method" @@ -175,18 +170,14 @@ proc filterPostPutSubscriptionRequestHandler( ): Future[RestApiResponse] {.async.} = ## handles any filter subscription requests, adds or modifies. - let decodedBody = decodeRequestBody[FilterSubscribeRequest](contentBody) - - if decodedBody.isErr(): + let req: FilterSubscribeRequest = decodeRequestBody[FilterSubscribeRequest]( + contentBody + ).valueOr: return makeRestResponse( "unknown", - FilterSubscribeError.badRequest( - fmt("Failed to decode request: {decodedBody.error}") - ), + FilterSubscribeError.badRequest(fmt("Failed to decode request: {error}")), ) - let req: FilterSubscribeRequest = decodedBody.value() - let peer = node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: let handler = discHandler.valueOr: return makeRestResponse(req.requestId, NoPeerNoDiscoError) @@ -256,18 +247,14 @@ proc installFilterDeleteSubscriptionsHandler( ## Subscribes a node to a list of contentTopics of a PubSub topic info "delete", ROUTE_FILTER_SUBSCRIPTIONS, contentBody - let decodedBody = decodeRequestBody[FilterUnsubscribeRequest](contentBody) - - if decodedBody.isErr(): + let req: FilterUnsubscribeRequest = decodeRequestBody[FilterUnsubscribeRequest]( + contentBody + ).valueOr: return makeRestResponse( "unknown", - FilterSubscribeError.badRequest( - fmt("Failed to decode request: {decodedBody.error}") - ), + FilterSubscribeError.badRequest(fmt("Failed to decode request: {error}")), ) - let req: FilterUnsubscribeRequest = decodedBody.value() - let peer = node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: let handler = discHandler.valueOr: return makeRestResponse(req.requestId, NoPeerNoDiscoError) @@ -308,18 +295,14 @@ proc installFilterDeleteAllSubscriptionsHandler( ## Subscribes a node to a list of contentTopics of a PubSub topic info "delete", ROUTE_FILTER_ALL_SUBSCRIPTIONS, contentBody - let decodedBody = decodeRequestBody[FilterUnsubscribeAllRequest](contentBody) - - if decodedBody.isErr(): + let req: FilterUnsubscribeAllRequest = decodeRequestBody[ + FilterUnsubscribeAllRequest + ](contentBody).valueOr: return makeRestResponse( "unknown", - FilterSubscribeError.badRequest( - fmt("Failed to decode request: {decodedBody.error}") - ), + FilterSubscribeError.badRequest(fmt("Failed to decode request: {error}")), ) - let req: FilterUnsubscribeAllRequest = decodedBody.value() - let peer = node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: let handler = discHandler.valueOr: return makeRestResponse(req.requestId, NoPeerNoDiscoError) @@ -399,24 +382,20 @@ proc installFilterGetMessagesHandler( ## TODO: ability to specify a return message limit, maybe use cursor to control paging response. info "get", ROUTE_FILTER_MESSAGES, contentTopic = contentTopic - if contentTopic.isErr(): + let contentTopic = contentTopic.valueOr: return RestApiResponse.badRequest("Missing contentTopic") - let contentTopic = contentTopic.get() - - let msgRes = cache.getAutoMessages(contentTopic, clear = true) - if msgRes.isErr(): + let msg = cache.getAutoMessages(contentTopic, clear = true).valueOr: return RestApiResponse.badRequest("Not subscribed to topic: " & contentTopic) - let data = FilterGetMessagesResponse(msgRes.get().map(toFilterWakuMessage)) - let resp = RestApiResponse.jsonResponse(data, status = Http200) - if resp.isErr(): - error "An error ocurred while building the json respose: ", error = resp.error + let data = FilterGetMessagesResponse(msg.map(toFilterWakuMessage)) + let resp = RestApiResponse.jsonResponse(data, status = Http200).valueOr: + error "An error ocurred while building the json respose: ", error = error return RestApiResponse.internalServerError( "An error ocurred while building the json respose" ) - return resp.get() + return resp proc installFilterRestApiHandlers*( router: var RestRouter, diff --git a/waku/waku_api/rest/legacy_lightpush/handlers.nim b/waku/waku_api/rest/legacy_lightpush/handlers.nim index b129f3ffc..7a3c5b1ed 100644 --- a/waku/waku_api/rest/legacy_lightpush/handlers.nim +++ b/waku/waku_api/rest/legacy_lightpush/handlers.nim @@ -50,12 +50,8 @@ proc installLightPushRequestHandler*( ## Send a request to push a waku message info "post", ROUTE_LIGHTPUSH, contentBody - let decodedBody = decodeRequestBody[PushRequest](contentBody) - - if decodedBody.isErr(): - return decodedBody.error() - - let req: PushRequest = decodedBody.value() + let req: PushRequest = decodeRequestBody[PushRequest](contentBody).valueOr: + return error let msg = req.message.toWakuMessage().valueOr: return RestApiResponse.badRequest("Invalid message: " & $error) @@ -80,12 +76,12 @@ proc installLightPushRequestHandler*( error "Failed to request a message push due to timeout!" return RestApiResponse.serviceUnavailable("Push request timed out") - if subFut.value().isErr(): - if subFut.value().error == TooManyRequestsMessage: + subFut.value().isOkOr: + if error == TooManyRequestsMessage: return RestApiResponse.tooManyRequests("Request rate limmit reached") return RestApiResponse.serviceUnavailable( - fmt("Failed to request a message push: {subFut.value().error}") + fmt("Failed to request a message push: {error}") ) return RestApiResponse.ok() diff --git a/waku/waku_api/rest/legacy_store/handlers.nim b/waku/waku_api/rest/legacy_store/handlers.nim index 96e1da780..4ed58f799 100644 --- a/waku/waku_api/rest/legacy_store/handlers.nim +++ b/waku/waku_api/rest/legacy_store/handlers.nim @@ -1,6 +1,7 @@ {.push raises: [].} -import std/strformat, results, chronicles, uri, json_serialization, presto/route +import + std/[strformat, sugar], results, chronicles, uri, json_serialization, presto/route import ../../../waku_core, ../../../waku_store_legacy/common, @@ -34,20 +35,17 @@ proc performHistoryQuery( error msg return RestApiResponse.internalServerError(msg) - let res = queryFut.read() - if res.isErr(): - const msg = "Error occurred in queryFut.read()" - error msg, error = res.error - return RestApiResponse.internalServerError(fmt("{msg} [{res.error}]")) + let storeResp = queryFut.read().map(res => res.toStoreResponseRest()).valueOr: + const msg = "Error occurred in queryFut.read()" + error msg, error = error + return RestApiResponse.internalServerError(fmt("{msg} [{error}]")) - let storeResp = res.value.toStoreResponseRest() - let resp = RestApiResponse.jsonResponse(storeResp, status = Http200) - if resp.isErr(): + let resp = RestApiResponse.jsonResponse(storeResp, status = Http200).valueOr: const msg = "Error building the json respose" - error msg, error = resp.error - return RestApiResponse.internalServerError(fmt("{msg} [{resp.error}]")) + error msg, error = error + return RestApiResponse.internalServerError(fmt("{msg} [{error}]")) - return resp.get() + return resp # Converts a string time representation into an Option[Timestamp]. # Only positive time is considered a valid Timestamp in the request @@ -70,16 +68,13 @@ proc parseCursor( digest: Option[string], ): Result[Option[HistoryCursor], string] = # Parse sender time - let parsedSenderTime = parseTime(senderTime).valueOr: - return err(error) + let parsedSenderTime = ?parseTime(senderTime) # Parse store time - let parsedStoreTime = parseTime(storeTime).valueOr: - return err(error) + let parsedStoreTime = ?parseTime(storeTime) # Parse message digest - let parsedMsgDigest = parseMsgDigest(digest).valueOr: - return err(error) + let parsedMsgDigest = ?parseMsgDigest(digest) # Parse cursor information if parsedPubsubTopic.isSome() and parsedSenderTime.isSome() and diff --git a/waku/waku_api/rest/legacy_store/types.nim b/waku/waku_api/rest/legacy_store/types.nim index 53a96bd69..0c547c7cc 100644 --- a/waku/waku_api/rest/legacy_store/types.nim +++ b/waku/waku_api/rest/legacy_store/types.nim @@ -60,8 +60,7 @@ proc parseMsgDigest*( return ok(none(waku_store_common.MessageDigest)) let decodedUrl = decodeUrl(input.get()) - let base64DecodedArr = base64.decode(Base64String(decodedUrl)).valueOr: - return err(error) + let base64DecodedArr = ?base64.decode(Base64String(decodedUrl)) var messageDigest = waku_store_common.MessageDigest() diff --git a/waku/waku_api/rest/origin_handler.nim b/waku/waku_api/rest/origin_handler.nim index 2317c945f..9752bfb56 100644 --- a/waku/waku_api/rest/origin_handler.nim +++ b/waku/waku_api/rest/origin_handler.nim @@ -74,13 +74,12 @@ proc originMiddlewareProc( reqfence: RequestFence, nextHandler: HttpProcessCallback2, ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = - if reqfence.isErr(): + let request = reqfence.valueOr: # Ignore request errors that detected before our middleware. # Let final handler deal with it. return await nextHandler(reqfence) let self = OriginHandlerMiddlewareRef(middleware) - let request = reqfence.get() var reqHeaders = request.headers var response = request.getResponse() diff --git a/waku/waku_api/rest/relay/handlers.nim b/waku/waku_api/rest/relay/handlers.nim index f59c445a8..4a1415361 100644 --- a/waku/waku_api/rest/relay/handlers.nim +++ b/waku/waku_api/rest/relay/handlers.nim @@ -126,29 +126,25 @@ proc installRelayApiHandlers*( # ## TODO: ability to specify a return message limit # info "get_waku_v2_relay_v1_messages", topic=topic - if pubsubTopic.isErr(): + let pubSubTopic = pubsubTopic.valueOr: return RestApiResponse.badRequest() - let pubSubTopic = pubsubTopic.get() - let messages = cache.getMessages(pubSubTopic, clear = true) - if messages.isErr(): + let messages = cache.getMessages(pubSubTopic, clear = true).valueOr: info "Not subscribed to topic", topic = pubSubTopic return RestApiResponse.notFound() - let data = RelayGetMessagesResponse(messages.get().map(toRelayWakuMessage)) - let resp = RestApiResponse.jsonResponse(data, status = Http200) - if resp.isErr(): - info "An error ocurred while building the json respose", error = resp.error + let data = RelayGetMessagesResponse(messages.map(toRelayWakuMessage)) + let resp = RestApiResponse.jsonResponse(data, status = Http200).valueOr: + info "An error ocurred while building the json respose", error = error return RestApiResponse.internalServerError() - return resp.get() + return resp router.api(MethodPost, ROUTE_RELAY_MESSAGESV1) do( pubsubTopic: string, contentBody: Option[ContentBody] ) -> RestApiResponse: - if pubsubTopic.isErr(): + let pubSubTopic = pubsubTopic.valueOr: return RestApiResponse.badRequest() - let pubSubTopic = pubsubTopic.get() # ensure the node is subscribed to the topic. otherwise it risks publishing # to a topic with no connected peers @@ -318,9 +314,7 @@ proc installRelayApiHandlers*( if not await publishFut.withTimeout(futTimeout): return RestApiResponse.internalServerError("Failed to publish: timedout") - var res = publishFut.read() - - if res.isErr(): - return RestApiResponse.badRequest("Failed to publish. " & res.error) + publishFut.read().isOkOr: + return RestApiResponse.badRequest("Failed to publish: " & error) return RestApiResponse.ok() diff --git a/waku/waku_api/rest/rest_serdes.nim b/waku/waku_api/rest/rest_serdes.nim index 1b6d5a98d..8dcb7c8f1 100644 --- a/waku/waku_api/rest/rest_serdes.nim +++ b/waku/waku_api/rest/rest_serdes.nim @@ -45,15 +45,12 @@ func decodeRequestBody*[T]( let reqBodyData = contentBody.get().data - let requestResult = decodeFromJsonBytes(T, reqBodyData) - if requestResult.isErr(): + let requestResult = decodeFromJsonBytes(T, reqBodyData).valueOr: return err( - RestApiResponse.badRequest( - "Invalid content body, could not decode. " & $requestResult.error - ) + RestApiResponse.badRequest("Invalid content body, could not decode: " & $error) ) - return ok(requestResult.get()) + return ok(requestResult) proc decodeBytes*( t: typedesc[string], value: openarray[byte], contentType: Opt[ContentTypeData] diff --git a/waku/waku_api/rest/serdes.nim b/waku/waku_api/rest/serdes.nim index 147184602..ab7ed8d25 100644 --- a/waku/waku_api/rest/serdes.nim +++ b/waku/waku_api/rest/serdes.nim @@ -117,8 +117,4 @@ proc encodeString*(value: SomeUnsignedInt): SerdesResult[string] = ok(Base10.toString(value)) proc decodeString*(T: typedesc[SomeUnsignedInt], value: string): SerdesResult[T] = - let v = Base10.decode(T, value) - if v.isErr(): - return err(v.error()) - else: - return ok(v.get()) + return Base10.decode(T, value) diff --git a/waku/waku_api/rest/server.nim b/waku/waku_api/rest/server.nim index e5db5ee5e..1b61425c8 100644 --- a/waku/waku_api/rest/server.nim +++ b/waku/waku_api/rest/server.nim @@ -91,23 +91,23 @@ proc new*( ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = discard - server.httpServer = HttpServerRef.new( - address, - defaultProcessCallback, - serverFlags, - socketFlags, - serverUri, - serverIdent, - maxConnections, - bufferSize, - backlogSize, - httpHeadersTimeout, - maxHeadersSize, - maxRequestBodySize, - dualstack = dualstack, - middlewares = middlewares, - ).valueOr: - return err(error) + server.httpServer = + ?HttpServerRef.new( + address, + defaultProcessCallback, + serverFlags, + socketFlags, + serverUri, + serverIdent, + maxConnections, + bufferSize, + backlogSize, + httpHeadersTimeout, + maxHeadersSize, + maxRequestBodySize, + dualstack = dualstack, + middlewares = middlewares, + ) return ok(server) proc getRouter(): RestRouter = diff --git a/waku/waku_api/rest/store/handlers.nim b/waku/waku_api/rest/store/handlers.nim index cf0e96710..79724b9d7 100644 --- a/waku/waku_api/rest/store/handlers.nim +++ b/waku/waku_api/rest/store/handlers.nim @@ -1,6 +1,7 @@ {.push raises: [].} -import std/strformat, results, chronicles, uri, json_serialization, presto/route +import + std/[strformat, sugar], results, chronicles, uri, json_serialization, presto/route import ../../../waku_core, ../../../waku_store/common, @@ -35,14 +36,10 @@ proc performStoreQuery( error msg return RestApiResponse.internalServerError(msg) - let futRes = queryFut.read() - - if futRes.isErr(): - const msg = "Error occurred in queryFut.read()" - error msg, error = futRes.error - return RestApiResponse.internalServerError(fmt("{msg} [{futRes.error}]")) - - let res = futRes.get().toHex() + let res = queryFut.read().map(val => val.toHex()).valueOr: + const msg = "Error occurred in queryFut.read()" + error msg, error = error + return RestApiResponse.internalServerError(fmt("{msg} [{error}]")) if res.statusCode == uint32(ErrorCode.TOO_MANY_REQUESTS): info "Request rate limit reached on peer ", storePeer diff --git a/waku/waku_archive/driver/builder.nim b/waku/waku_archive/driver/builder.nim index cc46afb4c..811b16999 100644 --- a/waku/waku_archive/driver/builder.nim +++ b/waku/waku_archive/driver/builder.nim @@ -32,71 +32,54 @@ proc new*( ## maxNumConn - defines the maximum number of connections to handle simultaneously (Postgres) ## onFatalErrorAction - called if, e.g., the connection with db got lost - let dbUrlValidationRes = dburl.validateDbUrl(url) - if dbUrlValidationRes.isErr(): - return err("DbUrl failure in ArchiveDriver.new: " & dbUrlValidationRes.error) + dburl.validateDbUrl(url).isOkOr: + return err("DbUrl failure in ArchiveDriver.new: " & error) - let engineRes = dburl.getDbEngine(url) - if engineRes.isErr(): - return err("error getting db engine in setupWakuArchiveDriver: " & engineRes.error) - - let engine = engineRes.get() + let engine = dburl.getDbEngine(url).valueOr: + return err("error getting db engine in setupWakuArchiveDriver: " & error) case engine of "sqlite": - let pathRes = dburl.getDbPath(url) - if pathRes.isErr(): - return err("error get path in setupWakuArchiveDriver: " & pathRes.error) + let path = dburl.getDbPath(url).valueOr: + return err("error get path in setupWakuArchiveDriver: " & error) - let dbRes = SqliteDatabase.new(pathRes.get()) - if dbRes.isErr(): - return err("error in setupWakuArchiveDriver: " & dbRes.error) - - let db = dbRes.get() + let db = SqliteDatabase.new(path).valueOr: + return err("error in setupWakuArchiveDriver: " & error) # SQLite vacuum - let sqliteStatsRes = db.gatherSqlitePageStats() - if sqliteStatsRes.isErr(): - return err("error while gathering sqlite stats: " & $sqliteStatsRes.error) + let (pageSize, pageCount, freelistCount) = db.gatherSqlitePageStats().valueOr: + return err("error while gathering sqlite stats: " & $error) - let (pageSize, pageCount, freelistCount) = sqliteStatsRes.get() info "sqlite database page stats", pageSize = pageSize, pages = pageCount, freePages = freelistCount if vacuum and (pageCount > 0 and freelistCount > 0): - let vacuumRes = db.performSqliteVacuum() - if vacuumRes.isErr(): - return err("error in vacuum sqlite: " & $vacuumRes.error) + db.performSqliteVacuum().isOkOr: + return err("error in vacuum sqlite: " & $error) # Database migration if migrate: - let migrateRes = archive_driver_sqlite_migrations.migrate(db) - if migrateRes.isErr(): - return err("error in migrate sqlite: " & $migrateRes.error) + archive_driver_sqlite_migrations.migrate(db).isOkOr: + return err("error in migrate sqlite: " & $error) info "setting up sqlite waku archive driver" - let res = SqliteDriver.new(db) - if res.isErr(): - return err("failed to init sqlite archive driver: " & res.error) + let res = SqliteDriver.new(db).valueOr: + return err("failed to init sqlite archive driver: " & error) - return ok(res.get()) + return ok(res) of "postgres": when defined(postgres): - let res = PostgresDriver.new( + let driver = PostgresDriver.new( dbUrl = url, maxConnections = maxNumConn, onFatalErrorAction = onFatalErrorAction, - ) - if res.isErr(): - return err("failed to init postgres archive driver: " & res.error) - - let driver = res.get() + ).valueOr: + return err("failed to init postgres archive driver: " & error) # Database migration if migrate: - let migrateRes = await archive_postgres_driver_migrations.migrate(driver) - if migrateRes.isErr(): - return err("ArchiveDriver build failed in migration: " & $migrateRes.error) + (await archive_postgres_driver_migrations.migrate(driver)).isOkOr: + return err("ArchiveDriver build failed in migration: " & $error) ## This should be started once we make sure the 'messages' table exists ## Hence, this should be run after the migration is completed. diff --git a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim index d5eba9a5c..842d7cbc2 100644 --- a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim @@ -186,11 +186,11 @@ proc timeCursorCallbackImpl(pqResult: ptr PGresult, timeCursor: var Option[Times let catchable = catch: parseBiggestInt(rawTimestamp) - if catchable.isErr(): - error "could not parse correctly", error = catchable.error.msg + let time = catchable.valueOr: + error "could not parse correctly", error = error.msg return - timeCursor = some(catchable.get()) + timeCursor = some(time) proc hashCallbackImpl( pqResult: ptr PGresult, rows: var seq[(WakuMessageHash, PubsubTopic, WakuMessage)] @@ -214,11 +214,10 @@ proc hashCallbackImpl( let catchable = catch: parseHexStr(rawHash) - if catchable.isErr(): - error "could not parse correctly", error = catchable.error.msg + let hashHex = catchable.valueOr: + error "could not parse correctly", error = error.msg return - let hashHex = catchable.get() let msgHash = fromBytes(hashHex.toOpenArrayByte(0, 31)) rows.add((msgHash, "", WakuMessage())) @@ -953,11 +952,10 @@ method getDatabaseSize*( method getMessagesCount*( s: PostgresDriver ): Future[ArchiveDriverResult[int64]] {.async.} = - let intRes = await s.getInt("SELECT COUNT(1) FROM messages") - if intRes.isErr(): - return err("error in getMessagesCount: " & intRes.error) + let intRes = (await s.getInt("SELECT COUNT(1) FROM messages")).valueOr: + return err("error in getMessagesCount: " & error) - return ok(intRes.get()) + return ok(intRes) method getOldestMessageTimestamp*( s: PostgresDriver @@ -970,47 +968,44 @@ method getOldestMessageTimestamp*( let oldestPartitionTimeNanoSec = oldestPartition.getPartitionStartTimeInNanosec() - let intRes = await s.getInt("SELECT MIN(timestamp) FROM messages") - if intRes.isErr(): + let intRes = (await s.getInt("SELECT MIN(timestamp) FROM messages")).valueOr: ## Just return the oldest partition time considering the partitions set return ok(Timestamp(oldestPartitionTimeNanoSec)) - return ok(Timestamp(min(intRes.get(), oldestPartitionTimeNanoSec))) + return ok(Timestamp(min(intRes, oldestPartitionTimeNanoSec))) method getNewestMessageTimestamp*( s: PostgresDriver ): Future[ArchiveDriverResult[Timestamp]] {.async.} = - let intRes = await s.getInt("SELECT MAX(timestamp) FROM messages") + let intRes = (await s.getInt("SELECT MAX(timestamp) FROM messages")).valueOr: + return err("error in getNewestMessageTimestamp: " & error) - if intRes.isErr(): - return err("error in getNewestMessageTimestamp: " & intRes.error) - - return ok(Timestamp(intRes.get())) + return ok(Timestamp(intRes)) method deleteOldestMessagesNotWithinLimit*( s: PostgresDriver, limit: int ): Future[ArchiveDriverResult[void]] {.async.} = - var execRes = await s.writeConnPool.pgQuery( - """DELETE FROM messages WHERE messageHash NOT IN + ( + await s.writeConnPool.pgQuery( + """DELETE FROM messages WHERE messageHash NOT IN ( SELECT messageHash FROM messages ORDER BY timestamp DESC LIMIT ? );""", - @[$limit], - ) - if execRes.isErr(): - return err("error in deleteOldestMessagesNotWithinLimit: " & execRes.error) - - execRes = await s.writeConnPool.pgQuery( - """DELETE FROM messages_lookup WHERE messageHash NOT IN - ( - SELECT messageHash FROM messages ORDER BY timestamp DESC LIMIT ? - );""", - @[$limit], - ) - if execRes.isErr(): - return err( - "error in deleteOldestMessagesNotWithinLimit messages_lookup: " & execRes.error + @[$limit], ) + ).isOkOr: + return err("error in deleteOldestMessagesNotWithinLimit: " & error) + + ( + await s.writeConnPool.pgQuery( + """DELETE FROM messages_lookup WHERE messageHash NOT IN + ( + SELECT messageHash FROM messages ORDER BY timestamp DESC LIMIT ? + );""", + @[$limit], + ) + ).isOkOr: + return err("error in deleteOldestMessagesNotWithinLimit messages_lookup: " & error) return ok() diff --git a/waku/waku_archive/driver/queue_driver/queue_driver.nim b/waku/waku_archive/driver/queue_driver/queue_driver.nim index 9dbf3c112..2ffc9ab00 100644 --- a/waku/waku_archive/driver/queue_driver/queue_driver.nim +++ b/waku/waku_archive/driver/queue_driver/queue_driver.nim @@ -97,8 +97,7 @@ proc getPage( # Find starting entry if cursor.isSome(): - let cursorEntry = w.walkToCursor(cursor.get(), forward) - if cursorEntry.isErr(): + w.walkToCursor(cursor.get(), forward).isOkOr: return err(QueueDriverErrorKind.INVALID_CURSOR) # Advance walker once more @@ -177,7 +176,7 @@ proc first*(driver: QueueDriver): ArchiveDriverResult[Index] = res = w.first() w.destroy() - if res.isErr(): + res.isOkOr: return err("Not found") return ok(res.value.key) @@ -188,7 +187,7 @@ proc last*(driver: QueueDriver): ArchiveDriverResult[Index] = res = w.last() w.destroy() - if res.isErr(): + res.isOkOr: return err("Not found") return ok(res.value.key) @@ -285,14 +284,11 @@ method getMessages*( let catchable = catch: driver.getPage(maxPageSize, ascendingOrder, index, matchesQuery) - let pageRes: QueueDriverGetPageResult = - if catchable.isErr(): - return err(catchable.error.msg) - else: - catchable.get() + let pageRes: QueueDriverGetPageResult = catchable.valueOr: + return err(catchable.error.msg) - if pageRes.isErr(): - return err($pageRes.error) + pageRes.isOkOr: + return err($error) return ok(pageRes.value) diff --git a/waku/waku_archive/driver/sqlite_driver/migrations.nim b/waku/waku_archive/driver/sqlite_driver/migrations.nim index 33de5fec3..b077de19a 100644 --- a/waku/waku_archive/driver/sqlite_driver/migrations.nim +++ b/waku/waku_archive/driver/sqlite_driver/migrations.nim @@ -36,9 +36,8 @@ proc isSchemaVersion7*(db: SqliteDatabase): DatabaseResult[bool] = let query = """SELECT l.name FROM pragma_table_info("Message") as l WHERE l.pk != 0;""" - let res = db.query(query, queryRowCallback) - if res.isErr(): - return err("failed to determine the current SchemaVersion: " & $res.error) + db.query(query, queryRowCallback).isOkOr: + return err("failed to determine the current SchemaVersion: " & $error) if pkColumns == @["pubsubTopic", "id", "storedAt"]: return ok(true) @@ -65,10 +64,8 @@ proc migrate*(db: SqliteDatabase, targetVersion = SchemaVersion): DatabaseResult ## Force the correct schema version ?db.setUserVersion(7) - let migrationRes = - migrate(db, targetVersion, migrationsScriptsDir = MessageStoreMigrationPath) - if migrationRes.isErr(): - return err("failed to execute migration scripts: " & migrationRes.error) + migrate(db, targetVersion, migrationsScriptsDir = MessageStoreMigrationPath).isOkOr: + return err("failed to execute migration scripts: " & error) info "finished message store's sqlite database migration" return ok() diff --git a/waku/waku_archive/driver/sqlite_driver/queries.nim b/waku/waku_archive/driver/sqlite_driver/queries.nim index 6fafc06eb..e7e31dbe0 100644 --- a/waku/waku_archive/driver/sqlite_driver/queries.nim +++ b/waku/waku_archive/driver/sqlite_driver/queries.nim @@ -129,8 +129,7 @@ proc getMessageCount*(db: SqliteDatabase): DatabaseResult[int64] = count = sqlite3_column_int64(s, 0) let query = countMessagesQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): + db.query(query, queryRowCallback).isOkOr: return err("failed to count number of messages in the database") return ok(count) @@ -146,8 +145,7 @@ proc selectOldestTimestamp*(db: SqliteDatabase): DatabaseResult[Timestamp] {.inl timestamp = queryRowTimestampCallback(s, 0) let query = selectOldestMessageTimestampQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): + db.query(query, queryRowCallback).isOkOr: return err("failed to get the oldest receiver timestamp from the database") return ok(timestamp) @@ -163,8 +161,7 @@ proc selectNewestTimestamp*(db: SqliteDatabase): DatabaseResult[Timestamp] {.inl timestamp = queryRowTimestampCallback(s, 0) let query = selectNewestMessageTimestampQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): + db.query(query, queryRowCallback).isOkOr: return err("failed to get the newest receiver timestamp from the database") return ok(timestamp) diff --git a/waku/waku_archive/driver/sqlite_driver/sqlite_driver.nim b/waku/waku_archive/driver/sqlite_driver/sqlite_driver.nim index 173dd3e81..ff7b0e7d3 100644 --- a/waku/waku_archive/driver/sqlite_driver/sqlite_driver.nim +++ b/waku/waku_archive/driver/sqlite_driver/sqlite_driver.nim @@ -20,14 +20,12 @@ proc init(db: SqliteDatabase): ArchiveDriverResult[void] = return err("db not initialized") # Create table, if doesn't exist - let resCreate = createTable(db) - if resCreate.isErr(): - return err("failed to create table: " & resCreate.error()) + createTable(db).isOkOr: + return err("failed to create table: " & error) # Create indices, if don't exist - let resRtIndex = createOldestMessageTimestampIndex(db) - if resRtIndex.isErr(): - return err("failed to create i_ts index: " & resRtIndex.error()) + createOldestMessageTimestampIndex(db).isOkOr: + return err("failed to create i_ts index: " & error) return ok() @@ -37,9 +35,7 @@ type SqliteDriver* = ref object of ArchiveDriver proc new*(T: type SqliteDriver, db: SqliteDatabase): ArchiveDriverResult[T] = # Database initialization - let resInit = init(db) - if resInit.isErr(): - return err(resInit.error()) + ?init(db) # General initialization let insertStmt = db.prepareInsertMessageStmt() diff --git a/waku/waku_archive/retention_policy/retention_policy_time.nim b/waku/waku_archive/retention_policy/retention_policy_time.nim index b0a548d2e..6d4c0815a 100644 --- a/waku/waku_archive/retention_policy/retention_policy_time.nim +++ b/waku/waku_archive/retention_policy/retention_policy_time.nim @@ -20,20 +20,18 @@ method execute*( ## Delete messages that exceed the retention time by 10% and more (batch delete for efficiency) info "beginning of executing message retention policy - time" - let omtRes = await driver.getOldestMessageTimestamp() - if omtRes.isErr(): - return err("failed to get oldest message timestamp: " & omtRes.error) + let omt = (await driver.getOldestMessageTimestamp()).valueOr: + return err("failed to get oldest message timestamp: " & error) let now = getNanosecondTime(getTime().toUnixFloat()) let retentionTimestamp = now - p.retentionTime.nanoseconds let thresholdTimestamp = retentionTimestamp - p.retentionTime.nanoseconds div 10 - if thresholdTimestamp <= omtRes.value: + if thresholdTimestamp <= omt: return ok() - let res = await driver.deleteMessagesOlderThanTimestamp(ts = retentionTimestamp) - if res.isErr(): - return err("failed to delete oldest messages: " & res.error) + (await driver.deleteMessagesOlderThanTimestamp(ts = retentionTimestamp)).isOkOr: + return err("failed to delete oldest messages: " & error) info "end of executing message retention policy - time" return ok() diff --git a/waku/waku_archive_legacy/driver/builder.nim b/waku/waku_archive_legacy/driver/builder.nim index d73803b81..0f19b3669 100644 --- a/waku/waku_archive_legacy/driver/builder.nim +++ b/waku/waku_archive_legacy/driver/builder.nim @@ -34,65 +34,50 @@ proc new*( ## maxNumConn - defines the maximum number of connections to handle simultaneously (Postgres) ## onFatalErrorAction - called if, e.g., the connection with db got lost - let dbUrlValidationRes = dburl.validateDbUrl(url) - if dbUrlValidationRes.isErr(): - return err("DbUrl failure in ArchiveDriver.new: " & dbUrlValidationRes.error) + dburl.validateDbUrl(url).isOkOr: + return err("DbUrl failure in ArchiveDriver.new: " & error) - let engineRes = dburl.getDbEngine(url) - if engineRes.isErr(): - return err("error getting db engine in setupWakuArchiveDriver: " & engineRes.error) - - let engine = engineRes.get() + let engine = dburl.getDbEngine(url).valueOr: + return err("error getting db engine in setupWakuArchiveDriver: " & error) case engine of "sqlite": - let pathRes = dburl.getDbPath(url) - if pathRes.isErr(): - return err("error get path in setupWakuArchiveDriver: " & pathRes.error) + let path = dburl.getDbPath(url).valueOr: + return err("error get path in setupWakuArchiveDriver: " & error) - let dbRes = SqliteDatabase.new(pathRes.get()) - if dbRes.isErr(): - return err("error in setupWakuArchiveDriver: " & dbRes.error) - - let db = dbRes.get() + let db = SqliteDatabase.new(path).valueOr: + return err("error in setupWakuArchiveDriver: " & error) # SQLite vacuum - let sqliteStatsRes = db.gatherSqlitePageStats() - if sqliteStatsRes.isErr(): - return err("error while gathering sqlite stats: " & $sqliteStatsRes.error) + let (pageSize, pageCount, freelistCount) = db.gatherSqlitePageStats().valueOr: + return err("error while gathering sqlite stats: " & $error) - let (pageSize, pageCount, freelistCount) = sqliteStatsRes.get() info "sqlite database page stats", pageSize = pageSize, pages = pageCount, freePages = freelistCount if vacuum and (pageCount > 0 and freelistCount > 0): - let vacuumRes = db.performSqliteVacuum() - if vacuumRes.isErr(): - return err("error in vacuum sqlite: " & $vacuumRes.error) + db.performSqliteVacuum().isOkOr: + return err("error in vacuum sqlite: " & $error) # Database migration if migrate: - let migrateRes = archive_driver_sqlite_migrations.migrate(db) - if migrateRes.isErr(): - return err("error in migrate sqlite: " & $migrateRes.error) + archive_driver_sqlite_migrations.migrate(db).isOkOr: + return err("error in migrate sqlite: " & $error) info "setting up sqlite waku archive driver" - let res = SqliteDriver.new(db) - if res.isErr(): - return err("failed to init sqlite archive driver: " & res.error) + let res = SqliteDriver.new(db).valueOr: + return err("failed to init sqlite archive driver: " & error) - return ok(res.get()) + return ok(res) of "postgres": when defined(postgres): - let res = PostgresDriver.new( + let driver = PostgresDriver.new( dbUrl = url, maxConnections = maxNumConn, onFatalErrorAction = onFatalErrorAction, - ) - if res.isErr(): - return err("failed to init postgres archive driver: " & res.error) + ).valueOr: + return err("failed to init postgres archive driver: " & error) - let driver = res.get() return ok(driver) else: return err( diff --git a/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim index 56d388b6d..1a39c1267 100644 --- a/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim @@ -798,11 +798,10 @@ method getDatabaseSize*( method getMessagesCount*( s: PostgresDriver ): Future[ArchiveDriverResult[int64]] {.async.} = - let intRes = await s.getInt("SELECT COUNT(1) FROM messages") - if intRes.isErr(): - return err("error in getMessagesCount: " & intRes.error) + let intRes = (await s.getInt("SELECT COUNT(1) FROM messages")).valueOr: + return err("error in getMessagesCount: " & error) - return ok(intRes.get()) + return ok(intRes) method getOldestMessageTimestamp*( s: PostgresDriver @@ -812,11 +811,10 @@ method getOldestMessageTimestamp*( method getNewestMessageTimestamp*( s: PostgresDriver ): Future[ArchiveDriverResult[Timestamp]] {.async.} = - let intRes = await s.getInt("SELECT MAX(timestamp) FROM messages") - if intRes.isErr(): - return err("error in getNewestMessageTimestamp: " & intRes.error) + let intRes = (await s.getInt("SELECT MAX(timestamp) FROM messages")).valueOr: + return err("error in getNewestMessageTimestamp: " & error) - return ok(Timestamp(intRes.get())) + return ok(Timestamp(intRes)) method deleteOldestMessagesNotWithinLimit*( s: PostgresDriver, limit: int diff --git a/waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim b/waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim index 942a720df..530a84034 100644 --- a/waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim +++ b/waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim @@ -100,8 +100,7 @@ proc getPage( # Find starting entry if cursor.isSome(): - let cursorEntry = w.walkToCursor(cursor.get(), forward) - if cursorEntry.isErr(): + w.walkToCursor(cursor.get(), forward).isOkOr: return err(QueueDriverErrorKind.INVALID_CURSOR) # Advance walker once more @@ -182,7 +181,7 @@ proc first*(driver: QueueDriver): ArchiveDriverResult[Index] = res = w.first() w.destroy() - if res.isErr(): + res.isOkOr: return err("Not found") return ok(res.value.key) @@ -193,7 +192,7 @@ proc last*(driver: QueueDriver): ArchiveDriverResult[Index] = res = w.last() w.destroy() - if res.isErr(): + res.isOkOr: return err("Not found") return ok(res.value.key) @@ -297,8 +296,8 @@ method getMessages*( except CatchableError, Exception: return err(getCurrentExceptionMsg()) - if pageRes.isErr(): - return err($pageRes.error) + pageRes.isOkOr: + return err($error) return ok(pageRes.value) diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim b/waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim index 5fccf8f3d..3d8905e7e 100644 --- a/waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim +++ b/waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim @@ -36,9 +36,8 @@ proc isSchemaVersion7*(db: SqliteDatabase): DatabaseResult[bool] = let query = """SELECT l.name FROM pragma_table_info("Message") as l WHERE l.pk != 0;""" - let res = db.query(query, queryRowCallback) - if res.isErr(): - return err("failed to determine the current SchemaVersion: " & $res.error) + db.query(query, queryRowCallback).isOkOr: + return err("failed to determine the current SchemaVersion: " & $error) if pkColumns == @["pubsubTopic", "id", "storedAt"]: return ok(true) @@ -65,10 +64,8 @@ proc migrate*(db: SqliteDatabase, targetVersion = SchemaVersion): DatabaseResult ## Force the correct schema version ?db.setUserVersion(7) - let migrationRes = - migrate(db, targetVersion, migrationsScriptsDir = MessageStoreMigrationPath) - if migrationRes.isErr(): - return err("failed to execute migration scripts: " & migrationRes.error) + migrate(db, targetVersion, migrationsScriptsDir = MessageStoreMigrationPath).isOkOr: + return err("failed to execute migration scripts: " & error) info "finished message store's sqlite database migration" return ok() diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim b/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim index 47f1d86ae..0cb2bf64d 100644 --- a/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim +++ b/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim @@ -166,8 +166,7 @@ proc getMessageCount*(db: SqliteDatabase): DatabaseResult[int64] = count = sqlite3_column_int64(s, 0) let query = countMessagesQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): + db.query(query, queryRowCallback).isOkOr: return err("failed to count number of messages in the database") return ok(count) @@ -185,8 +184,7 @@ proc selectOldestReceiverTimestamp*( timestamp = queryRowReceiverTimestampCallback(s, 0) let query = selectOldestMessageTimestampQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): + db.query(query, queryRowCallback).isOkOr: return err("failed to get the oldest receiver timestamp from the database") return ok(timestamp) @@ -204,8 +202,7 @@ proc selectNewestReceiverTimestamp*( timestamp = queryRowReceiverTimestampCallback(s, 0) let query = selectNewestMessageTimestampQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): + db.query(query, queryRowCallback).isOkOr: return err("failed to get the newest receiver timestamp from the database") return ok(timestamp) @@ -280,9 +277,7 @@ proc selectAllMessages*( rows.add((pubsubTopic, wakuMessage, digest, storedAt, hash)) let query = selectAllMessagesQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): - return err(res.error()) + discard ?db.query(query, queryRowCallback) return ok(rows) @@ -498,7 +493,7 @@ proc execSelectMessageByHash( except Exception, CatchableError: # release implicit transaction discard sqlite3_reset(s) # same return information as step - discard sqlite3_clear_bindings(s) # no errors possible + discard sqlite3_clear_bindings(s) # no errors possible proc selectMessageByHashQuery(): SqlQueryStr = var query: string diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim b/waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim index 5a6c12b05..63e7c7eac 100644 --- a/waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim +++ b/waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim @@ -24,18 +24,15 @@ proc init(db: SqliteDatabase): ArchiveDriverResult[void] = return err("db not initialized") # Create table, if doesn't exist - let resCreate = createTable(db) - if resCreate.isErr(): - return err("failed to create table: " & resCreate.error()) + createTable(db).isOkOr: + return err("failed to create table: " & error) # Create indices, if don't exist - let resRtIndex = createOldestMessageTimestampIndex(db) - if resRtIndex.isErr(): - return err("failed to create i_rt index: " & resRtIndex.error()) + createOldestMessageTimestampIndex(db).isOkOr: + return err("failed to create i_rt index: " & error) - let resMsgIndex = createHistoryQueryIndex(db) - if resMsgIndex.isErr(): - return err("failed to create i_query index: " & resMsgIndex.error()) + createHistoryQueryIndex(db).isOkOr: + return err("failed to create i_query index: " & error) return ok() @@ -45,9 +42,7 @@ type SqliteDriver* = ref object of ArchiveDriver proc new*(T: type SqliteDriver, db: SqliteDatabase): ArchiveDriverResult[T] = # Database initialization - let resInit = init(db) - if resInit.isErr(): - return err(resInit.error()) + ?init(db) # General initialization let insertStmt = db.prepareInsertMessageStmt() diff --git a/waku/waku_core/peers.nim b/waku/waku_core/peers.nim index 883f266bd..5591699c6 100644 --- a/waku/waku_core/peers.nim +++ b/waku/waku_core/peers.nim @@ -249,11 +249,10 @@ proc parseUrlPeerAddr*( return ok(none(RemotePeerInfo)) let parsedAddr = decodeUrl(peerAddr.get()) - let parsedPeerInfo = parsePeerInfo(parsedAddr) - if parsedPeerInfo.isErr(): - return err("Failed parsing remote peer info [" & parsedPeerInfo.error & "]") + let parsedPeerInfo = parsePeerInfo(parsedAddr).valueOr: + return err("Failed parsing remote peer info: " & error) - return ok(some(parsedPeerInfo.value)) + return ok(some(parsedPeerInfo)) proc toRemotePeerInfo*(enrRec: enr.Record): Result[RemotePeerInfo, cstring] = ## Converts an ENR to dialable RemotePeerInfo @@ -339,11 +338,10 @@ proc hasProtocol*(ma: MultiAddress, proto: string): bool = ## Returns ``true`` if ``ma`` contains protocol ``proto``. let proto = MultiCodec.codec(proto) - let protos = ma.protocols() - if protos.isErr(): + let protos = ma.protocols().valueOr: return false - return protos.get().anyIt(it == proto) + return protos.anyIt(it == proto) func hasUdpPort*(peer: RemotePeerInfo): bool = if peer.enr.isNone(): diff --git a/waku/waku_core/topics/content_topic.nim b/waku/waku_core/topics/content_topic.nim index 5984a760b..3eeb35771 100644 --- a/waku/waku_core/topics/content_topic.nim +++ b/waku/waku_core/topics/content_topic.nim @@ -127,11 +127,10 @@ proc parse*( ): ParsingResult[seq[NsContentTopic]] = var res: seq[NsContentTopic] = @[] for contentTopic in topics: - let parseRes = NsContentTopic.parse(contentTopic) - if parseRes.isErr(): - let error: ParsingError = parseRes.error - return ParsingResult[seq[NsContentTopic]].err(error) - res.add(parseRes.value) + let parseRes = NsContentTopic.parse(contentTopic).valueOr: + let pError: ParsingError = error + return ParsingResult[seq[NsContentTopic]].err(pError) + res.add(parseRes) return ParsingResult[seq[NsContentTopic]].ok(res) # Content topic compatibility diff --git a/waku/waku_core/topics/sharding.nim b/waku/waku_core/topics/sharding.nim index 006850acf..1cb5b37b3 100644 --- a/waku/waku_core/topics/sharding.nim +++ b/waku/waku_core/topics/sharding.nim @@ -59,12 +59,8 @@ proc getShardsFromContentTopics*( else: @[contentTopics] - let parseRes = NsContentTopic.parse(topics) - let nsContentTopics = - if parseRes.isErr(): - return err("Cannot parse content topic: " & $parseRes.error) - else: - parseRes.get() + let nsContentTopics = NsContentTopic.parse(topics).valueOr: + return err("Cannot parse content topic: " & $error) var topicMap = initTable[RelayShard, seq[NsContentTopic]]() for content in nsContentTopics: diff --git a/waku/waku_enr/capabilities.nim b/waku/waku_enr/capabilities.nim index b4e2bf37a..26899fbb4 100644 --- a/waku/waku_enr/capabilities.nim +++ b/waku/waku_enr/capabilities.nim @@ -94,11 +94,10 @@ func waku2*(record: TypedRecord): Option[CapabilitiesBitfield] = some(CapabilitiesBitfield(field.get()[0])) proc supportsCapability*(r: Record, cap: Capabilities): bool = - let recordRes = r.toTyped() - if recordRes.isErr(): + let recordRes = r.toTyped().valueOr: return false - let bitfieldOpt = recordRes.value.waku2 + let bitfieldOpt = recordRes.waku2 if bitfieldOpt.isNone(): return false @@ -106,11 +105,10 @@ proc supportsCapability*(r: Record, cap: Capabilities): bool = bitfield.supportsCapability(cap) proc getCapabilities*(r: Record): seq[Capabilities] = - let recordRes = r.toTyped() - if recordRes.isErr(): + let recordRes = r.toTyped().valueOr: return @[] - let bitfieldOpt = recordRes.value.waku2 + let bitfieldOpt = recordRes.waku2 if bitfieldOpt.isNone(): return @[] diff --git a/waku/waku_enr/multiaddr.nim b/waku/waku_enr/multiaddr.nim index 83e3d1992..c343fff51 100644 --- a/waku/waku_enr/multiaddr.nim +++ b/waku/waku_enr/multiaddr.nim @@ -88,8 +88,7 @@ func multiaddrs*(record: TypedRecord): Option[seq[MultiAddress]] = if field.isNone(): return none(seq[MultiAddress]) - let decodeRes = decodeMultiaddrs(field.get()) - if decodeRes.isErr(): + let decodeRes = decodeMultiaddrs(field.get()).valueOr: return none(seq[MultiAddress]) - some(decodeRes.value) + some(decodeRes) diff --git a/waku/waku_enr/sharding.nim b/waku/waku_enr/sharding.nim index d54464f94..392900cdb 100644 --- a/waku/waku_enr/sharding.nim +++ b/waku/waku_enr/sharding.nim @@ -64,8 +64,8 @@ func topicsToRelayShards*(topics: seq[string]): Result[Option[RelayShards], stri let parsedTopicsRes = topics.mapIt(RelayShard.parse(it)) for res in parsedTopicsRes: - if res.isErr(): - return err("failed to parse topic: " & $res.error) + res.isOkOr: + return err("failed to parse topic: " & $error) if parsedTopicsRes.anyIt(it.get().clusterId != parsedTopicsRes[0].get().clusterId): return err("use shards with the same cluster Id.") @@ -84,11 +84,10 @@ func contains*(rs: RelayShards, shard: RelayShard): bool = return rs.contains(shard.clusterId, shard.shardId) func contains*(rs: RelayShards, topic: PubsubTopic): bool = - let parseRes = RelayShard.parse(topic) - if parseRes.isErr(): + let parseRes = RelayShard.parse(topic).valueOr: return false - rs.contains(parseRes.value) + rs.contains(parseRes) # ENR builder extension @@ -239,12 +238,11 @@ proc containsShard*(r: Record, shard: RelayShard): bool = return containsShard(r, shard.clusterId, shard.shardId) proc containsShard*(r: Record, topic: PubsubTopic): bool = - let parseRes = RelayShard.parse(topic) - if parseRes.isErr(): - info "invalid static sharding topic", topic = topic, error = parseRes.error + let parseRes = RelayShard.parse(topic).valueOr: + info "invalid static sharding topic", topic = topic, error = error return false - containsShard(r, parseRes.value) + containsShard(r, parseRes) proc isClusterMismatched*(record: Record, clusterId: uint16): bool = ## Check the ENR sharding info for matching cluster id diff --git a/waku/waku_filter_v2/client.nim b/waku/waku_filter_v2/client.nim index 1dc018150..c42bca3db 100644 --- a/waku/waku_filter_v2/client.nim +++ b/waku/waku_filter_v2/client.nim @@ -80,14 +80,11 @@ proc sendSubscribeRequest( waku_filter_errors.inc(labelValues = [errMsg]) return err(FilterSubscribeError.badResponse(errMsg)) - let respDecodeRes = FilterSubscribeResponse.decode(respBuf) - if respDecodeRes.isErr(): + let response = FilterSubscribeResponse.decode(respBuf).valueOr: trace "Failed to decode filter subscribe response", servicePeer waku_filter_errors.inc(labelValues = [decodeRpcFailure]) return err(FilterSubscribeError.badResponse(decodeRpcFailure)) - let response = respDecodeRes.get() - # DOS protection rate limit checks does not know about request id if response.statusCode != FilterSubscribeErrorKind.TOO_MANY_REQUESTS.uint32 and response.requestId != filterSubscribeRequest.requestId: diff --git a/waku/waku_filter_v2/protocol.nim b/waku/waku_filter_v2/protocol.nim index 5e9b48496..451bf5cb2 100644 --- a/waku/waku_filter_v2/protocol.nim +++ b/waku/waku_filter_v2/protocol.nim @@ -157,15 +157,14 @@ proc handleSubscribeRequest*( requestDurationSec, labelValues = [$request.filterSubscribeType] ) - if subscribeResult.isErr(): + subscribeResult.isOkOr: error "subscription request error", peerId = shortLog(peerId), request = request return FilterSubscribeResponse( requestId: request.requestId, - statusCode: subscribeResult.error.kind.uint32, - statusDesc: some($subscribeResult.error), + statusCode: error.kind.uint32, + statusDesc: some($error), ) - else: - return FilterSubscribeResponse.ok(request.requestId) + return FilterSubscribeResponse.ok(request.requestId) proc pushToPeer( wf: WakuFilter, peerId: PeerId, buffer: seq[byte] @@ -309,15 +308,12 @@ proc initProtocolHandler(wf: WakuFilter) = amount = buf.len().int64, labelValues = [WakuFilterSubscribeCodec, "in"] ) - let decodeRes = FilterSubscribeRequest.decode(buf) - if decodeRes.isErr(): + let request = FilterSubscribeRequest.decode(buf).valueOr: error "failed to decode filter subscribe request", - peer_id = conn.peerId, err = decodeRes.error + peer_id = conn.peerId, err = error waku_filter_errors.inc(labelValues = [decodeRpcFailure]) return - let request = decodeRes.value #TODO: toAPI() split here - try: response = await wf.handleSubscribeRequest(conn.peerId, request) except CatchableError: diff --git a/waku/waku_keystore/keyfile.nim b/waku/waku_keystore/keyfile.nim index 488e241ab..c84a45dba 100644 --- a/waku/waku_keystore/keyfile.nim +++ b/waku/waku_keystore/keyfile.nim @@ -1,4 +1,4 @@ -# This implementation is originally taken from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile and adapted to +# This implementation is originally taken from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile and adapted to # - create keyfiles for arbitrary-long input byte data (rather than fixed-size private keys) # - allow storage of multiple keyfiles (encrypted with different passwords) in same file and iteration among successful decryptions # - enable/disable at compilation time the keyfile id and version fields @@ -517,26 +517,15 @@ func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] = proc decodeKeyFileJson*(j: JsonNode, password: string): KfResult[seq[byte]] = ## Decode secret from keyfile json object ``j`` using ## password string ``password``. - let res = decodeCrypto(j) - if res.isErr: - return err(res.error) - let crypto = res.get() + let crypto = ?decodeCrypto(j) case crypto.kind of PBKDF2: - let res = decodePbkdf2Params(crypto.kdfParams) - if res.isErr: - return err(res.error) - - let params = res.get() + let params = ?decodePbkdf2Params(crypto.kdfParams) let dkey = ?deriveKey(password, params.salt, PBKDF2, params.prf, params.c) return decryptSecret(crypto, dkey) of SCRYPT: - let res = decodeScryptParams(crypto.kdfParams) - if res.isErr: - return err(res.error) - - let params = res.get() + let params = ?decodeScryptParams(crypto.kdfParams) let dkey = ?deriveKey(password, params.salt, params.n, params.r, params.p) return decryptSecret(crypto, dkey) diff --git a/waku/waku_keystore/keystore.nim b/waku/waku_keystore/keystore.nim index 6cc4ef701..158f1a98e 100644 --- a/waku/waku_keystore/keystore.nim +++ b/waku/waku_keystore/keystore.nim @@ -50,9 +50,7 @@ proc loadAppKeystore*( # If no keystore exists at path we create a new empty one with passed keystore parameters if fileExists(path) == false: - let newKeystoreRes = createAppKeystore(path, appInfo, separator) - if newKeystoreRes.isErr(): - return err(newKeystoreRes.error) + ?createAppKeystore(path, appInfo, separator) try: # We read all the file contents @@ -175,13 +173,9 @@ proc addMembershipCredentials*( ): KeystoreResult[void] = # We load the keystore corresponding to the desired parameters # This call ensures that JSON has all required fields - let jsonKeystoreRes = loadAppKeystore(path, appInfo, separator) - - if jsonKeystoreRes.isErr(): - return err(jsonKeystoreRes.error) # We load the JSON node corresponding to the app keystore - var jsonKeystore = jsonKeystoreRes.get() + let jsonKeystore = ?loadAppKeystore(path, appInfo, separator) try: if jsonKeystore.hasKey("credentials"): @@ -193,21 +187,16 @@ proc addMembershipCredentials*( return ok() let encodedMembershipCredential = membership.encode() - let keyfileRes = createKeyFileJson(encodedMembershipCredential, password) - if keyfileRes.isErr(): - return err( - AppKeystoreError(kind: KeystoreCreateKeyfileError, msg: $keyfileRes.error) - ) - # We add it to the credentials field of the keystore - jsonKeystore["credentials"][key] = keyfileRes.get() + jsonKeystore["credentials"][key] = createKeyFileJson( + encodedMembershipCredential, password + ).valueOr: + return err(AppKeystoreError(kind: KeystoreCreateKeyfileError, msg: $error)) except CatchableError: return err(AppKeystoreError(kind: KeystoreJsonError, msg: getCurrentExceptionMsg())) # We save to disk the (updated) keystore. - let saveRes = save(jsonKeystore, path, separator) - if saveRes.isErr(): - return err(saveRes.error) + ?save(jsonKeystore, path, separator) return ok() @@ -218,13 +207,9 @@ proc getMembershipCredentials*( ): KeystoreResult[KeystoreMembership] = # We load the keystore corresponding to the desired parameters # This call ensures that JSON has all required fields - let jsonKeystoreRes = loadAppKeystore(path, appInfo) - - if jsonKeystoreRes.isErr(): - return err(jsonKeystoreRes.error) # We load the JSON node corresponding to the app keystore - var jsonKeystore = jsonKeystoreRes.get() + let jsonKeystore = ?loadAppKeystore(path, appInfo) try: if jsonKeystore.hasKey("credentials"): @@ -254,15 +239,10 @@ proc getMembershipCredentials*( ) keystoreCredential = keystoreCredentials[key] - let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password) - if decodedKeyfileRes.isErr(): - return err( - AppKeystoreError( - kind: KeystoreReadKeyfileError, msg: $decodedKeyfileRes.error - ) - ) + let decodedKeyfile = decodeKeyFileJson(keystoreCredential, password).valueOr: + return err(AppKeystoreError(kind: KeystoreReadKeyfileError, msg: $error)) # we parse the json decrypted keystoreCredential - let decodedCredentialRes = decode(decodedKeyfileRes.get()) + let decodedCredentialRes = decode(decodedKeyfile) let keyfileMembershipCredential = decodedCredentialRes.get() return ok(keyfileMembershipCredential) except CatchableError: diff --git a/waku/waku_lightpush/callbacks.nim b/waku/waku_lightpush/callbacks.nim index 4b362e6bb..bde4e3e26 100644 --- a/waku/waku_lightpush/callbacks.nim +++ b/waku/waku_lightpush/callbacks.nim @@ -26,8 +26,7 @@ proc checkAndGenerateRLNProof*( time = getTime().toUnix() senderEpochTime = float64(time) var msgWithProof = message - rlnPeer.get().appendRLNProof(msgWithProof, senderEpochTime).isOkOr: - return err(error) + ?(rlnPeer.get().appendRLNProof(msgWithProof, senderEpochTime)) return ok(msgWithProof) proc getNilPushHandler*(): PushMessageHandler = @@ -49,12 +48,10 @@ proc getRelayPushHandler*( (await wakuRelay.validateMessage(pubSubTopic, msgWithProof)).isOkOr: return lighpushErrorResult(LightPushErrorCode.INVALID_MESSAGE, $error) - let publishedResult = await wakuRelay.publish(pubsubTopic, msgWithProof) - - if publishedResult.isErr(): + let publishedResult = (await wakuRelay.publish(pubsubTopic, msgWithProof)).valueOr: let msgHash = computeMessageHash(pubsubTopic, message).to0xHex() notice "Lightpush request has not been published to any peers", - msg_hash = msgHash, reason = $publishedResult.error - return mapPubishingErrorToPushResult(publishedResult.error) + msg_hash = msgHash, reason = $error + return mapPubishingErrorToPushResult(error) - return lightpushSuccessResult(publishedResult.get().uint32) + return lightpushSuccessResult(publishedResult.uint32) diff --git a/waku/waku_lightpush_legacy/callbacks.nim b/waku/waku_lightpush_legacy/callbacks.nim index f5a79eadc..1fe4cf302 100644 --- a/waku/waku_lightpush_legacy/callbacks.nim +++ b/waku/waku_lightpush_legacy/callbacks.nim @@ -25,8 +25,7 @@ proc checkAndGenerateRLNProof*( time = getTime().toUnix() senderEpochTime = float64(time) var msgWithProof = message - rlnPeer.get().appendRLNProof(msgWithProof, senderEpochTime).isOkOr: - return err(error) + ?(rlnPeer.get().appendRLNProof(msgWithProof, senderEpochTime)) return ok(msgWithProof) proc getNilPushHandler*(): PushMessageHandler = @@ -42,19 +41,15 @@ proc getRelayPushHandler*( peer: PeerId, pubsubTopic: string, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = # append RLN proof - let msgWithProof = checkAndGenerateRLNProof(rlnPeer, message) - if msgWithProof.isErr(): - return err(msgWithProof.error) + let msgWithProof = ?checkAndGenerateRLNProof(rlnPeer, message) - (await wakuRelay.validateMessage(pubSubTopic, msgWithProof.value)).isOkOr: - return err(error) + ?(await wakuRelay.validateMessage(pubSubTopic, msgWithProof)) - let publishResult = await wakuRelay.publish(pubsubTopic, msgWithProof.value) - if publishResult.isErr(): + (await wakuRelay.publish(pubsubTopic, msgWithProof)).isOkOr: ## Agreed change expected to the lightpush protocol to better handle such case. https://github.com/waku-org/pm/issues/93 let msgHash = computeMessageHash(pubsubTopic, message).to0xHex() notice "Lightpush request has not been published to any peers", - msg_hash = msgHash, reason = $publishResult.error + msg_hash = msgHash, reason = $error # for legacy lightpush we do not detail the reason towards clients. All error during publish result in not-published-to-any-peer # this let client of the legacy protocol to react as they did so far. return err(protocol_metrics.notPublishedAnyPeer) diff --git a/waku/waku_lightpush_legacy/client.nim b/waku/waku_lightpush_legacy/client.nim index ee234c996..0e3c9bd6f 100644 --- a/waku/waku_lightpush_legacy/client.nim +++ b/waku/waku_lightpush_legacy/client.nim @@ -52,13 +52,11 @@ proc sendPushRequest( except LPStreamRemoteClosedError: return err("Exception reading: " & getCurrentExceptionMsg()) - let decodeRespRes = PushRPC.decode(buffer) - if decodeRespRes.isErr(): + let pushResponseRes = PushRPC.decode(buffer).valueOr: error "failed to decode response" waku_lightpush_errors.inc(labelValues = [decodeRpcFailure]) return err(decodeRpcFailure) - let pushResponseRes = decodeRespRes.get() if pushResponseRes.response.isNone(): waku_lightpush_errors.inc(labelValues = [emptyResponseBodyFailure]) return err(emptyResponseBodyFailure) diff --git a/waku/waku_metadata/protocol.nim b/waku/waku_metadata/protocol.nim index c112cc5d5..623cbb6c3 100644 --- a/waku/waku_metadata/protocol.nim +++ b/waku/waku_metadata/protocol.nim @@ -33,8 +33,8 @@ proc respond( let res = catch: await conn.writeLP(response.encode().buffer) - if res.isErr(): - return err(res.error.msg) + res.isOkOr: + return err(error.msg) return ok() @@ -53,17 +53,14 @@ proc request*( # close no matter what let closeRes = catch: await conn.closeWithEof() - if closeRes.isErr(): - return err("close failed: " & closeRes.error.msg) + closeRes.isOkOr: + return err("close failed: " & error.msg) - if writeRes.isErr(): - return err("write failed: " & writeRes.error.msg) + writeRes.isOkOr: + return err("write failed: " & error.msg) - let buffer = - if readRes.isErr(): - return err("read failed: " & readRes.error.msg) - else: - readRes.get() + let buffer = readRes.valueOr: + return err("read failed: " & error.msg) let response = WakuMetadataResponse.decode(buffer).valueOr: return err("decode failed: " & $error) diff --git a/waku/waku_peer_exchange/protocol.nim b/waku/waku_peer_exchange/protocol.nim index de81d366e..cf7ebc2a7 100644 --- a/waku/waku_peer_exchange/protocol.nim +++ b/waku/waku_peer_exchange/protocol.nim @@ -157,15 +157,14 @@ proc initProtocolHandler(wpx: WakuPeerExchange) = error "Failed to respond with BAD_REQUEST:", error = $error return - let decBuf = PeerExchangeRpc.decode(buffer) - if decBuf.isErr(): + let decBuf = PeerExchangeRpc.decode(buffer).valueOr: waku_px_errors.inc(labelValues = [decodeRpcFailure]) - error "Failed to decode PeerExchange request", error = $decBuf.error + error "Failed to decode PeerExchange request", error = $error ( try: await wpx.respondError( - PeerExchangeResponseStatusCode.BAD_REQUEST, some($decBuf.error), conn + PeerExchangeResponseStatusCode.BAD_REQUEST, some($error), conn ) except CatchableError: error "could not send error response decode", @@ -175,7 +174,7 @@ proc initProtocolHandler(wpx: WakuPeerExchange) = error "Failed to respond with BAD_REQUEST:", error = $error return - let enrs = wpx.getEnrsFromCache(decBuf.get().request.numPeers) + let enrs = wpx.getEnrsFromCache(decBuf.request.numPeers) info "peer exchange request received" trace "px enrs to respond", enrs = $enrs try: diff --git a/waku/waku_relay/protocol.nim b/waku/waku_relay/protocol.nim index 9ebbc480f..cbf9123dd 100644 --- a/waku/waku_relay/protocol.nim +++ b/waku/waku_relay/protocol.nim @@ -399,8 +399,7 @@ proc getPeersInMesh*( ): Result[seq[PeerId], string] = ## Returns the list of peerIds in a mesh defined by the passed pubsub topic. ## The 'mesh' atribute is defined in the GossipSub ref object. - let pubSubPeers = w.getPubSubPeersInMesh(pubsubTopic).valueOr: - return err(error) + let pubSubPeers = ?w.getPubSubPeersInMesh(pubsubTopic) let peerIds = toSeq(pubSubPeers).mapIt(it.peerId) return ok(peerIds) @@ -544,22 +543,20 @@ proc subscribe*(w: WakuRelay, pubsubTopic: PubsubTopic, handler: WakuRelayHandle let topicHandler = proc( pubsubTopic: string, data: seq[byte] ): Future[void] {.gcsafe, raises: [].} = - let decMsg = WakuMessage.decode(data) - if decMsg.isErr(): + let decMsg = WakuMessage.decode(data).valueOr: # fine if triggerSelf enabled, since validators are bypassed error "failed to decode WakuMessage, validator passed a wrong message", - pubsubTopic = pubsubTopic, error = decMsg.error + pubsubTopic = pubsubTopic, error = error let fut = newFuture[void]() fut.complete() return fut - else: - # this subscription handler is called once for every validated message - # that will be relayed, hence this is the place we can count net incoming traffic - waku_relay_network_bytes.inc( - data.len.int64 + pubsubTopic.len.int64, labelValues = [pubsubTopic, "net", "in"] - ) + # this subscription handler is called once for every validated message + # that will be relayed, hence this is the place we can count net incoming traffic + waku_relay_network_bytes.inc( + data.len.int64 + pubsubTopic.len.int64, labelValues = [pubsubTopic, "net", "in"] + ) - return handler(pubsubTopic, decMsg.get()) + return handler(pubsubTopic, decMsg) # Add the ordered validator to the topic # This assumes that if `w.validatorInserted.hasKey(pubSubTopic) is true`, it contains the ordered validator. @@ -670,8 +667,7 @@ proc getConnectedPeers*( ## Returns the list of peerIds of connected peers and subscribed to the passed pubsub topic. ## The 'gossipsub' atribute is defined in the GossipSub ref object. - let peers = w.getConnectedPubSubPeers(pubsubTopic).valueOr: - return err(error) + let peers = ?w.getConnectedPubSubPeers(pubsubTopic) let peerIds = toSeq(peers).mapIt(it.peerId) return ok(peerIds) diff --git a/waku/waku_rendezvous/protocol.nim b/waku/waku_rendezvous/protocol.nim index 876082210..0eb55d350 100644 --- a/waku/waku_rendezvous/protocol.nim +++ b/waku/waku_rendezvous/protocol.nim @@ -55,18 +55,16 @@ proc batchAdvertise*( let dialCatch = catch: await allFinished(futs) - if dialCatch.isErr(): - return err("batchAdvertise: " & dialCatch.error.msg) - - futs = dialCatch.get() + futs = dialCatch.valueOr: + return err("batchAdvertise: " & error.msg) let conns = collect(newSeq): for fut in futs: let catchable = catch: fut.read() - if catchable.isErr(): - warn "a rendezvous dial failed", cause = catchable.error.msg + catchable.isOkOr: + warn "a rendezvous dial failed", cause = error.msg continue let connOpt = catchable.get() @@ -82,8 +80,8 @@ proc batchAdvertise*( for conn in conns: await conn.close() - if advertCatch.isErr(): - return err("batchAdvertise: " & advertCatch.error.msg) + advertCatch.isOkOr: + return err("batchAdvertise: " & error.msg) return ok() @@ -104,18 +102,16 @@ proc batchRequest*( let dialCatch = catch: await allFinished(futs) - if dialCatch.isErr(): - return err("batchRequest: " & dialCatch.error.msg) - - futs = dialCatch.get() + futs = dialCatch.valueOr: + return err("batchRequest: " & error.msg) let conns = collect(newSeq): for fut in futs: let catchable = catch: fut.read() - if catchable.isErr(): - warn "a rendezvous dial failed", cause = catchable.error.msg + catchable.isOkOr: + warn "a rendezvous dial failed", cause = error.msg continue let connOpt = catchable.get() @@ -131,8 +127,8 @@ proc batchRequest*( for conn in conns: await conn.close() - if reqCatch.isErr(): - return err("batchRequest: " & reqCatch.error.msg) + reqCatch.isOkOr: + return err("batchRequest: " & error.msg) return ok(reqCatch.get()) @@ -164,8 +160,8 @@ proc advertiseAll( let catchable = catch: await allFinished(futs) - if catchable.isErr(): - return err(catchable.error.msg) + catchable.isOkOr: + return err(error.msg) for fut in catchable.get(): if fut.failed(): @@ -201,8 +197,8 @@ proc initialRequestAll*( let catchable = catch: await allFinished(futs) - if catchable.isErr(): - return err(catchable.error.msg) + catchable.isOkOr: + return err(error.msg) for fut in catchable.get(): if fut.failed(): @@ -211,7 +207,7 @@ proc initialRequestAll*( let res = fut.value() let records = res.valueOr: - warn "a rendezvous request failed", cause = $res.error + warn "a rendezvous request failed", cause = $error continue for record in records: @@ -268,16 +264,14 @@ proc new*( let rvCatchable = catch: RendezVous.new(switch = switch, minDuration = DefaultRegistrationTTL) - if rvCatchable.isErr(): - return err(rvCatchable.error.msg) - - let rv = rvCatchable.get() + let rv = rvCatchable.valueOr: + return err(error.msg) let mountCatchable = catch: switch.mount(rv) - if mountCatchable.isErr(): - return err(mountCatchable.error.msg) + mountCatchable.isOkOr: + return err(error.msg) var wrv = WakuRendezVous() wrv.rendezvous = rv 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 76c00408e..db68b2289 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 @@ -111,17 +111,17 @@ proc fetchMembershipStatus*( ): Future[Result[bool, string]] {.async.} = try: let params = idCommitment.reversed() - let resultBytes = await sendEthCallWithParams( - ethRpc = g.ethRpc.get(), - functionSignature = "isInMembershipSet(uint256)", - params = params, - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) - if resultBytes.isErr(): - return err("Failed to check membership: " & resultBytes.error) - let responseBytes = resultBytes.get() + let responseBytes = ( + await sendEthCallWithParams( + ethRpc = g.ethRpc.get(), + functionSignature = "isInMembershipSet(uint256)", + params = params, + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + ).valueOr: + return err("Failed to check membership: " & error) return ok(responseBytes.len == 32 and responseBytes[^1] == 1'u8) except CatchableError: @@ -155,11 +155,10 @@ template retryWrapper( body proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = - let rootRes = await g.fetchMerkleRoot() - if rootRes.isErr(): + let rootRes = (await g.fetchMerkleRoot()).valueOr: return false - let merkleRoot = UInt256ToField(rootRes.get()) + let merkleRoot = UInt256ToField(rootRes) if g.validRoots.len == 0: g.validRoots.addLast(merkleRoot) @@ -193,14 +192,12 @@ proc trackRootChanges*(g: OnchainGroupManager) {.async: (raises: [CatchableError else: g.merkleProofCache = proofResult.get() - let nextFreeIndex = await g.fetchNextFreeIndex() - if nextFreeIndex.isErr(): - error "Failed to fetch next free index", error = nextFreeIndex.error - raise newException( - CatchableError, "Failed to fetch next free index: " & nextFreeIndex.error - ) + let nextFreeIndex = (await g.fetchNextFreeIndex()).valueOr: + error "Failed to fetch next free index", error = error + raise + newException(CatchableError, "Failed to fetch next free index: " & error) - let memberCount = cast[int64](nextFreeIndex.get()) + let memberCount = cast[int64](nextFreeIndex) waku_rln_number_registered_memberships.set(float64(memberCount)) except CatchableError: error "Fatal error in trackRootChanges", error = getCurrentExceptionMsg() @@ -315,11 +312,9 @@ proc getRootFromProofAndIndex( # it's currently not used anywhere, but can be used to verify the root from the proof and index # Compute leaf hash from idCommitment and messageLimit let messageLimitField = uint64ToField(g.userMessageLimit.get()) - let leafHashRes = poseidon(@[g.idCredentials.get().idCommitment, @messageLimitField]) - if leafHashRes.isErr(): - return err("Failed to compute leaf hash: " & leafHashRes.error) + var hash = poseidon(@[g.idCredentials.get().idCommitment, @messageLimitField]).valueOr: + return err("Failed to compute leaf hash: " & error) - var hash = leafHashRes.get() for i in 0 ..< bits.len: let sibling = elements[i * 32 .. (i + 1) * 32 - 1] @@ -331,7 +326,6 @@ proc getRootFromProofAndIndex( hash = hashRes.valueOr: return err("Failed to compute poseidon hash: " & error) - hash = hashRes.get() return ok(hash) diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index 817bb8720..6a8fea2b5 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -178,12 +178,9 @@ proc validateMessage*( ## `timeOption` indicates Unix epoch time (fractional part holds sub-seconds) ## if `timeOption` is supplied, then the current epoch is calculated based on that - let decodeRes = RateLimitProof.init(msg.proof) - if decodeRes.isErr(): + let proof = RateLimitProof.init(msg.proof).valueOr: return MessageValidationResult.Invalid - let proof = decodeRes.get() - # track message count for metrics waku_rln_messages_total.inc() @@ -228,7 +225,7 @@ proc validateMessage*( let proofVerificationRes = rlnPeer.groupManager.verifyProof(msg.toRLNSignal(), proof) - if proofVerificationRes.isErr(): + proofVerificationRes.isOkOr: waku_rln_errors_total.inc(labelValues = ["proof_verification"]) warn "invalid message: proof verification failed", payloadLen = msg.payload.len return MessageValidationResult.Invalid @@ -240,13 +237,12 @@ proc validateMessage*( return MessageValidationResult.Invalid # check if double messaging has happened - let proofMetadataRes = proof.extractMetadata() - if proofMetadataRes.isErr(): + let proofMetadata = proof.extractMetadata().valueOr: waku_rln_errors_total.inc(labelValues = ["proof_metadata_extraction"]) return MessageValidationResult.Invalid let msgEpoch = proof.epoch - let hasDup = rlnPeer.hasDuplicate(msgEpoch, proofMetadataRes.get()) + let hasDup = rlnPeer.hasDuplicate(msgEpoch, proofMetadata) if hasDup.isErr(): waku_rln_errors_total.inc(labelValues = ["duplicate_check"]) elif hasDup.value == true: @@ -266,20 +262,16 @@ proc validateMessageAndUpdateLog*( let isValidMessage = rlnPeer.validateMessage(msg) - let decodeRes = RateLimitProof.init(msg.proof) - if decodeRes.isErr(): + let msgProof = RateLimitProof.init(msg.proof).valueOr: return MessageValidationResult.Invalid - let msgProof = decodeRes.get() - let proofMetadataRes = msgProof.extractMetadata() - - if proofMetadataRes.isErr(): + let proofMetadata = msgProof.extractMetadata().valueOr: return MessageValidationResult.Invalid # insert the message to the log (never errors) only if the # message is valid. if isValidMessage == MessageValidationResult.Valid: - discard rlnPeer.updateLog(msgProof.epoch, proofMetadataRes.get()) + discard rlnPeer.updateLog(msgProof.epoch, proofMetadata) return isValidMessage @@ -333,14 +325,10 @@ proc generateRlnValidator*( trace "rln-relay topic validator is called" wakuRlnRelay.clearNullifierLog() - let decodeRes = RateLimitProof.init(message.proof) - - if decodeRes.isErr(): - trace "generateRlnValidator reject", error = decodeRes.error + let msgProof = RateLimitProof.init(message.proof).valueOr: + trace "generateRlnValidator reject", error = error return pubsub.ValidationResult.Reject - let msgProof = decodeRes.get() - # validate the message and update log let validationRes = wakuRlnRelay.validateMessageAndUpdateLog(message) diff --git a/waku/waku_store/protocol.nim b/waku/waku_store/protocol.nim index 395936625..891c6a93c 100644 --- a/waku/waku_store/protocol.nim +++ b/waku/waku_store/protocol.nim @@ -132,8 +132,8 @@ proc initProtocolHandler(self: WakuStore) = let writeRes = catch: await conn.writeLp(resBuf.resp) - if writeRes.isErr(): - error "Connection write error", error = writeRes.error.msg + writeRes.isOkOr: + error "Connection write error", error = error.msg return if successfulQuery: diff --git a/waku/waku_store/resume.nim b/waku/waku_store/resume.nim index 208ba0aa6..b7864da94 100644 --- a/waku/waku_store/resume.nim +++ b/waku/waku_store/resume.nim @@ -92,8 +92,8 @@ proc initTransferHandler( let catchable = catch: await wakuStoreClient.query(req, peer) - if catchable.isErr(): - return err("store client error: " & catchable.error.msg) + catchable.isOkOr: + return err("store client error: " & error.msg) let res = catchable.get() let response = res.valueOr: @@ -105,8 +105,8 @@ proc initTransferHandler( let handleRes = catch: await wakuArchive.handleMessage(kv.pubsubTopic.get(), kv.message.get()) - if handleRes.isErr(): - error "message transfer failed", error = handleRes.error.msg + handleRes.isOkOr: + error "message transfer failed", error = error.msg continue if req.paginationCursor.isNone(): diff --git a/waku/waku_store/self_req_handler.nim b/waku/waku_store/self_req_handler.nim index 116946da5..315961307 100644 --- a/waku/waku_store/self_req_handler.nim +++ b/waku/waku_store/self_req_handler.nim @@ -25,11 +25,8 @@ proc handleSelfStoreRequest*( let handlerResult = catch: await self.requestHandler(req) - let resResult = - if handlerResult.isErr(): - return err("exception in handleSelfStoreRequest: " & handlerResult.error.msg) - else: - handlerResult.get() + let resResult = handlerResult.valueOr: + return err("exception in handleSelfStoreRequest: " & error.msg) let res = resResult.valueOr: return err("error in handleSelfStoreRequest: " & $error) diff --git a/waku/waku_store_legacy/client.nim b/waku/waku_store_legacy/client.nim index d3301cfa4..3965e06cf 100644 --- a/waku/waku_store_legacy/client.nim +++ b/waku/waku_store_legacy/client.nim @@ -58,14 +58,11 @@ proc sendHistoryQueryRPC( #TODO: I see a challenge here, if storeNode uses a different MaxRPCSize this read will fail. # Need to find a workaround for this. let buf = await connection.readLp(DefaultMaxRpcSize.int) - let respDecodeRes = HistoryRPC.decode(buf) - if respDecodeRes.isErr(): + let respRpc = HistoryRPC.decode(buf).valueOr: waku_legacy_store_errors.inc(labelValues = [decodeRpcFailure]) return err(HistoryError(kind: HistoryErrorKind.BAD_RESPONSE, cause: decodeRpcFailure)) - let respRpc = respDecodeRes.get() - # Disabled ,for now, since the default response is a possible case (no messages, pagesize = 0, error = NONE(0)) # TODO: Rework the RPC protocol to differentiate the default value from an empty value (e.g., status = 200 (OK)) # and rework the protobuf parsing to return Option[T] when empty values are received @@ -112,11 +109,8 @@ when defined(waku_exp_store_resume): var messageList: seq[WakuMessage] = @[] while true: - let queryRes = await w.query(req, peer) - if queryRes.isErr(): - return err($queryRes.error) - - let response = queryRes.get() + let response = (await w.query(req, peer)).valueOr: + return err($error) messageList.add(response.messages) @@ -232,15 +226,14 @@ when defined(waku_exp_store_resume): info "a peer is selected from peer manager" res = await w.queryAll(req, peerOpt.get()) - if res.isErr(): + res.isOkOr: info "failed to resume the history" return err("failed to resume the history") # Save the retrieved messages in the store var added: uint = 0 for msg in res.get(): - let putStoreRes = w.store.put(pubsubTopic, msg) - if putStoreRes.isErr(): + w.store.put(pubsubTopic, msg).isOkOr: continue added.inc() diff --git a/waku/waku_store_legacy/protocol.nim b/waku/waku_store_legacy/protocol.nim index 058bcbe78..8916e8ac0 100644 --- a/waku/waku_store_legacy/protocol.nim +++ b/waku/waku_store_legacy/protocol.nim @@ -42,14 +42,11 @@ type StoreResp = tuple[resp: seq[byte], requestId: string] proc handleLegacyQueryRequest( self: WakuStore, requestor: PeerId, raw_request: seq[byte] ): Future[StoreResp] {.async.} = - let decodeRes = HistoryRPC.decode(raw_request) - if decodeRes.isErr(): - error "failed to decode rpc", peerId = requestor, error = $decodeRes.error + let reqRpc = HistoryRPC.decode(raw_request).valueOr: + error "failed to decode rpc", peerId = requestor, error = $error waku_legacy_store_errors.inc(labelValues = [decodeRpcFailure]) return (newSeq[byte](), "failed to decode rpc") - let reqRpc = decodeRes.value - if reqRpc.query.isNone(): error "empty query rpc", peerId = requestor, requestId = reqRpc.requestId waku_legacy_store_errors.inc(labelValues = [emptyRpcQueryFailure]) @@ -77,9 +74,9 @@ proc handleLegacyQueryRequest( requestId, ) - if responseRes.isErr(): + responseRes.isOkOr: error "history query failed", - peerId = requestor, requestId = requestId, error = responseRes.error + peerId = requestor, requestId = requestId, error = error let response = responseRes.toRPC() return ( @@ -150,8 +147,8 @@ proc initProtocolHandler(ws: WakuStore) = let writeRes = catch: await conn.writeLp(resBuf.resp) - if writeRes.isErr(): - error "Connection write error", error = writeRes.error.msg + writeRes.isOkOr: + error "Connection write error", error = error.msg return if successfulQuery: diff --git a/waku/waku_store_legacy/rpc.nim b/waku/waku_store_legacy/rpc.nim index bce3e60cd..44aad8d07 100644 --- a/waku/waku_store_legacy/rpc.nim +++ b/waku/waku_store_legacy/rpc.nim @@ -187,25 +187,20 @@ proc toAPI*(err: HistoryResponseErrorRPC): HistoryError = HistoryError(kind: HistoryErrorKind.UNKNOWN) proc toRPC*(res: HistoryResult): HistoryResponseRPC = - if res.isErr(): - let error = res.error.toRPC() + let resp = res.valueOr: + return HistoryResponseRPC(error: error.toRPC()) + let + messages = resp.messages - HistoryResponseRPC(error: error) - else: - let resp = res.get() + pagingInfo = block: + if resp.cursor.isNone(): + none(PagingInfoRPC) + else: + some(PagingInfoRPC(cursor: resp.cursor.map(toRPC))) - let - messages = resp.messages + error = HistoryResponseErrorRPC.NONE - pagingInfo = block: - if resp.cursor.isNone(): - none(PagingInfoRPC) - else: - some(PagingInfoRPC(cursor: resp.cursor.map(toRPC))) - - error = HistoryResponseErrorRPC.NONE - - HistoryResponseRPC(messages: messages, pagingInfo: pagingInfo, error: error) + HistoryResponseRPC(messages: messages, pagingInfo: pagingInfo, error: error) proc toAPI*(rpc: HistoryResponseRPC): HistoryResult = if rpc.error != HistoryResponseErrorRPC.NONE: diff --git a/waku/waku_store_sync/reconciliation.nim b/waku/waku_store_sync/reconciliation.nim index 8b196a3e9..0cc15d0df 100644 --- a/waku/waku_store_sync/reconciliation.nim +++ b/waku/waku_store_sync/reconciliation.nim @@ -230,10 +230,9 @@ proc processRequest( let writeRes = catch: await conn.writeLP(rawPayload) - if writeRes.isErr(): + writeRes.isOkOr: await conn.close() - return - err("remote " & $conn.peerId & " connection write error: " & writeRes.error.msg) + return err("remote " & $conn.peerId & " connection write error: " & error.msg) trace "sync payload sent", local = self.peerManager.switch.peerInfo.peerId, @@ -286,11 +285,9 @@ proc initiate( let writeRes = catch: await connection.writeLP(sendPayload) - if writeRes.isErr(): + writeRes.isOkOr: await connection.close() - return err( - "remote " & $connection.peerId & " connection write error: " & writeRes.error.msg - ) + return err("remote " & $connection.peerId & " connection write error: " & error.msg) trace "sync payload sent", local = self.peerManager.switch.peerInfo.peerId, diff --git a/waku/waku_store_sync/transfer.nim b/waku/waku_store_sync/transfer.nim index 5e3e376d1..6a600b4e3 100644 --- a/waku/waku_store_sync/transfer.nim +++ b/waku/waku_store_sync/transfer.nim @@ -58,9 +58,8 @@ proc sendMessage( let writeRes = catch: await conn.writeLP(rawPayload) - if writeRes.isErr(): - return - err("remote " & $conn.peerId & " connection write error: " & writeRes.error.msg) + writeRes.isOkOr: + return err("remote [" & $conn.peerId & "] connection write error: " & error.msg) total_transfer_messages_exchanged.inc(labelValues = [Sending]) From 262d33e394dc58e498b226b8afee9626dcd6ff19 Mon Sep 17 00:00:00 2001 From: Simon-Pierre Vivier Date: Thu, 30 Oct 2025 10:53:45 -0400 Subject: [PATCH 006/155] Disable flaky test (#3585) --- tests/waku_store_sync/test_protocol.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/waku_store_sync/test_protocol.nim b/tests/waku_store_sync/test_protocol.nim index bd13716a2..d051eebd7 100644 --- a/tests/waku_store_sync/test_protocol.nim +++ b/tests/waku_store_sync/test_protocol.nim @@ -506,7 +506,7 @@ suite "Waku Sync: reconciliation": let (_, deliveredHash) = await remoteNeeds.get() check deliveredHash in diffMsgHashes - asyncTest "sync 2 nodes, 40 msgs: 18 in-window diff, 20 out-window ignored": + #[ asyncTest "sync 2 nodes, 40 msgs: 17 in-window diff, 20 out-window ignored": server = await newTestWakuRecon( serverSwitch, @[], @[], DefaultSyncRange, idsChannel, localWants, remoteNeeds ) @@ -515,10 +515,10 @@ suite "Waku Sync: reconciliation": ) const - diffInWin = 18 + diffInWin = 17 diffOutWin = 20 stepOutNs = 100_000_000'u64 - outOffsetNs = 2_300_000_000'u64 # for 20 mesg they sent 2 seconds earlier + outOffsetNs = 3_000_000_000'u64 # for 20 mesg they sent 2 seconds earlier randomize() @@ -572,7 +572,7 @@ suite "Waku Sync: reconciliation": for _ in 0 ..< diffInWin: let (_, deliveredHashes) = await remoteNeeds.popFirst() check deliveredHashes in inWinHashes - check deliveredHashes notin outWinHashes + check deliveredHashes notin outWinHashes ]# asyncTest "hash-fingerprint collision, same timestamp – stable sort": server = await newTestWakuRecon( From 1762548741b30206d91d53671f2345dd2514d0a0 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:31:09 +0100 Subject: [PATCH 007/155] chore: clarify api folders (#3637) * Rename waku_api to rest_api and underlying rest to endpoint for clearity * Rename node/api to node/kernel_api to suggest that it is an internal accessor to node interface + make everything compile after renaming * make waku api a top level import * fix use of relative path imports and use default to root rather in case of waku and tools modules --- apps/chat2bridge/chat2bridge.nim | 2 +- .../diagnose_connections.nim | 2 +- .../liteprotocoltester/liteprotocoltester.nim | 2 +- apps/liteprotocoltester/receiver.nim | 2 +- .../service_peer_management.nim | 2 +- apps/liteprotocoltester/statistics.nim | 2 ++ apps/liteprotocoltester/tester_message.nim | 2 +- apps/wakunode2/wakunode2.nim | 2 +- examples/wakustealthcommitments/node_spec.nim | 2 +- .../requests/discovery_request.nim | 2 +- .../requests/node_lifecycle_request.nim | 16 +++++---- .../requests/protocols/filter_request.nim | 2 +- .../requests/protocols/relay_request.nim | 22 ++++++------ tests/node/test_wakunode_filter.nim | 2 +- tests/node/test_wakunode_legacy_lightpush.nim | 2 +- tests/node/test_wakunode_legacy_store.nim | 4 +-- tests/node/test_wakunode_lightpush.nim | 2 +- tests/node/test_wakunode_peer_exchange.nim | 2 +- tests/node/test_wakunode_peer_manager.nim | 2 +- tests/node/test_wakunode_relay_rln.nim | 4 +-- tests/node/test_wakunode_sharding.nim | 2 +- tests/node/test_wakunode_store.nim | 4 +-- tests/test_message_cache.nim | 2 +- tests/waku_discv5/test_waku_discv5.nim | 2 +- tests/waku_filter_v2/test_waku_client.nim | 2 +- tests/wakunode_rest/test_rest_admin.nim | 12 +++---- tests/wakunode_rest/test_rest_cors.nim | 4 +-- tests/wakunode_rest/test_rest_debug.nim | 10 +++--- .../wakunode_rest/test_rest_debug_serdes.nim | 2 +- tests/wakunode_rest/test_rest_filter.nim | 18 +++++----- tests/wakunode_rest/test_rest_health.nim | 10 +++--- tests/wakunode_rest/test_rest_lightpush.nim | 14 ++++---- .../test_rest_lightpush_legacy.nim | 14 ++++---- tests/wakunode_rest/test_rest_relay.nim | 24 ++++++------- .../wakunode_rest/test_rest_relay_serdes.nim | 4 ++- tests/wakunode_rest/test_rest_serdes.nim | 4 +-- tests/wakunode_rest/test_rest_store.nim | 14 ++++---- tools/confutils/cli_args.nim | 4 ++- waku/api.nim | 3 ++ waku/factory/waku.nim | 6 ++-- waku/factory/waku_conf.nim | 4 +-- waku/node/api.nim | 9 ----- .../health_monitor/node_health_monitor.nim | 2 +- waku/node/kernel_api.nim | 9 +++++ waku/node/{api => kernel_api}/filter.nim | 0 waku/node/{api => kernel_api}/lightpush.nim | 0 .../{api => kernel_api}/peer_exchange.nim | 0 waku/node/{api => kernel_api}/ping.nim | 0 waku/node/{api => kernel_api}/relay.nim | 0 waku/node/{api => kernel_api}/store.nim | 0 .../endpoint}/admin/client.nim | 0 .../endpoint}/admin/handlers.nim | 0 .../endpoint}/admin/types.nim | 0 .../rest => rest_api/endpoint}/builder.nim | 36 ++++++++++--------- .../rest => rest_api/endpoint}/client.nim | 0 .../endpoint}/debug/client.nim | 0 .../endpoint}/debug/handlers.nim | 0 .../endpoint}/debug/types.nim | 0 .../endpoint}/filter/client.nim | 0 .../endpoint}/filter/handlers.nim | 0 .../endpoint}/filter/types.nim | 0 .../endpoint}/health/client.nim | 0 .../endpoint}/health/handlers.nim | 0 .../endpoint}/health/types.nim | 0 .../endpoint}/legacy_lightpush/client.nim | 0 .../endpoint}/legacy_lightpush/handlers.nim | 0 .../endpoint}/legacy_lightpush/types.nim | 0 .../endpoint}/legacy_store/client.nim | 0 .../endpoint}/legacy_store/handlers.nim | 0 .../endpoint}/legacy_store/types.nim | 0 .../endpoint}/lightpush/client.nim | 0 .../endpoint}/lightpush/handlers.nim | 0 .../endpoint}/lightpush/types.nim | 0 .../endpoint}/origin_handler.nim | 0 .../endpoint}/relay/client.nim | 0 .../endpoint}/relay/handlers.nim | 0 .../endpoint}/relay/types.nim | 0 .../rest => rest_api/endpoint}/responses.nim | 0 .../endpoint}/rest_serdes.nim | 0 .../rest => rest_api/endpoint}/serdes.nim | 0 .../rest => rest_api/endpoint}/server.nim | 0 .../endpoint}/store/client.nim | 0 .../endpoint}/store/handlers.nim | 0 .../endpoint}/store/types.nim | 0 waku/{waku_api => rest_api}/handlers.nim | 0 waku/{waku_api => rest_api}/message_cache.nim | 0 waku/waku_api.nim | 3 -- waku/waku_archive_legacy/common.nim | 2 +- waku/waku_node.nim | 4 +-- waku/waku_rest.nim | 3 ++ waku/waku_store/common.nim | 2 +- waku/waku_store_legacy/common.nim | 2 +- 92 files changed, 162 insertions(+), 147 deletions(-) create mode 100644 waku/api.nim delete mode 100644 waku/node/api.nim create mode 100644 waku/node/kernel_api.nim rename waku/node/{api => kernel_api}/filter.nim (100%) rename waku/node/{api => kernel_api}/lightpush.nim (100%) rename waku/node/{api => kernel_api}/peer_exchange.nim (100%) rename waku/node/{api => kernel_api}/ping.nim (100%) rename waku/node/{api => kernel_api}/relay.nim (100%) rename waku/node/{api => kernel_api}/store.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/admin/client.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/admin/handlers.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/admin/types.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/builder.nim (86%) rename waku/{waku_api/rest => rest_api/endpoint}/client.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/debug/client.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/debug/handlers.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/debug/types.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/filter/client.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/filter/handlers.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/filter/types.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/health/client.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/health/handlers.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/health/types.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/legacy_lightpush/client.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/legacy_lightpush/handlers.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/legacy_lightpush/types.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/legacy_store/client.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/legacy_store/handlers.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/legacy_store/types.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/lightpush/client.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/lightpush/handlers.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/lightpush/types.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/origin_handler.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/relay/client.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/relay/handlers.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/relay/types.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/responses.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/rest_serdes.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/serdes.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/server.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/store/client.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/store/handlers.nim (100%) rename waku/{waku_api/rest => rest_api/endpoint}/store/types.nim (100%) rename waku/{waku_api => rest_api}/handlers.nim (100%) rename waku/{waku_api => rest_api}/message_cache.nim (100%) delete mode 100644 waku/waku_api.nim create mode 100644 waku/waku_rest.nim diff --git a/apps/chat2bridge/chat2bridge.nim b/apps/chat2bridge/chat2bridge.nim index 9a22572cd..77c9e553e 100644 --- a/apps/chat2bridge/chat2bridge.nim +++ b/apps/chat2bridge/chat2bridge.nim @@ -240,7 +240,7 @@ proc stop*(cmb: Chat2MatterBridge) {.async: (raises: [Exception]).} = {.pop.} # @TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError when isMainModule: - import waku/common/utils/nat, waku/waku_api/message_cache + import waku/common/utils/nat, waku/rest_api/message_cache let rng = newRng() diff --git a/apps/liteprotocoltester/diagnose_connections.nim b/apps/liteprotocoltester/diagnose_connections.nim index 15c0768f4..d2cc75516 100644 --- a/apps/liteprotocoltester/diagnose_connections.nim +++ b/apps/liteprotocoltester/diagnose_connections.nim @@ -14,7 +14,7 @@ import libp2p/wire import - ../../tools/confutils/cli_args, + tools/confutils/cli_args, waku/[ node/peer_manager, waku_lightpush/common, diff --git a/apps/liteprotocoltester/liteprotocoltester.nim b/apps/liteprotocoltester/liteprotocoltester.nim index 7778183d1..adb1b0f8a 100644 --- a/apps/liteprotocoltester/liteprotocoltester.nim +++ b/apps/liteprotocoltester/liteprotocoltester.nim @@ -11,7 +11,7 @@ import confutils import - ../../tools/confutils/cli_args, + tools/confutils/cli_args, waku/[ common/enr, common/logging, diff --git a/apps/liteprotocoltester/receiver.nim b/apps/liteprotocoltester/receiver.nim index 9792549ca..b62094ec6 100644 --- a/apps/liteprotocoltester/receiver.nim +++ b/apps/liteprotocoltester/receiver.nim @@ -77,7 +77,7 @@ proc maintainSubscription( some(filterPubsubTopic), filterContentTopic, actualFilterPeer ) ).errorOr: - await sleepAsync(subscriptionMaintenanceMs) + await sleepAsync(SubscriptionMaintenanceMs) if noFailedSubscribes > 0: noFailedSubscribes -= 1 notice "subscribe request successful." diff --git a/apps/liteprotocoltester/service_peer_management.nim b/apps/liteprotocoltester/service_peer_management.nim index 053445740..d5cfafef1 100644 --- a/apps/liteprotocoltester/service_peer_management.nim +++ b/apps/liteprotocoltester/service_peer_management.nim @@ -11,7 +11,7 @@ import libp2p/wire import - ../wakunode2/cli_args, + tools/confutils/cli_args, waku/[ common/enr, waku_node, diff --git a/apps/liteprotocoltester/statistics.nim b/apps/liteprotocoltester/statistics.nim index 5ca215b2c..7feebd4cf 100644 --- a/apps/liteprotocoltester/statistics.nim +++ b/apps/liteprotocoltester/statistics.nim @@ -8,6 +8,8 @@ import results, libp2p/peerid +from std/sugar import `=>` + import ./tester_message, ./lpt_metrics type diff --git a/apps/liteprotocoltester/tester_message.nim b/apps/liteprotocoltester/tester_message.nim index eeff7b531..38028e4a7 100644 --- a/apps/liteprotocoltester/tester_message.nim +++ b/apps/liteprotocoltester/tester_message.nim @@ -6,7 +6,7 @@ import json_serialization/std/options, json_serialization/lexer -import ../../waku/waku_api/rest/serdes +import waku/rest_api/endpoint/serdes type ProtocolTesterMessage* = object sender*: string diff --git a/apps/wakunode2/wakunode2.nim b/apps/wakunode2/wakunode2.nim index 86db3fbc4..b50c7113b 100644 --- a/apps/wakunode2/wakunode2.nim +++ b/apps/wakunode2/wakunode2.nim @@ -14,7 +14,7 @@ import common/logging, factory/waku, node/health_monitor, - waku_api/rest/builder as rest_server_builder, + rest_api/endpoint/builder as rest_server_builder, waku_core/message/default_values, ] diff --git a/examples/wakustealthcommitments/node_spec.nim b/examples/wakustealthcommitments/node_spec.nim index 21286340e..d85e83a5b 100644 --- a/examples/wakustealthcommitments/node_spec.nim +++ b/examples/wakustealthcommitments/node_spec.nim @@ -1,6 +1,6 @@ {.push raises: [].} -import ../../apps/wakunode2/cli_args +import tools/confutils/cli_args import waku/[common/logging, factory/[waku, networks_config]] import std/[options, strutils, os, sequtils], diff --git a/library/waku_thread_requests/requests/discovery_request.nim b/library/waku_thread_requests/requests/discovery_request.nim index 6f6780a2f..405483a46 100644 --- a/library/waku_thread_requests/requests/discovery_request.nim +++ b/library/waku_thread_requests/requests/discovery_request.nim @@ -6,7 +6,7 @@ import ../../../waku/discovery/waku_discv5, ../../../waku/waku_core/peers, ../../../waku/node/waku_node, - ../../../waku/node/api, + ../../../waku/node/kernel_api, ../../alloc type DiscoveryMsgType* = enum diff --git a/library/waku_thread_requests/requests/node_lifecycle_request.nim b/library/waku_thread_requests/requests/node_lifecycle_request.nim index 270bdf1ce..aa71ac6bb 100644 --- a/library/waku_thread_requests/requests/node_lifecycle_request.nim +++ b/library/waku_thread_requests/requests/node_lifecycle_request.nim @@ -2,13 +2,15 @@ import std/[options, json, strutils, net] import chronos, chronicles, results, confutils, confutils/std/net import - ../../../waku/node/peer_manager/peer_manager, - ../../../tools/confutils/cli_args, - ../../../waku/factory/waku, - ../../../waku/factory/node_factory, - ../../../waku/factory/networks_config, - ../../../waku/factory/app_callbacks, - ../../../waku/waku_api/rest/builder, + waku/node/peer_manager/peer_manager, + tools/confutils/cli_args, + waku/factory/waku, + waku/factory/node_factory, + waku/factory/networks_config, + waku/factory/app_callbacks, + waku/rest_api/endpoint/builder + +import ../../alloc type NodeLifecycleMsgType* = enum diff --git a/library/waku_thread_requests/requests/protocols/filter_request.nim b/library/waku_thread_requests/requests/protocols/filter_request.nim index c0a99f1f9..cd401d443 100644 --- a/library/waku_thread_requests/requests/protocols/filter_request.nim +++ b/library/waku_thread_requests/requests/protocols/filter_request.nim @@ -8,7 +8,7 @@ import ../../../../waku/waku_core/subscription/push_handler, ../../../../waku/node/peer_manager/peer_manager, ../../../../waku/node/waku_node, - ../../../../waku/node/api, + ../../../../waku/node/kernel_api, ../../../../waku/waku_core/topics/pubsub_topic, ../../../../waku/waku_core/topics/content_topic, ../../../alloc diff --git a/library/waku_thread_requests/requests/protocols/relay_request.nim b/library/waku_thread_requests/requests/protocols/relay_request.nim index 5c0732768..e110f689e 100644 --- a/library/waku_thread_requests/requests/protocols/relay_request.nim +++ b/library/waku_thread_requests/requests/protocols/relay_request.nim @@ -1,16 +1,18 @@ import std/[net, sequtils, strutils] import chronicles, chronos, stew/byteutils, results import - ../../../../waku/waku_core/message/message, - ../../../../waku/factory/[validator_signed, waku], - ../../../../tools/confutils/cli_args, - ../../../../waku/waku_node, - ../../../../waku/waku_core/message, - ../../../../waku/waku_core/time, # Timestamp - ../../../../waku/waku_core/topics/pubsub_topic, - ../../../../waku/waku_core/topics, - ../../../../waku/waku_relay/protocol, - ../../../../waku/node/peer_manager, + waku/waku_core/message/message, + waku/factory/[validator_signed, waku], + tools/confutils/cli_args, + waku/waku_node, + waku/waku_core/message, + waku/waku_core/time, # Timestamp + waku/waku_core/topics/pubsub_topic, + waku/waku_core/topics, + waku/waku_relay/protocol, + waku/node/peer_manager + +import ../../../alloc type RelayMsgType* = enum diff --git a/tests/node/test_wakunode_filter.nim b/tests/node/test_wakunode_filter.nim index 04db575ab..2777b0124 100644 --- a/tests/node/test_wakunode_filter.nim +++ b/tests/node/test_wakunode_filter.nim @@ -12,7 +12,7 @@ import waku_core, node/peer_manager, node/waku_node, - node/api, + node/kernel_api, waku_filter_v2, waku_filter_v2/client, waku_filter_v2/subscriptions, diff --git a/tests/node/test_wakunode_legacy_lightpush.nim b/tests/node/test_wakunode_legacy_lightpush.nim index 9525892a1..a51ba60b9 100644 --- a/tests/node/test_wakunode_legacy_lightpush.nim +++ b/tests/node/test_wakunode_legacy_lightpush.nim @@ -12,7 +12,7 @@ import waku_core, node/peer_manager, node/waku_node, - node/api, + node/kernel_api, waku_lightpush_legacy, waku_lightpush_legacy/common, waku_lightpush_legacy/protocol_metrics, diff --git a/tests/node/test_wakunode_legacy_store.nim b/tests/node/test_wakunode_legacy_store.nim index 1863066bc..bf8003743 100644 --- a/tests/node/test_wakunode_legacy_store.nim +++ b/tests/node/test_wakunode_legacy_store.nim @@ -6,7 +6,7 @@ import waku/[ common/paging, node/waku_node, - node/api, + node/kernel_api, node/peer_manager, waku_core, waku_store_legacy, @@ -446,7 +446,7 @@ suite "Waku Store - End to End - Sorted Archive": await otherServer.start() let otherServerRemotePeerInfo = otherServer.peerInfo.toRemotePeerInfo() - # When making a history query to the first server node + # When making a history query to the first server node let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) # Then the response contains the messages diff --git a/tests/node/test_wakunode_lightpush.nim b/tests/node/test_wakunode_lightpush.nim index 27c3a4d3e..12bfdddd8 100644 --- a/tests/node/test_wakunode_lightpush.nim +++ b/tests/node/test_wakunode_lightpush.nim @@ -12,7 +12,7 @@ import waku_core, node/peer_manager, node/waku_node, - node/api, + node/kernel_api, waku_lightpush, waku_rln_relay, ], diff --git a/tests/node/test_wakunode_peer_exchange.nim b/tests/node/test_wakunode_peer_exchange.nim index 4ebeae4ae..9b0ea4c40 100644 --- a/tests/node/test_wakunode_peer_exchange.nim +++ b/tests/node/test_wakunode_peer_exchange.nim @@ -14,7 +14,7 @@ import import waku/[ waku_node, - node/api, + node/kernel_api, discovery/waku_discv5, waku_peer_exchange, node/peer_manager, diff --git a/tests/node/test_wakunode_peer_manager.nim b/tests/node/test_wakunode_peer_manager.nim index 6b1c2a427..ed58db7fe 100644 --- a/tests/node/test_wakunode_peer_manager.nim +++ b/tests/node/test_wakunode_peer_manager.nim @@ -17,7 +17,7 @@ import waku_core, node/peer_manager, node/waku_node, - node/api, + node/kernel_api, discovery/waku_discv5, waku_filter_v2/common, waku_relay/protocol, diff --git a/tests/node/test_wakunode_relay_rln.nim b/tests/node/test_wakunode_relay_rln.nim index 1acf6b590..9c5c928f0 100644 --- a/tests/node/test_wakunode_relay_rln.nim +++ b/tests/node/test_wakunode_relay_rln.nim @@ -17,7 +17,7 @@ import node/peer_manager, waku_core, waku_node, - node/api, + node/kernel_api, common/error_handling, waku_rln_relay, waku_rln_relay/rln, @@ -514,7 +514,7 @@ suite "Waku RlnRelay - End to End - OnChain": ## Issues ### TreeIndex For some reason the calls to `getWakuRlnConfigOnChain` need to be made with `treeIndex` = 0 and 1, in that order. - But the registration needs to be made with 1 and 2. + But the registration needs to be made with 1 and 2. #### Solutions Requires investigation ### Monkeypatching diff --git a/tests/node/test_wakunode_sharding.nim b/tests/node/test_wakunode_sharding.nim index 945c22eee..eefd8f06e 100644 --- a/tests/node/test_wakunode_sharding.nim +++ b/tests/node/test_wakunode_sharding.nim @@ -16,7 +16,7 @@ import waku_core/topics/sharding, waku_store_legacy/common, node/waku_node, - node/api, + node/kernel_api, common/paging, waku_core, waku_store/common, diff --git a/tests/node/test_wakunode_store.nim b/tests/node/test_wakunode_store.nim index 284b32e64..9f312afd5 100644 --- a/tests/node/test_wakunode_store.nim +++ b/tests/node/test_wakunode_store.nim @@ -6,7 +6,7 @@ import waku/[ common/paging, node/waku_node, - node/api, + node/kernel_api, node/peer_manager, waku_core, waku_core/message/digest, @@ -486,7 +486,7 @@ suite "Waku Store - End to End - Sorted Archive": await otherServer.start() let otherServerRemotePeerInfo = otherServer.peerInfo.toRemotePeerInfo() - # When making a history query to the first server node + # When making a history query to the first server node let queryResponse = await client.query(storeQuery, serverRemotePeerInfo) # Then the response contains the messages diff --git a/tests/test_message_cache.nim b/tests/test_message_cache.nim index cd2e882c1..95904f8f2 100644 --- a/tests/test_message_cache.nim +++ b/tests/test_message_cache.nim @@ -1,7 +1,7 @@ {.used.} import std/[sets, random], results, stew/byteutils, testutils/unittests -import waku/waku_core, waku/waku_api/message_cache, ./testlib/wakucore +import waku/waku_core, waku/rest_api/message_cache, ./testlib/wakucore randomize() diff --git a/tests/waku_discv5/test_waku_discv5.nim b/tests/waku_discv5/test_waku_discv5.nim index d1cd6c46f..6685bda32 100644 --- a/tests/waku_discv5/test_waku_discv5.nim +++ b/tests/waku_discv5/test_waku_discv5.nim @@ -22,7 +22,7 @@ import factory/conf_builder/conf_builder, factory/waku, node/waku_node, - node/api, + node/kernel_api, node/peer_manager, ], ../testlib/[wakucore, testasync, assertions, futures, wakunode, testutils], diff --git a/tests/waku_filter_v2/test_waku_client.nim b/tests/waku_filter_v2/test_waku_client.nim index 6ae1f2902..c57699d39 100644 --- a/tests/waku_filter_v2/test_waku_client.nim +++ b/tests/waku_filter_v2/test_waku_client.nim @@ -3,7 +3,7 @@ import std/[options, sequtils, json], testutils/unittests, results, chronos import - waku/node/[peer_manager, waku_node, api], + waku/node/[peer_manager, waku_node, kernel_api], waku/waku_core, waku/waku_filter_v2/[common, client, subscriptions, protocol, rpc_codec], ../testlib/[wakucore, testasync, testutils, futures, sequtils, wakunode], diff --git a/tests/wakunode_rest/test_rest_admin.nim b/tests/wakunode_rest/test_rest_admin.nim index e47207a42..6de886f74 100644 --- a/tests/wakunode_rest/test_rest_admin.nim +++ b/tests/wakunode_rest/test_rest_admin.nim @@ -14,12 +14,12 @@ import waku_node, waku_filter_v2/client, node/peer_manager, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/admin/types, - waku_api/rest/admin/handlers as admin_api, - waku_api/rest/admin/client as admin_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/admin/types, + rest_api/endpoint/admin/handlers as admin_rest_interface, + rest_api/endpoint/admin/client as admin_rest_client, waku_relay, waku_peer_exchange, ], diff --git a/tests/wakunode_rest/test_rest_cors.nim b/tests/wakunode_rest/test_rest_cors.nim index 58e70aa25..0393b0d72 100644 --- a/tests/wakunode_rest/test_rest_cors.nim +++ b/tests/wakunode_rest/test_rest_cors.nim @@ -11,8 +11,8 @@ import waku/[ waku_node, node/waku_node as waku_node2, - waku_api/rest/server, - waku_api/rest/debug/handlers as debug_api, + rest_api/endpoint/server, + rest_api/endpoint/debug/handlers as debug_rest_interface, ], ../testlib/common, ../testlib/wakucore, diff --git a/tests/wakunode_rest/test_rest_debug.nim b/tests/wakunode_rest/test_rest_debug.nim index 9add57cbe..4bd2e8c02 100644 --- a/tests/wakunode_rest/test_rest_debug.nim +++ b/tests/wakunode_rest/test_rest_debug.nim @@ -12,11 +12,11 @@ import waku_node, node/waku_node as waku_node2, # TODO: Remove after moving `git_version` to the app code. - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/debug/handlers as debug_api, - waku_api/rest/debug/client as debug_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/debug/handlers as debug_rest_interface, + rest_api/endpoint/debug/client as debug_rest_client, ], ../testlib/common, ../testlib/wakucore, diff --git a/tests/wakunode_rest/test_rest_debug_serdes.nim b/tests/wakunode_rest/test_rest_debug_serdes.nim index 13b791dc9..d3232e571 100644 --- a/tests/wakunode_rest/test_rest_debug_serdes.nim +++ b/tests/wakunode_rest/test_rest_debug_serdes.nim @@ -1,7 +1,7 @@ {.used.} import results, stew/byteutils, testutils/unittests, json_serialization -import waku/waku_api/rest/serdes, waku/waku_api/rest/debug/types +import waku/rest_api/endpoint/serdes, waku/rest_api/endpoint/debug/types suite "Waku v2 REST API - Debug - serialization": suite "DebugWakuInfo - decode": diff --git a/tests/wakunode_rest/test_rest_filter.nim b/tests/wakunode_rest/test_rest_filter.nim index f8dbf429a..3d4d741a5 100644 --- a/tests/wakunode_rest/test_rest_filter.nim +++ b/tests/wakunode_rest/test_rest_filter.nim @@ -9,21 +9,21 @@ import libp2p/crypto/crypto import waku/[ - waku_api/message_cache, + rest_api/message_cache, waku_core, waku_node, node/peer_manager, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/filter/types, - waku_api/rest/filter/handlers as filter_api, - waku_api/rest/filter/client as filter_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/filter/types, + rest_api/endpoint/filter/handlers as filter_rest_interface, + rest_api/endpoint/filter/client as filter_rest_client, waku_relay, waku_filter_v2/subscriptions, waku_filter_v2/common, - waku_api/rest/relay/handlers as relay_api, - waku_api/rest/relay/client as relay_api_client, + rest_api/endpoint/relay/handlers as relay_rest_interface, + rest_api/endpoint/relay/client as relay_rest_client, ], ../testlib/wakucore, ../testlib/wakunode diff --git a/tests/wakunode_rest/test_rest_health.nim b/tests/wakunode_rest/test_rest_health.nim index ec70b0874..dacfd801e 100644 --- a/tests/wakunode_rest/test_rest_health.nim +++ b/tests/wakunode_rest/test_rest_health.nim @@ -13,11 +13,11 @@ import waku_node, node/waku_node as waku_node2, # TODO: Remove after moving `git_version` to the app code. - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/health/handlers as health_api, - waku_api/rest/health/client as health_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/health/handlers as health_rest_interface, + rest_api/endpoint/health/client as health_rest_client, waku_rln_relay, node/health_monitor, ], diff --git a/tests/wakunode_rest/test_rest_lightpush.nim b/tests/wakunode_rest/test_rest_lightpush.nim index b09c72ee3..cc5c715b8 100644 --- a/tests/wakunode_rest/test_rest_lightpush.nim +++ b/tests/wakunode_rest/test_rest_lightpush.nim @@ -10,17 +10,17 @@ import import waku/[ - waku_api/message_cache, + rest_api/message_cache, waku_core, waku_node, node/peer_manager, waku_lightpush/common, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/lightpush/types, - waku_api/rest/lightpush/handlers as lightpush_api, - waku_api/rest/lightpush/client as lightpush_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/lightpush/types, + rest_api/endpoint/lightpush/handlers as lightpush_rest_interface, + rest_api/endpoint/lightpush/client as lightpush_rest_client, waku_relay, common/rate_limit/setting, ], diff --git a/tests/wakunode_rest/test_rest_lightpush_legacy.nim b/tests/wakunode_rest/test_rest_lightpush_legacy.nim index fea51554b..526a6c24e 100644 --- a/tests/wakunode_rest/test_rest_lightpush_legacy.nim +++ b/tests/wakunode_rest/test_rest_lightpush_legacy.nim @@ -10,17 +10,17 @@ import import waku/[ - waku_api/message_cache, + rest_api/message_cache, waku_core, waku_node, node/peer_manager, waku_lightpush_legacy/common, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/legacy_lightpush/types, - waku_api/rest/legacy_lightpush/handlers as lightpush_api, - waku_api/rest/legacy_lightpush/client as lightpush_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/legacy_lightpush/types, + rest_api/endpoint/legacy_lightpush/handlers as lightpush_rest_interface, + rest_api/endpoint/legacy_lightpush/client as lightpush_rest_client, waku_relay, common/rate_limit/setting, ], diff --git a/tests/wakunode_rest/test_rest_relay.nim b/tests/wakunode_rest/test_rest_relay.nim index 99470dbc8..ca9f7cb17 100644 --- a/tests/wakunode_rest/test_rest_relay.nim +++ b/tests/wakunode_rest/test_rest_relay.nim @@ -12,13 +12,13 @@ import common/base64, waku_core, waku_node, - waku_api/message_cache, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/relay/types, - waku_api/rest/relay/handlers as relay_api, - waku_api/rest/relay/client as relay_api_client, + rest_api/message_cache, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/relay/types, + rest_api/endpoint/relay/handlers as relay_rest_interface, + rest_api/endpoint/relay/client as relay_rest_client, waku_relay, waku_rln_relay, ], @@ -263,7 +263,7 @@ suite "Waku v2 Rest API - Relay": await node.mountRlnRelay(wakuRlnConfig) await node.start() - # Registration is mandatory before sending messages with rln-relay + # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() @@ -514,7 +514,7 @@ suite "Waku v2 Rest API - Relay": await node.mountRlnRelay(wakuRlnConfig) await node.start() - # Registration is mandatory before sending messages with rln-relay + # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() @@ -586,7 +586,7 @@ suite "Waku v2 Rest API - Relay": await node.mountRlnRelay(wakuRlnConfig) await node.start() - # Registration is mandatory before sending messages with rln-relay + # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() @@ -648,7 +648,7 @@ suite "Waku v2 Rest API - Relay": await node.mountRlnRelay(wakuRlnConfig) await node.start() - # Registration is mandatory before sending messages with rln-relay + # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() @@ -723,7 +723,7 @@ suite "Waku v2 Rest API - Relay": await node.mountRlnRelay(wakuRlnConfig) await node.start() - # Registration is mandatory before sending messages with rln-relay + # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() diff --git a/tests/wakunode_rest/test_rest_relay_serdes.nim b/tests/wakunode_rest/test_rest_relay_serdes.nim index 086aba22b..21d21e281 100644 --- a/tests/wakunode_rest/test_rest_relay_serdes.nim +++ b/tests/wakunode_rest/test_rest_relay_serdes.nim @@ -1,7 +1,9 @@ {.used.} import results, stew/byteutils, unittest2, json_serialization -import waku/[common/base64, waku_api/rest/serdes, waku_api/rest/relay/types, waku_core] +import + waku/ + [common/base64, rest_api/endpoint/serdes, rest_api/endpoint/relay/types, waku_core] suite "Waku v2 Rest API - Relay - serialization": suite "RelayWakuMessage - decode": diff --git a/tests/wakunode_rest/test_rest_serdes.nim b/tests/wakunode_rest/test_rest_serdes.nim index 719742bf8..2237e9216 100644 --- a/tests/wakunode_rest/test_rest_serdes.nim +++ b/tests/wakunode_rest/test_rest_serdes.nim @@ -1,9 +1,9 @@ {.used.} import results, stew/byteutils, chronicles, unittest2, json_serialization -import waku/waku_api/rest/serdes, waku/waku_api/rest/debug/types +import waku/rest_api/endpoint/serdes, waku/rest_api/endpoint/debug/types -# TODO: Decouple this test suite from the `debug_api` module by defining +# TODO: Decouple this test suite from the `debug_rest_interface` module by defining # private custom types for this test suite module suite "Waku v2 Rest API - Serdes": suite "decode": diff --git a/tests/wakunode_rest/test_rest_store.nim b/tests/wakunode_rest/test_rest_store.nim index b86513f0d..70a3c137a 100644 --- a/tests/wakunode_rest/test_rest_store.nim +++ b/tests/wakunode_rest/test_rest_store.nim @@ -17,12 +17,12 @@ import waku_core/time, waku_node, node/peer_manager, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/store/handlers as store_api, - waku_api/rest/store/client as store_api_client, - waku_api/rest/store/types, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/store/handlers as store_rest_interface, + rest_api/endpoint/store/client as store_rest_client, + rest_api/endpoint/store/types, waku_archive, waku_archive/driver/queue_driver, waku_archive/driver/sqlite_driver, @@ -34,7 +34,7 @@ import ../testlib/wakunode logScope: - topics = "waku node rest store_api test" + topics = "waku node rest store_rest_interface test" proc put( store: ArchiveDriver, pubsubTopic: PubsubTopic, message: WakuMessage diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index fb8437299..e6b3fc97d 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -34,7 +34,9 @@ import import ./envvar as confEnvvarDefs, ./envvar_net as confEnvvarNet -export confTomlDefs, confTomlNet, confEnvvarDefs, confEnvvarNet, ProtectedShard +export + confTomlDefs, confTomlNet, confEnvvarDefs, confEnvvarNet, ProtectedShard, + DefaultMaxWakuMessageSizeStr logScope: topics = "waku cli args" diff --git a/waku/api.nim b/waku/api.nim new file mode 100644 index 000000000..c3211867d --- /dev/null +++ b/waku/api.nim @@ -0,0 +1,3 @@ +import ./api/[api, api_conf, entry_nodes] + +export api, api_conf, entry_nodes diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 0adebd44e..bed8a9137 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -28,9 +28,9 @@ import ../node/health_monitor, ../node/waku_metrics, ../node/delivery_monitor/delivery_monitor, - ../waku_api/message_cache, - ../waku_api/rest/server, - ../waku_api/rest/builder as rest_server_builder, + ../rest_api/message_cache, + ../rest_api/endpoint/server, + ../rest_api/endpoint/builder as rest_server_builder, ../waku_archive, ../waku_relay/protocol, ../discovery/waku_dnsdisc, diff --git a/waku/factory/waku_conf.nim b/waku/factory/waku_conf.nim index fe6ead490..89ffb366c 100644 --- a/waku/factory/waku_conf.nim +++ b/waku/factory/waku_conf.nim @@ -9,7 +9,7 @@ import import ../waku_rln_relay/rln_relay, - ../waku_api/rest/builder, + ../rest_api/endpoint/builder, ../discovery/waku_discv5, ../node/waku_metrics, ../common/logging, @@ -38,7 +38,7 @@ type ProtectedShard* {.requiresInit.} = object type DnsDiscoveryConf* {.requiresInit.} = object enrTreeUrl*: string - # TODO: should probably only have one set of name servers (see dnsaddrs) + # TODO: should probably only have one set of name servers (see dnsaddrs) nameServers*: seq[IpAddress] type StoreSyncConf* {.requiresInit.} = object diff --git a/waku/node/api.nim b/waku/node/api.nim deleted file mode 100644 index 6f8f1cdd9..000000000 --- a/waku/node/api.nim +++ /dev/null @@ -1,9 +0,0 @@ -import - ./api/filter as filter_api, - ./api/lightpush as lightpush_api, - ./api/store as store_api, - ./api/relay as relay_api, - ./api/peer_exchange as peer_exchange_api, - ./api/ping as ping_api - -export filter_api, lightpush_api, store_api, relay_api, peer_exchange_api, ping_api diff --git a/waku/node/health_monitor/node_health_monitor.nim b/waku/node/health_monitor/node_health_monitor.nim index a98e6577a..eb5d0ed8c 100644 --- a/waku/node/health_monitor/node_health_monitor.nim +++ b/waku/node/health_monitor/node_health_monitor.nim @@ -8,7 +8,7 @@ import import ../waku_node, - ../api, + ../kernel_api, ../../waku_rln_relay, ../../waku_relay, ../peer_manager, diff --git a/waku/node/kernel_api.nim b/waku/node/kernel_api.nim new file mode 100644 index 000000000..9d19acb07 --- /dev/null +++ b/waku/node/kernel_api.nim @@ -0,0 +1,9 @@ +import + ./kernel_api/filter as filter_api, + ./kernel_api/lightpush as lightpush_api, + ./kernel_api/store as store_api, + ./kernel_api/relay as relay_api, + ./kernel_api/peer_exchange as peer_exchange_api, + ./kernel_api/ping as ping_api + +export filter_api, lightpush_api, store_api, relay_api, peer_exchange_api, ping_api diff --git a/waku/node/api/filter.nim b/waku/node/kernel_api/filter.nim similarity index 100% rename from waku/node/api/filter.nim rename to waku/node/kernel_api/filter.nim diff --git a/waku/node/api/lightpush.nim b/waku/node/kernel_api/lightpush.nim similarity index 100% rename from waku/node/api/lightpush.nim rename to waku/node/kernel_api/lightpush.nim diff --git a/waku/node/api/peer_exchange.nim b/waku/node/kernel_api/peer_exchange.nim similarity index 100% rename from waku/node/api/peer_exchange.nim rename to waku/node/kernel_api/peer_exchange.nim diff --git a/waku/node/api/ping.nim b/waku/node/kernel_api/ping.nim similarity index 100% rename from waku/node/api/ping.nim rename to waku/node/kernel_api/ping.nim diff --git a/waku/node/api/relay.nim b/waku/node/kernel_api/relay.nim similarity index 100% rename from waku/node/api/relay.nim rename to waku/node/kernel_api/relay.nim diff --git a/waku/node/api/store.nim b/waku/node/kernel_api/store.nim similarity index 100% rename from waku/node/api/store.nim rename to waku/node/kernel_api/store.nim diff --git a/waku/waku_api/rest/admin/client.nim b/waku/rest_api/endpoint/admin/client.nim similarity index 100% rename from waku/waku_api/rest/admin/client.nim rename to waku/rest_api/endpoint/admin/client.nim diff --git a/waku/waku_api/rest/admin/handlers.nim b/waku/rest_api/endpoint/admin/handlers.nim similarity index 100% rename from waku/waku_api/rest/admin/handlers.nim rename to waku/rest_api/endpoint/admin/handlers.nim diff --git a/waku/waku_api/rest/admin/types.nim b/waku/rest_api/endpoint/admin/types.nim similarity index 100% rename from waku/waku_api/rest/admin/types.nim rename to waku/rest_api/endpoint/admin/types.nim diff --git a/waku/waku_api/rest/builder.nim b/waku/rest_api/endpoint/builder.nim similarity index 86% rename from waku/waku_api/rest/builder.nim rename to waku/rest_api/endpoint/builder.nim index eb514439f..bbd8de422 100644 --- a/waku/waku_api/rest/builder.nim +++ b/waku/rest_api/endpoint/builder.nim @@ -5,18 +5,18 @@ import presto import waku/waku_node, waku/discovery/waku_discv5, - waku/waku_api/message_cache, - waku/waku_api/handlers, - waku/waku_api/rest/server, - waku/waku_api/rest/debug/handlers as rest_debug_api, - waku/waku_api/rest/relay/handlers as rest_relay_api, - waku/waku_api/rest/filter/handlers as rest_filter_api, - waku/waku_api/rest/legacy_lightpush/handlers as rest_legacy_lightpush_api, - waku/waku_api/rest/lightpush/handlers as rest_lightpush_api, - waku/waku_api/rest/store/handlers as rest_store_api, - waku/waku_api/rest/legacy_store/handlers as rest_store_legacy_api, - waku/waku_api/rest/health/handlers as rest_health_api, - waku/waku_api/rest/admin/handlers as rest_admin_api, + waku/rest_api/message_cache, + waku/rest_api/handlers, + waku/rest_api/endpoint/server, + waku/rest_api/endpoint/debug/handlers as rest_debug_endpoint, + waku/rest_api/endpoint/relay/handlers as rest_relay_endpoint, + waku/rest_api/endpoint/filter/handlers as rest_filter_endpoint, + waku/rest_api/endpoint/legacy_lightpush/handlers as rest_legacy_lightpush_endpoint, + waku/rest_api/endpoint/lightpush/handlers as rest_lightpush_endpoint, + waku/rest_api/endpoint/store/handlers as rest_store_endpoint, + waku/rest_api/endpoint/legacy_store/handlers as rest_store_legacy_endpoint, + waku/rest_api/endpoint/health/handlers as rest_health_endpoint, + waku/rest_api/endpoint/admin/handlers as rest_admin_endpoint, waku/waku_core/topics, waku/waku_relay/protocol @@ -180,7 +180,7 @@ proc startRestServerProtocolSupport*( else: none(DiscoveryHandler) - rest_filter_api.installFilterRestApiHandlers( + rest_filter_endpoint.installFilterRestApiHandlers( router, node, filterCache, filterDiscoHandler ) else: @@ -193,8 +193,8 @@ proc startRestServerProtocolSupport*( else: none(DiscoveryHandler) - rest_store_api.installStoreApiHandlers(router, node, storeDiscoHandler) - rest_store_legacy_api.installStoreApiHandlers(router, node, storeDiscoHandler) + rest_store_endpoint.installStoreApiHandlers(router, node, storeDiscoHandler) + rest_store_legacy_endpoint.installStoreApiHandlers(router, node, storeDiscoHandler) ## Light push API ## Install it either if client is mounted) @@ -208,10 +208,12 @@ proc startRestServerProtocolSupport*( else: none(DiscoveryHandler) - rest_legacy_lightpush_api.installLightPushRequestHandler( + rest_legacy_lightpush_endpoint.installLightPushRequestHandler( + router, node, lightDiscoHandler + ) + rest_lightpush_endpoint.installLightPushRequestHandler( router, node, lightDiscoHandler ) - rest_lightpush_api.installLightPushRequestHandler(router, node, lightDiscoHandler) else: restServerNotInstalledTab["lightpush"] = "/lightpush endpoints are not available." diff --git a/waku/waku_api/rest/client.nim b/waku/rest_api/endpoint/client.nim similarity index 100% rename from waku/waku_api/rest/client.nim rename to waku/rest_api/endpoint/client.nim diff --git a/waku/waku_api/rest/debug/client.nim b/waku/rest_api/endpoint/debug/client.nim similarity index 100% rename from waku/waku_api/rest/debug/client.nim rename to waku/rest_api/endpoint/debug/client.nim diff --git a/waku/waku_api/rest/debug/handlers.nim b/waku/rest_api/endpoint/debug/handlers.nim similarity index 100% rename from waku/waku_api/rest/debug/handlers.nim rename to waku/rest_api/endpoint/debug/handlers.nim diff --git a/waku/waku_api/rest/debug/types.nim b/waku/rest_api/endpoint/debug/types.nim similarity index 100% rename from waku/waku_api/rest/debug/types.nim rename to waku/rest_api/endpoint/debug/types.nim diff --git a/waku/waku_api/rest/filter/client.nim b/waku/rest_api/endpoint/filter/client.nim similarity index 100% rename from waku/waku_api/rest/filter/client.nim rename to waku/rest_api/endpoint/filter/client.nim diff --git a/waku/waku_api/rest/filter/handlers.nim b/waku/rest_api/endpoint/filter/handlers.nim similarity index 100% rename from waku/waku_api/rest/filter/handlers.nim rename to waku/rest_api/endpoint/filter/handlers.nim diff --git a/waku/waku_api/rest/filter/types.nim b/waku/rest_api/endpoint/filter/types.nim similarity index 100% rename from waku/waku_api/rest/filter/types.nim rename to waku/rest_api/endpoint/filter/types.nim diff --git a/waku/waku_api/rest/health/client.nim b/waku/rest_api/endpoint/health/client.nim similarity index 100% rename from waku/waku_api/rest/health/client.nim rename to waku/rest_api/endpoint/health/client.nim diff --git a/waku/waku_api/rest/health/handlers.nim b/waku/rest_api/endpoint/health/handlers.nim similarity index 100% rename from waku/waku_api/rest/health/handlers.nim rename to waku/rest_api/endpoint/health/handlers.nim diff --git a/waku/waku_api/rest/health/types.nim b/waku/rest_api/endpoint/health/types.nim similarity index 100% rename from waku/waku_api/rest/health/types.nim rename to waku/rest_api/endpoint/health/types.nim diff --git a/waku/waku_api/rest/legacy_lightpush/client.nim b/waku/rest_api/endpoint/legacy_lightpush/client.nim similarity index 100% rename from waku/waku_api/rest/legacy_lightpush/client.nim rename to waku/rest_api/endpoint/legacy_lightpush/client.nim diff --git a/waku/waku_api/rest/legacy_lightpush/handlers.nim b/waku/rest_api/endpoint/legacy_lightpush/handlers.nim similarity index 100% rename from waku/waku_api/rest/legacy_lightpush/handlers.nim rename to waku/rest_api/endpoint/legacy_lightpush/handlers.nim diff --git a/waku/waku_api/rest/legacy_lightpush/types.nim b/waku/rest_api/endpoint/legacy_lightpush/types.nim similarity index 100% rename from waku/waku_api/rest/legacy_lightpush/types.nim rename to waku/rest_api/endpoint/legacy_lightpush/types.nim diff --git a/waku/waku_api/rest/legacy_store/client.nim b/waku/rest_api/endpoint/legacy_store/client.nim similarity index 100% rename from waku/waku_api/rest/legacy_store/client.nim rename to waku/rest_api/endpoint/legacy_store/client.nim diff --git a/waku/waku_api/rest/legacy_store/handlers.nim b/waku/rest_api/endpoint/legacy_store/handlers.nim similarity index 100% rename from waku/waku_api/rest/legacy_store/handlers.nim rename to waku/rest_api/endpoint/legacy_store/handlers.nim diff --git a/waku/waku_api/rest/legacy_store/types.nim b/waku/rest_api/endpoint/legacy_store/types.nim similarity index 100% rename from waku/waku_api/rest/legacy_store/types.nim rename to waku/rest_api/endpoint/legacy_store/types.nim diff --git a/waku/waku_api/rest/lightpush/client.nim b/waku/rest_api/endpoint/lightpush/client.nim similarity index 100% rename from waku/waku_api/rest/lightpush/client.nim rename to waku/rest_api/endpoint/lightpush/client.nim diff --git a/waku/waku_api/rest/lightpush/handlers.nim b/waku/rest_api/endpoint/lightpush/handlers.nim similarity index 100% rename from waku/waku_api/rest/lightpush/handlers.nim rename to waku/rest_api/endpoint/lightpush/handlers.nim diff --git a/waku/waku_api/rest/lightpush/types.nim b/waku/rest_api/endpoint/lightpush/types.nim similarity index 100% rename from waku/waku_api/rest/lightpush/types.nim rename to waku/rest_api/endpoint/lightpush/types.nim diff --git a/waku/waku_api/rest/origin_handler.nim b/waku/rest_api/endpoint/origin_handler.nim similarity index 100% rename from waku/waku_api/rest/origin_handler.nim rename to waku/rest_api/endpoint/origin_handler.nim diff --git a/waku/waku_api/rest/relay/client.nim b/waku/rest_api/endpoint/relay/client.nim similarity index 100% rename from waku/waku_api/rest/relay/client.nim rename to waku/rest_api/endpoint/relay/client.nim diff --git a/waku/waku_api/rest/relay/handlers.nim b/waku/rest_api/endpoint/relay/handlers.nim similarity index 100% rename from waku/waku_api/rest/relay/handlers.nim rename to waku/rest_api/endpoint/relay/handlers.nim diff --git a/waku/waku_api/rest/relay/types.nim b/waku/rest_api/endpoint/relay/types.nim similarity index 100% rename from waku/waku_api/rest/relay/types.nim rename to waku/rest_api/endpoint/relay/types.nim diff --git a/waku/waku_api/rest/responses.nim b/waku/rest_api/endpoint/responses.nim similarity index 100% rename from waku/waku_api/rest/responses.nim rename to waku/rest_api/endpoint/responses.nim diff --git a/waku/waku_api/rest/rest_serdes.nim b/waku/rest_api/endpoint/rest_serdes.nim similarity index 100% rename from waku/waku_api/rest/rest_serdes.nim rename to waku/rest_api/endpoint/rest_serdes.nim diff --git a/waku/waku_api/rest/serdes.nim b/waku/rest_api/endpoint/serdes.nim similarity index 100% rename from waku/waku_api/rest/serdes.nim rename to waku/rest_api/endpoint/serdes.nim diff --git a/waku/waku_api/rest/server.nim b/waku/rest_api/endpoint/server.nim similarity index 100% rename from waku/waku_api/rest/server.nim rename to waku/rest_api/endpoint/server.nim diff --git a/waku/waku_api/rest/store/client.nim b/waku/rest_api/endpoint/store/client.nim similarity index 100% rename from waku/waku_api/rest/store/client.nim rename to waku/rest_api/endpoint/store/client.nim diff --git a/waku/waku_api/rest/store/handlers.nim b/waku/rest_api/endpoint/store/handlers.nim similarity index 100% rename from waku/waku_api/rest/store/handlers.nim rename to waku/rest_api/endpoint/store/handlers.nim diff --git a/waku/waku_api/rest/store/types.nim b/waku/rest_api/endpoint/store/types.nim similarity index 100% rename from waku/waku_api/rest/store/types.nim rename to waku/rest_api/endpoint/store/types.nim diff --git a/waku/waku_api/handlers.nim b/waku/rest_api/handlers.nim similarity index 100% rename from waku/waku_api/handlers.nim rename to waku/rest_api/handlers.nim diff --git a/waku/waku_api/message_cache.nim b/waku/rest_api/message_cache.nim similarity index 100% rename from waku/waku_api/message_cache.nim rename to waku/rest_api/message_cache.nim diff --git a/waku/waku_api.nim b/waku/waku_api.nim deleted file mode 100644 index b584bfa2f..000000000 --- a/waku/waku_api.nim +++ /dev/null @@ -1,3 +0,0 @@ -import ./waku_api/message_cache, ./waku_api/rest, ./waku_api/json_rpc - -export message_cache, rest diff --git a/waku/waku_archive_legacy/common.nim b/waku/waku_archive_legacy/common.nim index ee45181cb..ed2b7272d 100644 --- a/waku/waku_archive_legacy/common.nim +++ b/waku/waku_archive_legacy/common.nim @@ -31,7 +31,7 @@ proc computeDigest*(msg: WakuMessage): MessageDigest = # Computes the hash return ctx.finish() -## Public API types +## API types type #TODO Once Store v2 is removed, the cursor becomes the hash of the last message diff --git a/waku/waku_node.nim b/waku/waku_node.nim index c81e49bb6..e782e616b 100644 --- a/waku/waku_node.nim +++ b/waku/waku_node.nim @@ -3,6 +3,6 @@ import ./node/waku_switch as switch, ./node/waku_node as node, ./node/health_monitor as health_monitor, - ./node/api as api + ./node/kernel_api as kernel_api -export net_config, switch, node, health_monitor, api +export net_config, switch, node, health_monitor, kernel_api diff --git a/waku/waku_rest.nim b/waku/waku_rest.nim new file mode 100644 index 000000000..8a1737335 --- /dev/null +++ b/waku/waku_rest.nim @@ -0,0 +1,3 @@ +import ./rest_api/message_cache, ./rest_api/endpoint, ./rest_api/json_rpc + +export message_cache, rest diff --git a/waku/waku_store/common.nim b/waku/waku_store/common.nim index d11c803f9..70446be4b 100644 --- a/waku/waku_store/common.nim +++ b/waku/waku_store/common.nim @@ -15,7 +15,7 @@ const type WakuStoreResult*[T] = Result[T, string] -## Public API types +## API types type StoreQueryRequest* = object diff --git a/waku/waku_store_legacy/common.nim b/waku/waku_store_legacy/common.nim index c1958f201..6da7f267e 100644 --- a/waku/waku_store_legacy/common.nim +++ b/waku/waku_store_legacy/common.nim @@ -29,7 +29,7 @@ proc computeDigest*(msg: WakuMessage): MessageDigest = # Computes the hash return ctx.finish() -## Public API types +## API types type HistoryCursor* = object From cd5909fafe914014b4602492586e8c2b48b53349 Mon Sep 17 00:00:00 2001 From: Darshan K <35736874+darshankabariya@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:53:23 +0530 Subject: [PATCH 008/155] chore: first beta release v0.37.0 (#3607) --- CHANGELOG.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc073792c..61e818afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,62 @@ +## v0.37.0 (2025-10-01) + +### Notes + +- Deprecated parameters: + - `tree_path` and `rlnDB` (RLN-related storage paths) + - `--dns-discovery` (fully removed, including dns-discovery-name-server) + - `keepAlive` (deprecated, config updated accordingly) +- Legacy `store` protocol is no longer supported by default. +- Improved sharding configuration: now explicit and shard-specific metrics added. +- Mix nodes are limited to IPv4 addresses only. +- [lightpush legacy](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/19/lightpush.md) is being deprecated. Use [lightpush v3](https://github.com/waku-org/specs/blob/master/standards/core/lightpush.md) instead. + +### Features + +- Waku API: create node via API ([#3580](https://github.com/waku-org/nwaku/pull/3580)) ([bc8acf76](https://github.com/waku-org/nwaku/commit/bc8acf76)) +- Waku Sync: full topic support ([#3275](https://github.com/waku-org/nwaku/pull/3275)) ([9327da5a](https://github.com/waku-org/nwaku/commit/9327da5a)) +- Mix PoC implementation ([#3284](https://github.com/waku-org/nwaku/pull/3284)) ([eb7a3d13](https://github.com/waku-org/nwaku/commit/eb7a3d13)) +- Rendezvous: add request interval option ([#3569](https://github.com/waku-org/nwaku/pull/3569)) ([cc7a6406](https://github.com/waku-org/nwaku/commit/cc7a6406)) +- Shard-specific metrics tracking ([#3520](https://github.com/waku-org/nwaku/pull/3520)) ([c3da29fd](https://github.com/waku-org/nwaku/commit/c3da29fd)) +- Libwaku: build Windows DLL for Status-go ([#3460](https://github.com/waku-org/nwaku/pull/3460)) ([5c38a53f](https://github.com/waku-org/nwaku/commit/5c38a53f)) +- RLN: add Stateless RLN support ([#3621](https://github.com/waku-org/nwaku/pull/3621)) +- LOG: Reduce log level of messages from debug to info for better visibility ([#3622](https://github.com/waku-org/nwaku/pull/3622)) + +### Bug Fixes + +- Prevent invalid pubsub topic subscription via Relay REST API ([#3559](https://github.com/waku-org/nwaku/pull/3559)) ([a36601ab](https://github.com/waku-org/nwaku/commit/a36601ab)) +- Fixed node crash when RLN is unregistered ([#3573](https://github.com/waku-org/nwaku/pull/3573)) ([3d0c6279](https://github.com/waku-org/nwaku/commit/3d0c6279)) +- REST: fixed sync protocol issues ([#3503](https://github.com/waku-org/nwaku/pull/3503)) ([393e3cce](https://github.com/waku-org/nwaku/commit/393e3cce)) +- Regex pattern fix for `username:password@` in URLs ([#3517](https://github.com/waku-org/nwaku/pull/3517)) ([89a3f735](https://github.com/waku-org/nwaku/commit/89a3f735)) +- Sharding: applied modulus fix ([#3530](https://github.com/waku-org/nwaku/pull/3530)) ([f68d7999](https://github.com/waku-org/nwaku/commit/f68d7999)) +- Metrics: switched to counter instead of gauge ([#3355](https://github.com/waku-org/nwaku/pull/3355)) ([a27eec90](https://github.com/waku-org/nwaku/commit/a27eec90)) +- Fixed lightpush metrics and diagnostics ([#3486](https://github.com/waku-org/nwaku/pull/3486)) ([0ed3fc80](https://github.com/waku-org/nwaku/commit/0ed3fc80)) +- Misc sync, dashboard, and CI fixes ([#3434](https://github.com/waku-org/nwaku/pull/3434), [#3508](https://github.com/waku-org/nwaku/pull/3508), [#3464](https://github.com/waku-org/nwaku/pull/3464)) +- Raise log level of numerous operational messages from debug to info for better visibility ([#3622](https://github.com/waku-org/nwaku/pull/3622)) + +### Changes + +- Enable peer-exchange by default ([#3557](https://github.com/waku-org/nwaku/pull/3557)) ([7df526f8](https://github.com/waku-org/nwaku/commit/7df526f8)) +- Refactor peer-exchange client and service implementations ([#3523](https://github.com/waku-org/nwaku/pull/3523)) ([4379f9ec](https://github.com/waku-org/nwaku/commit/4379f9ec)) +- Updated rendezvous to use callback-based shard/capability updates ([#3558](https://github.com/waku-org/nwaku/pull/3558)) ([028bf297](https://github.com/waku-org/nwaku/commit/028bf297)) +- Config updates and explicit sharding setup ([#3468](https://github.com/waku-org/nwaku/pull/3468)) ([994d485b](https://github.com/waku-org/nwaku/commit/994d485b)) +- Bumped libp2p to v1.13.0 ([#3574](https://github.com/waku-org/nwaku/pull/3574)) ([b1616e55](https://github.com/waku-org/nwaku/commit/b1616e55)) +- Removed legacy dependencies (e.g., libpcre in Docker builds) ([#3552](https://github.com/waku-org/nwaku/pull/3552)) ([4db4f830](https://github.com/waku-org/nwaku/commit/4db4f830)) +- Benchmarks for RLN proof generation & verification ([#3567](https://github.com/waku-org/nwaku/pull/3567)) ([794c3a85](https://github.com/waku-org/nwaku/commit/794c3a85)) +- Various CI/CD & infra updates ([#3515](https://github.com/waku-org/nwaku/pull/3515), [#3505](https://github.com/waku-org/nwaku/pull/3505)) + +### This release supports the following [libp2p protocols](https://docs.libp2p.io/concepts/protocols/): + +| Protocol | Spec status | Protocol id | +| ---: | :---: | :--- | +| [`11/WAKU2-RELAY`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/11/relay.md) | `stable` | `/vac/waku/relay/2.0.0` | +| [`12/WAKU2-FILTER`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/12/filter.md) | `draft` | `/vac/waku/filter/2.0.0-beta1`
`/vac/waku/filter-subscribe/2.0.0-beta1`
`/vac/waku/filter-push/2.0.0-beta1` | +| [`13/WAKU2-STORE`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/13/store.md) | `draft` | `/vac/waku/store/2.0.0-beta4` | +| [`19/WAKU2-LIGHTPUSH`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/19/lightpush.md) | `draft` | `/vac/waku/lightpush/2.0.0-beta1` | +| [`WAKU2-LIGHTPUSH v3`](https://github.com/waku-org/specs/blob/master/standards/core/lightpush.md) | `draft` | `/vac/waku/lightpush/3.0.0` | +| [`66/WAKU2-METADATA`](https://github.com/waku-org/specs/blob/master/standards/core/metadata.md) | `raw` | `/vac/waku/metadata/1.0.0` | +| [`WAKU-SYNC`](https://github.com/waku-org/specs/blob/master/standards/core/sync.md) | `draft` | `/vac/waku/sync/1.0.0` | + ## v0.36.0 (2025-06-20) ### Notes From adeb1a928ec8943a02d5cbd3927fc178ba2f3686 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 20 Nov 2025 08:44:15 +0100 Subject: [PATCH 009/155] fix: wakucanary now fails correctly when ping fails (#3595) * wakucanary add some more detail if exception Co-authored-by: MorganaFuture --- apps/wakucanary/wakucanary.nim | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/wakucanary/wakucanary.nim b/apps/wakucanary/wakucanary.nim index bcff9653e..6e02c2a8f 100644 --- a/apps/wakucanary/wakucanary.nim +++ b/apps/wakucanary/wakucanary.nim @@ -143,16 +143,18 @@ proc areProtocolsSupported( proc pingNode( node: WakuNode, peerInfo: RemotePeerInfo -): Future[void] {.async, gcsafe.} = +): Future[bool] {.async, gcsafe.} = try: let conn = await node.switch.dial(peerInfo.peerId, peerInfo.addrs, PingCodec) let pingDelay = await node.libp2pPing.ping(conn) info "Peer response time (ms)", peerId = peerInfo.peerId, ping = pingDelay.millis + return true except CatchableError: var msg = getCurrentExceptionMsg() if msg == "Future operation cancelled!": msg = "timedout" error "Failed to ping the peer", peer = peerInfo, err = msg + return false proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = let conf: WakuCanaryConf = WakuCanaryConf.load() @@ -268,8 +270,13 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = let lp2pPeerStore = node.switch.peerStore let conStatus = node.peerManager.switch.peerStore[ConnectionBook][peer.peerId] + var pingSuccess = true if conf.ping: - discard await pingFut + try: + pingSuccess = await pingFut + except CatchableError as exc: + pingSuccess = false + error "Ping operation failed or timed out", error = exc.msg if conStatus in [Connected, CanConnect]: let nodeProtocols = lp2pPeerStore[ProtoBook][peer.peerId] @@ -278,6 +285,11 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = error "Not all protocols are supported", expected = conf.protocols, supported = nodeProtocols quit(QuitFailure) + + # Check ping result if ping was enabled + if conf.ping and not pingSuccess: + error "Node is reachable and supports protocols but ping failed - connection may be unstable" + quit(QuitFailure) elif conStatus == CannotConnect: error "Could not connect", peerId = peer.peerId quit(QuitFailure) From e54851d9d63fd9e7e5e70844364a4464c4dba8f4 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:12:16 +0100 Subject: [PATCH 010/155] fix: admin API peer shards field from metadata protocol (#3594) * fix: admin API peer shards field from metadata protocol Store and return peer shard info from metadata protocol exchange instead of only checking ENR records. * peer_manager set shard info and extend rest test to validate it Co-authored-by: MorganaFuture --- tests/wakunode_rest/test_rest_admin.nim | 14 +++++++++++++- waku/node/peer_manager/peer_manager.nim | 5 +++++ waku/node/peer_manager/waku_peer_store.nim | 8 ++++++++ waku/waku_core/peers.nim | 12 +++++++++++- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/wakunode_rest/test_rest_admin.nim b/tests/wakunode_rest/test_rest_admin.nim index 6de886f74..ef82b8dfc 100644 --- a/tests/wakunode_rest/test_rest_admin.nim +++ b/tests/wakunode_rest/test_rest_admin.nim @@ -65,7 +65,7 @@ suite "Waku v2 Rest API - Admin": ): Future[void] {.async, gcsafe.} = await sleepAsync(0.milliseconds) - let shard = RelayShard(clusterId: clusterId, shardId: 0) + let shard = RelayShard(clusterId: clusterId, shardId: 5) node1.subscribe((kind: PubsubSub, topic: $shard), simpleHandler).isOkOr: assert false, "Failed to subscribe to topic: " & $error node2.subscribe((kind: PubsubSub, topic: $shard), simpleHandler).isOkOr: @@ -212,6 +212,18 @@ suite "Waku v2 Rest API - Admin": let conn2 = await node1.peerManager.connectPeer(peerInfo2) let conn3 = await node1.peerManager.connectPeer(peerInfo3) + var count = 0 + while count < 20: + ## Wait ~1s at most for the peer store to update shard info + let getRes = await client.getPeers() + if getRes.data.allIt(it.shards == @[5.uint16]): + break + + count.inc() + await sleepAsync(50.milliseconds) + + assert count < 20, "Timeout waiting for shards to be updated in peer store" + # Check successful connections check: conn2 == true diff --git a/waku/node/peer_manager/peer_manager.nim b/waku/node/peer_manager/peer_manager.nim index 72b526aca..1abcc1ac0 100644 --- a/waku/node/peer_manager/peer_manager.nim +++ b/waku/node/peer_manager/peer_manager.nim @@ -658,6 +658,11 @@ proc onPeerMetadata(pm: PeerManager, peerId: PeerId) {.async.} = $clusterId break guardClauses + # Store the shard information from metadata in the peer store + if pm.switch.peerStore.peerExists(peerId): + let shards = metadata.shards.mapIt(it.uint16) + pm.switch.peerStore.setShardInfo(peerId, shards) + return info "disconnecting from peer", peerId = peerId, reason = reason diff --git a/waku/node/peer_manager/waku_peer_store.nim b/waku/node/peer_manager/waku_peer_store.nim index 0098c1687..c9e2d4817 100644 --- a/waku/node/peer_manager/waku_peer_store.nim +++ b/waku/node/peer_manager/waku_peer_store.nim @@ -39,6 +39,9 @@ type # Keeps track of the ENR (Ethereum Node Record) of a peer ENRBook* = ref object of PeerBook[enr.Record] + # Keeps track of peer shards + ShardBook* = ref object of PeerBook[seq[uint16]] + proc getPeer*(peerStore: PeerStore, peerId: PeerId): RemotePeerInfo = let addresses = if peerStore[LastSeenBook][peerId].isSome(): @@ -55,6 +58,7 @@ proc getPeer*(peerStore: PeerStore, peerId: PeerId): RemotePeerInfo = else: none(enr.Record), protocols: peerStore[ProtoBook][peerId], + shards: peerStore[ShardBook][peerId], agent: peerStore[AgentBook][peerId], protoVersion: peerStore[ProtoVersionBook][peerId], publicKey: peerStore[KeyBook][peerId], @@ -76,6 +80,7 @@ proc peers*(peerStore: PeerStore): seq[RemotePeerInfo] = toSeq(peerStore[AddressBook].book.keys()), toSeq(peerStore[ProtoBook].book.keys()), toSeq(peerStore[KeyBook].book.keys()), + toSeq(peerStore[ShardBook].book.keys()), ) .toHashSet() @@ -127,6 +132,9 @@ proc addPeer*(peerStore: PeerStore, peer: RemotePeerInfo, origin = UnknownOrigin if peer.enr.isSome(): peerStore[ENRBook][peer.peerId] = peer.enr.get() +proc setShardInfo*(peerStore: PeerStore, peerId: PeerID, shards: seq[uint16]) = + peerStore[ShardBook][peerId] = shards + proc peers*(peerStore: PeerStore, proto: string): seq[RemotePeerInfo] = peerStore.peers().filterIt(it.protocols.contains(proto)) diff --git a/waku/waku_core/peers.nim b/waku/waku_core/peers.nim index 5591699c6..76ff29aa0 100644 --- a/waku/waku_core/peers.nim +++ b/waku/waku_core/peers.nim @@ -48,6 +48,7 @@ type RemotePeerInfo* = ref object addrs*: seq[MultiAddress] enr*: Option[enr.Record] protocols*: seq[string] + shards*: seq[uint16] agent*: string protoVersion*: string @@ -73,6 +74,7 @@ proc init*( addrs: seq[MultiAddress] = @[], enr: Option[enr.Record] = none(enr.Record), protocols: seq[string] = @[], + shards: seq[uint16] = @[], publicKey: crypto.PublicKey = crypto.PublicKey(), agent: string = "", protoVersion: string = "", @@ -88,6 +90,7 @@ proc init*( addrs: addrs, enr: enr, protocols: protocols, + shards: shards, publicKey: publicKey, agent: agent, protoVersion: protoVersion, @@ -105,9 +108,12 @@ proc init*( addrs: seq[MultiAddress] = @[], enr: Option[enr.Record] = none(enr.Record), protocols: seq[string] = @[], + shards: seq[uint16] = @[], ): T {.raises: [Defect, ResultError[cstring], LPError].} = let peerId = PeerID.init(peerId).tryGet() - RemotePeerInfo(peerId: peerId, addrs: addrs, enr: enr, protocols: protocols) + RemotePeerInfo( + peerId: peerId, addrs: addrs, enr: enr, protocols: protocols, shards: shards + ) ## Parse @@ -326,6 +332,7 @@ converter toRemotePeerInfo*(peerInfo: PeerInfo): RemotePeerInfo = addrs: peerInfo.listenAddrs, enr: none(enr.Record), protocols: peerInfo.protocols, + shards: @[], agent: peerInfo.agentVersion, protoVersion: peerInfo.protoVersion, publicKey: peerInfo.publicKey, @@ -361,6 +368,9 @@ proc getAgent*(peer: RemotePeerInfo): string = return peer.agent proc getShards*(peer: RemotePeerInfo): seq[uint16] = + if peer.shards.len > 0: + return peer.shards + if peer.enr.isNone(): return @[] From 31e1a81552d72cd07133754e1953639da49ec954 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:40:08 +0100 Subject: [PATCH 011/155] nix: add wakucanary Flake package (#3599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jakub Sokołowski Co-authored-by: Jakub Sokołowski --- Makefile | 1 - flake.lock | 32 +++++++++++++++++++++++++++----- flake.nix | 17 ++++++++++++----- nix/atlas.nix | 12 ------------ nix/checksums.nix | 2 +- nix/default.nix | 28 ++++++++++++---------------- nix/nimble.nix | 4 ++-- nix/sat.nix | 5 +++-- 8 files changed, 57 insertions(+), 44 deletions(-) delete mode 100644 nix/atlas.nix diff --git a/Makefile b/Makefile index 37341792c..029313c99 100644 --- a/Makefile +++ b/Makefile @@ -543,4 +543,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/flake.lock b/flake.lock index 359ae2579..0700e6a43 100644 --- a/flake.lock +++ b/flake.lock @@ -22,24 +22,46 @@ "zerokit": "zerokit" } }, - "zerokit": { + "rust-overlay": { "inputs": { "nixpkgs": [ + "zerokit", "nixpkgs" ] }, "locked": { - "lastModified": 1743756626, - "narHash": "sha256-SvhfEl0bJcRsCd79jYvZbxQecGV2aT+TXjJ57WVv7Aw=", + "lastModified": 1748399823, + "narHash": "sha256-kahD8D5hOXOsGbNdoLLnqCL887cjHkx98Izc37nDjlA=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "d68a69dc71bc19beb3479800392112c2f6218159", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "zerokit": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1749115386, + "narHash": "sha256-UexIE2D7zr6aRajwnKongXwCZCeRZDXOL0kfjhqUFSU=", "owner": "vacp2p", "repo": "zerokit", - "rev": "c60e0c33fc6350a4b1c20e6b6727c44317129582", + "rev": "dc0b31752c91e7b4fefc441cfa6a8210ad7dba7b", "type": "github" }, "original": { "owner": "vacp2p", "repo": "zerokit", - "rev": "c60e0c33fc6350a4b1c20e6b6727c44317129582", + "rev": "dc0b31752c91e7b4fefc441cfa6a8210ad7dba7b", "type": "github" } } diff --git a/flake.nix b/flake.nix index 760f49337..72eaebef1 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs?rev=f44bd8ca21e026135061a0a57dcf3d0775b67a49"; zerokit = { - url = "github:vacp2p/zerokit?rev=c60e0c33fc6350a4b1c20e6b6727c44317129582"; + url = "github:vacp2p/zerokit?rev=dc0b31752c91e7b4fefc441cfa6a8210ad7dba7b"; inputs.nixpkgs.follows = "nixpkgs"; }; }; @@ -49,11 +49,18 @@ libwaku-android-arm64 = pkgs.callPackage ./nix/default.nix { inherit stableSystems; src = self; - targets = ["libwaku-android-arm64"]; - androidArch = "aarch64-linux-android"; + targets = ["libwaku-android-arm64"]; abidir = "arm64-v8a"; - zerokitPkg = zerokit.packages.${system}.zerokit-android-arm64; + zerokitRln = zerokit.packages.${system}.rln-android-arm64; }; + + wakucanary = pkgs.callPackage ./nix/default.nix { + inherit stableSystems; + src = self; + targets = ["wakucanary"]; + zerokitRln = zerokit.packages.${system}.rln; + }; + default = libwaku-android-arm64; }); @@ -61,4 +68,4 @@ default = pkgsFor.${system}.callPackage ./nix/shell.nix {}; }); }; -} \ No newline at end of file +} diff --git a/nix/atlas.nix b/nix/atlas.nix deleted file mode 100644 index 43336e07a..000000000 --- a/nix/atlas.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ pkgs ? import { } }: - -let - tools = pkgs.callPackage ./tools.nix {}; - sourceFile = ../vendor/nimbus-build-system/vendor/Nim/koch.nim; -in pkgs.fetchFromGitHub { - owner = "nim-lang"; - repo = "atlas"; - rev = tools.findKeyValue "^ +AtlasStableCommit = \"([a-f0-9]+)\"$" sourceFile; - # WARNING: Requires manual updates when Nim compiler version changes. - hash = "sha256-G1TZdgbRPSgxXZ3VsBP2+XFCLHXVb3an65MuQx67o/k="; -} \ No newline at end of file diff --git a/nix/checksums.nix b/nix/checksums.nix index d79345d24..510f2b41a 100644 --- a/nix/checksums.nix +++ b/nix/checksums.nix @@ -6,7 +6,7 @@ let in pkgs.fetchFromGitHub { owner = "nim-lang"; repo = "checksums"; - rev = tools.findKeyValue "^ +ChecksumsStableCommit = \"([a-f0-9]+)\"$" sourceFile; + rev = tools.findKeyValue "^ +ChecksumsStableCommit = \"([a-f0-9]+)\".*$" sourceFile; # WARNING: Requires manual updates when Nim compiler version changes. hash = "sha256-Bm5iJoT2kAvcTexiLMFBa9oU5gf7d4rWjo3OiN7obWQ="; } diff --git a/nix/default.nix b/nix/default.nix index 29eec844d..d78f9935f 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -9,9 +9,8 @@ stableSystems ? [ "x86_64-linux" "aarch64-linux" ], - androidArch, - abidir, - zerokitPkg, + abidir ? null, + zerokitRln, }: assert pkgs.lib.assertMsg ((src.submodules or true) == true) @@ -51,7 +50,7 @@ in stdenv.mkDerivation rec { cmake which lsb-release - zerokitPkg + zerokitRln nim-unwrapped-2_0 fakeGit fakeCargo @@ -84,27 +83,24 @@ in stdenv.mkDerivation rec { pushd vendor/nimbus-build-system/vendor/Nim mkdir dist cp -r ${callPackage ./nimble.nix {}} dist/nimble - chmod 777 -R dist/nimble - mkdir -p dist/nimble/dist - cp -r ${callPackage ./checksums.nix {}} dist/checksums # need both - cp -r ${callPackage ./checksums.nix {}} dist/nimble/dist/checksums - cp -r ${callPackage ./atlas.nix {}} dist/atlas - chmod 777 -R dist/atlas - mkdir dist/atlas/dist - cp -r ${callPackage ./sat.nix {}} dist/nimble/dist/sat - cp -r ${callPackage ./sat.nix {}} dist/atlas/dist/sat + cp -r ${callPackage ./checksums.nix {}} dist/checksums cp -r ${callPackage ./csources.nix {}} csources_v2 chmod 777 -R dist/nimble csources_v2 popd - mkdir -p vendor/zerokit/target/${androidArch}/release - cp ${zerokitPkg}/librln.so vendor/zerokit/target/${androidArch}/release/ + cp -r ${zerokitRln}/target vendor/zerokit/ + find vendor/zerokit/target + # FIXME + cp vendor/zerokit/target/*/release/librln.a librln_v${zerokitRln.version}.a ''; - installPhase = '' + installPhase = if abidir != null then '' mkdir -p $out/jni cp -r ./build/android/${abidir}/* $out/jni/ echo '${androidManifest}' > $out/jni/AndroidManifest.xml cd $out && zip -r libwaku.aar * + '' else '' + mkdir -p $out/bin + cp -r build/* $out/bin ''; meta = with pkgs.lib; { diff --git a/nix/nimble.nix b/nix/nimble.nix index 5bd7b0f32..f9d87da6d 100644 --- a/nix/nimble.nix +++ b/nix/nimble.nix @@ -6,7 +6,7 @@ let in pkgs.fetchFromGitHub { owner = "nim-lang"; repo = "nimble"; - rev = tools.findKeyValue "^ +NimbleStableCommit = \"([a-f0-9]+)\".+" sourceFile; + rev = tools.findKeyValue "^ +NimbleStableCommit = \"([a-f0-9]+)\".*$" sourceFile; # WARNING: Requires manual updates when Nim compiler version changes. hash = "sha256-MVHf19UbOWk8Zba2scj06PxdYYOJA6OXrVyDQ9Ku6Us="; -} \ No newline at end of file +} diff --git a/nix/sat.nix b/nix/sat.nix index 31f264468..92db58a2e 100644 --- a/nix/sat.nix +++ b/nix/sat.nix @@ -6,7 +6,8 @@ let in pkgs.fetchFromGitHub { owner = "nim-lang"; repo = "sat"; - rev = tools.findKeyValue "^ +SatStableCommit = \"([a-f0-9]+)\"$" sourceFile; + rev = tools.findKeyValue "^ +SatStableCommit = \"([a-f0-9]+)\".*$" sourceFile; + # WARNING: Requires manual updates when Nim compiler version changes. # WARNING: Requires manual updates when Nim compiler version changes. hash = "sha256-JFrrSV+mehG0gP7NiQ8hYthL0cjh44HNbXfuxQNhq7c="; -} \ No newline at end of file +} From b0cd75f4cb7e98d3f5b74962f12469d6012b0a57 Mon Sep 17 00:00:00 2001 From: Prem Chaitanya Prathi Date: Fri, 21 Nov 2025 23:15:12 +0530 Subject: [PATCH 012/155] feat: update rendezvous to broadcast and discover WakuPeerRecords (#3617) * update rendezvous to work with WakuPeeRecord and use libp2p updated version * split rendezvous client and service implementation * mount rendezvous client by default --- Dockerfile.lightpushWithMix.compile | 2 +- apps/chat2mix/chat2mix.nim | 18 +- .../lightpush_mix/lightpush_publisher_mix.nim | 60 ++-- .../lightpush_publisher_mix_metrics.nim | 3 + simulations/mixnet/run_chat_mix.sh | 3 +- simulations/mixnet/run_chat_mix1.sh | 3 +- tests/test_waku_rendezvous.nim | 36 ++- tests/waku_discv5/test_waku_discv5.nim | 6 +- vendor/nim-libp2p | 2 +- waku.nimble | 2 +- waku/common/callbacks.nim | 4 +- waku/factory/node_factory.nim | 23 +- waku/factory/waku_conf.nim | 3 +- waku/node/peer_manager/waku_peer_store.nim | 19 +- waku/node/waku_node.nim | 43 ++- waku/waku_core/codecs.nim | 1 + waku/waku_core/peers.nim | 4 + waku/waku_lightpush/client.nim | 12 +- waku/waku_mix/protocol.nim | 79 +++-- waku/waku_rendezvous/client.nim | 142 ++++++++ waku/waku_rendezvous/common.nim | 8 + waku/waku_rendezvous/protocol.nim | 306 +++++++----------- waku/waku_rendezvous/waku_peer_record.nim | 74 +++++ 23 files changed, 564 insertions(+), 289 deletions(-) create mode 100644 waku/waku_rendezvous/client.nim create mode 100644 waku/waku_rendezvous/waku_peer_record.nim diff --git a/Dockerfile.lightpushWithMix.compile b/Dockerfile.lightpushWithMix.compile index 381ee60ef..8006ec50b 100644 --- a/Dockerfile.lightpushWithMix.compile +++ b/Dockerfile.lightpushWithMix.compile @@ -1,5 +1,5 @@ # BUILD NIM APP ---------------------------------------------------------------- -FROM rust:1.81.0-alpine3.19 AS nim-build +FROM rustlang/rust:nightly-alpine3.19 AS nim-build ARG NIMFLAGS ARG MAKE_TARGET=lightpushwithmix diff --git a/apps/chat2mix/chat2mix.nim b/apps/chat2mix/chat2mix.nim index 5979e2936..3fdd7bc9c 100644 --- a/apps/chat2mix/chat2mix.nim +++ b/apps/chat2mix/chat2mix.nim @@ -124,7 +124,7 @@ proc encode*(message: Chat2Message): ProtoBuffer = return serialised -proc toString*(message: Chat2Message): string = +proc `$`*(message: Chat2Message): string = # Get message date and timestamp in local time let time = message.timestamp.fromUnix().local().format("'<'MMM' 'dd,' 'HH:mm'>'") @@ -331,13 +331,14 @@ proc maintainSubscription( const maxFailedServiceNodeSwitches = 10 var noFailedSubscribes = 0 var noFailedServiceNodeSwitches = 0 - const RetryWaitMs = 2.seconds # Quick retry interval - const SubscriptionMaintenanceMs = 30.seconds # Subscription maintenance interval + # Use chronos.Duration explicitly to avoid mismatch with std/times.Duration + let RetryWait = chronos.seconds(2) # Quick retry interval + let SubscriptionMaintenance = chronos.seconds(30) # Subscription maintenance interval while true: info "maintaining subscription at", peer = constructMultiaddrStr(actualFilterPeer) # First use filter-ping to check if we have an active subscription let pingErr = (await wakuNode.wakuFilterClient.ping(actualFilterPeer)).errorOr: - await sleepAsync(SubscriptionMaintenanceMs) + await sleepAsync(SubscriptionMaintenance) info "subscription is live." continue @@ -350,7 +351,7 @@ proc maintainSubscription( some(filterPubsubTopic), filterContentTopic, actualFilterPeer ) ).errorOr: - await sleepAsync(SubscriptionMaintenanceMs) + await sleepAsync(SubscriptionMaintenance) if noFailedSubscribes > 0: noFailedSubscribes -= 1 notice "subscribe request successful." @@ -365,7 +366,7 @@ proc maintainSubscription( # wakunode.peerManager.peerStore.delete(actualFilterPeer) if noFailedSubscribes < maxFailedSubscribes: - await sleepAsync(RetryWaitMs) # Wait a bit before retrying + await sleepAsync(RetryWait) # Wait a bit before retrying elif not preventPeerSwitch: # try again with new peer without delay let actualFilterPeer = selectRandomServicePeer( @@ -380,7 +381,7 @@ proc maintainSubscription( noFailedSubscribes = 0 else: - await sleepAsync(SubscriptionMaintenanceMs) + await sleepAsync(SubscriptionMaintenance) {.pop.} # @TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError @@ -450,6 +451,8 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = (await node.mountMix(conf.clusterId, mixPrivKey, conf.mixnodes)).isOkOr: error "failed to mount waku mix protocol: ", error = $error quit(QuitFailure) + await node.mountRendezvousClient(conf.clusterId) + await node.start() node.peerManager.start() @@ -587,7 +590,6 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = error "Couldn't find any service peer" quit(QuitFailure) - #await mountLegacyLightPush(node) node.peerManager.addServicePeer(servicePeerInfo, WakuLightpushCodec) node.peerManager.addServicePeer(servicePeerInfo, WakuPeerExchangeCodec) diff --git a/examples/lightpush_mix/lightpush_publisher_mix.nim b/examples/lightpush_mix/lightpush_publisher_mix.nim index 1e26daa9b..bb4bb4c4e 100644 --- a/examples/lightpush_mix/lightpush_publisher_mix.nim +++ b/examples/lightpush_mix/lightpush_publisher_mix.nim @@ -51,7 +51,6 @@ proc splitPeerIdAndAddr(maddr: string): (string, string) = proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} = # use notice to filter all waku messaging setupLog(logging.LogLevel.DEBUG, logging.LogFormat.TEXT) - notice "starting publisher", wakuPort = conf.port let @@ -114,17 +113,8 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} let dPeerId = PeerId.init(destPeerId).valueOr: error "Failed to initialize PeerId", error = error return - var conn: Connection - if not conf.mixDisabled: - conn = node.wakuMix.toConnection( - MixDestination.init(dPeerId, pxPeerInfo.addrs[0]), # destination lightpush peer - WakuLightPushCodec, # protocol codec which will be used over the mix connection - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - # mix parameters indicating we expect a single reply - ).valueOr: - error "failed to create mix connection", error = error - return + await node.mountRendezvousClient(clusterId) await node.start() node.peerManager.start() node.startPeerExchangeLoop() @@ -145,20 +135,26 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} var i = 0 while i < conf.numMsgs: + var conn: Connection if conf.mixDisabled: let connOpt = await node.peerManager.dialPeer(dPeerId, WakuLightPushCodec) if connOpt.isNone(): error "failed to dial peer with WakuLightPushCodec", target_peer_id = dPeerId return conn = connOpt.get() + else: + conn = node.wakuMix.toConnection( + MixDestination.init(dPeerId, pxPeerInfo.addrs[0]), # destination lightpush peer + WakuLightPushCodec, # protocol codec which will be used over the mix connection + MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), + # mix parameters indicating we expect a single reply + ).valueOr: + error "failed to create mix connection", error = error + return i = i + 1 let text = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam venenatis magna ut tortor faucibus, in vestibulum nibh commodo. Aenean eget vestibulum augue. Nullam suscipit urna non nunc efficitur, at iaculis nisl consequat. Mauris quis ultrices elit. Suspendisse lobortis odio vitae laoreet facilisis. Cras ornare sem felis, at vulputate magna aliquam ac. Duis quis est ultricies, euismod nulla ac, interdum dui. Maecenas sit amet est vitae enim commodo gravida. Proin vitae elit nulla. Donec tempor dolor lectus, in faucibus velit elementum quis. Donec non mauris eu nibh faucibus cursus ut egestas dolor. Aliquam venenatis ligula id velit pulvinar malesuada. Vestibulum scelerisque, justo non porta gravida, nulla justo tempor purus, at sollicitudin erat erat vel libero. - Fusce nec eros eu metus tristique aliquet. Sed ut magna sagittis, vulputate diam sit amet, aliquam magna. Aenean sollicitudin velit lacus, eu ultrices magna semper at. Integer vitae felis ligula. In a eros nec risus condimentum tincidunt fermentum sit amet ex. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nullam vitae justo maximus, fringilla tellus nec, rutrum purus. Etiam efficitur nisi dapibus euismod vestibulum. Phasellus at felis elementum, tristique nulla ac, consectetur neque. - Maecenas hendrerit nibh eget velit rutrum, in ornare mauris molestie. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Praesent dignissim efficitur eros, sit amet rutrum justo mattis a. Fusce mollis neque at erat placerat bibendum. Ut fringilla fringilla orci, ut fringilla metus fermentum vel. In hac habitasse platea dictumst. Donec hendrerit porttitor odio. Suspendisse ornare sollicitudin mauris, sodales pulvinar velit finibus vel. Fusce id pulvinar neque. Suspendisse eget tincidunt sapien, ac accumsan turpis. - Curabitur cursus tincidunt leo at aliquet. Nunc dapibus quam id venenatis varius. Aenean eget augue vel velit dapibus aliquam. Nulla facilisi. Curabitur cursus, turpis vel congue volutpat, tellus eros cursus lacus, eu fringilla turpis orci non ipsum. In hac habitasse platea dictumst. Nulla aliquam nisl a nunc placerat, eget dignissim felis pulvinar. Fusce sed porta mauris. Donec sodales arcu in nisl sodales, quis posuere massa ultricies. Nam feugiat massa eget felis ultricies finibus. Nunc magna nulla, interdum a elit vel, egestas efficitur urna. Ut posuere tincidunt odio in maximus. Sed at dignissim est. - Morbi accumsan elementum ligula ut fringilla. Praesent in ex metus. Phasellus urna est, tempus sit amet elementum vitae, sollicitudin vel ipsum. Fusce hendrerit eleifend dignissim. Maecenas tempor dapibus dui quis laoreet. Cras tincidunt sed ipsum sed pellentesque. Proin ut tellus nec ipsum varius interdum. Curabitur id velit ligula. Etiam sapien nulla, cursus sodales orci eu, porta lobortis nunc. Nunc at dapibus velit. Nulla et nunc vehicula, condimentum erat quis, elementum dolor. Quisque eu metus fermentum, vestibulum tellus at, sollicitudin odio. Ut vel neque justo. - Praesent porta porta velit, vel porttitor sem. Donec sagittis at nulla venenatis iaculis. Nullam vel eleifend felis. Nullam a pellentesque lectus. Aliquam tincidunt semper dui sed bibendum. Donec hendrerit, urna et cursus dictum, neque neque convallis magna, id condimentum sem urna quis massa. Fusce non quam vulputate, fermentum mauris at, malesuada ipsum. Mauris id pellentesque libero. Donec vel erat ullamcorper, dapibus quam id, imperdiet urna. Praesent sed ligula ut est pellentesque pharetra quis et diam. Ut placerat lorem eget mi fermentum aliquet. + Fusce nec eros eu metus tristique aliquet. This is message #""" & $i & """ sent from a publisher using mix. End of transmission.""" let message = WakuMessage( @@ -168,25 +164,31 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} timestamp: getNowInNanosecondTime(), ) # current timestamp - let res = await node.wakuLightpushClient.publishWithConn( - LightpushPubsubTopic, message, conn, dPeerId - ) + let startTime = getNowInNanosecondTime() - if res.isOk(): - lp_mix_success.inc() - notice "published message", - text = text, - timestamp = message.timestamp, - psTopic = LightpushPubsubTopic, - contentTopic = LightpushContentTopic - else: - error "failed to publish message", error = $res.error + ( + await node.wakuLightpushClient.publishWithConn( + LightpushPubsubTopic, message, conn, dPeerId + ) + ).isOkOr: + error "failed to publish message via mix", error = error.desc lp_mix_failed.inc(labelValues = ["publish_error"]) + return + + let latency = float64(getNowInNanosecondTime() - startTime) / 1_000_000.0 + lp_mix_latency.observe(latency) + lp_mix_success.inc() + notice "published message", + text = text, + timestamp = message.timestamp, + latency = latency, + psTopic = LightpushPubsubTopic, + contentTopic = LightpushContentTopic if conf.mixDisabled: await conn.close() await sleepAsync(conf.msgIntervalMilliseconds) - info "###########Sent all messages via mix" + info "Sent all messages via mix" quit(0) when isMainModule: diff --git a/examples/lightpush_mix/lightpush_publisher_mix_metrics.nim b/examples/lightpush_mix/lightpush_publisher_mix_metrics.nim index cd06b3e3e..3c467e28c 100644 --- a/examples/lightpush_mix/lightpush_publisher_mix_metrics.nim +++ b/examples/lightpush_mix/lightpush_publisher_mix_metrics.nim @@ -6,3 +6,6 @@ declarePublicCounter lp_mix_success, "number of lightpush messages sent via mix" declarePublicCounter lp_mix_failed, "number of lightpush messages failed via mix", labels = ["error"] + +declarePublicHistogram lp_mix_latency, + "lightpush publish latency via mix in milliseconds" diff --git a/simulations/mixnet/run_chat_mix.sh b/simulations/mixnet/run_chat_mix.sh index 11a28c06b..3dd6f5932 100755 --- a/simulations/mixnet/run_chat_mix.sh +++ b/simulations/mixnet/run_chat_mix.sh @@ -1 +1,2 @@ -../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE --mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" +../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE +#--mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" diff --git a/simulations/mixnet/run_chat_mix1.sh b/simulations/mixnet/run_chat_mix1.sh index 11a28c06b..7323bb3a9 100755 --- a/simulations/mixnet/run_chat_mix1.sh +++ b/simulations/mixnet/run_chat_mix1.sh @@ -1 +1,2 @@ -../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE --mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" +../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE +#--mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" diff --git a/tests/test_waku_rendezvous.nim b/tests/test_waku_rendezvous.nim index fa2efbd47..d3dd6f920 100644 --- a/tests/test_waku_rendezvous.nim +++ b/tests/test_waku_rendezvous.nim @@ -1,12 +1,20 @@ {.used.} -import std/options, chronos, testutils/unittests, libp2p/builders +import + std/options, + chronos, + testutils/unittests, + libp2p/builders, + libp2p/protocols/rendezvous import waku/waku_core/peers, + waku/waku_core/codecs, waku/node/waku_node, waku/node/peer_manager/peer_manager, waku/waku_rendezvous/protocol, + waku/waku_rendezvous/common, + waku/waku_rendezvous/waku_peer_record, ./testlib/[wakucore, wakunode] procSuite "Waku Rendezvous": @@ -50,18 +58,26 @@ procSuite "Waku Rendezvous": node2.peerManager.addPeer(peerInfo3) node3.peerManager.addPeer(peerInfo2) - let namespace = "test/name/space" - - let res = await node1.wakuRendezvous.batchAdvertise( - namespace, 60.seconds, @[peerInfo2.peerId] - ) + let res = await node1.wakuRendezvous.advertiseAll() assert res.isOk(), $res.error + # Rendezvous Request API requires dialing first + let connOpt = + await node3.peerManager.dialPeer(peerInfo2.peerId, WakuRendezVousCodec) + require: + connOpt.isSome - let response = - await node3.wakuRendezvous.batchRequest(namespace, 1, @[peerInfo2.peerId]) - assert response.isOk(), $response.error - let records = response.get() + var records: seq[WakuPeerRecord] + try: + records = await rendezvous.request[WakuPeerRecord]( + node3.wakuRendezvous, + Opt.some(computeMixNamespace(clusterId)), + Opt.some(1), + Opt.some(@[peerInfo2.peerId]), + ) + except CatchableError as e: + assert false, "Request failed with exception: " & e.msg check: records.len == 1 records[0].peerId == peerInfo1.peerId + #records[0].mixPubKey == $node1.wakuMix.pubKey diff --git a/tests/waku_discv5/test_waku_discv5.nim b/tests/waku_discv5/test_waku_discv5.nim index 6685bda32..20a0c6965 100644 --- a/tests/waku_discv5/test_waku_discv5.nim +++ b/tests/waku_discv5/test_waku_discv5.nim @@ -426,7 +426,6 @@ suite "Waku Discovery v5": confBuilder.withNodeKey(libp2p_keys.PrivateKey.random(Secp256k1, myRng[])[]) confBuilder.discv5Conf.withEnabled(true) confBuilder.discv5Conf.withUdpPort(9000.Port) - let conf = confBuilder.build().valueOr: raiseAssert error @@ -468,6 +467,9 @@ suite "Waku Discovery v5": # leave some time for discv5 to act await sleepAsync(chronos.seconds(10)) + # Connect peers via peer manager to ensure identify happens + discard await waku0.node.peerManager.connectPeer(waku1.node.switch.peerInfo) + var r = waku0.node.peerManager.selectPeer(WakuPeerExchangeCodec) assert r.isSome(), "could not retrieve peer mounting WakuPeerExchangeCodec" @@ -480,7 +482,7 @@ suite "Waku Discovery v5": r = waku2.node.peerManager.selectPeer(WakuPeerExchangeCodec) assert r.isSome(), "could not retrieve peer mounting WakuPeerExchangeCodec" - r = waku2.node.peerManager.selectPeer(RendezVousCodec) + r = waku2.node.peerManager.selectPeer(WakuRendezVousCodec) assert r.isSome(), "could not retrieve peer mounting RendezVousCodec" asyncTest "Discv5 bootstrap nodes should be added to the peer store": diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index 0309685cd..e82080f7b 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit 0309685cd27d4bf763c8b3be86a76c33bcfe67ea +Subproject commit e82080f7b1aa61c6d35fa5311b873f41eff4bb52 diff --git a/waku.nimble b/waku.nimble index c63d20246..79fdd9fd6 100644 --- a/waku.nimble +++ b/waku.nimble @@ -24,7 +24,7 @@ requires "nim >= 2.2.4", "stew", "stint", "metrics", - "libp2p >= 1.14.2", + "libp2p >= 1.14.3", "web3", "presto", "regex", diff --git a/waku/common/callbacks.nim b/waku/common/callbacks.nim index 9b8590152..83209ef24 100644 --- a/waku/common/callbacks.nim +++ b/waku/common/callbacks.nim @@ -1,5 +1,7 @@ -import ../waku_enr/capabilities +import waku/waku_enr/capabilities, waku/waku_rendezvous/waku_peer_record type GetShards* = proc(): seq[uint16] {.closure, gcsafe, raises: [].} type GetCapabilities* = proc(): seq[Capabilities] {.closure, gcsafe, raises: [].} + +type GetWakuPeerRecord* = proc(): WakuPeerRecord {.closure, gcsafe, raises: [].} diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index 488d07c06..34fc958fe 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -163,6 +163,15 @@ proc setupProtocols( error "Unrecoverable error occurred", error = msg quit(QuitFailure) + #mount mix + if conf.mixConf.isSome(): + ( + await node.mountMix( + conf.clusterId, conf.mixConf.get().mixKey, conf.mixConf.get().mixnodes + ) + ).isOkOr: + return err("failed to mount waku mix protocol: " & $error) + if conf.storeServiceConf.isSome(): let storeServiceConf = conf.storeServiceConf.get() if storeServiceConf.supportV2: @@ -327,9 +336,9 @@ proc setupProtocols( protectedShard = shardKey.shard, publicKey = shardKey.key node.wakuRelay.addSignedShardsValidator(subscribedProtectedShards, conf.clusterId) - # Only relay nodes should be rendezvous points. - if conf.rendezvous: - await node.mountRendezvous(conf.clusterId) + if conf.rendezvous: + await node.mountRendezvous(conf.clusterId) + await node.mountRendezvousClient(conf.clusterId) # Keepalive mounted on all nodes try: @@ -414,14 +423,6 @@ proc setupProtocols( if conf.peerExchangeDiscovery: await node.mountPeerExchangeClient() - #mount mix - if conf.mixConf.isSome(): - ( - await node.mountMix( - conf.clusterId, conf.mixConf.get().mixKey, conf.mixConf.get().mixnodes - ) - ).isOkOr: - return err("failed to mount waku mix protocol: " & $error) return ok() ## Start node diff --git a/waku/factory/waku_conf.nim b/waku/factory/waku_conf.nim index 89ffb366c..899008221 100644 --- a/waku/factory/waku_conf.nim +++ b/waku/factory/waku_conf.nim @@ -154,7 +154,8 @@ proc logConf*(conf: WakuConf) = store = conf.storeServiceConf.isSome(), filter = conf.filterServiceConf.isSome(), lightPush = conf.lightPush, - peerExchange = conf.peerExchangeService + peerExchange = conf.peerExchangeService, + rendezvous = conf.rendezvous info "Configuration. Network", cluster = conf.clusterId diff --git a/waku/node/peer_manager/waku_peer_store.nim b/waku/node/peer_manager/waku_peer_store.nim index c9e2d4817..9cde53fe1 100644 --- a/waku/node/peer_manager/waku_peer_store.nim +++ b/waku/node/peer_manager/waku_peer_store.nim @@ -6,7 +6,8 @@ import chronicles, eth/p2p/discoveryv5/enr, libp2p/builders, - libp2p/peerstore + libp2p/peerstore, + libp2p/crypto/curve25519 import ../../waku_core, @@ -42,6 +43,9 @@ type # Keeps track of peer shards ShardBook* = ref object of PeerBook[seq[uint16]] + # Keeps track of Mix protocol public keys of peers + MixPubKeyBook* = ref object of PeerBook[Curve25519Key] + proc getPeer*(peerStore: PeerStore, peerId: PeerId): RemotePeerInfo = let addresses = if peerStore[LastSeenBook][peerId].isSome(): @@ -68,6 +72,11 @@ proc getPeer*(peerStore: PeerStore, peerId: PeerId): RemotePeerInfo = direction: peerStore[DirectionBook][peerId], lastFailedConn: peerStore[LastFailedConnBook][peerId], numberFailedConn: peerStore[NumberFailedConnBook][peerId], + mixPubKey: + if peerStore[MixPubKeyBook][peerId] != default(Curve25519Key): + some(peerStore[MixPubKeyBook][peerId]) + else: + none(Curve25519Key), ) proc delete*(peerStore: PeerStore, peerId: PeerId) = @@ -87,6 +96,13 @@ proc peers*(peerStore: PeerStore): seq[RemotePeerInfo] = return allKeys.mapIt(peerStore.getPeer(it)) proc addPeer*(peerStore: PeerStore, peer: RemotePeerInfo, origin = UnknownOrigin) = + ## Storing MixPubKey even if peer is already present as this info might be new + ## or updated. + if peer.mixPubKey.isSome(): + trace "adding mix pub key to peer store", + peer_id = $peer.peerId, mix_pub_key = $peer.mixPubKey.get() + peerStore[MixPubKeyBook].book[peer.peerId] = peer.mixPubKey.get() + ## Notice that the origin parameter is used to manually override the given peer origin. ## At the time of writing, this is used in waku_discv5 or waku_node (peer exchange.) if peerStore[AddressBook][peer.peerId] == peer.addrs and @@ -113,6 +129,7 @@ proc addPeer*(peerStore: PeerStore, peer: RemotePeerInfo, origin = UnknownOrigin peerStore[ProtoBook][peer.peerId] = protos ## We don't care whether the item was already present in the table or not. Hence, we always discard the hasKeyOrPut's bool returned value + discard peerStore[AgentBook].book.hasKeyOrPut(peer.peerId, peer.agent) discard peerStore[ProtoVersionBook].book.hasKeyOrPut(peer.peerId, peer.protoVersion) discard peerStore[KeyBook].book.hasKeyOrPut(peer.peerId, peer.publicKey) diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 114775951..65b2093bb 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -22,6 +22,7 @@ import libp2p/transports/tcptransport, libp2p/transports/wstransport, libp2p/utility, + libp2p/utils/offsettedseq, libp2p/protocols/mix, libp2p/protocols/mix/mix_protocol @@ -43,6 +44,8 @@ import ../waku_filter_v2/client as filter_client, ../waku_metadata, ../waku_rendezvous/protocol, + ../waku_rendezvous/client as rendezvous_client, + ../waku_rendezvous/waku_peer_record, ../waku_lightpush_legacy/client as legacy_ligntpuhs_client, ../waku_lightpush_legacy as legacy_lightpush_protocol, ../waku_lightpush/client as ligntpuhs_client, @@ -121,6 +124,7 @@ type libp2pPing*: Ping rng*: ref rand.HmacDrbgContext wakuRendezvous*: WakuRendezVous + wakuRendezvousClient*: rendezvous_client.WakuRendezVousClient announcedAddresses*: seq[MultiAddress] started*: bool # Indicates that node has started listening topicSubscriptionQueue*: AsyncEventQueue[SubscriptionEvent] @@ -148,6 +152,17 @@ proc getCapabilitiesGetter(node: WakuNode): GetCapabilities = return @[] return node.enr.getCapabilities() +proc getWakuPeerRecordGetter(node: WakuNode): GetWakuPeerRecord = + return proc(): WakuPeerRecord {.closure, gcsafe, raises: [].} = + var mixKey: string + if not node.wakuMix.isNil(): + mixKey = node.wakuMix.pubKey.to0xHex() + return WakuPeerRecord.init( + peerId = node.switch.peerInfo.peerId, + addresses = node.announcedAddresses, + mixKey = mixKey, + ) + proc new*( T: type WakuNode, netConfig: NetConfig, @@ -257,12 +272,12 @@ proc mountMix*( return err("Failed to convert multiaddress to string.") info "local addr", localaddr = localaddrStr - let nodeAddr = localaddrStr & "/p2p/" & $node.peerId node.wakuMix = WakuMix.new( - nodeAddr, node.peerManager, clusterId, mixPrivKey, mixnodes + localaddrStr, node.peerManager, clusterId, mixPrivKey, mixnodes ).valueOr: error "Waku Mix protocol initialization failed", err = error return + #TODO: should we do the below only for exit node? Also, what if multiple protocols use mix? node.wakuMix.registerDestReadBehavior(WakuLightPushCodec, readLp(int(-1))) let catchRes = catch: node.switch.mount(node.wakuMix) @@ -346,6 +361,18 @@ proc selectRandomPeers*(peers: seq[PeerId], numRandomPeers: int): seq[PeerId] = shuffle(randomPeers) return randomPeers[0 ..< min(len(randomPeers), numRandomPeers)] +proc mountRendezvousClient*(node: WakuNode, clusterId: uint16) {.async: (raises: []).} = + info "mounting rendezvous client" + + node.wakuRendezvousClient = rendezvous_client.WakuRendezVousClient.new( + node.switch, node.peerManager, clusterId + ).valueOr: + error "initializing waku rendezvous client failed", error = error + return + + if node.started: + await node.wakuRendezvousClient.start() + proc mountRendezvous*(node: WakuNode, clusterId: uint16) {.async: (raises: []).} = info "mounting rendezvous discovery protocol" @@ -355,6 +382,7 @@ proc mountRendezvous*(node: WakuNode, clusterId: uint16) {.async: (raises: []).} clusterId, node.getShardsGetter(), node.getCapabilitiesGetter(), + node.getWakuPeerRecordGetter(), ).valueOr: error "initializing waku rendezvous failed", error = error return @@ -362,6 +390,11 @@ proc mountRendezvous*(node: WakuNode, clusterId: uint16) {.async: (raises: []).} if node.started: await node.wakuRendezvous.start() + try: + node.switch.mount(node.wakuRendezvous, protocolMatcher(WakuRendezVousCodec)) + except LPError: + error "failed to mount wakuRendezvous", error = getCurrentExceptionMsg() + proc isBindIpWithZeroPort(inputMultiAdd: MultiAddress): bool = let inputStr = $inputMultiAdd if inputStr.contains("0.0.0.0/tcp/0") or inputStr.contains("127.0.0.1/tcp/0"): @@ -438,6 +471,9 @@ proc start*(node: WakuNode) {.async.} = if not node.wakuRendezvous.isNil(): await node.wakuRendezvous.start() + if not node.wakuRendezvousClient.isNil(): + await node.wakuRendezvousClient.start() + if not node.wakuStoreReconciliation.isNil(): node.wakuStoreReconciliation.start() @@ -499,6 +535,9 @@ proc stop*(node: WakuNode) {.async.} = if not node.wakuRendezvous.isNil(): await node.wakuRendezvous.stopWait() + if not node.wakuRendezvousClient.isNil(): + await node.wakuRendezvousClient.stopWait() + node.started = false proc isReady*(node: WakuNode): Future[bool] {.async: (raises: [Exception]).} = diff --git a/waku/waku_core/codecs.nim b/waku/waku_core/codecs.nim index 6dcdfe2f5..0d9394c71 100644 --- a/waku/waku_core/codecs.nim +++ b/waku/waku_core/codecs.nim @@ -10,3 +10,4 @@ const WakuMetadataCodec* = "/vac/waku/metadata/1.0.0" WakuPeerExchangeCodec* = "/vac/waku/peer-exchange/2.0.0-alpha1" WakuLegacyStoreCodec* = "/vac/waku/store/2.0.0-beta4" + WakuRendezVousCodec* = "/vac/waku/rendezvous/1.0.0" diff --git a/waku/waku_core/peers.nim b/waku/waku_core/peers.nim index 76ff29aa0..48c994403 100644 --- a/waku/waku_core/peers.nim +++ b/waku/waku_core/peers.nim @@ -9,6 +9,7 @@ import eth/p2p/discoveryv5/enr, eth/net/utils, libp2p/crypto/crypto, + libp2p/crypto/curve25519, libp2p/crypto/secp, libp2p/errors, libp2p/multiaddress, @@ -49,6 +50,7 @@ type RemotePeerInfo* = ref object enr*: Option[enr.Record] protocols*: seq[string] shards*: seq[uint16] + mixPubKey*: Option[Curve25519Key] agent*: string protoVersion*: string @@ -84,6 +86,7 @@ proc init*( direction: PeerDirection = UnknownDirection, lastFailedConn: Moment = Moment.init(0, Second), numberFailedConn: int = 0, + mixPubKey: Option[Curve25519Key] = none(Curve25519Key), ): T = RemotePeerInfo( peerId: peerId, @@ -100,6 +103,7 @@ proc init*( direction: direction, lastFailedConn: lastFailedConn, numberFailedConn: numberFailedConn, + mixPubKey: mixPubKey, ) proc init*( diff --git a/waku/waku_lightpush/client.nim b/waku/waku_lightpush/client.nim index 4d0c49a84..d68552304 100644 --- a/waku/waku_lightpush/client.nim +++ b/waku/waku_lightpush/client.nim @@ -45,8 +45,13 @@ proc sendPushRequest( defer: await connection.closeWithEOF() - - await connection.writeLP(req.encode().buffer) + try: + await connection.writeLP(req.encode().buffer) + except CatchableError: + error "failed to send push request", error = getCurrentExceptionMsg() + return lightpushResultInternalError( + "failed to send push request: " & getCurrentExceptionMsg() + ) var buffer: seq[byte] try: @@ -56,9 +61,8 @@ proc sendPushRequest( return lightpushResultInternalError( "Failed to read response from peer: " & getCurrentExceptionMsg() ) - let response = LightpushResponse.decode(buffer).valueOr: - error "failed to decode response" + error "failed to decode response", error = $error waku_lightpush_v3_errors.inc(labelValues = [decodeRpcFailure]) return lightpushResultInternalError(decodeRpcFailure) diff --git a/waku/waku_mix/protocol.nim b/waku/waku_mix/protocol.nim index 34b50f8a9..d3d765df8 100644 --- a/waku/waku_mix/protocol.nim +++ b/waku/waku_mix/protocol.nim @@ -6,6 +6,8 @@ import libp2p/crypto/curve25519, libp2p/protocols/mix, libp2p/protocols/mix/mix_node, + libp2p/protocols/mix/mix_protocol, + libp2p/protocols/mix/mix_metrics, libp2p/[multiaddress, multicodec, peerid], eth/common/keys @@ -34,22 +36,18 @@ type multiAddr*: string pubKey*: Curve25519Key -proc mixPoolFilter*(cluster: Option[uint16], peer: RemotePeerInfo): bool = +proc filterMixNodes(cluster: Option[uint16], peer: RemotePeerInfo): bool = # Note that origin based(discv5) filtering is not done intentionally # so that more mix nodes can be discovered. - if peer.enr.isNone(): - trace "peer has no ENR", peer = $peer + if peer.mixPubKey.isNone(): + trace "remote peer has no mix Pub Key", peer = $peer return false - if cluster.isSome() and peer.enr.get().isClusterMismatched(cluster.get()): + if cluster.isSome() and peer.enr.isSome() and + peer.enr.get().isClusterMismatched(cluster.get()): trace "peer has mismatching cluster", peer = $peer return false - # Filter if mix is enabled - if not peer.enr.get().supportsCapability(Capabilities.Mix): - trace "peer doesn't support mix", peer = $peer - return false - return true proc appendPeerIdToMultiaddr*(multiaddr: MultiAddress, peerId: PeerId): MultiAddress = @@ -74,34 +72,52 @@ func getIPv4Multiaddr*(maddrs: seq[MultiAddress]): Option[MultiAddress] = trace "no ipv4 multiaddr found" return none(MultiAddress) -#[ Not deleting as these can be reused once discovery is sorted - proc populateMixNodePool*(mix: WakuMix) = +proc populateMixNodePool*(mix: WakuMix) = # populate only peers that i) are reachable ii) share cluster iii) support mix let remotePeers = mix.peerManager.switch.peerStore.peers().filterIt( - mixPoolFilter(some(mix.clusterId), it) + filterMixNodes(some(mix.clusterId), it) ) var mixNodes = initTable[PeerId, MixPubInfo]() for i in 0 ..< min(remotePeers.len, 100): - let remotePeerENR = remotePeers[i].enr.get() let ipv4addr = getIPv4Multiaddr(remotePeers[i].addrs).valueOr: trace "peer has no ipv4 address", peer = $remotePeers[i] continue - let maddrWithPeerId = - toString(appendPeerIdToMultiaddr(ipv4addr, remotePeers[i].peerId)) - trace "remote peer ENR", - peerId = remotePeers[i].peerId, enr = remotePeerENR, maddr = maddrWithPeerId + let maddrWithPeerId = appendPeerIdToMultiaddr(ipv4addr, remotePeers[i].peerId) + trace "remote peer info", info = remotePeers[i] - let peerMixPubKey = mixKey(remotePeerENR).get() - let mixNodePubInfo = - createMixPubInfo(maddrWithPeerId.value, intoCurve25519Key(peerMixPubKey)) + if remotePeers[i].mixPubKey.isNone(): + trace "peer has no mix Pub Key", remotePeerId = $remotePeers[i] + continue + + let peerMixPubKey = remotePeers[i].mixPubKey.get() + var peerPubKey: crypto.PublicKey + if not remotePeers[i].peerId.extractPublicKey(peerPubKey): + warn "Failed to extract public key from peerId, skipping node", + remotePeerId = remotePeers[i].peerId + continue + + if peerPubKey.scheme != PKScheme.Secp256k1: + warn "Peer public key is not Secp256k1, skipping node", + remotePeerId = remotePeers[i].peerId, scheme = peerPubKey.scheme + continue + + let mixNodePubInfo = MixPubInfo.init( + remotePeers[i].peerId, + ipv4addr, + intoCurve25519Key(peerMixPubKey), + peerPubKey.skkey, + ) + trace "adding mix node to pool", + remotePeerId = remotePeers[i].peerId, multiAddr = $ipv4addr mixNodes[remotePeers[i].peerId] = mixNodePubInfo - mix_pool_size.set(len(mixNodes)) # set the mix node pool mix.setNodePool(mixNodes) + mix_pool_size.set(len(mixNodes)) trace "mix node pool updated", poolSize = mix.getNodePoolSize() +# Once mix protocol starts to use info from PeerStore, then this can be removed. proc startMixNodePoolMgr*(mix: WakuMix) {.async.} = info "starting mix node pool manager" # try more aggressively to populate the pool at startup @@ -115,9 +131,10 @@ proc startMixNodePoolMgr*(mix: WakuMix) {.async.} = # TODO: make interval configurable heartbeat "Updating mix node pool", 5.seconds: mix.populateMixNodePool() - ]# -proc toMixNodeTable(bootnodes: seq[MixNodePubInfo]): Table[PeerId, MixPubInfo] = +proc processBootNodes( + bootnodes: seq[MixNodePubInfo], peermgr: PeerManager +): Table[PeerId, MixPubInfo] = var mixNodes = initTable[PeerId, MixPubInfo]() for node in bootnodes: let pInfo = parsePeerInfo(node.multiAddr).valueOr: @@ -140,6 +157,11 @@ proc toMixNodeTable(bootnodes: seq[MixNodePubInfo]): Table[PeerId, MixPubInfo] = continue mixNodes[peerId] = MixPubInfo.init(peerId, multiAddr, node.pubKey, peerPubKey.skkey) + + peermgr.addPeer( + RemotePeerInfo.init(peerId, @[multiAddr], mixPubKey = some(node.pubKey)) + ) + mix_pool_size.set(len(mixNodes)) info "using mix bootstrap nodes ", bootNodes = mixNodes return mixNodes @@ -152,7 +174,7 @@ proc new*( bootnodes: seq[MixNodePubInfo], ): WakuMixResult[T] = let mixPubKey = public(mixPrivKey) - info "mixPrivKey", mixPrivKey = mixPrivKey, mixPubKey = mixPubKey + info "mixPubKey", mixPubKey = mixPubKey let nodeMultiAddr = MultiAddress.init(nodeAddr).valueOr: return err("failed to parse mix node address: " & $nodeAddr & ", error: " & error) let localMixNodeInfo = initMixNodeInfo( @@ -160,17 +182,18 @@ proc new*( peermgr.switch.peerInfo.publicKey.skkey, peermgr.switch.peerInfo.privateKey.skkey, ) if bootnodes.len < mixMixPoolSize: - warn "publishing with mix won't work as there are less than 3 mix nodes in node pool" - let initTable = toMixNodeTable(bootnodes) + warn "publishing with mix won't work until there are 3 mix nodes in node pool" + let initTable = processBootNodes(bootnodes, peermgr) + if len(initTable) < mixMixPoolSize: - warn "publishing with mix won't work as there are less than 3 mix nodes in node pool" + warn "publishing with mix won't work until there are 3 mix nodes in node pool" var m = WakuMix(peerManager: peermgr, clusterId: clusterId, pubKey: mixPubKey) procCall MixProtocol(m).init(localMixNodeInfo, initTable, peermgr.switch) return ok(m) method start*(mix: WakuMix) = info "starting waku mix protocol" - #mix.nodePoolLoopHandle = mix.startMixNodePoolMgr() This can be re-enabled once discovery is addressed + mix.nodePoolLoopHandle = mix.startMixNodePoolMgr() method stop*(mix: WakuMix) {.async.} = if mix.nodePoolLoopHandle.isNil(): diff --git a/waku/waku_rendezvous/client.nim b/waku/waku_rendezvous/client.nim new file mode 100644 index 000000000..09e789774 --- /dev/null +++ b/waku/waku_rendezvous/client.nim @@ -0,0 +1,142 @@ +{.push raises: [].} + +import + std/[options, sequtils, tables], + results, + chronos, + chronicles, + libp2p/protocols/rendezvous, + libp2p/crypto/curve25519, + libp2p/switch, + libp2p/utils/semaphore + +import metrics except collect + +import + waku/node/peer_manager, + waku/waku_core/peers, + waku/waku_core/codecs, + ./common, + ./waku_peer_record + +logScope: + topics = "waku rendezvous client" + +declarePublicCounter rendezvousPeerFoundTotal, + "total number of peers found via rendezvous" + +type WakuRendezVousClient* = ref object + switch: Switch + peerManager: PeerManager + clusterId: uint16 + requestInterval: timer.Duration + periodicRequestFut: Future[void] + # Internal rendezvous instance for making requests + rdv: GenericRendezVous[WakuPeerRecord] + +const MaxSimultanesousAdvertisements = 5 +const RendezVousLookupInterval = 10.seconds + +proc requestAll*( + self: WakuRendezVousClient +): Future[Result[void, string]] {.async: (raises: []).} = + trace "waku rendezvous client requests started" + + let namespace = computeMixNamespace(self.clusterId) + + # Get a random WakuRDV peer + let rpi = self.peerManager.selectPeer(WakuRendezVousCodec).valueOr: + return err("could not get a peer supporting WakuRendezVousCodec") + + var records: seq[WakuPeerRecord] + try: + # Use the libp2p rendezvous request method + records = await self.rdv.request( + Opt.some(namespace), Opt.some(PeersRequestedCount), Opt.some(@[rpi.peerId]) + ) + except CatchableError as e: + return err("rendezvous request failed: " & e.msg) + + trace "waku rendezvous client request got peers", count = records.len + for record in records: + if not self.switch.peerStore.peerExists(record.peerId): + rendezvousPeerFoundTotal.inc() + if record.mixKey.len == 0 or record.peerId == self.switch.peerInfo.peerId: + continue + trace "adding peer from rendezvous", + peerId = record.peerId, addresses = $record.addresses, mixKey = record.mixKey + let rInfo = RemotePeerInfo.init( + record.peerId, + record.addresses, + mixPubKey = some(intoCurve25519Key(fromHex(record.mixKey))), + ) + self.peerManager.addPeer(rInfo) + + trace "waku rendezvous client request finished" + + return ok() + +proc periodicRequests(self: WakuRendezVousClient) {.async.} = + info "waku rendezvous periodic requests started", interval = self.requestInterval + + # infinite loop + while true: + await sleepAsync(self.requestInterval) + + (await self.requestAll()).isOkOr: + error "waku rendezvous requests failed", error = error + + # Exponential backoff + +#[ TODO: Reevaluate for mix, maybe be aggresive in the start until a sizeable pool is built and then backoff + self.requestInterval += self.requestInterval + + if self.requestInterval >= 1.days: + break ]# + +proc new*( + T: type WakuRendezVousClient, + switch: Switch, + peerManager: PeerManager, + clusterId: uint16, +): Result[T, string] {.raises: [].} = + # Create a minimal GenericRendezVous instance for client-side requests + # We don't need the full server functionality, just the request method + let rng = newRng() + let rdv = GenericRendezVous[WakuPeerRecord]( + switch: switch, + rng: rng, + sema: newAsyncSemaphore(MaxSimultanesousAdvertisements), + minDuration: rendezvous.MinimumAcceptedDuration, + maxDuration: rendezvous.MaximumDuration, + minTTL: rendezvous.MinimumAcceptedDuration.seconds.uint64, + maxTTL: rendezvous.MaximumDuration.seconds.uint64, + peers: @[], # Will be populated from selectPeer calls + cookiesSaved: initTable[PeerId, Table[string, seq[byte]]](), + peerRecordValidator: checkWakuPeerRecord, + ) + + # Set codec separately as it's inherited from LPProtocol + rdv.codec = WakuRendezVousCodec + + let client = T( + switch: switch, + peerManager: peerManager, + clusterId: clusterId, + requestInterval: RendezVousLookupInterval, + rdv: rdv, + ) + + info "waku rendezvous client initialized", clusterId = clusterId + + return ok(client) + +proc start*(self: WakuRendezVousClient) {.async: (raises: []).} = + self.periodicRequestFut = self.periodicRequests() + info "waku rendezvous client started" + +proc stopWait*(self: WakuRendezVousClient) {.async: (raises: []).} = + if not self.periodicRequestFut.isNil(): + await self.periodicRequestFut.cancelAndWait() + + info "waku rendezvous client stopped" diff --git a/waku/waku_rendezvous/common.nim b/waku/waku_rendezvous/common.nim index 6125ac860..18c633efb 100644 --- a/waku/waku_rendezvous/common.nim +++ b/waku/waku_rendezvous/common.nim @@ -11,6 +11,14 @@ const DefaultRequestsInterval* = 1.minutes const MaxRegistrationInterval* = 5.minutes const PeersRequestedCount* = 12 +proc computeMixNamespace*(clusterId: uint16): string = + var namespace = "rs/" + + namespace &= $clusterId + namespace &= "/mix" + + return namespace + proc computeNamespace*(clusterId: uint16, shard: uint16): string = var namespace = "rs/" diff --git a/waku/waku_rendezvous/protocol.nim b/waku/waku_rendezvous/protocol.nim index 0eb55d350..ed414fa42 100644 --- a/waku/waku_rendezvous/protocol.nim +++ b/waku/waku_rendezvous/protocol.nim @@ -1,70 +1,91 @@ {.push raises: [].} import - std/[sugar, options], + std/[sugar, options, sequtils, tables], results, chronos, chronicles, - metrics, + stew/byteutils, libp2p/protocols/rendezvous, + libp2p/protocols/rendezvous/protobuf, + libp2p/discovery/discoverymngr, + libp2p/utils/semaphore, + libp2p/utils/offsettedseq, + libp2p/crypto/curve25519, libp2p/switch, libp2p/utility +import metrics except collect + import ../node/peer_manager, ../common/callbacks, ../waku_enr/capabilities, ../waku_core/peers, - ../waku_core/topics, - ../waku_core/topics/pubsub_topic, - ./common + ../waku_core/codecs, + ./common, + ./waku_peer_record logScope: topics = "waku rendezvous" -declarePublicCounter rendezvousPeerFoundTotal, - "total number of peers found via rendezvous" - -type WakuRendezVous* = ref object - rendezvous: Rendezvous +type WakuRendezVous* = ref object of GenericRendezVous[WakuPeerRecord] peerManager: PeerManager clusterId: uint16 getShards: GetShards getCapabilities: GetCapabilities + getPeerRecord: GetWakuPeerRecord registrationInterval: timer.Duration periodicRegistrationFut: Future[void] - requestInterval: timer.Duration - periodicRequestFut: Future[void] +const MaximumNamespaceLen = 255 -proc batchAdvertise*( +method discover*( + self: WakuRendezVous, conn: Connection, d: Discover +) {.async: (raises: [CancelledError, LPStreamError]).} = + # Override discover method to avoid collect macro generic instantiation issues + trace "Received Discover", peerId = conn.peerId, ns = d.ns + await procCall GenericRendezVous[WakuPeerRecord](self).discover(conn, d) + +proc advertise*( self: WakuRendezVous, namespace: string, - ttl: Duration = DefaultRegistrationTTL, peers: seq[PeerId], + ttl: timer.Duration = self.minDuration, ): Future[Result[void, string]] {.async: (raises: []).} = - ## Register with all rendezvous peers under a namespace + trace "advertising via waku rendezvous", + namespace = namespace, ttl = ttl, peers = $peers, peerRecord = $self.getPeerRecord() + let se = SignedPayload[WakuPeerRecord].init( + self.switch.peerInfo.privateKey, self.getPeerRecord() + ).valueOr: + return + err("rendezvous advertisement failed: Failed to sign Waku Peer Record: " & $error) + let sprBuff = se.encode().valueOr: + return err("rendezvous advertisement failed: Wrong Signed Peer Record: " & $error) # rendezvous.advertise expects already opened connections # must dial first + var futs = collect(newSeq): for peerId in peers: - self.peerManager.dialPeer(peerId, RendezVousCodec) + self.peerManager.dialPeer(peerId, self.codec) let dialCatch = catch: await allFinished(futs) - futs = dialCatch.valueOr: - return err("batchAdvertise: " & error.msg) + if dialCatch.isErr(): + return err("advertise: " & dialCatch.error.msg) + + futs = dialCatch.get() let conns = collect(newSeq): for fut in futs: let catchable = catch: fut.read() - catchable.isOkOr: - warn "a rendezvous dial failed", cause = error.msg + if catchable.isErr(): + warn "a rendezvous dial failed", cause = catchable.error.msg continue let connOpt = catchable.get() @@ -74,149 +95,34 @@ proc batchAdvertise*( conn - let advertCatch = catch: - await self.rendezvous.advertise(namespace, Opt.some(ttl)) - - for conn in conns: - await conn.close() - - advertCatch.isOkOr: - return err("batchAdvertise: " & error.msg) + if conns.len == 0: + return err("could not establish any connections to rendezvous peers") + try: + await self.advertise(namespace, ttl, peers, sprBuff) + except Exception as e: + return err("rendezvous advertisement failed: " & e.msg) + finally: + for conn in conns: + await conn.close() return ok() -proc batchRequest*( - self: WakuRendezVous, - namespace: string, - count: int = DiscoverLimit, - peers: seq[PeerId], -): Future[Result[seq[PeerRecord], string]] {.async: (raises: []).} = - ## Request all records from all rendezvous peers matching a namespace - - # rendezvous.request expects already opened connections - # must dial first - var futs = collect(newSeq): - for peerId in peers: - self.peerManager.dialPeer(peerId, RendezVousCodec) - - let dialCatch = catch: - await allFinished(futs) - - futs = dialCatch.valueOr: - return err("batchRequest: " & error.msg) - - let conns = collect(newSeq): - for fut in futs: - let catchable = catch: - fut.read() - - catchable.isOkOr: - warn "a rendezvous dial failed", cause = error.msg - continue - - let connOpt = catchable.get() - - let conn = connOpt.valueOr: - continue - - conn - - let reqCatch = catch: - await self.rendezvous.request(Opt.some(namespace), Opt.some(count), Opt.some(peers)) - - for conn in conns: - await conn.close() - - reqCatch.isOkOr: - return err("batchRequest: " & error.msg) - - return ok(reqCatch.get()) - -proc advertiseAll( +proc advertiseAll*( self: WakuRendezVous ): Future[Result[void, string]] {.async: (raises: []).} = - info "waku rendezvous advertisements started" + trace "waku rendezvous advertisements started" - let shards = self.getShards() - - let futs = collect(newSeq): - for shardId in shards: - # Get a random RDV peer for that shard - - let pubsub = - toPubsubTopic(RelayShard(clusterId: self.clusterId, shardId: shardId)) - - let rpi = self.peerManager.selectPeer(RendezVousCodec, some(pubsub)).valueOr: - continue - - let namespace = computeNamespace(self.clusterId, shardId) - - # Advertise yourself on that peer - self.batchAdvertise(namespace, DefaultRegistrationTTL, @[rpi.peerId]) - - if futs.len < 1: + let rpi = self.peerManager.selectPeer(self.codec).valueOr: return err("could not get a peer supporting RendezVousCodec") - let catchable = catch: - await allFinished(futs) + let namespace = computeMixNamespace(self.clusterId) - catchable.isOkOr: - return err(error.msg) + # Advertise yourself on that peer + let res = await self.advertise(namespace, @[rpi.peerId]) - for fut in catchable.get(): - if fut.failed(): - warn "a rendezvous advertisement failed", cause = fut.error.msg + trace "waku rendezvous advertisements finished" - info "waku rendezvous advertisements finished" - - return ok() - -proc initialRequestAll*( - self: WakuRendezVous -): Future[Result[void, string]] {.async: (raises: []).} = - info "waku rendezvous initial requests started" - - let shards = self.getShards() - - let futs = collect(newSeq): - for shardId in shards: - let namespace = computeNamespace(self.clusterId, shardId) - # Get a random RDV peer for that shard - let rpi = self.peerManager.selectPeer( - RendezVousCodec, - some(toPubsubTopic(RelayShard(clusterId: self.clusterId, shardId: shardId))), - ).valueOr: - continue - - # Ask for peer records for that shard - self.batchRequest(namespace, PeersRequestedCount, @[rpi.peerId]) - - if futs.len < 1: - return err("could not get a peer supporting RendezVousCodec") - - let catchable = catch: - await allFinished(futs) - - catchable.isOkOr: - return err(error.msg) - - for fut in catchable.get(): - if fut.failed(): - warn "a rendezvous request failed", cause = fut.error.msg - elif fut.finished(): - let res = fut.value() - - let records = res.valueOr: - warn "a rendezvous request failed", cause = $error - continue - - for record in records: - rendezvousPeerFoundTotal.inc() - self.peerManager.addPeer(record) - - info "waku rendezvous initial request finished" - - return ok() + return res proc periodicRegistration(self: WakuRendezVous) {.async.} = info "waku rendezvous periodic registration started", @@ -237,22 +143,6 @@ proc periodicRegistration(self: WakuRendezVous) {.async.} = # Back to normal interval if no errors self.registrationInterval = DefaultRegistrationInterval -proc periodicRequests(self: WakuRendezVous) {.async.} = - info "waku rendezvous periodic requests started", interval = self.requestInterval - - # infinite loop - while true: - (await self.initialRequestAll()).isOkOr: - error "waku rendezvous requests failed", error = error - - await sleepAsync(self.requestInterval) - - # Exponential backoff - self.requestInterval += self.requestInterval - - if self.requestInterval >= 1.days: - break - proc new*( T: type WakuRendezVous, switch: Switch, @@ -260,46 +150,88 @@ proc new*( clusterId: uint16, getShards: GetShards, getCapabilities: GetCapabilities, + getPeerRecord: GetWakuPeerRecord, ): Result[T, string] {.raises: [].} = - let rvCatchable = catch: - RendezVous.new(switch = switch, minDuration = DefaultRegistrationTTL) + let rng = newRng() + let wrv = T( + rng: rng, + salt: string.fromBytes(generateBytes(rng[], 8)), + registered: initOffsettedSeq[RegisteredData](), + expiredDT: Moment.now() - 1.days, + sema: newAsyncSemaphore(SemaphoreDefaultSize), + minDuration: rendezvous.MinimumAcceptedDuration, + maxDuration: rendezvous.MaximumDuration, + minTTL: rendezvous.MinimumAcceptedDuration.seconds.uint64, + maxTTL: rendezvous.MaximumDuration.seconds.uint64, + peerRecordValidator: checkWakuPeerRecord, + ) - let rv = rvCatchable.valueOr: - return err(error.msg) - - let mountCatchable = catch: - switch.mount(rv) - - mountCatchable.isOkOr: - return err(error.msg) - - var wrv = WakuRendezVous() - wrv.rendezvous = rv wrv.peerManager = peerManager wrv.clusterId = clusterId wrv.getShards = getShards wrv.getCapabilities = getCapabilities wrv.registrationInterval = DefaultRegistrationInterval - wrv.requestInterval = DefaultRequestsInterval + wrv.getPeerRecord = getPeerRecord + wrv.switch = switch + wrv.codec = WakuRendezVousCodec + + proc handleStream( + conn: Connection, proto: string + ) {.async: (raises: [CancelledError]).} = + try: + let + buf = await conn.readLp(4096) + msg = Message.decode(buf).tryGet() + case msg.msgType + of MessageType.Register: + #TODO: override this to store peers registered with us in peerstore with their info as well. + await wrv.register(conn, msg.register.tryGet(), wrv.getPeerRecord()) + of MessageType.RegisterResponse: + trace "Got an unexpected Register Response", response = msg.registerResponse + of MessageType.Unregister: + wrv.unregister(conn, msg.unregister.tryGet()) + of MessageType.Discover: + await wrv.discover(conn, msg.discover.tryGet()) + of MessageType.DiscoverResponse: + trace "Got an unexpected Discover Response", response = msg.discoverResponse + except CancelledError as exc: + trace "cancelled rendezvous handler" + raise exc + except CatchableError as exc: + trace "exception in rendezvous handler", description = exc.msg + finally: + await conn.close() + + wrv.handler = handleStream info "waku rendezvous initialized", - clusterId = clusterId, shards = getShards(), capabilities = getCapabilities() + clusterId = clusterId, + shards = getShards(), + capabilities = getCapabilities(), + wakuPeerRecord = getPeerRecord() return ok(wrv) proc start*(self: WakuRendezVous) {.async: (raises: []).} = + # Start the parent GenericRendezVous (starts the register deletion loop) + if self.started: + warn "waku rendezvous already started" + return + try: + await procCall GenericRendezVous[WakuPeerRecord](self).start() + except CancelledError as exc: + error "failed to start GenericRendezVous", cause = exc.msg + return # start registering forever self.periodicRegistrationFut = self.periodicRegistration() - self.periodicRequestFut = self.periodicRequests() - info "waku rendezvous discovery started" proc stopWait*(self: WakuRendezVous) {.async: (raises: []).} = if not self.periodicRegistrationFut.isNil(): await self.periodicRegistrationFut.cancelAndWait() - if not self.periodicRequestFut.isNil(): - await self.periodicRequestFut.cancelAndWait() + # Stop the parent GenericRendezVous (stops the register deletion loop) + await GenericRendezVous[WakuPeerRecord](self).stop() info "waku rendezvous discovery stopped" diff --git a/waku/waku_rendezvous/waku_peer_record.nim b/waku/waku_rendezvous/waku_peer_record.nim new file mode 100644 index 000000000..d6e700eb5 --- /dev/null +++ b/waku/waku_rendezvous/waku_peer_record.nim @@ -0,0 +1,74 @@ +import std/times, sugar + +import + libp2p/[ + protocols/rendezvous, + signed_envelope, + multicodec, + multiaddress, + protobuf/minprotobuf, + peerid, + ] + +type WakuPeerRecord* = object + # Considering only mix as of now, but we can keep extending this to include all capabilities part of Waku ENR + peerId*: PeerId + seqNo*: uint64 + addresses*: seq[MultiAddress] + mixKey*: string + +proc payloadDomain*(T: typedesc[WakuPeerRecord]): string = + $multiCodec("libp2p-custom-peer-record") + +proc payloadType*(T: typedesc[WakuPeerRecord]): seq[byte] = + @[(byte) 0x30, (byte) 0x00, (byte) 0x00] + +proc init*( + T: typedesc[WakuPeerRecord], + peerId: PeerId, + seqNo = getTime().toUnix().uint64, + addresses: seq[MultiAddress], + mixKey: string, +): T = + WakuPeerRecord(peerId: peerId, seqNo: seqNo, addresses: addresses, mixKey: mixKey) + +proc decode*( + T: typedesc[WakuPeerRecord], buffer: seq[byte] +): Result[WakuPeerRecord, ProtoError] = + let pb = initProtoBuffer(buffer) + var record = WakuPeerRecord() + + ?pb.getRequiredField(1, record.peerId) + ?pb.getRequiredField(2, record.seqNo) + discard ?pb.getRepeatedField(3, record.addresses) + + if record.addresses.len == 0: + return err(ProtoError.RequiredFieldMissing) + + ?pb.getRequiredField(4, record.mixKey) + + return ok(record) + +proc encode*(record: WakuPeerRecord): seq[byte] = + var pb = initProtoBuffer() + + pb.write(1, record.peerId) + pb.write(2, record.seqNo) + + for address in record.addresses: + pb.write(3, address) + + pb.write(4, record.mixKey) + + pb.finish() + return pb.buffer + +proc checkWakuPeerRecord*( + _: WakuPeerRecord, spr: seq[byte], peerId: PeerId +): Result[void, string] {.gcsafe.} = + if spr.len == 0: + return err("Empty peer record") + let signedEnv = ?SignedPayload[WakuPeerRecord].decode(spr).mapErr(x => $x) + if signedEnv.data.peerId != peerId: + return err("Bad Peer ID") + return ok() From 088e3108c86085c2d3c072c29964ae96f285c6d2 Mon Sep 17 00:00:00 2001 From: Prem Chaitanya Prathi Date: Sat, 22 Nov 2025 08:11:05 +0530 Subject: [PATCH 013/155] use exit==dest approach for mix (#3642) --- Makefile | 3 +++ apps/chat2mix/chat2mix.nim | 7 +++++-- examples/lightpush_mix/lightpush_publisher_mix.nim | 5 ++++- simulations/mixnet/config.toml | 4 ++-- .../mixnet/{run_lp_service_node.sh => run_mix_node.sh} | 0 waku/node/kernel_api/lightpush.nim | 2 +- waku/waku_mix/protocol.nim | 10 +++++----- waku/waku_rendezvous/protocol.nim | 3 +++ 8 files changed, 23 insertions(+), 11 deletions(-) rename simulations/mixnet/{run_lp_service_node.sh => run_mix_node.sh} (100%) diff --git a/Makefile b/Makefile index 029313c99..f65daff71 100644 --- a/Makefile +++ b/Makefile @@ -143,6 +143,9 @@ ifeq ($(USE_LIBBACKTRACE), 0) NIM_PARAMS := $(NIM_PARAMS) -d:disable_libbacktrace endif +# enable experimental exit is dest feature in libp2p mix +NIM_PARAMS := $(NIM_PARAMS) -d:libp2p_mix_experimental_exit_is_dest + libbacktrace: + $(MAKE) -C vendor/nim-libbacktrace --no-print-directory BUILD_CXX_LIB=0 diff --git a/apps/chat2mix/chat2mix.nim b/apps/chat2mix/chat2mix.nim index 3fdd7bc9c..45fd1fa2d 100644 --- a/apps/chat2mix/chat2mix.nim +++ b/apps/chat2mix/chat2mix.nim @@ -82,6 +82,8 @@ type PrivateKey* = crypto.PrivateKey Topic* = waku_core.PubsubTopic +const MinMixNodePoolSize = 4 + ##################### ## chat2 protobufs ## ##################### @@ -592,6 +594,7 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = node.peerManager.addServicePeer(servicePeerInfo, WakuLightpushCodec) node.peerManager.addServicePeer(servicePeerInfo, WakuPeerExchangeCodec) + #node.peerManager.addServicePeer(servicePeerInfo, WakuRendezVousCodec) # Start maintaining subscription asyncSpawn maintainSubscription( @@ -599,12 +602,12 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = ) echo "waiting for mix nodes to be discovered..." while true: - if node.getMixNodePoolSize() >= 3: + if node.getMixNodePoolSize() >= MinMixNodePoolSize: break discard await node.fetchPeerExchangePeers() await sleepAsync(1000) - while node.getMixNodePoolSize() < 3: + while node.getMixNodePoolSize() < MinMixNodePoolSize: info "waiting for mix nodes to be discovered", currentpoolSize = node.getMixNodePoolSize() await sleepAsync(1000) diff --git a/examples/lightpush_mix/lightpush_publisher_mix.nim b/examples/lightpush_mix/lightpush_publisher_mix.nim index bb4bb4c4e..4219cd665 100644 --- a/examples/lightpush_mix/lightpush_publisher_mix.nim +++ b/examples/lightpush_mix/lightpush_publisher_mix.nim @@ -144,7 +144,7 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} conn = connOpt.get() else: conn = node.wakuMix.toConnection( - MixDestination.init(dPeerId, pxPeerInfo.addrs[0]), # destination lightpush peer + MixDestination.exitNode(dPeerId), # destination lightpush peer WakuLightPushCodec, # protocol codec which will be used over the mix connection MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), # mix parameters indicating we expect a single reply @@ -163,6 +163,9 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} ephemeral: true, # tell store nodes to not store it timestamp: getNowInNanosecondTime(), ) # current timestamp + let res = await node.wakuLightpushClient.publishWithConn( + LightpushPubsubTopic, message, conn, dPeerId + ) let startTime = getNowInNanosecondTime() diff --git a/simulations/mixnet/config.toml b/simulations/mixnet/config.toml index 17e9242d3..3719d8177 100644 --- a/simulations/mixnet/config.toml +++ b/simulations/mixnet/config.toml @@ -1,6 +1,6 @@ log-level = "INFO" relay = true -#mix = true +mix = true filter = true store = false lightpush = true @@ -18,7 +18,7 @@ num-shards-in-network = 1 shard = [0] agent-string = "nwaku-mix" nodekey = "f98e3fba96c32e8d1967d460f1b79457380e1a895f7971cecc8528abe733781a" -#mixkey = "a87db88246ec0eedda347b9b643864bee3d6933eb15ba41e6d58cb678d813258" +mixkey = "a87db88246ec0eedda347b9b643864bee3d6933eb15ba41e6d58cb678d813258" rendezvous = true listen-address = "127.0.0.1" nat = "extip:127.0.0.1" diff --git a/simulations/mixnet/run_lp_service_node.sh b/simulations/mixnet/run_mix_node.sh similarity index 100% rename from simulations/mixnet/run_lp_service_node.sh rename to simulations/mixnet/run_mix_node.sh diff --git a/waku/node/kernel_api/lightpush.nim b/waku/node/kernel_api/lightpush.nim index f42cb146e..8df6291b1 100644 --- a/waku/node/kernel_api/lightpush.nim +++ b/waku/node/kernel_api/lightpush.nim @@ -199,7 +199,7 @@ proc lightpushPublishHandler( if mixify: #indicates we want to use mix to send the message #TODO: How to handle multiple addresses? let conn = node.wakuMix.toConnection( - MixDestination.init(peer.peerId, peer.addrs[0]), + MixDestination.exitNode(peer.peerId), WakuLightPushCodec, MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), # indicating we only want a single path to be used for reply hence numSurbs = 1 diff --git a/waku/waku_mix/protocol.nim b/waku/waku_mix/protocol.nim index d3d765df8..366d5da91 100644 --- a/waku/waku_mix/protocol.nim +++ b/waku/waku_mix/protocol.nim @@ -21,7 +21,7 @@ import logScope: topics = "waku mix" -const mixMixPoolSize = 3 +const minMixPoolSize = 4 type WakuMix* = ref object of MixProtocol @@ -181,12 +181,12 @@ proc new*( peermgr.switch.peerInfo.peerId, nodeMultiAddr, mixPubKey, mixPrivKey, peermgr.switch.peerInfo.publicKey.skkey, peermgr.switch.peerInfo.privateKey.skkey, ) - if bootnodes.len < mixMixPoolSize: - warn "publishing with mix won't work until there are 3 mix nodes in node pool" + if bootnodes.len < minMixPoolSize: + warn "publishing with mix won't work until atleast 3 mix nodes in node pool" let initTable = processBootNodes(bootnodes, peermgr) - if len(initTable) < mixMixPoolSize: - warn "publishing with mix won't work until there are 3 mix nodes in node pool" + if len(initTable) < minMixPoolSize: + warn "publishing with mix won't work until atleast 3 mix nodes in node pool" var m = WakuMix(peerManager: peermgr, clusterId: clusterId, pubKey: mixPubKey) procCall MixProtocol(m).init(localMixNodeInfo, initTable, peermgr.switch) return ok(m) diff --git a/waku/waku_rendezvous/protocol.nim b/waku/waku_rendezvous/protocol.nim index ed414fa42..7b97375ff 100644 --- a/waku/waku_rendezvous/protocol.nim +++ b/waku/waku_rendezvous/protocol.nim @@ -234,4 +234,7 @@ proc stopWait*(self: WakuRendezVous) {.async: (raises: []).} = # Stop the parent GenericRendezVous (stops the register deletion loop) await GenericRendezVous[WakuPeerRecord](self).stop() + # Stop the parent GenericRendezVous (stops the register deletion loop) + await GenericRendezVous[WakuPeerRecord](self).stop() + info "waku rendezvous discovery stopped" From 454b098ac52df75e5d5de5010f9edb42cf8b0d52 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:16:37 +0100 Subject: [PATCH 014/155] new metric in postgres_driver to estimate payload stats (#3596) --- .../driver/postgres_driver/postgres_driver.nim | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim index 842d7cbc2..9b0e14c84 100644 --- a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim @@ -5,6 +5,7 @@ import stew/[byteutils, arrayops], results, chronos, + metrics, db_connector/[postgres, db_common], chronicles import @@ -16,6 +17,9 @@ import ./postgres_healthcheck, ./partitions_manager +declarePublicGauge postgres_payload_size_bytes, + "Payload size in bytes of correctly stored messages" + type PostgresDriver* = ref object of ArchiveDriver ## Establish a separate pools for read/write operations writeConnPool: PgAsyncPool @@ -333,7 +337,7 @@ method put*( return err("could not put msg in messages table: " & $error) ## Now add the row to messages_lookup - return await s.writeConnPool.runStmt( + let ret = await s.writeConnPool.runStmt( InsertRowInMessagesLookupStmtName, InsertRowInMessagesLookupStmtDefinition, @[messageHash, timestamp], @@ -341,6 +345,10 @@ method put*( @[int32(0), int32(0)], ) + if ret.isOk(): + postgres_payload_size_bytes.set(message.payload.len) + return ret + method getAllMessages*( s: PostgresDriver ): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = From c0a7debfd157a158c7973fc1135150df804f97e5 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:05:40 +0100 Subject: [PATCH 015/155] Adapt makefile for libwaku windows (#3648) --- Makefile | 6 +++- scripts/libwaku_windows_setup.mk | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 scripts/libwaku_windows_setup.mk diff --git a/Makefile b/Makefile index f65daff71..2f15ccd71 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,9 @@ ifeq ($(detected_OS),Windows) LIBS = -lws2_32 -lbcrypt -liphlpapi -luserenv -lntdll -lminiupnpc -lnatpmp -lpq NIM_PARAMS += $(foreach lib,$(LIBS),--passL:"$(lib)") + + export PATH := /c/msys64/usr/bin:/c/msys64/mingw64/bin:/c/msys64/usr/lib:/c/msys64/mingw64/lib:$(PATH) + endif ########## @@ -424,13 +427,13 @@ docker-liteprotocoltester-push: STATIC ?= 0 - libwaku: | build deps librln rm -f build/libwaku* ifeq ($(STATIC), 1) echo -e $(BUILD_MSG) "build/$@.a" && $(ENV_SCRIPT) nim libwakuStatic $(NIM_PARAMS) waku.nims else ifeq ($(detected_OS),Windows) + 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 @@ -546,3 +549,4 @@ 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/scripts/libwaku_windows_setup.mk b/scripts/libwaku_windows_setup.mk new file mode 100644 index 000000000..503d0c405 --- /dev/null +++ b/scripts/libwaku_windows_setup.mk @@ -0,0 +1,53 @@ +# --------------------------------------------------------- +# Windows Setup Makefile +# --------------------------------------------------------- + +# Extend PATH (Make preserves environment variables) +export PATH := /c/msys64/usr/bin:/c/msys64/mingw64/bin:/c/msys64/usr/lib:/c/msys64/mingw64/lib:$(PATH) + +# Tools required +DEPS = gcc g++ make cmake cargo upx rustc python + +# Default target +.PHONY: windows-setup +windows-setup: check-deps update-submodules create-tmp libunwind miniupnpc libnatpmp + @echo "Windows setup completed successfully!" + +.PHONY: check-deps +check-deps: + @echo "Checking libwaku build dependencies..." + @for dep in $(DEPS); do \ + if ! which $$dep >/dev/null 2>&1; then \ + echo "✗ Missing dependency: $$dep"; \ + exit 1; \ + else \ + echo "✓ Found: $$dep"; \ + fi; \ + done + +.PHONY: update-submodules +update-submodules: + @echo "Updating libwaku git submodules..." + git submodule update --init --recursive + +.PHONY: create-tmp +create-tmp: + @echo "Creating tmp directory..." + mkdir -p tmp + +.PHONY: libunwind +libunwind: + @echo "Building libunwind..." + cd vendor/nim-libbacktrace && make all V=1 + +.PHONY: miniupnpc +miniupnpc: + @echo "Building miniupnpc..." + cd vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc && \ + make -f Makefile.mingw CC=gcc CXX=g++ libminiupnpc.a V=1 + +.PHONY: libnatpmp +libnatpmp: + @echo "Building libnatpmp..." + cd vendor/nim-nat-traversal/vendor/libnatpmp-upstream && \ + make CC="gcc -fPIC -D_WIN32_WINNT=0x0600 -DNATPMP_STATICLIB" libnatpmp.a V=1 From 1e73213a3604b0113a13b1ca2157db3276c78a4d Mon Sep 17 00:00:00 2001 From: Sergei Tikhomirov Date: Fri, 28 Nov 2025 10:41:20 +0100 Subject: [PATCH 016/155] chore: Lightpush minor refactor (#3538) * chore: refactor Lightpush (more DRY) * chore: apply review suggestions Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> --------- Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> --- .../lightpush_mix/lightpush_publisher_mix.nim | 6 +- tests/waku_lightpush/test_ratelimit.nim | 8 +- waku/node/kernel_api/lightpush.nim | 4 +- waku/waku_lightpush/client.nim | 145 +++++++----------- waku/waku_lightpush/common.nim | 15 +- waku/waku_lightpush/protocol.nim | 12 +- 6 files changed, 76 insertions(+), 114 deletions(-) diff --git a/examples/lightpush_mix/lightpush_publisher_mix.nim b/examples/lightpush_mix/lightpush_publisher_mix.nim index 4219cd665..104de8552 100644 --- a/examples/lightpush_mix/lightpush_publisher_mix.nim +++ b/examples/lightpush_mix/lightpush_publisher_mix.nim @@ -163,9 +163,9 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} ephemeral: true, # tell store nodes to not store it timestamp: getNowInNanosecondTime(), ) # current timestamp - let res = await node.wakuLightpushClient.publishWithConn( - LightpushPubsubTopic, message, conn, dPeerId - ) + + let res = + await node.wakuLightpushClient.publish(some(LightpushPubsubTopic), message, conn) let startTime = getNowInNanosecondTime() diff --git a/tests/waku_lightpush/test_ratelimit.nim b/tests/waku_lightpush/test_ratelimit.nim index b2dcdc7b5..7420a4e56 100644 --- a/tests/waku_lightpush/test_ratelimit.nim +++ b/tests/waku_lightpush/test_ratelimit.nim @@ -37,7 +37,7 @@ suite "Rate limited push service": handlerFuture = newFuture[(string, WakuMessage)]() let requestRes = - await client.publish(some(DefaultPubsubTopic), message, peer = serverPeerId) + await client.publish(some(DefaultPubsubTopic), message, serverPeerId) check await handlerFuture.withTimeout(50.millis) @@ -66,7 +66,7 @@ suite "Rate limited push service": var endTime = Moment.now() var elapsed: Duration = (endTime - startTime) await sleepAsync(tokenPeriod - elapsed + firstWaitExtend) - firstWaitEXtend = 100.millis + firstWaitExtend = 100.millis ## Cleanup await allFutures(clientSwitch.stop(), serverSwitch.stop()) @@ -99,7 +99,7 @@ suite "Rate limited push service": let message = fakeWakuMessage() handlerFuture = newFuture[(string, WakuMessage)]() let requestRes = - await client.publish(some(DefaultPubsubTopic), message, peer = serverPeerId) + await client.publish(some(DefaultPubsubTopic), message, serverPeerId) discard await handlerFuture.withTimeout(10.millis) check: @@ -114,7 +114,7 @@ suite "Rate limited push service": let message = fakeWakuMessage() handlerFuture = newFuture[(string, WakuMessage)]() let requestRes = - await client.publish(some(DefaultPubsubTopic), message, peer = serverPeerId) + await client.publish(some(DefaultPubsubTopic), message, serverPeerId) discard await handlerFuture.withTimeout(10.millis) check: diff --git a/waku/node/kernel_api/lightpush.nim b/waku/node/kernel_api/lightpush.nim index 8df6291b1..9451767ac 100644 --- a/waku/node/kernel_api/lightpush.nim +++ b/waku/node/kernel_api/lightpush.nim @@ -210,9 +210,7 @@ proc lightpushPublishHandler( "Waku lightpush with mix not available", ) - return await node.wakuLightpushClient.publishWithConn( - pubsubTopic, message, conn, peer.peerId - ) + return await node.wakuLightpushClient.publish(some(pubsubTopic), message, conn) else: return await node.wakuLightpushClient.publish(some(pubsubTopic), message, peer) diff --git a/waku/waku_lightpush/client.nim b/waku/waku_lightpush/client.nim index d68552304..b528b4c76 100644 --- a/waku/waku_lightpush/client.nim +++ b/waku/waku_lightpush/client.nim @@ -17,8 +17,8 @@ logScope: topics = "waku lightpush client" type WakuLightPushClient* = ref object - peerManager*: PeerManager rng*: ref rand.HmacDrbgContext + peerManager*: PeerManager publishObservers: seq[PublishObserver] proc new*( @@ -29,33 +29,31 @@ proc new*( proc addPublishObserver*(wl: WakuLightPushClient, obs: PublishObserver) = wl.publishObservers.add(obs) -proc sendPushRequest( - wl: WakuLightPushClient, - req: LightPushRequest, - peer: PeerId | RemotePeerInfo, - conn: Option[Connection] = none(Connection), -): Future[WakuLightPushResult] {.async.} = - let connection = conn.valueOr: - (await wl.peerManager.dialPeer(peer, WakuLightPushCodec)).valueOr: - waku_lightpush_v3_errors.inc(labelValues = [dialFailure]) - return lighpushErrorResult( - LightPushErrorCode.NO_PEERS_TO_RELAY, - dialFailure & ": " & $peer & " is not accessible", - ) +proc ensureTimestampSet(message: var WakuMessage) = + if message.timestamp == 0: + message.timestamp = getNowInNanosecondTime() - defer: - await connection.closeWithEOF() +## Short log string for peer identifiers (overloads for convenience) +func shortPeerId(peer: PeerId): string = + shortLog(peer) + +func shortPeerId(peer: RemotePeerInfo): string = + shortLog(peer.peerId) + +proc sendPushRequestToConn( + wl: WakuLightPushClient, request: LightPushRequest, conn: Connection +): Future[WakuLightPushResult] {.async.} = try: - await connection.writeLP(req.encode().buffer) - except CatchableError: - error "failed to send push request", error = getCurrentExceptionMsg() + await conn.writeLp(request.encode().buffer) + except LPStreamRemoteClosedError: + error "Failed to write request to peer", error = getCurrentExceptionMsg() return lightpushResultInternalError( - "failed to send push request: " & getCurrentExceptionMsg() + "Failed to write request to peer: " & getCurrentExceptionMsg() ) var buffer: seq[byte] try: - buffer = await connection.readLp(DefaultMaxRpcSize.int) + buffer = await conn.readLp(DefaultMaxRpcSize.int) except LPStreamRemoteClosedError: error "Failed to read response from peer", error = getCurrentExceptionMsg() return lightpushResultInternalError( @@ -66,10 +64,12 @@ proc sendPushRequest( waku_lightpush_v3_errors.inc(labelValues = [decodeRpcFailure]) return lightpushResultInternalError(decodeRpcFailure) - if response.requestId != req.requestId and - response.statusCode != LightPushErrorCode.TOO_MANY_REQUESTS: + let requestIdMismatch = response.requestId != request.requestId + let tooManyRequests = response.statusCode == LightPushErrorCode.TOO_MANY_REQUESTS + if requestIdMismatch and (not tooManyRequests): + # response with TOO_MANY_REQUESTS error code has no requestId by design error "response failure, requestId mismatch", - requestId = req.requestId, responseRequestId = response.requestId + requestId = request.requestId, responseRequestId = response.requestId return lightpushResultInternalError("response failure, requestId mismatch") return toPushResult(response) @@ -78,88 +78,49 @@ proc publish*( wl: WakuLightPushClient, pubSubTopic: Option[PubsubTopic] = none(PubsubTopic), wakuMessage: WakuMessage, - peer: PeerId | RemotePeerInfo, + dest: Connection | PeerId | RemotePeerInfo, ): Future[WakuLightPushResult] {.async, gcsafe.} = + let conn = + when dest is Connection: + dest + else: + (await wl.peerManager.dialPeer(dest, WakuLightPushCodec)).valueOr: + waku_lightpush_v3_errors.inc(labelValues = [dialFailure]) + return lighpushErrorResult( + LightPushErrorCode.NO_PEERS_TO_RELAY, + "Peer is not accessible: " & dialFailure & " - " & $dest, + ) + + defer: + await conn.closeWithEOF() + var message = wakuMessage - if message.timestamp == 0: - message.timestamp = getNowInNanosecondTime() + ensureTimestampSet(message) - when peer is PeerId: - info "publish", - peerId = shortLog(peer), - msg_hash = computeMessageHash(pubsubTopic.get(""), message).to0xHex - else: - info "publish", - peerId = shortLog(peer.peerId), - msg_hash = computeMessageHash(pubsubTopic.get(""), message).to0xHex + let msgHash = computeMessageHash(pubSubTopic.get(""), message).to0xHex() + info "publish", + myPeerId = wl.peerManager.switch.peerInfo.peerId, + peerId = shortPeerId(conn.peerId), + msgHash = msgHash, + sentTime = getNowInNanosecondTime() - let pushRequest = LightpushRequest( - requestId: generateRequestId(wl.rng), pubSubTopic: pubSubTopic, message: message + let request = LightpushRequest( + requestId: generateRequestId(wl.rng), pubsubTopic: pubSubTopic, message: message ) - let publishedCount = ?await wl.sendPushRequest(pushRequest, peer) + let relayPeerCount = ?await wl.sendPushRequestToConn(request, conn) for obs in wl.publishObservers: obs.onMessagePublished(pubSubTopic.get(""), message) - return lightpushSuccessResult(publishedCount) + return lightpushSuccessResult(relayPeerCount) proc publishToAny*( - wl: WakuLightPushClient, pubSubTopic: PubsubTopic, wakuMessage: WakuMessage + wl: WakuLightPushClient, pubsubTopic: PubsubTopic, wakuMessage: WakuMessage ): Future[WakuLightPushResult] {.async, gcsafe.} = - ## This proc is similar to the publish one but in this case - ## we don't specify a particular peer and instead we get it from peer manager - - var message = wakuMessage - if message.timestamp == 0: - message.timestamp = getNowInNanosecondTime() - + # Like publish, but selects a peer automatically from the peer manager let peer = wl.peerManager.selectPeer(WakuLightPushCodec).valueOr: # TODO: check if it is matches the situation - shall we distinguish client side missing peers from server side? return lighpushErrorResult( LightPushErrorCode.NO_PEERS_TO_RELAY, "no suitable remote peers" ) - - info "publishToAny", - my_peer_id = wl.peerManager.switch.peerInfo.peerId, - peer_id = peer.peerId, - msg_hash = computeMessageHash(pubsubTopic, message).to0xHex, - sentTime = getNowInNanosecondTime() - - let pushRequest = LightpushRequest( - requestId: generateRequestId(wl.rng), - pubSubTopic: some(pubSubTopic), - message: message, - ) - let publishedCount = ?await wl.sendPushRequest(pushRequest, peer) - - for obs in wl.publishObservers: - obs.onMessagePublished(pubSubTopic, message) - - return lightpushSuccessResult(publishedCount) - -proc publishWithConn*( - wl: WakuLightPushClient, - pubSubTopic: PubsubTopic, - message: WakuMessage, - conn: Connection, - destPeer: PeerId, -): Future[WakuLightPushResult] {.async, gcsafe.} = - info "publishWithConn", - my_peer_id = wl.peerManager.switch.peerInfo.peerId, - peer_id = destPeer, - msg_hash = computeMessageHash(pubsubTopic, message).to0xHex, - sentTime = getNowInNanosecondTime() - - let pushRequest = LightpushRequest( - requestId: generateRequestId(wl.rng), - pubSubTopic: some(pubSubTopic), - message: message, - ) - #TODO: figure out how to not pass destPeer as this is just a hack - let publishedCount = - ?await wl.sendPushRequest(pushRequest, destPeer, conn = some(conn)) - - for obs in wl.publishObservers: - obs.onMessagePublished(pubSubTopic, message) - - return lightpushSuccessResult(publishedCount) + return await wl.publish(some(pubsubTopic), wakuMessage, peer) diff --git a/waku/waku_lightpush/common.nim b/waku/waku_lightpush/common.nim index f2687834e..9c2ea7ced 100644 --- a/waku/waku_lightpush/common.nim +++ b/waku/waku_lightpush/common.nim @@ -35,7 +35,15 @@ func isSuccess*(response: LightPushResponse): bool = func toPushResult*(response: LightPushResponse): WakuLightPushResult = if isSuccess(response): - return ok(response.relayPeerCount.get(0)) + let relayPeerCount = response.relayPeerCount.get(0) + return ( + if (relayPeerCount == 0): + # Consider publishing to zero peers an error even if the service node + # sent us a "successful" response with zero peers + err((LightPushErrorCode.NO_PEERS_TO_RELAY, response.statusDesc)) + else: + ok(relayPeerCount) + ) else: return err((response.statusCode, response.statusDesc)) @@ -51,11 +59,6 @@ func lightpushResultBadRequest*(msg: string): WakuLightPushResult = func lightpushResultServiceUnavailable*(msg: string): WakuLightPushResult = return err((LightPushErrorCode.SERVICE_NOT_AVAILABLE, some(msg))) -func lighpushErrorResult*( - statusCode: LightpushStatusCode, desc: Option[string] -): WakuLightPushResult = - return err((statusCode, desc)) - func lighpushErrorResult*( statusCode: LightpushStatusCode, desc: string ): WakuLightPushResult = diff --git a/waku/waku_lightpush/protocol.nim b/waku/waku_lightpush/protocol.nim index 2e8c9c2f1..95bfc003e 100644 --- a/waku/waku_lightpush/protocol.nim +++ b/waku/waku_lightpush/protocol.nim @@ -78,9 +78,9 @@ proc handleRequest( proc handleRequest*( wl: WakuLightPush, peerId: PeerId, buffer: seq[byte] ): Future[LightPushResponse] {.async.} = - let pushRequest = LightPushRequest.decode(buffer).valueOr: + let request = LightPushRequest.decode(buffer).valueOr: let desc = decodeRpcFailure & ": " & $error - error "failed to push message", error = desc + error "failed to decode Lightpush request", error = desc let errorCode = LightPushErrorCode.BAD_REQUEST waku_lightpush_v3_errors.inc(labelValues = [$errorCode]) return LightPushResponse( @@ -89,16 +89,16 @@ proc handleRequest*( statusDesc: some(desc), ) - let relayPeerCount = (await handleRequest(wl, peerId, pushRequest)).valueOr: + let relayPeerCount = (await wl.handleRequest(peerId, request)).valueOr: let desc = error.desc waku_lightpush_v3_errors.inc(labelValues = [$error.code]) error "failed to push message", error = desc return LightPushResponse( - requestId: pushRequest.requestId, statusCode: error.code, statusDesc: desc + requestId: request.requestId, statusCode: error.code, statusDesc: desc ) return LightPushResponse( - requestId: pushRequest.requestId, + requestId: request.requestId, statusCode: LightPushSuccessCode.SUCCESS, statusDesc: none[string](), relayPeerCount: some(relayPeerCount), @@ -123,7 +123,7 @@ proc initProtocolHandler(wl: WakuLightPush) = ) try: - rpc = await handleRequest(wl, conn.peerId, buffer) + rpc = await wl.handleRequest(conn.peerId, buffer) except CatchableError: error "lightpush failed handleRequest", error = getCurrentExceptionMsg() do: From c6cf34df067a1362207a815e2363d033367abac8 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Fri, 28 Nov 2025 14:20:36 -0300 Subject: [PATCH 017/155] feat(tests): robustify waku_rln_relay test utils (#3650) --- tests/waku_rln_relay/utils_onchain.nim | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/waku_rln_relay/utils_onchain.nim b/tests/waku_rln_relay/utils_onchain.nim index 85f627aa0..06e4fcdcf 100644 --- a/tests/waku_rln_relay/utils_onchain.nim +++ b/tests/waku_rln_relay/utils_onchain.nim @@ -82,6 +82,10 @@ proc getForgePath(): string = forgePath = joinPath(forgePath, ".foundry/bin/forge") return $forgePath +template execForge(cmd: string): tuple[output: string, exitCode: int] = + # unset env vars that affect e.g. "forge script" before running forge + execCmdEx("unset ETH_FROM ETH_PASSWORD && " & cmd) + contract(ERC20Token): proc allowance(owner: Address, spender: Address): UInt256 {.view.} proc balanceOf(account: Address): UInt256 {.view.} @@ -225,11 +229,14 @@ proc deployTestToken*( # Deploy TestToken contract let forgeCmdTestToken = fmt"""cd {submodulePath} && {forgePath} script test/TestToken.sol --broadcast -vvv --rpc-url http://localhost:8540 --tc TestTokenFactory --private-key {pk} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json""" - let (outputDeployTestToken, exitCodeDeployTestToken) = execCmdEx(forgeCmdTestToken) + let (outputDeployTestToken, exitCodeDeployTestToken) = execForge(forgeCmdTestToken) trace "Executed forge command to deploy TestToken contract", output = outputDeployTestToken if exitCodeDeployTestToken != 0: - return error("Forge command to deploy TestToken contract failed") + error "Forge command to deploy TestToken contract failed", + error = outputDeployTestToken + return + err("Forge command to deploy TestToken contract failed: " & outputDeployTestToken) # Parse the command output to find contract address let testTokenAddress = getContractAddressFromDeployScriptOutput(outputDeployTestToken).valueOr: @@ -351,7 +358,7 @@ proc executeForgeContractDeployScripts*( let forgeCmdPriceCalculator = fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployPriceCalculator --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json""" let (outputDeployPriceCalculator, exitCodeDeployPriceCalculator) = - execCmdEx(forgeCmdPriceCalculator) + execForge(forgeCmdPriceCalculator) trace "Executed forge command to deploy LinearPriceCalculator contract", output = outputDeployPriceCalculator if exitCodeDeployPriceCalculator != 0: @@ -368,7 +375,7 @@ proc executeForgeContractDeployScripts*( let forgeCmdWakuRln = fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployWakuRlnV2 --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json""" - let (outputDeployWakuRln, exitCodeDeployWakuRln) = execCmdEx(forgeCmdWakuRln) + let (outputDeployWakuRln, exitCodeDeployWakuRln) = execForge(forgeCmdWakuRln) trace "Executed forge command to deploy WakuRlnV2 contract", output = outputDeployWakuRln if exitCodeDeployWakuRln != 0: @@ -388,7 +395,7 @@ proc executeForgeContractDeployScripts*( # Deploy Proxy contract let forgeCmdProxy = fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployProxy --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json""" - let (outputDeployProxy, exitCodeDeployProxy) = execCmdEx(forgeCmdProxy) + let (outputDeployProxy, exitCodeDeployProxy) = execForge(forgeCmdProxy) trace "Executed forge command to deploy proxy contract", output = outputDeployProxy if exitCodeDeployProxy != 0: error "Forge command to deploy Proxy failed", error = outputDeployProxy @@ -503,7 +510,7 @@ proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process = "--chain-id", $chainId, ], - options = {poUsePath}, + options = {poUsePath, poStdErrToStdOut}, ) let anvilPID = runAnvil.processID @@ -516,7 +523,13 @@ proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process = anvilStartLog.add(cmdline) if cmdline.contains("Listening on 127.0.0.1:" & $port): break + else: + error "Anvil daemon exited (closed output)", + pid = anvilPID, startLog = anvilStartLog + return except Exception, CatchableError: + warn "Anvil daemon stdout reading error; assuming it started OK", + pid = anvilPID, startLog = anvilStartLog, err = getCurrentExceptionMsg() break info "Anvil daemon is running and ready", pid = anvilPID, startLog = anvilStartLog return runAnvil From 7eb1fdb0ac3eed0b39c5d1456ba6d1b9881e7980 Mon Sep 17 00:00:00 2001 From: Darshan K <35736874+darshankabariya@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:03:59 +0530 Subject: [PATCH 018/155] chore: new release process ( beta and full ) (#3647) --- .../ISSUE_TEMPLATE/prepare_beta_release.md | 56 ++++++++ .../ISSUE_TEMPLATE/prepare_full_release.md | 76 +++++++++++ .github/ISSUE_TEMPLATE/prepare_release.md | 72 ---------- docs/contributors/release-process.md | 124 ++++++++++++------ 4 files changed, 213 insertions(+), 115 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/prepare_beta_release.md create mode 100644 .github/ISSUE_TEMPLATE/prepare_full_release.md delete mode 100644 .github/ISSUE_TEMPLATE/prepare_release.md diff --git a/.github/ISSUE_TEMPLATE/prepare_beta_release.md b/.github/ISSUE_TEMPLATE/prepare_beta_release.md new file mode 100644 index 000000000..270f6a8e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/prepare_beta_release.md @@ -0,0 +1,56 @@ +--- +name: Prepare Beta Release +about: Execute tasks for the creation and publishing of a new beta release +title: 'Prepare beta release 0.0.0' +labels: beta-release +assignees: '' + +--- + + + +### Items to complete + +All items below are to be completed by the owner of the given release. + +- [ ] Create release branch with major and minor only ( e.g. release/v0.X ) if it doesn't exist. +- [ ] Assign release candidate tag to the release branch HEAD (e.g. `v0.X.0-beta-rc.0`, `v0.X.0-beta-rc.1`, ... `v0.X.0-beta-rc.N`). +- [ ] Generate and edit release notes in CHANGELOG.md. + +- [ ] **Waku test and fleets validation** + - [ ] Ensure all the unit tests (specifically js-waku tests) are green against the release candidate. + - [ ] Deploy the release candidate to `waku.test` only through [deploy-waku-test job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-test/) and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). + - After completion, disable [deployment job](https://ci.infra.status.im/job/nim-waku/) so that its version is not updated on every merge to master. + - Verify the deployed version at https://fleets.waku.org/. + - Confirm the container image exists on [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab). + - [ ] Analyze Kibana logs from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test`. + - Most relevant logs are `(fleet: "waku.test" AND message: "SIGSEGV")`. + - [ ] Enable again the `waku.test` fleet to resume auto-deployment of the latest `master` commit. + +- [ ] **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). + - [ ] 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** + - [ ] Ask the PM lead to announce the release. + - [ ] Update infra config with any deprecated arguments or changed options. + - [ ] Update waku.sandbox with [this deployment job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox/). + +### 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) +- [Fleet ownership](https://www.notion.so/Fleet-Ownership-7532aad8896d46599abac3c274189741?pvs=4#d2d2f0fe4b3c429fbd860a1d64f89a64) +- [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) +- [Jenkins](https://ci.infra.status.im/job/nim-waku/) +- [Fleets](https://fleets.waku.org/) +- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) diff --git a/.github/ISSUE_TEMPLATE/prepare_full_release.md b/.github/ISSUE_TEMPLATE/prepare_full_release.md new file mode 100644 index 000000000..18c668d16 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/prepare_full_release.md @@ -0,0 +1,76 @@ +--- +name: Prepare Full Release +about: Execute tasks for the creation and publishing of a new full release +title: 'Prepare full release 0.0.0' +labels: full-release +assignees: '' + +--- + + + +### Items to complete + +All items below are to be completed by the owner of the given release. + +- [ ] Create release branch with major and minor only ( e.g. release/v0.X ) if it doesn't exist. +- [ ] Assign release candidate tag to the release branch HEAD (e.g. `v0.X.0-rc.0`, `v0.X.0-rc.1`, ... `v0.X.0-rc.N`). +- [ ] Generate and edit release notes in CHANGELOG.md. + +- [ ] **Validation of release candidate** + + - [ ] **Automated testing** + - [ ] Ensure all the unit tests (specifically js-waku tests) are green against the release candidate. + - [ ] Ask Vac-QA and Vac-DST to perform the available tests against the release candidate. + - [ ] Vac-DST (an additional report is needed; see [this](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f)) + + - [ ] **Waku fleet testing** + - [ ] Deploy the release candidate to `waku.test` and `waku.sandbox` fleets. + - Start the [deployment job](https://ci.infra.status.im/job/nim-waku/) for both fleets and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). + - After completion, disable [deployment job](https://ci.infra.status.im/job/nim-waku/) so that its version is not updated on every merge to `master`. + - Verify the deployed version at https://fleets.waku.org/. + - Confirm the container image exists on [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab). + - [ ] Search _Kibana_ logs from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test` and `waku.sandbox`. + - Most relevant logs are `(fleet: "waku.test" AND message: "SIGSEGV")` OR `(fleet: "waku.sandbox" AND message: "SIGSEGV")`. + - [ ] Enable again the `waku.test` fleet to resume auto-deployment of the latest `master` commit. + +- [ ] **Status fleet testing** + - [ ] Deploy release candidate to `status.staging` + - [ ] Perform [sanity check](https://www.notion.so/How-to-test-Nwaku-on-Status-12c6e4b9bf06420ca868bd199129b425) and log results as comments in this issue. + - [ ] Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client. + - 1:1 Chats with each other + - Send and receive messages in a community + - Close one instance, send messages with second instance, reopen first instance and confirm messages sent while offline are retrieved from store + - [ ] Perform checks based on _end user impact_ + - [ ] Inform other (Waku and Status) CCs to point their instances to `status.staging` for a few days. Ping Status colleagues on their Discord server or in the [Status community](https://status.app/c/G3kAAMSQtb05kog3aGbr3kiaxN4tF5xy4BAGEkkLwILk2z3GcoYlm5hSJXGn7J3laft-tnTwDWmYJ18dP_3bgX96dqr_8E3qKAvxDf3NrrCMUBp4R9EYkQez9XSM4486mXoC3mIln2zc-TNdvjdfL9eHVZ-mGgs=#zQ3shZeEJqTC1xhGUjxuS4rtHSrhJ8vUYp64v6qWkLpvdy9L9) (this is not a blocking point.) + - [ ] Ask Status-QA to perform sanity checks (as described above) and checks based on _end user impact_; specify the version being tested + - [ ] Ask Status-QA or infra to run the automated Status e2e tests against `status.staging` + - [ ] Get other CCs' sign-off: they should comment on this PR, e.g., "Used the app for a week, no problem." If problems are reported, resolve them and create a new RC. + - [ ] **Get Status-QA sign-off**, ensuring that the `status.test` update will not disturb ongoing activities. + +- [ ] **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). + - [ ] 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** + - [ ] Ask the PM lead to announce the release. + - [ ] Update infra config with any deprecated arguments or changed options. + +### 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) +- [Fleet ownership](https://www.notion.so/Fleet-Ownership-7532aad8896d46599abac3c274189741?pvs=4#d2d2f0fe4b3c429fbd860a1d64f89a64) +- [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) +- [Jenkins](https://ci.infra.status.im/job/nim-waku/) +- [Fleets](https://fleets.waku.org/) +- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) diff --git a/.github/ISSUE_TEMPLATE/prepare_release.md b/.github/ISSUE_TEMPLATE/prepare_release.md deleted file mode 100644 index 9553d5685..000000000 --- a/.github/ISSUE_TEMPLATE/prepare_release.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: Prepare release -about: Execute tasks for the creation and publishing of a new release -title: 'Prepare release 0.0.0' -labels: release -assignees: '' - ---- - - - -### Items to complete - -All items below are to be completed by the owner of the given release. - -- [ ] Create release branch -- [ ] Assign release candidate tag to the release branch HEAD. e.g. v0.30.0-rc.0 -- [ ] Generate and edit releases notes in CHANGELOG.md -- [ ] Review possible update of [config-options](https://github.com/waku-org/docs.waku.org/blob/develop/docs/guides/nwaku/config-options.md) -- [ ] _End user impact_: Summarize impact of changes on Status end users (can be a comment in this issue). -- [ ] **Validate release candidate** - - [ ] Bump nwaku dependency in [waku-rust-bindings](https://github.com/waku-org/waku-rust-bindings) and make sure all examples and tests work - -- [ ] Automated testing - - [ ] Ensures js-waku tests are green against release candidate - - [ ] Ask Vac-QA and Vac-DST to perform available tests against release candidate - - [ ] Vac-QA - - [ ] Vac-DST (we need additional report. see [this](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f)) - - - [ ] **On Waku fleets** - - [ ] Lock `waku.test` fleet to release candidate version - - [ ] Continuously stress `waku.test` fleet for a week (e.g. from `wakudev`) - - [ ] Search _Kibana_ logs from the previous month (since last release was deployed), for possible crashes or errors in `waku.test` and `waku.sandbox`. - - Most relevant logs are `(fleet: "waku.test" OR fleet: "waku.sandbox") AND message: "SIGSEGV"` - - [ ] Run release candidate with `waku-simulator`, ensure that nodes connected to each other - - [ ] Unlock `waku.test` to resume auto-deployment of latest `master` commit - - - [ ] **On Status fleet** - - [ ] Deploy release candidate to `status.staging` - - [ ] Perform [sanity check](https://www.notion.so/How-to-test-Nwaku-on-Status-12c6e4b9bf06420ca868bd199129b425) and log results as comments in this issue. - - [ ] Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client. - - [ ] 1:1 Chats with each other - - [ ] Send and receive messages in a community - - [ ] Close one instance, send messages with second instance, reopen first instance and confirm messages sent while offline are retrieved from store - - [ ] Perform checks based _end user impact_ - - [ ] Inform other (Waku and Status) CCs to point their instance to `status.staging` for a few days. Ping Status colleagues from their Discord server or [Status community](https://status.app/c/G3kAAMSQtb05kog3aGbr3kiaxN4tF5xy4BAGEkkLwILk2z3GcoYlm5hSJXGn7J3laft-tnTwDWmYJ18dP_3bgX96dqr_8E3qKAvxDf3NrrCMUBp4R9EYkQez9XSM4486mXoC3mIln2zc-TNdvjdfL9eHVZ-mGgs=#zQ3shZeEJqTC1xhGUjxuS4rtHSrhJ8vUYp64v6qWkLpvdy9L9) (not blocking point.) - - [ ] Ask Status-QA to perform sanity checks (as described above) + checks based on _end user impact_; do specify the version being tested - - [ ] Ask Status-QA or infra to run the automated Status e2e tests against `status.staging` - - [ ] Get other CCs sign-off: they comment on this PR "used app for a week, no problem", or problem reported, resolved and new RC - - [ ] **Get Status-QA sign-off**. Ensuring that `status.test` update will not disturb ongoing activities. - -- [ ] **Proceed with release** - - - [ ] Assign a release tag to the same commit that contains the validated release-candidate tag - - [ ] Create GitHub release - - [ ] Deploy the release to DockerHub - - [ ] Announce the release - -- [ ] **Promote release to fleets**. - - [ ] Update infra config with any deprecated arguments or changed options - - [ ] [Deploy final release to `waku.sandbox` fleet](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox) - - [ ] [Deploy final release to `status.staging` fleet](https://ci.infra.status.im/job/nim-waku/job/deploy-shards-staging/) - - [ ] [Deploy final release to `status.prod` fleet](https://ci.infra.status.im/job/nim-waku/job/deploy-shards-test/) - -- [ ] **Post release** - - [ ] Submit a PR from the release branch to master. Important to commit the PR with "create a merge commit" option. - - [ ] Update waku-org/nwaku-compose with the new release version. - - [ ] Update version in js-waku repo. [update only this](https://github.com/waku-org/js-waku/blob/7c0ce7b2eca31cab837da0251e1e4255151be2f7/.github/workflows/ci.yml#L135) by submitting a PR. diff --git a/docs/contributors/release-process.md b/docs/contributors/release-process.md index c0fb12d1c..bde63aa6f 100644 --- a/docs/contributors/release-process.md +++ b/docs/contributors/release-process.md @@ -6,44 +6,52 @@ For more context, see https://trunkbaseddevelopment.com/branch-for-release/ ## How to do releases -### Before release +### Prerequisites + +- All issues under the corresponding release [milestone](https://github.com/waku-org/nwaku/milestones) have been closed or, after consultation, deferred to the next release. +- All submodules are up to date. + > Updating submodules requires a PR (and very often several "fixes" to maintain compatibility with the changes in submodules). That PR process must be done and merged a couple of days before the release. -Ensure all items in this list are ticked: -- [ ] All issues under the corresponding release [milestone](https://github.com/waku-org/nwaku/milestones) has been closed or, after consultation, deferred to a next release. -- [ ] All submodules are up to date. - > **IMPORTANT:** Updating submodules requires a PR (and very often several "fixes" to maintain compatibility with the changes in submodules). That PR process must be done and merged a couple of days before the release. > In case the submodules update has a low effort and/or risk for the release, follow the ["Update submodules"](./git-submodules.md) instructions. - > If the effort or risk is too high, consider postponing the submodules upgrade for the subsequent release or delaying the current release until the submodules updates are included in the release candidate. -- [ ] The [js-waku CI tests](https://github.com/waku-org/js-waku/actions/workflows/ci.yml) pass against the release candidate (i.e. nwaku latest `master`). - > **NOTE:** This serves as a basic regression test against typical clients of nwaku. - > The specific job that needs to pass is named `node_with_nwaku_master`. -### Performing the release + > If the effort or risk is too high, consider postponing the submodules upgrade for the subsequent release or delaying the current release until the submodules updates are included in the release candidate. + +### Release types + +- **Full release**: follow the entire [Release process](#release-process--step-by-step). + +- **Beta release**: skip just `6a` and `6c` steps from [Release process](#release-process--step-by-step). + +- Choose the appropriate release process based on the release type: + - [Full Release](../../.github/ISSUE_TEMPLATE/prepare_full_release.md) + - [Beta Release](../../.github/ISSUE_TEMPLATE/prepare_beta_release.md) + +### Release process ( step by step ) 1. Checkout a release branch from master ``` - git checkout -b release/v0.1.0 + git checkout -b release/v0.X.0 ``` -1. Update `CHANGELOG.md` and ensure it is up to date. Use the helper Make target to get PR based release-notes/changelog update. +2. Update `CHANGELOG.md` and ensure it is up to date. Use the helper Make target to get PR based release-notes/changelog update. ``` make release-notes ``` -1. Create a release-candidate tag with the same name as release and `-rc.N` suffix a few days before the official release and push it +3. Create a release-candidate tag with the same name as release and `-rc.N` suffix a few days before the official release and push it ``` - git tag -as v0.1.0-rc.0 -m "Initial release." - git push origin v0.1.0-rc.0 + git tag -as v0.X.0-rc.0 -m "Initial release." + git push origin v0.X.0-rc.0 ``` - This will trigger a [workflow](../../.github/workflows/pre-release.yml) which will build RC artifacts and create and publish a Github release + This will trigger a [workflow](../../.github/workflows/pre-release.yml) which will build RC artifacts and create and publish a GitHub release -1. Open a PR from the release branch for others to review the included changes and the release-notes +4. Open a PR from the release branch for others to review the included changes and the release-notes -1. In case additional changes are needed, create a new RC tag +5. In case additional changes are needed, create a new RC tag Make sure the new tag is associated with CHANGELOG update. @@ -52,25 +60,57 @@ Ensure all items in this list are ticked: # Make changes, rebase and create new tag # Squash to one commit and make a nice commit message git rebase -i origin/master - git tag -as v0.1.0-rc.1 -m "Initial release." - git push origin v0.1.0-rc.1 + git tag -as v0.X.0-rc.1 -m "Initial release." + git push origin v0.X.0-rc.1 ``` -1. Validate the release. For the release validation process, please refer to the following [guide](https://www.notion.so/Release-Process-61234f335b904cd0943a5033ed8f42b4#47af557e7f9744c68fdbe5240bf93ca9) + Similarly use v0.X.0-rc.2, v0.X.0-rc.3 etc. for additional RC tags. -1. Once the release-candidate has been validated, create a final release tag and push it. -We also need to merge release branch back to master as a final step. +6. **Validation of release candidate** + + 6a. **Automated testing** + - Ensure all the unit tests (specifically js-waku tests) are green against the release candidate. + - Ask Vac-QA and Vac-DST to run their available tests against the release candidate; share all release candidates with both teams. + + > We need an additional report like [this](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f) specifically from the DST team. + + 6b. **Waku fleet testing** + - Start job on `waku.sandbox` and `waku.test` [Deployment job](https://ci.infra.status.im/job/nim-waku/), wait for completion of the job. If it fails, then debug it. + - After completion, disable [deployment job](https://ci.infra.status.im/job/nim-waku/) so that its version is not updated on every merge to `master`. + - Verify at https://fleets.waku.org/ that the fleet is locked to the release candidate version. + - Check if the image is created at [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab). + - Search _Kibana_ logs from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test` and `waku.sandbox`. + - Most relevant logs are `(fleet: "waku.test" AND message: "SIGSEGV")` OR `(fleet: "waku.sandbox" AND message: "SIGSEGV")`. + - Enable the `waku.test` fleet again to resume auto-deployment of the latest `master` commit. + + 6c. **Status fleet testing** + - Deploy release candidate to `status.staging` + - Perform [sanity check](https://www.notion.so/How-to-test-Nwaku-on-Status-12c6e4b9bf06420ca868bd199129b425) and log results as comments in this issue. + - Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client. + - 1:1 Chats with each other + - Send and receive messages in a community + - Close one instance, send messages with second instance, reopen first instance and confirm messages sent while offline are retrieved from store + - Perform checks based on _end-user impact_. + - Inform other (Waku and Status) CCs to point their instances to `status.staging` for a few days. Ping Status colleagues from their Discord server or [Status community](https://status.app) (not a blocking point). + - Ask Status-QA to perform sanity checks (as described above) and checks based on _end user impact_; specify the version being tested. + - Ask Status-QA or infra to run the automated Status e2e tests against `status.staging`. + - Get other CCs' sign-off: they should comment on this PR, e.g., "Used the app for a week, no problem." If problems are reported, resolve them and create a new RC. + - **Get Status-QA sign-off**, ensuring that the `status.test` update will not disturb ongoing activities. + +7. Once the release-candidate has been validated, create a final release tag and push it. +We also need to merge the release branch back into master as a final step. ``` - git checkout release/v0.1.0 - git tag -as v0.1.0 -m "Initial release." - git push origin v0.1.0 + git checkout release/v0.X.0 + git tag -as v0.X.0 -m "final release." (use v0.X.0-beta as the tag if you are creating a beta release) + git push origin v0.X.0 git switch master git pull - git merge release/v0.1.0 + git merge release/v0.X.0 ``` +8. Update `waku-rust-bindings`, `waku-simulator` and `nwaku-compose` to use the new release. -1. Create a [Github release](https://github.com/waku-org/nwaku/releases) from the release tag. +9. Create a [GitHub release](https://github.com/waku-org/nwaku/releases) from the release tag. * Add binaries produced by the ["Upload Release Asset"](https://github.com/waku-org/nwaku/actions/workflows/release-assets.yml) workflow. Where possible, test the binaries before uploading to the release. @@ -80,22 +120,10 @@ We also need to merge release branch back to master as a final step. 2. Deploy the release image to [Dockerhub](https://hub.docker.com/r/wakuorg/nwaku) by triggering [the manual Jenkins deployment job](https://ci.infra.status.im/job/nim-waku/job/docker-manual/). > Ensure the following build parameters are set: > - `MAKE_TARGET`: `wakunode2` - > - `IMAGE_TAG`: the release tag (e.g. `v0.16.0`) + > - `IMAGE_TAG`: the release tag (e.g. `v0.36.0`) > - `IMAGE_NAME`: `wakuorg/nwaku` > - `NIMFLAGS`: `--colors:off -d:disableMarchNative -d:chronicles_colors:none -d:postgres` - > - `GIT_REF` the release tag (e.g. `v0.16.0`) -3. Update the default nwaku image in [nwaku-compose](https://github.com/waku-org/nwaku-compose/blob/master/docker-compose.yml) -4. Deploy the release to appropriate fleets: - - Inform clients - > **NOTE:** known clients are currently using some version of js-waku, go-waku, nwaku or waku-rs. - > Clients are reachable via the corresponding channels on the Vac Discord server. - > It should be enough to inform clients on the `#nwaku` and `#announce` channels on Discord. - > Informal conversations with specific repo maintainers are often part of this process. - - Check if nwaku configuration parameters changed. If so [update fleet configuration](https://www.notion.so/Fleet-Ownership-7532aad8896d46599abac3c274189741?pvs=4#d2d2f0fe4b3c429fbd860a1d64f89a64) in [infra-nim-waku](https://github.com/status-im/infra-nim-waku) - - Deploy release to the `waku.sandbox` fleet from [Jenkins](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox/). - - Ensure that nodes successfully start up and monitor health using [Grafana](https://grafana.infra.status.im/d/qrp_ZCTGz/nim-waku-v2?orgId=1) and [Kibana](https://kibana.infra.status.im/goto/a7728e70-eb26-11ec-81d1-210eb3022c76). - - If necessary, revert by deploying the previous release. Download logs and open a bug report issue. -5. Submit a PR to merge the release branch back to `master`. Make sure you use the option `Merge pull request (Create a merge commit)` to perform such merge. + > - `GIT_REF` the release tag (e.g. `v0.36.0`) ### Performing a patch release @@ -116,4 +144,14 @@ We also need to merge release branch back to master as a final step. 4. Once the release-candidate has been validated and changelog PR got merged, cherry-pick the changelog update from master to the release branch. Create a final release tag and push it. -5. Create a [Github release](https://github.com/waku-org/nwaku/releases) from the release tag and follow the same post-release process as usual. +5. Create a [GitHub release](https://github.com/waku-org/nwaku/releases) from the release tag and follow the same post-release process as usual. + +### 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) +- [Fleet ownership](https://www.notion.so/Fleet-Ownership-7532aad8896d46599abac3c274189741?pvs=4#d2d2f0fe4b3c429fbd860a1d64f89a64) +- [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) +- [Jenkins](https://ci.infra.status.im/job/nim-waku/) +- [Fleets](https://fleets.waku.org/) +- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) \ No newline at end of file From ae74b9018a248ae4f641205d60e7122c024d47f6 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:24:46 +0100 Subject: [PATCH 019/155] chore: Introduce EventBroker, RequestBroker and MultiRequestBroker (#3644) * Introduce EventBroker and RequestBroker as decoupling helpers that represent reactive (event-driven) and proactive (request/response) patterns without tight coupling between modules * Address copilot observation. error log if failed listener call exception, handling listener overuse - run out of IDs * Address review observations: no exception to leak, listeners must raise no exception, adding listener now reports error with Result. * Added MultiRequestBroker utility to collect results from many providers * Support an arbitrary number of arguments for RequestBroker's request/provider signature * MultiRequestBroker allows provider procs to throw exceptions, which will be handled during request processing. * MultiRequestBroker supports one zero arg signature and/or multi arg signature * test no exception leaks from RequestBroker and MultiRequestBroker * Embed MultiRequestBroker tests into common * EventBroker: removed all ...Broker typed public procs to simplify EventBroker interface, forger is renamed to dropListener * Make Request's broker type private * MultiRequestBroker: Use explicit returns in generated procs * Updated descriptions of EventBroker and RequestBroker, updated RequestBroker.setProvider, returns error if already set. * Better description for MultiRequestBroker and its usage * Add EventBroker support for ref objects, fix emit variant with event object ctor * Add RequestBroker support for ref objects * Add MultiRequestBroker support for ref objects * Mover brokers under waku/common --- tests/common/test_all.nim | 5 +- tests/common/test_event_broker.nim | 125 +++++ tests/common/test_multi_request_broker.nim | 234 ++++++++ tests/common/test_request_broker.nim | 198 +++++++ waku/common/broker/event_broker.nim | 308 +++++++++++ waku/common/broker/helper/broker_utils.nim | 43 ++ waku/common/broker/multi_request_broker.nim | 583 ++++++++++++++++++++ waku/common/broker/request_broker.nim | 438 +++++++++++++++ 8 files changed, 1933 insertions(+), 1 deletion(-) create mode 100644 tests/common/test_event_broker.nim create mode 100644 tests/common/test_multi_request_broker.nim create mode 100644 tests/common/test_request_broker.nim create mode 100644 waku/common/broker/event_broker.nim create mode 100644 waku/common/broker/helper/broker_utils.nim create mode 100644 waku/common/broker/multi_request_broker.nim create mode 100644 waku/common/broker/request_broker.nim diff --git a/tests/common/test_all.nim b/tests/common/test_all.nim index 5b4515093..7495c7c9e 100644 --- a/tests/common/test_all.nim +++ b/tests/common/test_all.nim @@ -9,4 +9,7 @@ import ./test_tokenbucket, ./test_requestratelimiter, ./test_ratelimit_setting, - ./test_timed_map + ./test_timed_map, + ./test_event_broker, + ./test_request_broker, + ./test_multi_request_broker diff --git a/tests/common/test_event_broker.nim b/tests/common/test_event_broker.nim new file mode 100644 index 000000000..cead1277f --- /dev/null +++ b/tests/common/test_event_broker.nim @@ -0,0 +1,125 @@ +import chronos +import std/sequtils +import testutils/unittests + +import waku/common/broker/event_broker + +EventBroker: + type SampleEvent = object + value*: int + label*: string + +EventBroker: + type BinaryEvent = object + flag*: bool + +EventBroker: + type RefEvent = ref object + payload*: seq[int] + +template waitForListeners() = + waitFor sleepAsync(1.milliseconds) + +suite "EventBroker": + test "delivers events to all listeners": + var seen: seq[(int, string)] = @[] + + discard SampleEvent.listen( + proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = + seen.add((evt.value, evt.label)) + ) + + discard SampleEvent.listen( + proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = + seen.add((evt.value * 2, evt.label & "!")) + ) + + let evt = SampleEvent(value: 5, label: "hi") + SampleEvent.emit(evt) + waitForListeners() + + check seen.len == 2 + check seen.anyIt(it == (5, "hi")) + check seen.anyIt(it == (10, "hi!")) + + SampleEvent.dropAllListeners() + + test "forget removes a single listener": + var counter = 0 + + let handleA = SampleEvent.listen( + proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = + inc counter + ) + + let handleB = SampleEvent.listen( + proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = + inc(counter, 2) + ) + + SampleEvent.dropListener(handleA.get()) + let eventVal = SampleEvent(value: 1, label: "one") + SampleEvent.emit(eventVal) + waitForListeners() + check counter == 2 + + SampleEvent.dropAllListeners() + + test "forgetAll clears every listener": + var triggered = false + + let handle1 = SampleEvent.listen( + proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = + triggered = true + ) + let handle2 = SampleEvent.listen( + proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = + discard + ) + + SampleEvent.dropAllListeners() + SampleEvent.emit(42, "noop") + SampleEvent.emit(label = "noop", value = 42) + waitForListeners() + check not triggered + + let freshHandle = SampleEvent.listen( + proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = + discard + ) + check freshHandle.get().id > 0'u64 + SampleEvent.dropListener(freshHandle.get()) + + test "broker helpers operate via typedesc": + var toggles: seq[bool] = @[] + + let handle = BinaryEvent.listen( + proc(evt: BinaryEvent): Future[void] {.async: (raises: []).} = + toggles.add(evt.flag) + ) + + BinaryEvent(flag: true).emit() + waitForListeners() + let binaryEvent = BinaryEvent(flag: false) + BinaryEvent.emit(binaryEvent) + waitForListeners() + + check toggles == @[true, false] + BinaryEvent.dropAllListeners() + + test "ref typed event": + var counter: int = 0 + + let handle = RefEvent.listen( + proc(evt: RefEvent): Future[void] {.async: (raises: []).} = + for n in evt.payload: + counter += n + ) + + RefEvent(payload: @[1, 2, 3]).emit() + waitForListeners() + RefEvent.emit(payload = @[4, 5, 6]) + waitForListeners() + + check counter == 21 # 1+2+3 + 4+5+6 + RefEvent.dropAllListeners() diff --git a/tests/common/test_multi_request_broker.nim b/tests/common/test_multi_request_broker.nim new file mode 100644 index 000000000..3bf10a54d --- /dev/null +++ b/tests/common/test_multi_request_broker.nim @@ -0,0 +1,234 @@ +{.used.} + +import testutils/unittests +import chronos +import std/sequtils +import std/strutils + +import waku/common/broker/multi_request_broker + +MultiRequestBroker: + type NoArgResponse = object + label*: string + + proc signatureFetch*(): Future[Result[NoArgResponse, string]] {.async.} + +MultiRequestBroker: + type ArgResponse = object + id*: string + + proc signatureFetch*( + suffix: string, numsuffix: int + ): Future[Result[ArgResponse, string]] {.async.} + +MultiRequestBroker: + type DualResponse = ref object + note*: string + suffix*: string + + proc signatureBase*(): Future[Result[DualResponse, string]] {.async.} + proc signatureWithInput*( + suffix: string + ): Future[Result[DualResponse, string]] {.async.} + +suite "MultiRequestBroker": + test "aggregates zero-argument providers": + discard NoArgResponse.setProvider( + proc(): Future[Result[NoArgResponse, string]] {.async.} = + ok(NoArgResponse(label: "one")) + ) + + discard NoArgResponse.setProvider( + proc(): Future[Result[NoArgResponse, string]] {.async.} = + discard catch: + await sleepAsync(1.milliseconds) + ok(NoArgResponse(label: "two")) + ) + + let responses = waitFor NoArgResponse.request() + check responses.get().len == 2 + check responses.get().anyIt(it.label == "one") + check responses.get().anyIt(it.label == "two") + + NoArgResponse.clearProviders() + + test "aggregates argument providers": + discard ArgResponse.setProvider( + proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} = + ok(ArgResponse(id: suffix & "-a-" & $num)) + ) + + discard ArgResponse.setProvider( + proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} = + ok(ArgResponse(id: suffix & "-b-" & $num)) + ) + + let keyed = waitFor ArgResponse.request("topic", 1) + check keyed.get().len == 2 + check keyed.get().anyIt(it.id == "topic-a-1") + check keyed.get().anyIt(it.id == "topic-b-1") + + ArgResponse.clearProviders() + + test "clearProviders resets both provider lists": + discard DualResponse.setProvider( + proc(): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "base", suffix: "")) + ) + + discard DualResponse.setProvider( + proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "base" & suffix, suffix: suffix)) + ) + + let noArgs = waitFor DualResponse.request() + check noArgs.get().len == 1 + + let param = waitFor DualResponse.request("-extra") + check param.get().len == 1 + check param.get()[0].suffix == "-extra" + + DualResponse.clearProviders() + + let emptyNoArgs = waitFor DualResponse.request() + check emptyNoArgs.get().len == 0 + + let emptyWithArgs = waitFor DualResponse.request("-extra") + check emptyWithArgs.get().len == 0 + + test "request returns empty seq when no providers registered": + let empty = waitFor NoArgResponse.request() + check empty.get().len == 0 + + test "failed providers will fail the request": + NoArgResponse.clearProviders() + discard NoArgResponse.setProvider( + proc(): Future[Result[NoArgResponse, string]] {.async.} = + err("boom") + ) + + discard NoArgResponse.setProvider( + proc(): Future[Result[NoArgResponse, string]] {.async.} = + ok(NoArgResponse(label: "survivor")) + ) + + let filtered = waitFor NoArgResponse.request() + check filtered.isErr() + + NoArgResponse.clearProviders() + + test "deduplicates identical zero-argument providers": + NoArgResponse.clearProviders() + var invocations = 0 + let sharedHandler = proc(): Future[Result[NoArgResponse, string]] {.async.} = + inc invocations + ok(NoArgResponse(label: "dup")) + + let first = NoArgResponse.setProvider(sharedHandler) + let second = NoArgResponse.setProvider(sharedHandler) + + check first.get().id == second.get().id + check first.get().kind == second.get().kind + + let dupResponses = waitFor NoArgResponse.request() + check dupResponses.get().len == 1 + check invocations == 1 + + NoArgResponse.clearProviders() + + test "removeProvider deletes registered handlers": + var removedCalled = false + var keptCalled = false + + let removable = NoArgResponse.setProvider( + proc(): Future[Result[NoArgResponse, string]] {.async.} = + removedCalled = true + ok(NoArgResponse(label: "removed")) + ) + + discard NoArgResponse.setProvider( + proc(): Future[Result[NoArgResponse, string]] {.async.} = + keptCalled = true + ok(NoArgResponse(label: "kept")) + ) + + NoArgResponse.removeProvider(removable.get()) + + let afterRemoval = (waitFor NoArgResponse.request()).valueOr: + assert false, "request failed" + @[] + check afterRemoval.len == 1 + check afterRemoval[0].label == "kept" + check not removedCalled + check keptCalled + + NoArgResponse.clearProviders() + + test "removeProvider works for argument signatures": + var invoked: seq[string] = @[] + + discard ArgResponse.setProvider( + proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} = + invoked.add("first" & suffix) + ok(ArgResponse(id: suffix & "-one-" & $num)) + ) + + let handle = ArgResponse.setProvider( + proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} = + invoked.add("second" & suffix) + ok(ArgResponse(id: suffix & "-two-" & $num)) + ) + + ArgResponse.removeProvider(handle.get()) + + let single = (waitFor ArgResponse.request("topic", 1)).valueOr: + assert false, "request failed" + @[] + check single.len == 1 + check single[0].id == "topic-one-1" + check invoked == @["firsttopic"] + + ArgResponse.clearProviders() + + test "catches exception from providers and report error": + let firstHandler = NoArgResponse.setProvider( + proc(): Future[Result[NoArgResponse, string]] {.async.} = + raise newException(ValueError, "first handler raised") + ok(NoArgResponse(label: "any")) + ) + + discard NoArgResponse.setProvider( + proc(): Future[Result[NoArgResponse, string]] {.async.} = + ok(NoArgResponse(label: "just ok")) + ) + + let afterException = waitFor NoArgResponse.request() + check afterException.isErr() + check afterException.error().contains("first handler raised") + + NoArgResponse.clearProviders() + + test "ref providers returning nil fail request": + DualResponse.clearProviders() + + discard DualResponse.setProvider( + proc(): Future[Result[DualResponse, string]] {.async.} = + let nilResponse: DualResponse = nil + ok(nilResponse) + ) + + let zeroArg = waitFor DualResponse.request() + check zeroArg.isErr() + + DualResponse.clearProviders() + + discard DualResponse.setProvider( + proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = + let nilResponse: DualResponse = nil + ok(nilResponse) + ) + + let withInput = waitFor DualResponse.request("-extra") + check withInput.isErr() + + DualResponse.clearProviders() diff --git a/tests/common/test_request_broker.nim b/tests/common/test_request_broker.nim new file mode 100644 index 000000000..2ffd9cbf8 --- /dev/null +++ b/tests/common/test_request_broker.nim @@ -0,0 +1,198 @@ +{.used.} + +import testutils/unittests +import chronos +import std/strutils + +import waku/common/broker/request_broker + +RequestBroker: + type SimpleResponse = object + value*: string + + proc signatureFetch*(): Future[Result[SimpleResponse, string]] {.async.} + +RequestBroker: + type KeyedResponse = object + key*: string + payload*: string + + proc signatureFetchWithKey*( + key: string, subKey: int + ): Future[Result[KeyedResponse, string]] {.async.} + +RequestBroker: + type DualResponse = object + note*: string + count*: int + + proc signatureNoInput*(): Future[Result[DualResponse, string]] {.async.} + proc signatureWithInput*( + suffix: string + ): Future[Result[DualResponse, string]] {.async.} + +RequestBroker: + type ImplicitResponse = ref object + note*: string + +suite "RequestBroker macro": + test "serves zero-argument providers": + check SimpleResponse + .setProvider( + proc(): Future[Result[SimpleResponse, string]] {.async.} = + ok(SimpleResponse(value: "hi")) + ) + .isOk() + + let res = waitFor SimpleResponse.request() + check res.isOk() + check res.value.value == "hi" + + SimpleResponse.clearProvider() + + test "zero-argument request errors when unset": + let res = waitFor SimpleResponse.request() + check res.isErr + check res.error.contains("no zero-arg provider") + + test "serves input-based providers": + var seen: seq[string] = @[] + check KeyedResponse + .setProvider( + proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = + seen.add(key) + ok(KeyedResponse(key: key, payload: key & "-payload+" & $subKey)) + ) + .isOk() + + let res = waitFor KeyedResponse.request("topic", 1) + check res.isOk() + check res.value.key == "topic" + check res.value.payload == "topic-payload+1" + check seen == @["topic"] + + KeyedResponse.clearProvider() + + test "catches provider exception": + check KeyedResponse + .setProvider( + proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = + raise newException(ValueError, "simulated failure") + ok(KeyedResponse(key: key, payload: "")) + ) + .isOk() + + let res = waitFor KeyedResponse.request("neglected", 11) + check res.isErr() + check res.error.contains("simulated failure") + + KeyedResponse.clearProvider() + + test "input request errors when unset": + let res = waitFor KeyedResponse.request("foo", 2) + check res.isErr + check res.error.contains("input signature") + + test "supports both provider types simultaneously": + check DualResponse + .setProvider( + proc(): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "base", count: 1)) + ) + .isOk() + + check DualResponse + .setProvider( + proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "base" & suffix, count: suffix.len)) + ) + .isOk() + + let noInput = waitFor DualResponse.request() + check noInput.isOk + check noInput.value.note == "base" + + let withInput = waitFor DualResponse.request("-extra") + check withInput.isOk + check withInput.value.note == "base-extra" + check withInput.value.count == 6 + + DualResponse.clearProvider() + + test "clearProvider resets both entries": + check DualResponse + .setProvider( + proc(): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "temp", count: 0)) + ) + .isOk() + DualResponse.clearProvider() + + let res = waitFor DualResponse.request() + check res.isErr + + test "implicit zero-argument provider works by default": + check ImplicitResponse + .setProvider( + proc(): Future[Result[ImplicitResponse, string]] {.async.} = + ok(ImplicitResponse(note: "auto")) + ) + .isOk() + + let res = waitFor ImplicitResponse.request() + check res.isOk + + ImplicitResponse.clearProvider() + check res.value.note == "auto" + + test "implicit zero-argument request errors when unset": + let res = waitFor ImplicitResponse.request() + check res.isErr + check res.error.contains("no zero-arg provider") + + test "no provider override": + check DualResponse + .setProvider( + proc(): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "base", count: 1)) + ) + .isOk() + + check DualResponse + .setProvider( + proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "base" & suffix, count: suffix.len)) + ) + .isOk() + + let overrideProc = proc(): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "something else", count: 1)) + + check DualResponse.setProvider(overrideProc).isErr() + + let noInput = waitFor DualResponse.request() + check noInput.isOk + check noInput.value.note == "base" + + let stillResponse = waitFor DualResponse.request(" still works") + check stillResponse.isOk() + check stillResponse.value.note.contains("base still works") + + DualResponse.clearProvider() + + let noResponse = waitFor DualResponse.request() + check noResponse.isErr() + check noResponse.error.contains("no zero-arg provider") + + let noResponseArg = waitFor DualResponse.request("Should not work") + check noResponseArg.isErr() + check noResponseArg.error.contains("no provider") + + check DualResponse.setProvider(overrideProc).isOk() + + let nowSuccWithOverride = waitFor DualResponse.request() + check nowSuccWithOverride.isOk + check nowSuccWithOverride.value.note == "something else" + check nowSuccWithOverride.value.count == 1 + + DualResponse.clearProvider() diff --git a/waku/common/broker/event_broker.nim b/waku/common/broker/event_broker.nim new file mode 100644 index 000000000..05d7b50ab --- /dev/null +++ b/waku/common/broker/event_broker.nim @@ -0,0 +1,308 @@ +## EventBroker +## ------------------- +## EventBroker represents a reactive decoupling pattern, that +## allows event-driven development without +## need for direct dependencies in between emitters and listeners. +## Worth considering using it in a single or many emitters to many listeners scenario. +## +## Generates a standalone, type-safe event broker for the declared object type. +## The macro exports the value type itself plus a broker companion that manages +## listeners via thread-local storage. +## +## Usage: +## Declare your desired event type inside an `EventBroker` macro, add any number of fields.: +## ```nim +## EventBroker: +## type TypeName = object +## field1*: FieldType +## field2*: AnotherFieldType +## ``` +## +## After this, you can register async listeners anywhere in your code with +## `TypeName.listen(...)`, which returns a handle to the registered listener. +## Listeners are async procs or lambdas that take a single argument of the event type. +## Any number of listeners can be registered in different modules. +## +## Events can be emitted from anywhere with no direct dependency on the listeners by +## calling `TypeName.emit(...)` with an instance of the event type. +## This will asynchronously notify all registered listeners with the emitted event. +## +## Whenever you no longer need a listener (or your object instance that listen to the event goes out of scope), +## you can remove it from the broker with the handle returned by `listen`. +## This is done by calling `TypeName.dropListener(handle)`. +## Alternatively, you can remove all registered listeners through `TypeName.dropAllListeners()`. +## +## +## Example: +## ```nim +## EventBroker: +## type GreetingEvent = object +## text*: string +## +## let handle = GreetingEvent.listen( +## proc(evt: GreetingEvent): Future[void] {.async.} = +## echo evt.text +## ) +## GreetingEvent.emit(text= "hi") +## GreetingEvent.dropListener(handle) +## ``` + +import std/[macros, tables] +import chronos, chronicles, results +import ./helper/broker_utils + +export chronicles, results, chronos + +macro EventBroker*(body: untyped): untyped = + when defined(eventBrokerDebug): + echo body.treeRepr + var typeIdent: NimNode = nil + var objectDef: NimNode = nil + var fieldNames: seq[NimNode] = @[] + var fieldTypes: seq[NimNode] = @[] + var isRefObject = false + for stmt in body: + if stmt.kind == nnkTypeSection: + for def in stmt: + if def.kind != nnkTypeDef: + continue + let rhs = def[2] + var objectType: NimNode + case rhs.kind + of nnkObjectTy: + objectType = rhs + of nnkRefTy: + isRefObject = true + if rhs.len != 1 or rhs[0].kind != nnkObjectTy: + error("EventBroker ref object must wrap a concrete object definition", rhs) + objectType = rhs[0] + else: + continue + if not typeIdent.isNil(): + error("Only one object type may be declared inside EventBroker", def) + typeIdent = baseTypeIdent(def[0]) + let recList = objectType[2] + if recList.kind != nnkRecList: + error("EventBroker object must declare a standard field list", objectType) + var exportedRecList = newTree(nnkRecList) + for field in recList: + case field.kind + of nnkIdentDefs: + ensureFieldDef(field) + let fieldTypeNode = field[field.len - 2] + for i in 0 ..< field.len - 2: + let baseFieldIdent = baseTypeIdent(field[i]) + fieldNames.add(copyNimTree(baseFieldIdent)) + fieldTypes.add(copyNimTree(fieldTypeNode)) + var cloned = copyNimTree(field) + for i in 0 ..< cloned.len - 2: + cloned[i] = exportIdentNode(cloned[i]) + exportedRecList.add(cloned) + of nnkEmpty: + discard + else: + error( + "EventBroker object definition only supports simple field declarations", + field, + ) + let exportedObjectType = newTree( + nnkObjectTy, + copyNimTree(objectType[0]), + copyNimTree(objectType[1]), + exportedRecList, + ) + if isRefObject: + objectDef = newTree(nnkRefTy, exportedObjectType) + else: + objectDef = exportedObjectType + if typeIdent.isNil(): + error("EventBroker body must declare exactly one object type", body) + + let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*") + let sanitized = sanitizeIdentName(typeIdent) + let typeNameLit = newLit($typeIdent) + let isRefObjectLit = newLit(isRefObject) + let handlerProcIdent = ident(sanitized & "ListenerProc") + let listenerHandleIdent = ident(sanitized & "Listener") + let brokerTypeIdent = ident(sanitized & "Broker") + let exportedHandlerProcIdent = postfix(copyNimTree(handlerProcIdent), "*") + let exportedListenerHandleIdent = postfix(copyNimTree(listenerHandleIdent), "*") + let exportedBrokerTypeIdent = postfix(copyNimTree(brokerTypeIdent), "*") + let accessProcIdent = ident("access" & sanitized & "Broker") + let globalVarIdent = ident("g" & sanitized & "Broker") + let listenImplIdent = ident("register" & sanitized & "Listener") + let dropListenerImplIdent = ident("drop" & sanitized & "Listener") + let dropAllListenersImplIdent = ident("dropAll" & sanitized & "Listeners") + let emitImplIdent = ident("emit" & sanitized & "Value") + let listenerTaskIdent = ident("notify" & sanitized & "Listener") + + result = newStmtList() + + result.add( + quote do: + type + `exportedTypeIdent` = `objectDef` + `exportedListenerHandleIdent` = object + id*: uint64 + + `exportedHandlerProcIdent` = + proc(event: `typeIdent`): Future[void] {.async: (raises: []), gcsafe.} + `exportedBrokerTypeIdent` = ref object + listeners: Table[uint64, `handlerProcIdent`] + nextId: uint64 + + ) + + result.add( + quote do: + var `globalVarIdent` {.threadvar.}: `brokerTypeIdent` + ) + + result.add( + quote do: + proc `accessProcIdent`(): `brokerTypeIdent` = + if `globalVarIdent`.isNil(): + new(`globalVarIdent`) + `globalVarIdent`.listeners = initTable[uint64, `handlerProcIdent`]() + `globalVarIdent` + + ) + + result.add( + quote do: + proc `listenImplIdent`( + handler: `handlerProcIdent` + ): Result[`listenerHandleIdent`, string] = + if handler.isNil(): + return err("Must provide a non-nil event handler") + var broker = `accessProcIdent`() + if broker.nextId == 0'u64: + broker.nextId = 1'u64 + if broker.nextId == high(uint64): + error "Cannot add more listeners: ID space exhausted", nextId = $broker.nextId + return err("Cannot add more listeners, listener ID space exhausted") + let newId = broker.nextId + inc broker.nextId + broker.listeners[newId] = handler + return ok(`listenerHandleIdent`(id: newId)) + + ) + + result.add( + quote do: + proc `dropListenerImplIdent`(handle: `listenerHandleIdent`) = + if handle.id == 0'u64: + return + var broker = `accessProcIdent`() + if broker.listeners.len == 0: + return + broker.listeners.del(handle.id) + + ) + + result.add( + quote do: + proc `dropAllListenersImplIdent`() = + var broker = `accessProcIdent`() + if broker.listeners.len > 0: + broker.listeners.clear() + + ) + + result.add( + quote do: + proc listen*( + _: typedesc[`typeIdent`], handler: `handlerProcIdent` + ): Result[`listenerHandleIdent`, string] = + return `listenImplIdent`(handler) + + ) + + result.add( + quote do: + proc dropListener*(_: typedesc[`typeIdent`], handle: `listenerHandleIdent`) = + `dropListenerImplIdent`(handle) + + proc dropAllListeners*(_: typedesc[`typeIdent`]) = + `dropAllListenersImplIdent`() + + ) + + result.add( + quote do: + proc `listenerTaskIdent`( + callback: `handlerProcIdent`, event: `typeIdent` + ) {.async: (raises: []), gcsafe.} = + if callback.isNil(): + return + try: + await callback(event) + except Exception: + error "Failed to execute event listener", error = getCurrentExceptionMsg() + + proc `emitImplIdent`( + event: `typeIdent` + ): Future[void] {.async: (raises: []), gcsafe.} = + when `isRefObjectLit`: + if event.isNil(): + error "Cannot emit uninitialized event object", eventType = `typeNameLit` + return + let broker = `accessProcIdent`() + if broker.listeners.len == 0: + # nothing to do as nobody is listening + return + var callbacks: seq[`handlerProcIdent`] = @[] + for cb in broker.listeners.values: + callbacks.add(cb) + for cb in callbacks: + asyncSpawn `listenerTaskIdent`(cb, event) + + proc emit*(event: `typeIdent`) = + asyncSpawn `emitImplIdent`(event) + + proc emit*(_: typedesc[`typeIdent`], event: `typeIdent`) = + asyncSpawn `emitImplIdent`(event) + + ) + + var emitCtorParams = newTree(nnkFormalParams, newEmptyNode()) + let typedescParamType = + newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)) + emitCtorParams.add( + newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode()) + ) + for i in 0 ..< fieldNames.len: + emitCtorParams.add( + newTree( + nnkIdentDefs, + copyNimTree(fieldNames[i]), + copyNimTree(fieldTypes[i]), + newEmptyNode(), + ) + ) + + var emitCtorExpr = newTree(nnkObjConstr, copyNimTree(typeIdent)) + for i in 0 ..< fieldNames.len: + emitCtorExpr.add( + newTree(nnkExprColonExpr, copyNimTree(fieldNames[i]), copyNimTree(fieldNames[i])) + ) + + let emitCtorCall = newCall(copyNimTree(emitImplIdent), emitCtorExpr) + let emitCtorBody = quote: + asyncSpawn `emitCtorCall` + + let typedescEmitProc = newTree( + nnkProcDef, + postfix(ident("emit"), "*"), + newEmptyNode(), + newEmptyNode(), + emitCtorParams, + newEmptyNode(), + newEmptyNode(), + emitCtorBody, + ) + + result.add(typedescEmitProc) + + when defined(eventBrokerDebug): + echo result.repr diff --git a/waku/common/broker/helper/broker_utils.nim b/waku/common/broker/helper/broker_utils.nim new file mode 100644 index 000000000..ea9f85750 --- /dev/null +++ b/waku/common/broker/helper/broker_utils.nim @@ -0,0 +1,43 @@ +import std/macros + +proc sanitizeIdentName*(node: NimNode): string = + var raw = $node + var sanitizedName = newStringOfCap(raw.len) + for ch in raw: + case ch + of 'A' .. 'Z', 'a' .. 'z', '0' .. '9', '_': + sanitizedName.add(ch) + else: + sanitizedName.add('_') + sanitizedName + +proc ensureFieldDef*(node: NimNode) = + if node.kind != nnkIdentDefs or node.len < 3: + error("Expected field definition of the form `name: Type`", node) + let typeSlot = node.len - 2 + if node[typeSlot].kind == nnkEmpty: + error("Field `" & $node[0] & "` must declare a type", node) + +proc exportIdentNode*(node: NimNode): NimNode = + case node.kind + of nnkIdent: + postfix(copyNimTree(node), "*") + of nnkPostfix: + node + else: + error("Unsupported identifier form in field definition", node) + +proc baseTypeIdent*(defName: NimNode): NimNode = + case defName.kind + of nnkIdent: + defName + of nnkAccQuoted: + if defName.len != 1: + error("Unsupported quoted identifier", defName) + defName[0] + of nnkPostfix: + baseTypeIdent(defName[1]) + of nnkPragmaExpr: + baseTypeIdent(defName[0]) + else: + error("Unsupported type name in broker definition", defName) diff --git a/waku/common/broker/multi_request_broker.nim b/waku/common/broker/multi_request_broker.nim new file mode 100644 index 000000000..7f4161f5a --- /dev/null +++ b/waku/common/broker/multi_request_broker.nim @@ -0,0 +1,583 @@ +## MultiRequestBroker +## -------------------- +## MultiRequestBroker represents a proactive decoupling pattern, that +## allows defining request-response style interactions between modules without +## need for direct dependencies in between. +## Worth considering using it for use cases where you need to collect data from multiple providers. +## +## Provides a declarative way to define an immutable value type together with a +## thread-local broker that can register multiple asynchronous providers, dispatch +## typed requests, and clear handlers. Unlike `RequestBroker`, +## every call to `request` fan-outs to every registered provider and returns with +## collected responses. +## Request succeeds if all providers succeed, otherwise fails with an error. +## +## Usage: +## +## Declare collectable request data type inside a `MultiRequestBroker` macro, add any number of fields: +## ```nim +## MultiRequestBroker: +## type TypeName = object +## field1*: Type1 +## field2*: Type2 +## +## ## Define the request and provider signature, that is enforced at compile time. +## proc signature*(): Future[Result[TypeName, string]] {.async: (raises: []).} +## +## ## Also possible to define signature with arbitrary input arguments. +## proc signature*(arg1: ArgType, arg2: AnotherArgType): Future[Result[TypeName, string]] {.async: (raises: []).} +## +## ``` +## +## You regiser request processor (proveder) at any place of the code without the need to know of who ever may request. +## Respectively to the defined signatures register provider functions with `TypeName.setProvider(...)`. +## Providers are async procs or lambdas that return with a Future[Result[seq[TypeName], string]]. +## Notice MultiRequestBroker's `setProvider` return with a handler that can be used to remove the provider later (or error). + +## Requests can be made from anywhere with no direct dependency on the provider(s) by +## calling `TypeName.request()` - with arguments respecting the signature(s). +## This will asynchronously call the registered provider and return the collected data, in form of `Future[Result[seq[TypeName], string]]`. +## +## Whenever you don't want to process requests anymore (or your object instance that provides the request goes out of scope), +## you can remove it from the broker with `TypeName.removeProvider(handle)`. +## Alternatively, you can remove all registered providers through `TypeName.clearProviders()`. +## +## Example: +## ```nim +## MultiRequestBroker: +## type Greeting = object +## text*: string +## +## ## Define the request and provider signature, that is enforced at compile time. +## proc signature*(): Future[Result[Greeting, string]] {.async: (raises: []).} +## +## ## Also possible to define signature with arbitrary input arguments. +## proc signature*(lang: string): Future[Result[Greeting, string]] {.async: (raises: []).} +## +## ... +## let handle = Greeting.setProvider( +## proc(): Future[Result[Greeting, string]] {.async: (raises: []).} = +## ok(Greeting(text: "hello")) +## ) +## +## let anotherHandle = Greeting.setProvider( +## proc(): Future[Result[Greeting, string]] {.async: (raises: []).} = +## ok(Greeting(text: "szia")) +## ) +## +## let responses = (await Greeting.request()).valueOr(@[Greeting(text: "default")]) +## +## echo responses.len +## Greeting.clearProviders() +## ``` +## If no `signature` proc is declared, a zero-argument form is generated +## automatically, so the caller only needs to provide the type definition. + +import std/[macros, strutils, tables, sugar] +import chronos +import results +import ./helper/broker_utils + +export results, chronos + +proc isReturnTypeValid(returnType, typeIdent: NimNode): bool = + ## Accept Future[Result[TypeIdent, string]] as the contract. + if returnType.kind != nnkBracketExpr or returnType.len != 2: + return false + if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Future"): + return false + let inner = returnType[1] + if inner.kind != nnkBracketExpr or inner.len != 3: + return false + if inner[0].kind != nnkIdent or not inner[0].eqIdent("Result"): + return false + if inner[1].kind != nnkIdent or not inner[1].eqIdent($typeIdent): + return false + inner[2].kind == nnkIdent and inner[2].eqIdent("string") + +proc cloneParams(params: seq[NimNode]): seq[NimNode] = + ## Deep copy parameter definitions so they can be reused in generated nodes. + result = @[] + for param in params: + result.add(copyNimTree(param)) + +proc collectParamNames(params: seq[NimNode]): seq[NimNode] = + ## Extract identifiers declared in parameter definitions. + result = @[] + for param in params: + assert param.kind == nnkIdentDefs + for i in 0 ..< param.len - 2: + let nameNode = param[i] + if nameNode.kind == nnkEmpty: + continue + result.add(ident($nameNode)) + +proc makeProcType(returnType: NimNode, params: seq[NimNode]): NimNode = + var formal = newTree(nnkFormalParams) + formal.add(returnType) + for param in params: + formal.add(param) + + let pragmas = quote: + {.async.} + + newTree(nnkProcTy, formal, pragmas) + +macro MultiRequestBroker*(body: untyped): untyped = + when defined(requestBrokerDebug): + echo body.treeRepr + var typeIdent: NimNode = nil + var objectDef: NimNode = nil + var isRefObject = false + for stmt in body: + if stmt.kind == nnkTypeSection: + for def in stmt: + if def.kind != nnkTypeDef: + continue + let rhs = def[2] + var objectType: NimNode + case rhs.kind + of nnkObjectTy: + objectType = rhs + of nnkRefTy: + isRefObject = true + if rhs.len != 1 or rhs[0].kind != nnkObjectTy: + error( + "MultiRequestBroker ref object must wrap a concrete object definition", + rhs, + ) + objectType = rhs[0] + else: + continue + if not typeIdent.isNil(): + error("Only one object type may be declared inside MultiRequestBroker", def) + typeIdent = baseTypeIdent(def[0]) + let recList = objectType[2] + if recList.kind != nnkRecList: + error( + "MultiRequestBroker object must declare a standard field list", objectType + ) + var exportedRecList = newTree(nnkRecList) + for field in recList: + case field.kind + of nnkIdentDefs: + ensureFieldDef(field) + var cloned = copyNimTree(field) + for i in 0 ..< cloned.len - 2: + cloned[i] = exportIdentNode(cloned[i]) + exportedRecList.add(cloned) + of nnkEmpty: + discard + else: + error( + "MultiRequestBroker object definition only supports simple field declarations", + field, + ) + let exportedObjectType = newTree( + nnkObjectTy, + copyNimTree(objectType[0]), + copyNimTree(objectType[1]), + exportedRecList, + ) + if isRefObject: + objectDef = newTree(nnkRefTy, exportedObjectType) + else: + objectDef = exportedObjectType + if typeIdent.isNil(): + error("MultiRequestBroker body must declare exactly one object type", body) + + when defined(requestBrokerDebug): + echo "MultiRequestBroker generating type: ", $typeIdent + + let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*") + let sanitized = sanitizeIdentName(typeIdent) + let typeNameLit = newLit($typeIdent) + let isRefObjectLit = newLit(isRefObject) + let tableSym = bindSym"Table" + let initTableSym = bindSym"initTable" + let uint64Ident = ident("uint64") + let providerKindIdent = ident(sanitized & "ProviderKind") + let providerHandleIdent = ident(sanitized & "ProviderHandle") + let exportedProviderHandleIdent = postfix(copyNimTree(providerHandleIdent), "*") + let zeroKindIdent = ident("pk" & sanitized & "NoArgs") + let argKindIdent = ident("pk" & sanitized & "WithArgs") + var zeroArgSig: NimNode = nil + var zeroArgProviderName: NimNode = nil + var zeroArgFieldName: NimNode = nil + var argSig: NimNode = nil + var argParams: seq[NimNode] = @[] + var argProviderName: NimNode = nil + var argFieldName: NimNode = nil + + for stmt in body: + case stmt.kind + of nnkProcDef: + let procName = stmt[0] + let procNameIdent = + case procName.kind + of nnkIdent: + procName + of nnkPostfix: + procName[1] + else: + procName + let procNameStr = $procNameIdent + if not procNameStr.startsWith("signature"): + error("Signature proc names must start with `signature`", procName) + let params = stmt.params + if params.len == 0: + error("Signature must declare a return type", stmt) + let returnType = params[0] + if not isReturnTypeValid(returnType, typeIdent): + error( + "Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt + ) + let paramCount = params.len - 1 + if paramCount == 0: + if zeroArgSig != nil: + error("Only one zero-argument signature is allowed", stmt) + zeroArgSig = stmt + zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs") + zeroArgFieldName = ident("providerNoArgs") + elif paramCount >= 1: + if argSig != nil: + error("Only one argument-based signature is allowed", stmt) + argSig = stmt + argParams = @[] + for idx in 1 ..< params.len: + let paramDef = params[idx] + if paramDef.kind != nnkIdentDefs: + error( + "Signature parameter must be a standard identifier declaration", paramDef + ) + let paramTypeNode = paramDef[paramDef.len - 2] + if paramTypeNode.kind == nnkEmpty: + error("Signature parameter must declare a type", paramDef) + var hasName = false + for i in 0 ..< paramDef.len - 2: + if paramDef[i].kind != nnkEmpty: + hasName = true + if not hasName: + error("Signature parameter must declare a name", paramDef) + argParams.add(copyNimTree(paramDef)) + argProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderWithArgs") + argFieldName = ident("providerWithArgs") + of nnkTypeSection, nnkEmpty: + discard + else: + error("Unsupported statement inside MultiRequestBroker definition", stmt) + + if zeroArgSig.isNil() and argSig.isNil(): + zeroArgSig = newEmptyNode() + zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs") + zeroArgFieldName = ident("providerNoArgs") + + var typeSection = newTree(nnkTypeSection) + typeSection.add(newTree(nnkTypeDef, exportedTypeIdent, newEmptyNode(), objectDef)) + + var kindEnum = newTree(nnkEnumTy, newEmptyNode()) + if not zeroArgSig.isNil(): + kindEnum.add(zeroKindIdent) + if not argSig.isNil(): + kindEnum.add(argKindIdent) + typeSection.add(newTree(nnkTypeDef, providerKindIdent, newEmptyNode(), kindEnum)) + + var handleRecList = newTree(nnkRecList) + handleRecList.add(newTree(nnkIdentDefs, ident("id"), uint64Ident, newEmptyNode())) + handleRecList.add( + newTree(nnkIdentDefs, ident("kind"), providerKindIdent, newEmptyNode()) + ) + typeSection.add( + newTree( + nnkTypeDef, + exportedProviderHandleIdent, + newEmptyNode(), + newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), handleRecList), + ) + ) + + let returnType = quote: + Future[Result[`typeIdent`, string]] + + if not zeroArgSig.isNil(): + let procType = makeProcType(returnType, @[]) + typeSection.add(newTree(nnkTypeDef, zeroArgProviderName, newEmptyNode(), procType)) + if not argSig.isNil(): + let procType = makeProcType(returnType, cloneParams(argParams)) + typeSection.add(newTree(nnkTypeDef, argProviderName, newEmptyNode(), procType)) + + var brokerRecList = newTree(nnkRecList) + if not zeroArgSig.isNil(): + brokerRecList.add( + newTree( + nnkIdentDefs, + zeroArgFieldName, + newTree(nnkBracketExpr, tableSym, uint64Ident, zeroArgProviderName), + newEmptyNode(), + ) + ) + if not argSig.isNil(): + brokerRecList.add( + newTree( + nnkIdentDefs, + argFieldName, + newTree(nnkBracketExpr, tableSym, uint64Ident, argProviderName), + newEmptyNode(), + ) + ) + brokerRecList.add(newTree(nnkIdentDefs, ident("nextId"), uint64Ident, newEmptyNode())) + let brokerTypeIdent = ident(sanitizeIdentName(typeIdent) & "Broker") + let brokerTypeDef = newTree( + nnkTypeDef, + brokerTypeIdent, + newEmptyNode(), + newTree( + nnkRefTy, newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), brokerRecList) + ), + ) + typeSection.add(brokerTypeDef) + result = newStmtList() + result.add(typeSection) + + let globalVarIdent = ident("g" & sanitizeIdentName(typeIdent) & "Broker") + let accessProcIdent = ident("access" & sanitizeIdentName(typeIdent) & "Broker") + var initStatements = newStmtList() + if not zeroArgSig.isNil(): + initStatements.add( + quote do: + `globalVarIdent`.`zeroArgFieldName` = + `initTableSym`[`uint64Ident`, `zeroArgProviderName`]() + ) + if not argSig.isNil(): + initStatements.add( + quote do: + `globalVarIdent`.`argFieldName` = + `initTableSym`[`uint64Ident`, `argProviderName`]() + ) + result.add( + quote do: + var `globalVarIdent` {.threadvar.}: `brokerTypeIdent` + + proc `accessProcIdent`(): `brokerTypeIdent` = + if `globalVarIdent`.isNil(): + new(`globalVarIdent`) + `globalVarIdent`.nextId = 1'u64 + `initStatements` + return `globalVarIdent` + + ) + + var clearBody = newStmtList() + if not zeroArgSig.isNil(): + result.add( + quote do: + proc setProvider*( + _: typedesc[`typeIdent`], handler: `zeroArgProviderName` + ): Result[`providerHandleIdent`, string] = + if handler.isNil(): + return err("Provider handler must be provided") + let broker = `accessProcIdent`() + if broker.nextId == 0'u64: + broker.nextId = 1'u64 + for existingId, existing in broker.`zeroArgFieldName`.pairs: + if existing == handler: + return ok(`providerHandleIdent`(id: existingId, kind: `zeroKindIdent`)) + let newId = broker.nextId + inc broker.nextId + broker.`zeroArgFieldName`[newId] = handler + return ok(`providerHandleIdent`(id: newId, kind: `zeroKindIdent`)) + + ) + clearBody.add( + quote do: + let broker = `accessProcIdent`() + if not broker.isNil() and broker.`zeroArgFieldName`.len > 0: + broker.`zeroArgFieldName`.clear() + ) + result.add( + quote do: + proc request*( + _: typedesc[`typeIdent`] + ): Future[Result[seq[`typeIdent`], string]] {.async: (raises: []), gcsafe.} = + var aggregated: seq[`typeIdent`] = @[] + let providers = `accessProcIdent`().`zeroArgFieldName` + if providers.len == 0: + return ok(aggregated) + # var providersFut: seq[Future[Result[`typeIdent`, string]]] = collect: + var providersFut = collect(newSeq): + for provider in providers.values: + if provider.isNil(): + continue + provider() + + let catchable = catch: + await allFinished(providersFut) + + catchable.isOkOr: + return err("Some provider(s) failed:" & error.msg) + + for fut in catchable.get(): + if fut.failed(): + return err("Some provider(s) failed:" & fut.error.msg) + elif fut.finished(): + let providerResult = fut.value() + if providerResult.isOk: + let providerValue = providerResult.get() + when `isRefObjectLit`: + if providerValue.isNil(): + return err( + "MultiRequestBroker(" & `typeNameLit` & + "): provider returned nil result" + ) + aggregated.add(providerValue) + else: + return err("Some provider(s) failed:" & providerResult.error) + + return ok(aggregated) + + ) + if not argSig.isNil(): + result.add( + quote do: + proc setProvider*( + _: typedesc[`typeIdent`], handler: `argProviderName` + ): Result[`providerHandleIdent`, string] = + if handler.isNil(): + return err("Provider handler must be provided") + let broker = `accessProcIdent`() + if broker.nextId == 0'u64: + broker.nextId = 1'u64 + for existingId, existing in broker.`argFieldName`.pairs: + if existing == handler: + return ok(`providerHandleIdent`(id: existingId, kind: `argKindIdent`)) + let newId = broker.nextId + inc broker.nextId + broker.`argFieldName`[newId] = handler + return ok(`providerHandleIdent`(id: newId, kind: `argKindIdent`)) + + ) + clearBody.add( + quote do: + let broker = `accessProcIdent`() + if not broker.isNil() and broker.`argFieldName`.len > 0: + broker.`argFieldName`.clear() + ) + let requestParamDefs = cloneParams(argParams) + let argNameIdents = collectParamNames(requestParamDefs) + let providerSym = genSym(nskLet, "providerVal") + var providerCall = newCall(providerSym) + for argName in argNameIdents: + providerCall.add(argName) + var formalParams = newTree(nnkFormalParams) + formalParams.add( + quote do: + Future[Result[seq[`typeIdent`], string]] + ) + formalParams.add( + newTree( + nnkIdentDefs, + ident("_"), + newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)), + newEmptyNode(), + ) + ) + for paramDef in requestParamDefs: + formalParams.add(paramDef) + let requestPragmas = quote: + {.async: (raises: []), gcsafe.} + let requestBody = quote: + var aggregated: seq[`typeIdent`] = @[] + let providers = `accessProcIdent`().`argFieldName` + if providers.len == 0: + return ok(aggregated) + var providersFut = collect(newSeq): + for provider in providers.values: + if provider.isNil(): + continue + let `providerSym` = provider + `providerCall` + let catchable = catch: + await allFinished(providersFut) + catchable.isOkOr: + return err("Some provider(s) failed:" & error.msg) + for fut in catchable.get(): + if fut.failed(): + return err("Some provider(s) failed:" & fut.error.msg) + elif fut.finished(): + let providerResult = fut.value() + if providerResult.isOk: + let providerValue = providerResult.get() + when `isRefObjectLit`: + if providerValue.isNil(): + return err( + "MultiRequestBroker(" & `typeNameLit` & + "): provider returned nil result" + ) + aggregated.add(providerValue) + else: + return err("Some provider(s) failed:" & providerResult.error) + return ok(aggregated) + + result.add( + newTree( + nnkProcDef, + postfix(ident("request"), "*"), + newEmptyNode(), + newEmptyNode(), + formalParams, + requestPragmas, + newEmptyNode(), + requestBody, + ) + ) + + result.add( + quote do: + proc clearProviders*(_: typedesc[`typeIdent`]) = + `clearBody` + let broker = `accessProcIdent`() + if not broker.isNil(): + broker.nextId = 1'u64 + + ) + + let removeHandleSym = genSym(nskParam, "handle") + let removeBrokerSym = genSym(nskLet, "broker") + var removeBody = newStmtList() + removeBody.add( + quote do: + if `removeHandleSym`.id == 0'u64: + return + let `removeBrokerSym` = `accessProcIdent`() + if `removeBrokerSym`.isNil(): + return + ) + if not zeroArgSig.isNil(): + removeBody.add( + quote do: + if `removeHandleSym`.kind == `zeroKindIdent`: + `removeBrokerSym`.`zeroArgFieldName`.del(`removeHandleSym`.id) + return + ) + if not argSig.isNil(): + removeBody.add( + quote do: + if `removeHandleSym`.kind == `argKindIdent`: + `removeBrokerSym`.`argFieldName`.del(`removeHandleSym`.id) + return + ) + removeBody.add( + quote do: + discard + ) + result.add( + quote do: + proc removeProvider*( + _: typedesc[`typeIdent`], `removeHandleSym`: `providerHandleIdent` + ) = + `removeBody` + + ) + + when defined(requestBrokerDebug): + echo result.repr diff --git a/waku/common/broker/request_broker.nim b/waku/common/broker/request_broker.nim new file mode 100644 index 000000000..a8a6651d7 --- /dev/null +++ b/waku/common/broker/request_broker.nim @@ -0,0 +1,438 @@ +## RequestBroker +## -------------------- +## RequestBroker represents a proactive decoupling pattern, that +## allows defining request-response style interactions between modules without +## need for direct dependencies in between. +## Worth considering using it in a single provider, many requester scenario. +## +## Provides a declarative way to define an immutable value type together with a +## thread-local broker that can register an asynchronous provider, dispatch typed +## requests and clear provider. +## +## Usage: +## Declare your desired request type inside a `RequestBroker` macro, add any number of fields. +## Define the provider signature, that is enforced at compile time. +## +## ```nim +## RequestBroker: +## type TypeName = object +## field1*: FieldType +## field2*: AnotherFieldType +## +## proc signature*(): Future[Result[TypeName, string]] +## ## Also possible to define signature with arbitrary input arguments. +## proc signature*(arg1: ArgType, arg2: AnotherArgType): Future[Result[TypeName, string]] +## +## ``` +## The 'TypeName' object defines the requestable data (but also can be seen as request for action with return value). +## The 'signature' proc defines the provider(s) signature, that is enforced at compile time. +## One signature can be with no arguments, another with any number of arguments - where the input arguments are +## not related to the request type - but alternative inputs for the request to be processed. +## +## After this, you can register a provider anywhere in your code with +## `TypeName.setProvider(...)`, which returns error if already having a provider. +## Providers are async procs or lambdas that take no arguments and return a Future[Result[TypeName, string]]. +## Only one provider can be registered at a time per signature type (zero arg and/or multi arg). +## +## Requests can be made from anywhere with no direct dependency on the provider by +## calling `TypeName.request()` - with arguments respecting the signature(s). +## This will asynchronously call the registered provider and return a Future[Result[TypeName, string]]. +## +## Whenever you no want to process requests (or your object instance that provides the request goes out of scope), +## you can remove it from the broker with `TypeName.clearProvider()`. +## +## +## Example: +## ```nim +## RequestBroker: +## type Greeting = object +## text*: string +## +## ## Define the request and provider signature, that is enforced at compile time. +## proc signature*(): Future[Result[Greeting, string]] +## +## ## Also possible to define signature with arbitrary input arguments. +## proc signature*(lang: string): Future[Result[Greeting, string]] +## +## ... +## Greeting.setProvider( +## proc(): Future[Result[Greeting, string]] {.async.} = +## ok(Greeting(text: "hello")) +## ) +## let res = await Greeting.request() +## ``` +## If no `signature` proc is declared, a zero-argument form is generated +## automatically, so the caller only needs to provide the type definition. + +import std/[macros, strutils] +import chronos +import results +import ./helper/broker_utils + +export results, chronos + +proc errorFuture[T](message: string): Future[Result[T, string]] {.inline.} = + ## Build a future that is already completed with an error result. + let fut = newFuture[Result[T, string]]("request_broker.errorFuture") + fut.complete(err(Result[T, string], message)) + fut + +proc isReturnTypeValid(returnType, typeIdent: NimNode): bool = + ## Accept Future[Result[TypeIdent, string]] as the contract. + if returnType.kind != nnkBracketExpr or returnType.len != 2: + return false + if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Future"): + return false + let inner = returnType[1] + if inner.kind != nnkBracketExpr or inner.len != 3: + return false + if inner[0].kind != nnkIdent or not inner[0].eqIdent("Result"): + return false + if inner[1].kind != nnkIdent or not inner[1].eqIdent($typeIdent): + return false + inner[2].kind == nnkIdent and inner[2].eqIdent("string") + +proc cloneParams(params: seq[NimNode]): seq[NimNode] = + ## Deep copy parameter definitions so they can be inserted in multiple places. + result = @[] + for param in params: + result.add(copyNimTree(param)) + +proc collectParamNames(params: seq[NimNode]): seq[NimNode] = + ## Extract all identifier symbols declared across IdentDefs nodes. + result = @[] + for param in params: + assert param.kind == nnkIdentDefs + for i in 0 ..< param.len - 2: + let nameNode = param[i] + if nameNode.kind == nnkEmpty: + continue + result.add(ident($nameNode)) + +proc makeProcType(returnType: NimNode, params: seq[NimNode]): NimNode = + var formal = newTree(nnkFormalParams) + formal.add(returnType) + for param in params: + formal.add(param) + let pragmas = newTree(nnkPragma, ident("async")) + newTree(nnkProcTy, formal, pragmas) + +macro RequestBroker*(body: untyped): untyped = + when defined(requestBrokerDebug): + echo body.treeRepr + var typeIdent: NimNode = nil + var objectDef: NimNode = nil + var isRefObject = false + for stmt in body: + if stmt.kind == nnkTypeSection: + for def in stmt: + if def.kind != nnkTypeDef: + continue + let rhs = def[2] + var objectType: NimNode + case rhs.kind + of nnkObjectTy: + objectType = rhs + of nnkRefTy: + isRefObject = true + if rhs.len != 1 or rhs[0].kind != nnkObjectTy: + error( + "RequestBroker ref object must wrap a concrete object definition", rhs + ) + objectType = rhs[0] + else: + continue + if not typeIdent.isNil(): + error("Only one object type may be declared inside RequestBroker", def) + typeIdent = baseTypeIdent(def[0]) + let recList = objectType[2] + if recList.kind != nnkRecList: + error("RequestBroker object must declare a standard field list", objectType) + var exportedRecList = newTree(nnkRecList) + for field in recList: + case field.kind + of nnkIdentDefs: + ensureFieldDef(field) + var cloned = copyNimTree(field) + for i in 0 ..< cloned.len - 2: + cloned[i] = exportIdentNode(cloned[i]) + exportedRecList.add(cloned) + of nnkEmpty: + discard + else: + error( + "RequestBroker object definition only supports simple field declarations", + field, + ) + let exportedObjectType = newTree( + nnkObjectTy, + copyNimTree(objectType[0]), + copyNimTree(objectType[1]), + exportedRecList, + ) + if isRefObject: + objectDef = newTree(nnkRefTy, exportedObjectType) + else: + objectDef = exportedObjectType + if typeIdent.isNil(): + error("RequestBroker body must declare exactly one object type", body) + + when defined(requestBrokerDebug): + echo "RequestBroker generating type: ", $typeIdent + + let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*") + let typeDisplayName = sanitizeIdentName(typeIdent) + let typeNameLit = newLit(typeDisplayName) + let isRefObjectLit = newLit(isRefObject) + var zeroArgSig: NimNode = nil + var zeroArgProviderName: NimNode = nil + var zeroArgFieldName: NimNode = nil + var argSig: NimNode = nil + var argParams: seq[NimNode] = @[] + var argProviderName: NimNode = nil + var argFieldName: NimNode = nil + + for stmt in body: + case stmt.kind + of nnkProcDef: + let procName = stmt[0] + let procNameIdent = + case procName.kind + of nnkIdent: + procName + of nnkPostfix: + procName[1] + else: + procName + let procNameStr = $procNameIdent + if not procNameStr.startsWith("signature"): + error("Signature proc names must start with `signature`", procName) + let params = stmt.params + if params.len == 0: + error("Signature must declare a return type", stmt) + let returnType = params[0] + if not isReturnTypeValid(returnType, typeIdent): + error( + "Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt + ) + let paramCount = params.len - 1 + if paramCount == 0: + if zeroArgSig != nil: + error("Only one zero-argument signature is allowed", stmt) + zeroArgSig = stmt + zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs") + zeroArgFieldName = ident("providerNoArgs") + elif paramCount >= 1: + if argSig != nil: + error("Only one argument-based signature is allowed", stmt) + argSig = stmt + argParams = @[] + for idx in 1 ..< params.len: + let paramDef = params[idx] + if paramDef.kind != nnkIdentDefs: + error( + "Signature parameter must be a standard identifier declaration", paramDef + ) + let paramTypeNode = paramDef[paramDef.len - 2] + if paramTypeNode.kind == nnkEmpty: + error("Signature parameter must declare a type", paramDef) + var hasName = false + for i in 0 ..< paramDef.len - 2: + if paramDef[i].kind != nnkEmpty: + hasName = true + if not hasName: + error("Signature parameter must declare a name", paramDef) + argParams.add(copyNimTree(paramDef)) + argProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderWithArgs") + argFieldName = ident("providerWithArgs") + of nnkTypeSection, nnkEmpty: + discard + else: + error("Unsupported statement inside RequestBroker definition", stmt) + + if zeroArgSig.isNil() and argSig.isNil(): + zeroArgSig = newEmptyNode() + zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs") + zeroArgFieldName = ident("providerNoArgs") + + var typeSection = newTree(nnkTypeSection) + typeSection.add(newTree(nnkTypeDef, exportedTypeIdent, newEmptyNode(), objectDef)) + + let returnType = quote: + Future[Result[`typeIdent`, string]] + + if not zeroArgSig.isNil(): + let procType = makeProcType(returnType, @[]) + typeSection.add(newTree(nnkTypeDef, zeroArgProviderName, newEmptyNode(), procType)) + if not argSig.isNil(): + let procType = makeProcType(returnType, cloneParams(argParams)) + typeSection.add(newTree(nnkTypeDef, argProviderName, newEmptyNode(), procType)) + + var brokerRecList = newTree(nnkRecList) + if not zeroArgSig.isNil(): + brokerRecList.add( + newTree(nnkIdentDefs, zeroArgFieldName, zeroArgProviderName, newEmptyNode()) + ) + if not argSig.isNil(): + brokerRecList.add( + newTree(nnkIdentDefs, argFieldName, argProviderName, newEmptyNode()) + ) + let brokerTypeIdent = ident(sanitizeIdentName(typeIdent) & "Broker") + let brokerTypeDef = newTree( + nnkTypeDef, + brokerTypeIdent, + newEmptyNode(), + newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), brokerRecList), + ) + typeSection.add(brokerTypeDef) + result = newStmtList() + result.add(typeSection) + + let globalVarIdent = ident("g" & sanitizeIdentName(typeIdent) & "Broker") + let accessProcIdent = ident("access" & sanitizeIdentName(typeIdent) & "Broker") + result.add( + quote do: + var `globalVarIdent` {.threadvar.}: `brokerTypeIdent` + + proc `accessProcIdent`(): var `brokerTypeIdent` = + `globalVarIdent` + + ) + + var clearBody = newStmtList() + if not zeroArgSig.isNil(): + result.add( + quote do: + proc setProvider*( + _: typedesc[`typeIdent`], handler: `zeroArgProviderName` + ): Result[void, string] = + if not `accessProcIdent`().`zeroArgFieldName`.isNil(): + return err("Zero-arg provider already set") + `accessProcIdent`().`zeroArgFieldName` = handler + return ok() + + ) + clearBody.add( + quote do: + `accessProcIdent`().`zeroArgFieldName` = nil + ) + result.add( + quote do: + proc request*( + _: typedesc[`typeIdent`] + ): Future[Result[`typeIdent`, string]] {.async: (raises: []).} = + let provider = `accessProcIdent`().`zeroArgFieldName` + if provider.isNil(): + return err( + "RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered" + ) + let catchedRes = catch: + await provider() + + if catchedRes.isErr(): + return err("Request failed:" & catchedRes.error.msg) + + let providerRes = catchedRes.get() + when `isRefObjectLit`: + if providerRes.isOk(): + let resultValue = providerRes.get() + if resultValue.isNil(): + return err( + "RequestBroker(" & `typeNameLit` & "): provider returned nil result" + ) + return providerRes + + ) + if not argSig.isNil(): + result.add( + quote do: + proc setProvider*( + _: typedesc[`typeIdent`], handler: `argProviderName` + ): Result[void, string] = + if not `accessProcIdent`().`argFieldName`.isNil(): + return err("Provider already set") + `accessProcIdent`().`argFieldName` = handler + return ok() + + ) + clearBody.add( + quote do: + `accessProcIdent`().`argFieldName` = nil + ) + let requestParamDefs = cloneParams(argParams) + let argNameIdents = collectParamNames(requestParamDefs) + let providerSym = genSym(nskLet, "provider") + var formalParams = newTree(nnkFormalParams) + formalParams.add( + quote do: + Future[Result[`typeIdent`, string]] + ) + formalParams.add( + newTree( + nnkIdentDefs, + ident("_"), + newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)), + newEmptyNode(), + ) + ) + for paramDef in requestParamDefs: + formalParams.add(paramDef) + + let requestPragmas = quote: + {.async: (raises: []), gcsafe.} + var providerCall = newCall(providerSym) + for argName in argNameIdents: + providerCall.add(argName) + var requestBody = newStmtList() + requestBody.add( + quote do: + let `providerSym` = `accessProcIdent`().`argFieldName` + ) + requestBody.add( + quote do: + if `providerSym`.isNil(): + return err( + "RequestBroker(" & `typeNameLit` & + "): no provider registered for input signature" + ) + ) + requestBody.add( + quote do: + let catchedRes = catch: + await `providerCall` + if catchedRes.isErr(): + return err("Request failed:" & catchedRes.error.msg) + + let providerRes = catchedRes.get() + when `isRefObjectLit`: + if providerRes.isOk(): + let resultValue = providerRes.get() + if resultValue.isNil(): + return err( + "RequestBroker(" & `typeNameLit` & "): provider returned nil result" + ) + return providerRes + ) + # requestBody.add(providerCall) + result.add( + newTree( + nnkProcDef, + postfix(ident("request"), "*"), + newEmptyNode(), + newEmptyNode(), + formalParams, + requestPragmas, + newEmptyNode(), + requestBody, + ) + ) + + result.add( + quote do: + proc clearProvider*(_: typedesc[`typeIdent`]) = + `clearBody` + + ) + + when defined(requestBrokerDebug): + echo result.repr From 54f4ad8fa2ed452df670e25213a7fa34e5cc5432 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Tue, 2 Dec 2025 11:00:26 -0300 Subject: [PATCH 020/155] 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 021/155] 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 022/155] 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 023/155] 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 024/155] 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 025/155] 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 4664b96a7af691990659d03106264de8e4a05beb Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 8 Dec 2025 06:34:57 -0300 Subject: [PATCH 026/155] 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 0098c1687..2653153bf 100644 --- a/waku/node/peer_manager/waku_peer_store.nim +++ b/waku/node/peer_manager/waku_peer_store.nim @@ -202,3 +202,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 114775951..ffc2acaa4 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -489,9 +489,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 bbc089f20e0dd1caef4b9f4987ae0382ec7e562e Mon Sep 17 00:00:00 2001 From: darshankabariya Date: Wed, 10 Dec 2025 17:19:59 +0530 Subject: [PATCH 027/155] chore: update CHANGELOG for v0.37.1-beta --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e818afd..b7a00228e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 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 (2025-10-01) ### Notes 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 028/155] 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 029/155] 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 030/155] 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 031/155] 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 032/155] 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: From 3323325526bfa4898ca0c5c289638585c341af22 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:52:20 +0100 Subject: [PATCH 033/155] chore: extend RequestBroker with supporting native and external types and added possibility to define non-async (aka sync) requests for simplicity and performance (#3665) * chore: extend RequestBroker with supporting native and external types and added possibility to define non-async (aka sync) requests for simplicity and performance * Adapt gcsafe pragma for RequestBroker sync requests and provider signatures as requirement --------- Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> --- tests/common/test_request_broker.nim | 328 ++++++++++++++++++- waku/common/broker/request_broker.nim | 438 ++++++++++++++++++++------ 2 files changed, 651 insertions(+), 115 deletions(-) diff --git a/tests/common/test_request_broker.nim b/tests/common/test_request_broker.nim index 2ffd9cbf8..a534216dc 100644 --- a/tests/common/test_request_broker.nim +++ b/tests/common/test_request_broker.nim @@ -6,6 +6,10 @@ import std/strutils import waku/common/broker/request_broker +## --------------------------------------------------------------------------- +## Async-mode brokers + tests +## --------------------------------------------------------------------------- + RequestBroker: type SimpleResponse = object value*: string @@ -31,11 +35,14 @@ RequestBroker: suffix: string ): Future[Result[DualResponse, string]] {.async.} -RequestBroker: +RequestBroker(async): type ImplicitResponse = ref object note*: string -suite "RequestBroker macro": +static: + doAssert typeof(SimpleResponse.request()) is Future[Result[SimpleResponse, string]] + +suite "RequestBroker macro (async mode)": test "serves zero-argument providers": check SimpleResponse .setProvider( @@ -52,7 +59,7 @@ suite "RequestBroker macro": test "zero-argument request errors when unset": let res = waitFor SimpleResponse.request() - check res.isErr + check res.isErr() check res.error.contains("no zero-arg provider") test "serves input-based providers": @@ -78,7 +85,6 @@ suite "RequestBroker macro": .setProvider( proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = raise newException(ValueError, "simulated failure") - ok(KeyedResponse(key: key, payload: "")) ) .isOk() @@ -90,7 +96,7 @@ suite "RequestBroker macro": test "input request errors when unset": let res = waitFor KeyedResponse.request("foo", 2) - check res.isErr + check res.isErr() check res.error.contains("input signature") test "supports both provider types simultaneously": @@ -109,11 +115,11 @@ suite "RequestBroker macro": .isOk() let noInput = waitFor DualResponse.request() - check noInput.isOk + check noInput.isOk() check noInput.value.note == "base" let withInput = waitFor DualResponse.request("-extra") - check withInput.isOk + check withInput.isOk() check withInput.value.note == "base-extra" check withInput.value.count == 6 @@ -129,7 +135,7 @@ suite "RequestBroker macro": DualResponse.clearProvider() let res = waitFor DualResponse.request() - check res.isErr + check res.isErr() test "implicit zero-argument provider works by default": check ImplicitResponse @@ -140,14 +146,14 @@ suite "RequestBroker macro": .isOk() let res = waitFor ImplicitResponse.request() - check res.isOk + check res.isOk() ImplicitResponse.clearProvider() check res.value.note == "auto" test "implicit zero-argument request errors when unset": let res = waitFor ImplicitResponse.request() - check res.isErr + check res.isErr() check res.error.contains("no zero-arg provider") test "no provider override": @@ -171,7 +177,7 @@ suite "RequestBroker macro": check DualResponse.setProvider(overrideProc).isErr() let noInput = waitFor DualResponse.request() - check noInput.isOk + check noInput.isOk() check noInput.value.note == "base" let stillResponse = waitFor DualResponse.request(" still works") @@ -191,8 +197,306 @@ suite "RequestBroker macro": check DualResponse.setProvider(overrideProc).isOk() let nowSuccWithOverride = waitFor DualResponse.request() - check nowSuccWithOverride.isOk + check nowSuccWithOverride.isOk() check nowSuccWithOverride.value.note == "something else" check nowSuccWithOverride.value.count == 1 DualResponse.clearProvider() + +## --------------------------------------------------------------------------- +## Sync-mode brokers + tests +## --------------------------------------------------------------------------- + +RequestBroker(sync): + type SimpleResponseSync = object + value*: string + + proc signatureFetch*(): Result[SimpleResponseSync, string] + +RequestBroker(sync): + type KeyedResponseSync = object + key*: string + payload*: string + + proc signatureFetchWithKey*( + key: string, subKey: int + ): Result[KeyedResponseSync, string] + +RequestBroker(sync): + type DualResponseSync = object + note*: string + count*: int + + proc signatureNoInput*(): Result[DualResponseSync, string] + proc signatureWithInput*(suffix: string): Result[DualResponseSync, string] + +RequestBroker(sync): + type ImplicitResponseSync = ref object + note*: string + +static: + doAssert typeof(SimpleResponseSync.request()) is Result[SimpleResponseSync, string] + doAssert not ( + typeof(SimpleResponseSync.request()) is Future[Result[SimpleResponseSync, string]] + ) + doAssert typeof(KeyedResponseSync.request("topic", 1)) is + Result[KeyedResponseSync, string] + +suite "RequestBroker macro (sync mode)": + test "serves zero-argument providers (sync)": + check SimpleResponseSync + .setProvider( + proc(): Result[SimpleResponseSync, string] = + ok(SimpleResponseSync(value: "hi")) + ) + .isOk() + + let res = SimpleResponseSync.request() + check res.isOk() + check res.value.value == "hi" + + SimpleResponseSync.clearProvider() + + test "zero-argument request errors when unset (sync)": + let res = SimpleResponseSync.request() + check res.isErr() + check res.error.contains("no zero-arg provider") + + test "serves input-based providers (sync)": + var seen: seq[string] = @[] + check KeyedResponseSync + .setProvider( + proc(key: string, subKey: int): Result[KeyedResponseSync, string] = + seen.add(key) + ok(KeyedResponseSync(key: key, payload: key & "-payload+" & $subKey)) + ) + .isOk() + + let res = KeyedResponseSync.request("topic", 1) + check res.isOk() + check res.value.key == "topic" + check res.value.payload == "topic-payload+1" + check seen == @["topic"] + + KeyedResponseSync.clearProvider() + + test "catches provider exception (sync)": + check KeyedResponseSync + .setProvider( + proc(key: string, subKey: int): Result[KeyedResponseSync, string] = + raise newException(ValueError, "simulated failure") + ) + .isOk() + + let res = KeyedResponseSync.request("neglected", 11) + check res.isErr() + check res.error.contains("simulated failure") + + KeyedResponseSync.clearProvider() + + test "input request errors when unset (sync)": + let res = KeyedResponseSync.request("foo", 2) + check res.isErr() + check res.error.contains("input signature") + + test "supports both provider types simultaneously (sync)": + check DualResponseSync + .setProvider( + proc(): Result[DualResponseSync, string] = + ok(DualResponseSync(note: "base", count: 1)) + ) + .isOk() + + check DualResponseSync + .setProvider( + proc(suffix: string): Result[DualResponseSync, string] = + ok(DualResponseSync(note: "base" & suffix, count: suffix.len)) + ) + .isOk() + + let noInput = DualResponseSync.request() + check noInput.isOk() + check noInput.value.note == "base" + + let withInput = DualResponseSync.request("-extra") + check withInput.isOk() + check withInput.value.note == "base-extra" + check withInput.value.count == 6 + + DualResponseSync.clearProvider() + + test "clearProvider resets both entries (sync)": + check DualResponseSync + .setProvider( + proc(): Result[DualResponseSync, string] = + ok(DualResponseSync(note: "temp", count: 0)) + ) + .isOk() + DualResponseSync.clearProvider() + + let res = DualResponseSync.request() + check res.isErr() + + test "implicit zero-argument provider works by default (sync)": + check ImplicitResponseSync + .setProvider( + proc(): Result[ImplicitResponseSync, string] = + ok(ImplicitResponseSync(note: "auto")) + ) + .isOk() + + let res = ImplicitResponseSync.request() + check res.isOk() + + ImplicitResponseSync.clearProvider() + check res.value.note == "auto" + + test "implicit zero-argument request errors when unset (sync)": + let res = ImplicitResponseSync.request() + check res.isErr() + check res.error.contains("no zero-arg provider") + + test "implicit zero-argument provider raises error (sync)": + check ImplicitResponseSync + .setProvider( + proc(): Result[ImplicitResponseSync, string] = + raise newException(ValueError, "simulated failure") + ) + .isOk() + + let res = ImplicitResponseSync.request() + check res.isErr() + check res.error.contains("simulated failure") + + ImplicitResponseSync.clearProvider() + +## --------------------------------------------------------------------------- +## POD / external type brokers + tests (distinct/alias behavior) +## --------------------------------------------------------------------------- + +type ExternalDefinedTypeAsync = object + label*: string + +type ExternalDefinedTypeSync = object + label*: string + +type ExternalDefinedTypeShared = object + label*: string + +RequestBroker: + type PodResponse = int + + proc signatureFetch*(): Future[Result[PodResponse, string]] {.async.} + +RequestBroker: + type ExternalAliasedResponse = ExternalDefinedTypeAsync + + proc signatureFetch*(): Future[Result[ExternalAliasedResponse, string]] {.async.} + +RequestBroker(sync): + type ExternalAliasedResponseSync = ExternalDefinedTypeSync + + proc signatureFetch*(): Result[ExternalAliasedResponseSync, string] + +RequestBroker(sync): + type DistinctStringResponseA = distinct string + +RequestBroker(sync): + type DistinctStringResponseB = distinct string + +RequestBroker(sync): + type ExternalDistinctResponseA = distinct ExternalDefinedTypeShared + +RequestBroker(sync): + type ExternalDistinctResponseB = distinct ExternalDefinedTypeShared + +suite "RequestBroker macro (POD/external types)": + test "supports non-object response types (async)": + check PodResponse + .setProvider( + proc(): Future[Result[PodResponse, string]] {.async.} = + ok(PodResponse(123)) + ) + .isOk() + + let res = waitFor PodResponse.request() + check res.isOk() + check int(res.value) == 123 + + PodResponse.clearProvider() + + test "supports aliased external types (async)": + check ExternalAliasedResponse + .setProvider( + proc(): Future[Result[ExternalAliasedResponse, string]] {.async.} = + ok(ExternalAliasedResponse(ExternalDefinedTypeAsync(label: "ext"))) + ) + .isOk() + + let res = waitFor ExternalAliasedResponse.request() + check res.isOk() + check ExternalDefinedTypeAsync(res.value).label == "ext" + + ExternalAliasedResponse.clearProvider() + + test "supports aliased external types (sync)": + check ExternalAliasedResponseSync + .setProvider( + proc(): Result[ExternalAliasedResponseSync, string] = + ok(ExternalAliasedResponseSync(ExternalDefinedTypeSync(label: "ext"))) + ) + .isOk() + + let res = ExternalAliasedResponseSync.request() + check res.isOk() + check ExternalDefinedTypeSync(res.value).label == "ext" + + ExternalAliasedResponseSync.clearProvider() + + test "distinct response types avoid overload ambiguity (sync)": + check DistinctStringResponseA + .setProvider( + proc(): Result[DistinctStringResponseA, string] = + ok(DistinctStringResponseA("a")) + ) + .isOk() + + check DistinctStringResponseB + .setProvider( + proc(): Result[DistinctStringResponseB, string] = + ok(DistinctStringResponseB("b")) + ) + .isOk() + + check ExternalDistinctResponseA + .setProvider( + proc(): Result[ExternalDistinctResponseA, string] = + ok(ExternalDistinctResponseA(ExternalDefinedTypeShared(label: "ea"))) + ) + .isOk() + + check ExternalDistinctResponseB + .setProvider( + proc(): Result[ExternalDistinctResponseB, string] = + ok(ExternalDistinctResponseB(ExternalDefinedTypeShared(label: "eb"))) + ) + .isOk() + + let resA = DistinctStringResponseA.request() + let resB = DistinctStringResponseB.request() + check resA.isOk() + check resB.isOk() + check string(resA.value) == "a" + check string(resB.value) == "b" + + let resEA = ExternalDistinctResponseA.request() + let resEB = ExternalDistinctResponseB.request() + check resEA.isOk() + check resEB.isOk() + check ExternalDefinedTypeShared(resEA.value).label == "ea" + check ExternalDefinedTypeShared(resEB.value).label == "eb" + + DistinctStringResponseA.clearProvider() + DistinctStringResponseB.clearProvider() + ExternalDistinctResponseA.clearProvider() + ExternalDistinctResponseB.clearProvider() diff --git a/waku/common/broker/request_broker.nim b/waku/common/broker/request_broker.nim index a8a6651d7..dece77381 100644 --- a/waku/common/broker/request_broker.nim +++ b/waku/common/broker/request_broker.nim @@ -6,8 +6,15 @@ ## Worth considering using it in a single provider, many requester scenario. ## ## Provides a declarative way to define an immutable value type together with a -## thread-local broker that can register an asynchronous provider, dispatch typed -## requests and clear provider. +## thread-local broker that can register an asynchronous or synchronous provider, +## dispatch typed requests and clear provider. +## +## For consideration use `sync` mode RequestBroker when you need to provide simple value(s) +## where there is no long-running async operation involved. +## Typically it act as a accessor for the local state of generic setting. +## +## `async` mode is better to be used when you request date that may involve some long IO operation +## or action. ## ## Usage: ## Declare your desired request type inside a `RequestBroker` macro, add any number of fields. @@ -24,6 +31,56 @@ ## proc signature*(arg1: ArgType, arg2: AnotherArgType): Future[Result[TypeName, string]] ## ## ``` +## +## Sync mode (no `async` / `Future`) can be generated with: +## +## ```nim +## RequestBroker(sync): +## type TypeName = object +## field1*: FieldType +## +## proc signature*(): Result[TypeName, string] +## proc signature*(arg1: ArgType): Result[TypeName, string] +## ``` +## +## Note: When the request type is declared as a native type / alias / externally-defined +## type (i.e. not an inline `object` / `ref object` definition), RequestBroker +## will wrap it in `distinct` automatically unless you already used `distinct`. +## This avoids overload ambiguity when multiple brokers share the same +## underlying base type (Nim overload resolution does not consider return type). +## +## This means that for non-object request types you typically: +## - construct values with an explicit cast/constructor, e.g. `MyType("x")` +## - unwrap with a cast when needed, e.g. `string(myVal)` or `BaseType(myVal)` +## +## Example (native response type): +## ```nim +## RequestBroker(sync): +## type MyCount = int # exported as: `distinct int` +## +## MyCount.setProvider(proc(): Result[MyCount, string] = ok(MyCount(42))) +## let res = MyCount.request() +## if res.isOk(): +## let raw = int(res.get()) +## ``` +## +## Example (externally-defined type): +## ```nim +## type External = object +## label*: string +## +## RequestBroker: +## type MyExternal = External # exported as: `distinct External` +## +## MyExternal.setProvider( +## proc(): Future[Result[MyExternal, string]] {.async.} = +## ok(MyExternal(External(label: "hi"))) +## ) +## let res = await MyExternal.request() +## if res.isOk(): +## let base = External(res.get()) +## echo base.label +## ``` ## The 'TypeName' object defines the requestable data (but also can be seen as request for action with return value). ## The 'signature' proc defines the provider(s) signature, that is enforced at compile time. ## One signature can be with no arguments, another with any number of arguments - where the input arguments are @@ -31,12 +88,12 @@ ## ## After this, you can register a provider anywhere in your code with ## `TypeName.setProvider(...)`, which returns error if already having a provider. -## Providers are async procs or lambdas that take no arguments and return a Future[Result[TypeName, string]]. +## Providers are async procs/lambdas in default mode and sync procs in sync mode. ## Only one provider can be registered at a time per signature type (zero arg and/or multi arg). ## ## Requests can be made from anywhere with no direct dependency on the provider by ## calling `TypeName.request()` - with arguments respecting the signature(s). -## This will asynchronously call the registered provider and return a Future[Result[TypeName, string]]. +## In async mode, this returns a Future[Result[TypeName, string]]. In sync mode, it returns Result[TypeName, string]. ## ## Whenever you no want to process requests (or your object instance that provides the request goes out of scope), ## you can remove it from the broker with `TypeName.clearProvider()`. @@ -49,10 +106,10 @@ ## text*: string ## ## ## Define the request and provider signature, that is enforced at compile time. -## proc signature*(): Future[Result[Greeting, string]] +## proc signature*(): Future[Result[Greeting, string]] {.async.} ## ## ## Also possible to define signature with arbitrary input arguments. -## proc signature*(lang: string): Future[Result[Greeting, string]] +## proc signature*(lang: string): Future[Result[Greeting, string]] {.async.} ## ## ... ## Greeting.setProvider( @@ -60,6 +117,23 @@ ## ok(Greeting(text: "hello")) ## ) ## let res = await Greeting.request() +## +## +## ... +## # using native type as response for a synchronous request. +## RequestBroker(sync): +## type NeedThatInfo = string +## +##... +## NeedThatInfo.setProvider( +## proc(): Result[NeedThatInfo, string] = +## ok("this is the info you wanted") +## ) +## let res = NeedThatInfo.request().valueOr: +## echo "not ok due to: " & error +## NeedThatInfo(":-(") +## +## echo string(res) ## ``` ## If no `signature` proc is declared, a zero-argument form is generated ## automatically, so the caller only needs to provide the type definition. @@ -77,7 +151,11 @@ proc errorFuture[T](message: string): Future[Result[T, string]] {.inline.} = fut.complete(err(Result[T, string], message)) fut -proc isReturnTypeValid(returnType, typeIdent: NimNode): bool = +type RequestBrokerMode = enum + rbAsync + rbSync + +proc isAsyncReturnTypeValid(returnType, typeIdent: NimNode): bool = ## Accept Future[Result[TypeIdent, string]] as the contract. if returnType.kind != nnkBracketExpr or returnType.len != 2: return false @@ -92,6 +170,23 @@ proc isReturnTypeValid(returnType, typeIdent: NimNode): bool = return false inner[2].kind == nnkIdent and inner[2].eqIdent("string") +proc isSyncReturnTypeValid(returnType, typeIdent: NimNode): bool = + ## Accept Result[TypeIdent, string] as the contract. + if returnType.kind != nnkBracketExpr or returnType.len != 3: + return false + if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Result"): + return false + if returnType[1].kind != nnkIdent or not returnType[1].eqIdent($typeIdent): + return false + returnType[2].kind == nnkIdent and returnType[2].eqIdent("string") + +proc isReturnTypeValid(returnType, typeIdent: NimNode, mode: RequestBrokerMode): bool = + case mode + of rbAsync: + isAsyncReturnTypeValid(returnType, typeIdent) + of rbSync: + isSyncReturnTypeValid(returnType, typeIdent) + proc cloneParams(params: seq[NimNode]): seq[NimNode] = ## Deep copy parameter definitions so they can be inserted in multiple places. result = @[] @@ -109,73 +204,122 @@ proc collectParamNames(params: seq[NimNode]): seq[NimNode] = continue result.add(ident($nameNode)) -proc makeProcType(returnType: NimNode, params: seq[NimNode]): NimNode = +proc makeProcType( + returnType: NimNode, params: seq[NimNode], mode: RequestBrokerMode +): NimNode = var formal = newTree(nnkFormalParams) formal.add(returnType) for param in params: formal.add(param) - let pragmas = newTree(nnkPragma, ident("async")) - newTree(nnkProcTy, formal, pragmas) + case mode + of rbAsync: + let pragmas = newTree(nnkPragma, ident("async")) + newTree(nnkProcTy, formal, pragmas) + of rbSync: + let raisesPragma = newTree( + nnkExprColonExpr, ident("raises"), newTree(nnkBracket, ident("CatchableError")) + ) + let pragmas = newTree(nnkPragma, raisesPragma, ident("gcsafe")) + newTree(nnkProcTy, formal, pragmas) -macro RequestBroker*(body: untyped): untyped = +proc parseMode(modeNode: NimNode): RequestBrokerMode = + ## Parses the mode selector for the 2-argument macro overload. + ## Supported spellings: `sync` / `async` (case-insensitive). + let raw = ($modeNode).strip().toLowerAscii() + case raw + of "sync": + rbSync + of "async": + rbAsync + else: + error("RequestBroker mode must be `sync` or `async` (default is async)", modeNode) + +proc ensureDistinctType(rhs: NimNode): NimNode = + ## For PODs / aliases / externally-defined types, wrap in `distinct` unless + ## it's already distinct. + if rhs.kind == nnkDistinctTy: + return copyNimTree(rhs) + newTree(nnkDistinctTy, copyNimTree(rhs)) + +proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = when defined(requestBrokerDebug): echo body.treeRepr + echo "RequestBroker mode: ", $mode var typeIdent: NimNode = nil var objectDef: NimNode = nil - var isRefObject = false for stmt in body: if stmt.kind == nnkTypeSection: for def in stmt: if def.kind != nnkTypeDef: continue + if not typeIdent.isNil(): + error("Only one type may be declared inside RequestBroker", def) + + typeIdent = baseTypeIdent(def[0]) let rhs = def[2] - var objectType: NimNode + + ## Support inline object types (fields are auto-exported) + ## AND non-object types / aliases (e.g. `string`, `int`, `OtherType`). case rhs.kind of nnkObjectTy: - objectType = rhs + let recList = rhs[2] + if recList.kind != nnkRecList: + error("RequestBroker object must declare a standard field list", rhs) + var exportedRecList = newTree(nnkRecList) + for field in recList: + case field.kind + of nnkIdentDefs: + ensureFieldDef(field) + var cloned = copyNimTree(field) + for i in 0 ..< cloned.len - 2: + cloned[i] = exportIdentNode(cloned[i]) + exportedRecList.add(cloned) + of nnkEmpty: + discard + else: + error( + "RequestBroker object definition only supports simple field declarations", + field, + ) + objectDef = newTree( + nnkObjectTy, copyNimTree(rhs[0]), copyNimTree(rhs[1]), exportedRecList + ) of nnkRefTy: - isRefObject = true - if rhs.len != 1 or rhs[0].kind != nnkObjectTy: - error( - "RequestBroker ref object must wrap a concrete object definition", rhs + if rhs.len != 1: + error("RequestBroker ref type must have a single base", rhs) + if rhs[0].kind == nnkObjectTy: + let obj = rhs[0] + let recList = obj[2] + if recList.kind != nnkRecList: + error("RequestBroker object must declare a standard field list", obj) + var exportedRecList = newTree(nnkRecList) + for field in recList: + case field.kind + of nnkIdentDefs: + ensureFieldDef(field) + var cloned = copyNimTree(field) + for i in 0 ..< cloned.len - 2: + cloned[i] = exportIdentNode(cloned[i]) + exportedRecList.add(cloned) + of nnkEmpty: + discard + else: + error( + "RequestBroker object definition only supports simple field declarations", + field, + ) + let exportedObjectType = newTree( + nnkObjectTy, copyNimTree(obj[0]), copyNimTree(obj[1]), exportedRecList ) - objectType = rhs[0] - else: - continue - if not typeIdent.isNil(): - error("Only one object type may be declared inside RequestBroker", def) - typeIdent = baseTypeIdent(def[0]) - let recList = objectType[2] - if recList.kind != nnkRecList: - error("RequestBroker object must declare a standard field list", objectType) - var exportedRecList = newTree(nnkRecList) - for field in recList: - case field.kind - of nnkIdentDefs: - ensureFieldDef(field) - var cloned = copyNimTree(field) - for i in 0 ..< cloned.len - 2: - cloned[i] = exportIdentNode(cloned[i]) - exportedRecList.add(cloned) - of nnkEmpty: - discard + objectDef = newTree(nnkRefTy, exportedObjectType) else: - error( - "RequestBroker object definition only supports simple field declarations", - field, - ) - let exportedObjectType = newTree( - nnkObjectTy, - copyNimTree(objectType[0]), - copyNimTree(objectType[1]), - exportedRecList, - ) - if isRefObject: - objectDef = newTree(nnkRefTy, exportedObjectType) + ## `ref SomeType` (SomeType can be defined elsewhere) + objectDef = ensureDistinctType(rhs) else: - objectDef = exportedObjectType + ## Non-object type / alias (e.g. `string`, `int`, `SomeExternalType`). + objectDef = ensureDistinctType(rhs) if typeIdent.isNil(): - error("RequestBroker body must declare exactly one object type", body) + error("RequestBroker body must declare exactly one type", body) when defined(requestBrokerDebug): echo "RequestBroker generating type: ", $typeIdent @@ -183,7 +327,6 @@ macro RequestBroker*(body: untyped): untyped = let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*") let typeDisplayName = sanitizeIdentName(typeIdent) let typeNameLit = newLit(typeDisplayName) - let isRefObjectLit = newLit(isRefObject) var zeroArgSig: NimNode = nil var zeroArgProviderName: NimNode = nil var zeroArgFieldName: NimNode = nil @@ -211,10 +354,14 @@ macro RequestBroker*(body: untyped): untyped = if params.len == 0: error("Signature must declare a return type", stmt) let returnType = params[0] - if not isReturnTypeValid(returnType, typeIdent): - error( - "Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt - ) + if not isReturnTypeValid(returnType, typeIdent, mode): + case mode + of rbAsync: + error( + "Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt + ) + of rbSync: + error("Signature must return Result[`" & $typeIdent & "`, string]", stmt) let paramCount = params.len - 1 if paramCount == 0: if zeroArgSig != nil: @@ -258,14 +405,20 @@ macro RequestBroker*(body: untyped): untyped = var typeSection = newTree(nnkTypeSection) typeSection.add(newTree(nnkTypeDef, exportedTypeIdent, newEmptyNode(), objectDef)) - let returnType = quote: - Future[Result[`typeIdent`, string]] + let returnType = + case mode + of rbAsync: + quote: + Future[Result[`typeIdent`, string]] + of rbSync: + quote: + Result[`typeIdent`, string] if not zeroArgSig.isNil(): - let procType = makeProcType(returnType, @[]) + let procType = makeProcType(returnType, @[], mode) typeSection.add(newTree(nnkTypeDef, zeroArgProviderName, newEmptyNode(), procType)) if not argSig.isNil(): - let procType = makeProcType(returnType, cloneParams(argParams)) + let procType = makeProcType(returnType, cloneParams(argParams), mode) typeSection.add(newTree(nnkTypeDef, argProviderName, newEmptyNode(), procType)) var brokerRecList = newTree(nnkRecList) @@ -316,33 +469,69 @@ macro RequestBroker*(body: untyped): untyped = quote do: `accessProcIdent`().`zeroArgFieldName` = nil ) - result.add( - quote do: - proc request*( - _: typedesc[`typeIdent`] - ): Future[Result[`typeIdent`, string]] {.async: (raises: []).} = - let provider = `accessProcIdent`().`zeroArgFieldName` - if provider.isNil(): - return err( - "RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered" - ) - let catchedRes = catch: - await provider() + case mode + of rbAsync: + result.add( + quote do: + proc request*( + _: typedesc[`typeIdent`] + ): Future[Result[`typeIdent`, string]] {.async: (raises: []).} = + let provider = `accessProcIdent`().`zeroArgFieldName` + if provider.isNil(): + return err( + "RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered" + ) + let catchedRes = catch: + await provider() - if catchedRes.isErr(): - return err("Request failed:" & catchedRes.error.msg) + if catchedRes.isErr(): + return err( + "RequestBroker(" & `typeNameLit` & "): provider threw exception: " & + catchedRes.error.msg + ) - let providerRes = catchedRes.get() - when `isRefObjectLit`: + let providerRes = catchedRes.get() if providerRes.isOk(): let resultValue = providerRes.get() - if resultValue.isNil(): - return err( - "RequestBroker(" & `typeNameLit` & "): provider returned nil result" - ) - return providerRes + when compiles(resultValue.isNil()): + if resultValue.isNil(): + return err( + "RequestBroker(" & `typeNameLit` & "): provider returned nil result" + ) + return providerRes - ) + ) + of rbSync: + result.add( + quote do: + proc request*( + _: typedesc[`typeIdent`] + ): Result[`typeIdent`, string] {.gcsafe, raises: [].} = + let provider = `accessProcIdent`().`zeroArgFieldName` + if provider.isNil(): + return err( + "RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered" + ) + + var providerRes: Result[`typeIdent`, string] + try: + providerRes = provider() + except CatchableError as e: + return err( + "RequestBroker(" & `typeNameLit` & "): provider threw exception: " & + e.msg + ) + + if providerRes.isOk(): + let resultValue = providerRes.get() + when compiles(resultValue.isNil()): + if resultValue.isNil(): + return err( + "RequestBroker(" & `typeNameLit` & "): provider returned nil result" + ) + return providerRes + + ) if not argSig.isNil(): result.add( quote do: @@ -363,10 +552,7 @@ macro RequestBroker*(body: untyped): untyped = let argNameIdents = collectParamNames(requestParamDefs) let providerSym = genSym(nskLet, "provider") var formalParams = newTree(nnkFormalParams) - formalParams.add( - quote do: - Future[Result[`typeIdent`, string]] - ) + formalParams.add(copyNimTree(returnType)) formalParams.add( newTree( nnkIdentDefs, @@ -378,8 +564,14 @@ macro RequestBroker*(body: untyped): untyped = for paramDef in requestParamDefs: formalParams.add(paramDef) - let requestPragmas = quote: - {.async: (raises: []), gcsafe.} + let requestPragmas = + case mode + of rbAsync: + quote: + {.async: (raises: []).} + of rbSync: + quote: + {.gcsafe, raises: [].} var providerCall = newCall(providerSym) for argName in argNameIdents: providerCall.add(argName) @@ -396,23 +588,49 @@ macro RequestBroker*(body: untyped): untyped = "): no provider registered for input signature" ) ) - requestBody.add( - quote do: - let catchedRes = catch: - await `providerCall` - if catchedRes.isErr(): - return err("Request failed:" & catchedRes.error.msg) - let providerRes = catchedRes.get() - when `isRefObjectLit`: + case mode + of rbAsync: + requestBody.add( + quote do: + let catchedRes = catch: + await `providerCall` + if catchedRes.isErr(): + return err( + "RequestBroker(" & `typeNameLit` & "): provider threw exception: " & + catchedRes.error.msg + ) + + let providerRes = catchedRes.get() if providerRes.isOk(): let resultValue = providerRes.get() - if resultValue.isNil(): - return err( - "RequestBroker(" & `typeNameLit` & "): provider returned nil result" - ) - return providerRes - ) + when compiles(resultValue.isNil()): + if resultValue.isNil(): + return err( + "RequestBroker(" & `typeNameLit` & "): provider returned nil result" + ) + return providerRes + ) + of rbSync: + requestBody.add( + quote do: + var providerRes: Result[`typeIdent`, string] + try: + providerRes = `providerCall` + except CatchableError as e: + return err( + "RequestBroker(" & `typeNameLit` & "): provider threw exception: " & e.msg + ) + + if providerRes.isOk(): + let resultValue = providerRes.get() + when compiles(resultValue.isNil()): + if resultValue.isNil(): + return err( + "RequestBroker(" & `typeNameLit` & "): provider returned nil result" + ) + return providerRes + ) # requestBody.add(providerCall) result.add( newTree( @@ -436,3 +654,17 @@ macro RequestBroker*(body: untyped): untyped = when defined(requestBrokerDebug): echo result.repr + + return result + +macro RequestBroker*(body: untyped): untyped = + ## Default (async) mode. + generateRequestBroker(body, rbAsync) + +macro RequestBroker*(mode: untyped, body: untyped): untyped = + ## Explicit mode selector. + ## Example: + ## RequestBroker(sync): + ## type Foo = object + ## proc signature*(): Result[Foo, string] + generateRequestBroker(body, parseMode(mode)) From bc5059083ec0af6bfb91aa98cb546758ad52e6db Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Tue, 16 Dec 2025 13:49:03 -0300 Subject: [PATCH 034/155] chore: pin logos-messaging-interop-tests to `SMOKE_TEST_STABLE` (#3667) * pin to interop-tests SMOKE_TEST_STABLE --- .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 9c94577f0..2b12a5109 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,7 +143,7 @@ jobs: nwaku-nwaku-interop-tests: needs: build-docker-image - uses: logos-messaging/logos-messaging-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_STABLE with: node_nwaku: ${{ needs.build-docker-image.outputs.image }} From 7c24a15459a1892ffa17421b981f3f3dcf652523 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 18 Dec 2025 00:07:29 +0100 Subject: [PATCH 035/155] simple cleanup rm unused DiscoveryManager from waku.nim (#3671) --- waku/factory/waku.nim | 2 -- 1 file changed, 2 deletions(-) diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index bed8a9137..c0380ccc9 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -13,7 +13,6 @@ import libp2p/services/autorelayservice, libp2p/services/hpservice, libp2p/peerid, - libp2p/discovery/discoverymngr, libp2p/discovery/rendezvousinterface, eth/keys, eth/p2p/discoveryv5/enr, @@ -63,7 +62,6 @@ type Waku* = ref object dynamicBootstrapNodes*: seq[RemotePeerInfo] dnsRetryLoopHandle: Future[void] networkConnLoopHandle: Future[void] - discoveryMngr: DiscoveryManager node*: WakuNode From 2d40cb9d62ba24eac9f58da1be3a3c6eb4357253 Mon Sep 17 00:00:00 2001 From: Arseniy Klempner Date: Wed, 17 Dec 2025 18:51:10 -0800 Subject: [PATCH 036/155] fix: hash inputs for external nullifier, remove length prefix for sha256 (#3660) * fix: hash inputs for external nullifier, remove length prefix for sha256 * feat: use nimcrypto keccak instead of sha256 ffi * feat: wrapper function to generate external nullifier --- tests/waku_rln_relay/test_waku_rln_relay.nim | 47 ------------------- .../group_manager/on_chain/group_manager.nim | 7 ++- waku/waku_rln_relay/rln/wrappers.nim | 34 +++++--------- 3 files changed, 16 insertions(+), 72 deletions(-) diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index ea3a5ca62..3430657ad 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -70,53 +70,6 @@ suite "Waku rln relay": info "the generated identity credential: ", idCredential - test "hash Nim Wrappers": - # create an RLN instance - let rlnInstance = createRLNInstanceWrapper() - require: - rlnInstance.isOk() - - # prepare the input - let - msg = "Hello".toBytes() - hashInput = encodeLengthPrefix(msg) - hashInputBuffer = toBuffer(hashInput) - - # prepare other inputs to the hash function - let outputBuffer = default(Buffer) - - let hashSuccess = sha256(unsafeAddr hashInputBuffer, unsafeAddr outputBuffer, true) - require: - hashSuccess - let outputArr = cast[ptr array[32, byte]](outputBuffer.`ptr`)[] - - check: - "1e32b3ab545c07c8b4a7ab1ca4f46bc31e4fdc29ac3b240ef1d54b4017a26e4c" == - outputArr.inHex() - - let - hashOutput = cast[ptr array[32, byte]](outputBuffer.`ptr`)[] - hashOutputHex = hashOutput.toHex() - - info "hash output", hashOutputHex - - test "sha256 hash utils": - # create an RLN instance - let rlnInstance = createRLNInstanceWrapper() - require: - rlnInstance.isOk() - let rln = rlnInstance.get() - - # prepare the input - let msg = "Hello".toBytes() - - let hashRes = sha256(msg) - - check: - hashRes.isOk() - "1e32b3ab545c07c8b4a7ab1ca4f46bc31e4fdc29ac3b240ef1d54b4017a26e4c" == - hashRes.get().inHex() - test "poseidon hash utils": # create an RLN instance let rlnInstance = createRLNInstanceWrapper() 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 bdb272c1f..2ce7d4423 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 @@ -379,7 +379,7 @@ method generateProof*( let x = keccak.keccak256.digest(data) - let extNullifier = poseidon(@[@(epoch), @(rlnIdentifier)]).valueOr: + let extNullifier = generateExternalNullifier(epoch, rlnIdentifier).valueOr: return err("Failed to compute external nullifier: " & error) let witness = RLNWitnessInput( @@ -457,10 +457,9 @@ method verifyProof*( var normalizedProof = proof - normalizedProof.externalNullifier = poseidon( - @[@(proof.epoch), @(proof.rlnIdentifier)] - ).valueOr: + let externalNullifier = generateExternalNullifier(proof.epoch, proof.rlnIdentifier).valueOr: return err("Failed to compute external nullifier: " & error) + normalizedProof.externalNullifier = externalNullifier let proofBytes = serialize(normalizedProof, input) let proofBuffer = proofBytes.toBuffer() diff --git a/waku/waku_rln_relay/rln/wrappers.nim b/waku/waku_rln_relay/rln/wrappers.nim index d1dec2b38..1b2b0270f 100644 --- a/waku/waku_rln_relay/rln/wrappers.nim +++ b/waku/waku_rln_relay/rln/wrappers.nim @@ -6,7 +6,8 @@ import stew/[arrayops, byteutils, endians2], stint, results, - std/[sequtils, strutils, tables] + std/[sequtils, strutils, tables], + nimcrypto/keccak as keccak import ./rln_interface, ../conversion_utils, ../protocol_types, ../protocol_metrics import ../../waku_core, ../../waku_keystore @@ -119,24 +120,6 @@ proc createRLNInstance*(): RLNResult = res = createRLNInstanceLocal() return res -proc sha256*(data: openArray[byte]): RlnRelayResult[MerkleNode] = - ## a thin layer on top of the Nim wrapper of the sha256 hasher - var lenPrefData = encodeLengthPrefix(data) - var - hashInputBuffer = lenPrefData.toBuffer() - outputBuffer: Buffer # will holds the hash output - - trace "sha256 hash input buffer length", bufflen = hashInputBuffer.len - let hashSuccess = sha256(addr hashInputBuffer, addr outputBuffer, true) - - # check whether the hash call is done successfully - if not hashSuccess: - return err("error in sha256 hash") - - let output = cast[ptr MerkleNode](outputBuffer.`ptr`)[] - - return ok(output) - proc poseidon*(data: seq[seq[byte]]): RlnRelayResult[array[32, byte]] = ## a thin layer on top of the Nim wrapper of the poseidon hasher var inputBytes = serialize(data) @@ -180,9 +163,18 @@ proc toLeaves*(rateCommitments: seq[RateCommitment]): RlnRelayResult[seq[seq[byt leaves.add(leaf) return ok(leaves) +proc generateExternalNullifier*( + epoch: Epoch, rlnIdentifier: RlnIdentifier +): RlnRelayResult[ExternalNullifier] = + let epochHash = keccak.keccak256.digest(@(epoch)) + let rlnIdentifierHash = keccak.keccak256.digest(@(rlnIdentifier)) + let externalNullifier = poseidon(@[@(epochHash), @(rlnIdentifierHash)]).valueOr: + return err("Failed to compute external nullifier: " & error) + return ok(externalNullifier) + proc extractMetadata*(proof: RateLimitProof): RlnRelayResult[ProofMetadata] = - let externalNullifier = poseidon(@[@(proof.epoch), @(proof.rlnIdentifier)]).valueOr: - return err("could not construct the external nullifier") + let externalNullifier = generateExternalNullifier(proof.epoch, proof.rlnIdentifier).valueOr: + return err("Failed to compute external nullifier: " & error) return ok( ProofMetadata( nullifier: proof.nullifier, From 834eea945d05b4092466f3953467b28467e6b24c Mon Sep 17 00:00:00 2001 From: Tanya S <120410716+stubbsta@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:55:53 +0200 Subject: [PATCH 037/155] chore: pin rln dependencies to specific version (#3649) * Add foundry version in makefile and install scripts * revert to older verison of Anvil for rln tests and anvil_install fix * pin pnpm version to be installed as rln dep * source pnpm after new install * Add to github path * use npm to install pnpm for rln ci * Update foundry and pnpm versions in Makefile --- Makefile | 6 ++- scripts/install_anvil.sh | 47 ++++++++++++++++++++--- scripts/install_pnpm.sh | 35 +++++++++++++++-- scripts/install_rln_tests_dependencies.sh | 8 ++-- 4 files changed, 84 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 44f1c6495..35c107d2d 100644 --- a/Makefile +++ b/Makefile @@ -119,6 +119,10 @@ endif ################## .PHONY: deps libbacktrace +FOUNDRY_VERSION := 1.5.0 +PNPM_VERSION := 10.23.0 + + rustup: ifeq (, $(shell which cargo)) # Install Rustup if it's not installed @@ -128,7 +132,7 @@ ifeq (, $(shell which cargo)) endif rln-deps: rustup - ./scripts/install_rln_tests_dependencies.sh + ./scripts/install_rln_tests_dependencies.sh $(FOUNDRY_VERSION) $(PNPM_VERSION) deps: | deps-common nat-libs waku.nims diff --git a/scripts/install_anvil.sh b/scripts/install_anvil.sh index 1bf4bd7b1..c573ac31c 100755 --- a/scripts/install_anvil.sh +++ b/scripts/install_anvil.sh @@ -2,14 +2,51 @@ # Install Anvil -if ! command -v anvil &> /dev/null; then +REQUIRED_FOUNDRY_VERSION="$1" + +if command -v anvil &> /dev/null; then + # Foundry is already installed; check the current version. + CURRENT_FOUNDRY_VERSION=$(anvil --version 2>/dev/null | awk '{print $2}') + + if [ -n "$CURRENT_FOUNDRY_VERSION" ]; then + # Compare CURRENT_FOUNDRY_VERSION < REQUIRED_FOUNDRY_VERSION using sort -V + lower_version=$(printf '%s\n%s\n' "$CURRENT_FOUNDRY_VERSION" "$REQUIRED_FOUNDRY_VERSION" | sort -V | head -n1) + + if [ "$lower_version" != "$REQUIRED_FOUNDRY_VERSION" ]; then + echo "Anvil is already installed with version $CURRENT_FOUNDRY_VERSION, which is older than the required $REQUIRED_FOUNDRY_VERSION. Please update Foundry manually if needed." + fi + fi +else BASE_DIR="${XDG_CONFIG_HOME:-$HOME}" FOUNDRY_DIR="${FOUNDRY_DIR:-"$BASE_DIR/.foundry"}" FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin" + echo "Installing Foundry..." curl -L https://foundry.paradigm.xyz | bash - # Extract the source path from the download result - echo "foundryup_path: $FOUNDRY_BIN_DIR" - # run foundryup - $FOUNDRY_BIN_DIR/foundryup + + # Add Foundry to PATH for this script session + export PATH="$FOUNDRY_BIN_DIR:$PATH" + + # Verify foundryup is available + if ! command -v foundryup >/dev/null 2>&1; then + echo "Error: foundryup installation failed or not found in $FOUNDRY_BIN_DIR" + exit 1 + fi + + # Run foundryup to install the required version + if [ -n "$REQUIRED_FOUNDRY_VERSION" ]; then + echo "Installing Foundry tools version $REQUIRED_FOUNDRY_VERSION..." + foundryup --install "$REQUIRED_FOUNDRY_VERSION" + else + echo "Installing latest Foundry tools..." + foundryup + fi + + # Verify anvil was installed + if ! command -v anvil >/dev/null 2>&1; then + echo "Error: anvil installation failed" + exit 1 + fi + + echo "Anvil successfully installed: $(anvil --version)" fi \ No newline at end of file diff --git a/scripts/install_pnpm.sh b/scripts/install_pnpm.sh index 34ba47b07..fcfc82ccd 100755 --- a/scripts/install_pnpm.sh +++ b/scripts/install_pnpm.sh @@ -1,8 +1,37 @@ #!/usr/bin/env bash # Install pnpm -if ! command -v pnpm &> /dev/null; then - echo "pnpm is not installed, installing it now..." - npm i pnpm --global + +REQUIRED_PNPM_VERSION="$1" + +if command -v pnpm &> /dev/null; then + # pnpm is already installed; check the current version. + CURRENT_PNPM_VERSION=$(pnpm --version 2>/dev/null) + + if [ -n "$CURRENT_PNPM_VERSION" ]; then + # Compare CURRENT_PNPM_VERSION < REQUIRED_PNPM_VERSION using sort -V + lower_version=$(printf '%s\n%s\n' "$CURRENT_PNPM_VERSION" "$REQUIRED_PNPM_VERSION" | sort -V | head -n1) + + if [ "$lower_version" != "$REQUIRED_PNPM_VERSION" ]; then + echo "pnpm is already installed with version $CURRENT_PNPM_VERSION, which is older than the required $REQUIRED_PNPM_VERSION. Please update pnpm manually if needed." + fi + fi +else + # Install pnpm using npm + if [ -n "$REQUIRED_PNPM_VERSION" ]; then + echo "Installing pnpm version $REQUIRED_PNPM_VERSION..." + npm install -g pnpm@$REQUIRED_PNPM_VERSION + else + echo "Installing latest pnpm..." + npm install -g pnpm + fi + + # Verify pnpm was installed + if ! command -v pnpm >/dev/null 2>&1; then + echo "Error: pnpm installation failed" + exit 1 + fi + + echo "pnpm successfully installed: $(pnpm --version)" fi diff --git a/scripts/install_rln_tests_dependencies.sh b/scripts/install_rln_tests_dependencies.sh index e19e0ef3c..c8c083b54 100755 --- a/scripts/install_rln_tests_dependencies.sh +++ b/scripts/install_rln_tests_dependencies.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash # Install Anvil -./scripts/install_anvil.sh +FOUNDRY_VERSION="$1" +./scripts/install_anvil.sh "$FOUNDRY_VERSION" -#Install pnpm -./scripts/install_pnpm.sh \ No newline at end of file +# Install pnpm +PNPM_VERSION="$2" +./scripts/install_pnpm.sh "$PNPM_VERSION" \ No newline at end of file From e3dd6203ae8fc291fb9f83351252887e5fc6b328 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Fri, 19 Dec 2025 17:00:43 +0100 Subject: [PATCH 038/155] Start using nim-ffi to implement libwaku (#3656) * deep changes in libwaku to adap to nim-ffi * start using ffi pragma in library * update some binding examples * add missing declare_lib.nim file * properly rename api files in library folder --- .gitmodules | 5 + examples/cbindings/waku_example.c | 520 ++++++----- examples/cpp/waku.cpp | 283 +++--- examples/golang/waku.go | 107 ++- examples/python/waku.py | 24 +- examples/qt/waku_handler.h | 2 +- examples/rust/src/main.rs | 12 +- library/alloc.nim | 42 - library/declare_lib.nim | 10 + .../events/json_waku_not_responding_event.nim | 9 - library/ffi_types.nim | 30 - library/kernel_api/debug_node_api.nim | 49 + library/kernel_api/discovery_api.nim | 96 ++ .../node_lifecycle_api.nim} | 79 +- library/kernel_api/peer_manager_api.nim | 123 +++ library/kernel_api/ping_api.nim | 43 + library/kernel_api/protocols/filter_api.nim | 109 +++ .../kernel_api/protocols/lightpush_api.nim | 51 ++ library/kernel_api/protocols/relay_api.nim | 171 ++++ .../protocols/store_api.nim} | 79 +- library/libwaku.h | 379 ++++---- library/libwaku.nim | 853 +----------------- library/waku_context.nim | 223 ----- .../requests/debug_node_request.nim | 63 -- .../requests/discovery_request.nim | 151 ---- .../requests/peer_manager_request.nim | 135 --- .../requests/ping_request.nim | 54 -- .../requests/protocols/filter_request.nim | 106 --- .../requests/protocols/lightpush_request.nim | 109 --- .../requests/protocols/relay_request.nim | 168 ---- .../waku_thread_request.nim | 104 --- vendor/nim-ffi | 1 + waku.nimble | 3 +- 33 files changed, 1441 insertions(+), 2752 deletions(-) delete mode 100644 library/alloc.nim create mode 100644 library/declare_lib.nim delete mode 100644 library/events/json_waku_not_responding_event.nim delete mode 100644 library/ffi_types.nim create mode 100644 library/kernel_api/debug_node_api.nim create mode 100644 library/kernel_api/discovery_api.nim rename library/{waku_thread_requests/requests/node_lifecycle_request.nim => kernel_api/node_lifecycle_api.nim} (60%) create mode 100644 library/kernel_api/peer_manager_api.nim create mode 100644 library/kernel_api/ping_api.nim create mode 100644 library/kernel_api/protocols/filter_api.nim create mode 100644 library/kernel_api/protocols/lightpush_api.nim create mode 100644 library/kernel_api/protocols/relay_api.nim rename library/{waku_thread_requests/requests/protocols/store_request.nim => kernel_api/protocols/store_api.nim} (57%) delete mode 100644 library/waku_context.nim delete mode 100644 library/waku_thread_requests/requests/debug_node_request.nim delete mode 100644 library/waku_thread_requests/requests/discovery_request.nim delete mode 100644 library/waku_thread_requests/requests/peer_manager_request.nim delete mode 100644 library/waku_thread_requests/requests/ping_request.nim delete mode 100644 library/waku_thread_requests/requests/protocols/filter_request.nim delete mode 100644 library/waku_thread_requests/requests/protocols/lightpush_request.nim delete mode 100644 library/waku_thread_requests/requests/protocols/relay_request.nim delete mode 100644 library/waku_thread_requests/waku_thread_request.nim create mode 160000 vendor/nim-ffi diff --git a/.gitmodules b/.gitmodules index 93a3a006f..4d56c4333 100644 --- a/.gitmodules +++ b/.gitmodules @@ -184,3 +184,8 @@ url = https://github.com/logos-messaging/waku-rlnv2-contract.git ignore = untracked branch = master +[submodule "vendor/nim-ffi"] + path = vendor/nim-ffi + url = https://github.com/logos-messaging/nim-ffi/ + ignore = untracked + branch = master diff --git a/examples/cbindings/waku_example.c b/examples/cbindings/waku_example.c index 35ac8a2e2..f337203ae 100644 --- a/examples/cbindings/waku_example.c +++ b/examples/cbindings/waku_example.c @@ -19,283 +19,309 @@ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int callback_executed = 0; -void waitForCallback() { - pthread_mutex_lock(&mutex); - while (!callback_executed) { - pthread_cond_wait(&cond, &mutex); - } - callback_executed = 0; - pthread_mutex_unlock(&mutex); +void waitForCallback() +{ + pthread_mutex_lock(&mutex); + while (!callback_executed) + { + pthread_cond_wait(&cond, &mutex); + } + callback_executed = 0; + pthread_mutex_unlock(&mutex); } -#define WAKU_CALL(call) \ -do { \ - int ret = call; \ - if (ret != 0) { \ - printf("Failed the call to: %s. Returned code: %d\n", #call, ret); \ - exit(1); \ - } \ - waitForCallback(); \ -} while (0) +#define WAKU_CALL(call) \ + do \ + { \ + int ret = call; \ + if (ret != 0) \ + { \ + printf("Failed the call to: %s. Returned code: %d\n", #call, ret); \ + exit(1); \ + } \ + waitForCallback(); \ + } while (0) -struct ConfigNode { - char host[128]; - int port; - char key[128]; - int relay; - char peers[2048]; - int store; - char storeNode[2048]; - char storeRetentionPolicy[64]; - char storeDbUrl[256]; - int storeVacuum; - int storeDbMigration; - int storeMaxNumDbConnections; +struct ConfigNode +{ + char host[128]; + int port; + char key[128]; + int relay; + char peers[2048]; + int store; + char storeNode[2048]; + char storeRetentionPolicy[64]; + char storeDbUrl[256]; + int storeVacuum; + int storeDbMigration; + int storeMaxNumDbConnections; }; // libwaku Context -void* ctx; +void *ctx; // For the case of C language we don't need to store a particular userData -void* userData = NULL; +void *userData = NULL; // Arguments parsing static char doc[] = "\nC example that shows how to use the waku library."; static char args_doc[] = ""; static struct argp_option options[] = { - { "host", 'h', "HOST", 0, "IP to listen for for LibP2P traffic. (default: \"0.0.0.0\")"}, - { "port", 'p', "PORT", 0, "TCP listening port. (default: \"60000\")"}, - { "key", 'k', "KEY", 0, "P2P node private key as 64 char hex string."}, - { "relay", 'r', "RELAY", 0, "Enable relay protocol: 1 or 0. (default: 1)"}, - { "peers", 'a', "PEERS", 0, "Comma-separated list of peer-multiaddress to connect\ + {"host", 'h', "HOST", 0, "IP to listen for for LibP2P traffic. (default: \"0.0.0.0\")"}, + {"port", 'p', "PORT", 0, "TCP listening port. (default: \"60000\")"}, + {"key", 'k', "KEY", 0, "P2P node private key as 64 char hex string."}, + {"relay", 'r', "RELAY", 0, "Enable relay protocol: 1 or 0. (default: 1)"}, + {"peers", 'a', "PEERS", 0, "Comma-separated list of peer-multiaddress to connect\ to. (default: \"\") e.g. \"/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\""}, - { 0 } -}; + {0}}; -static error_t parse_opt(int key, char *arg, struct argp_state *state) { +static error_t parse_opt(int key, char *arg, struct argp_state *state) +{ - struct ConfigNode *cfgNode = state->input; - switch (key) { - case 'h': - snprintf(cfgNode->host, 128, "%s", arg); - break; - case 'p': - cfgNode->port = atoi(arg); - break; - case 'k': - snprintf(cfgNode->key, 128, "%s", arg); - break; - case 'r': - cfgNode->relay = atoi(arg); - break; - case 'a': - snprintf(cfgNode->peers, 2048, "%s", arg); - break; - case ARGP_KEY_ARG: - if (state->arg_num >= 1) /* Too many arguments. */ - argp_usage(state); - break; - case ARGP_KEY_END: - break; - default: - return ARGP_ERR_UNKNOWN; - } + struct ConfigNode *cfgNode = state->input; + switch (key) + { + case 'h': + snprintf(cfgNode->host, 128, "%s", arg); + break; + case 'p': + cfgNode->port = atoi(arg); + break; + case 'k': + snprintf(cfgNode->key, 128, "%s", arg); + break; + case 'r': + cfgNode->relay = atoi(arg); + break; + case 'a': + snprintf(cfgNode->peers, 2048, "%s", arg); + break; + case ARGP_KEY_ARG: + if (state->arg_num >= 1) /* Too many arguments. */ + argp_usage(state); + break; + case ARGP_KEY_END: + break; + default: + return ARGP_ERR_UNKNOWN; + } - return 0; + return 0; } -void signal_cond() { - pthread_mutex_lock(&mutex); - callback_executed = 1; - pthread_cond_signal(&cond); - pthread_mutex_unlock(&mutex); +void signal_cond() +{ + pthread_mutex_lock(&mutex); + callback_executed = 1; + pthread_cond_signal(&cond); + pthread_mutex_unlock(&mutex); } -static struct argp argp = { options, parse_opt, args_doc, doc, 0, 0, 0 }; +static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0}; -void event_handler(int callerRet, const char* msg, size_t len, void* userData) { - if (callerRet == RET_ERR) { - printf("Error: %s\n", msg); - exit(1); - } - else if (callerRet == RET_OK) { - printf("Receiving event: %s\n", msg); - } +void event_handler(int callerRet, const char *msg, size_t len, void *userData) +{ + if (callerRet == RET_ERR) + { + printf("Error: %s\n", msg); + exit(1); + } + else if (callerRet == RET_OK) + { + printf("Receiving event: %s\n", msg); + } - signal_cond(); + signal_cond(); } -void on_event_received(int callerRet, const char* msg, size_t len, void* userData) { - if (callerRet == RET_ERR) { - printf("Error: %s\n", msg); - exit(1); - } - else if (callerRet == RET_OK) { - printf("Receiving event: %s\n", msg); - } +void on_event_received(int callerRet, const char *msg, size_t len, void *userData) +{ + if (callerRet == RET_ERR) + { + printf("Error: %s\n", msg); + exit(1); + } + else if (callerRet == RET_OK) + { + printf("Receiving event: %s\n", msg); + } } -char* contentTopic = NULL; -void handle_content_topic(int callerRet, const char* msg, size_t len, void* userData) { - if (contentTopic != NULL) { - free(contentTopic); - } +char *contentTopic = NULL; +void handle_content_topic(int callerRet, const char *msg, size_t len, void *userData) +{ + if (contentTopic != NULL) + { + free(contentTopic); + } - contentTopic = malloc(len * sizeof(char) + 1); - strcpy(contentTopic, msg); - signal_cond(); + contentTopic = malloc(len * sizeof(char) + 1); + strcpy(contentTopic, msg); + signal_cond(); } -char* publishResponse = NULL; -void handle_publish_ok(int callerRet, const char* msg, size_t len, void* userData) { - printf("Publish Ok: %s %lu\n", msg, len); +char *publishResponse = NULL; +void handle_publish_ok(int callerRet, const char *msg, size_t len, void *userData) +{ + printf("Publish Ok: %s %lu\n", msg, len); - if (publishResponse != NULL) { - free(publishResponse); - } + if (publishResponse != NULL) + { + free(publishResponse); + } - publishResponse = malloc(len * sizeof(char) + 1); - strcpy(publishResponse, msg); + publishResponse = malloc(len * sizeof(char) + 1); + strcpy(publishResponse, msg); } #define MAX_MSG_SIZE 65535 -void publish_message(const char* msg) { - char jsonWakuMsg[MAX_MSG_SIZE]; - char *msgPayload = b64_encode(msg, strlen(msg)); +void publish_message(const char *msg) +{ + char jsonWakuMsg[MAX_MSG_SIZE]; + char *msgPayload = b64_encode(msg, strlen(msg)); - WAKU_CALL( waku_content_topic(ctx, - "appName", - 1, - "contentTopicName", - "encoding", - handle_content_topic, - userData) ); - snprintf(jsonWakuMsg, - MAX_MSG_SIZE, - "{\"payload\":\"%s\",\"contentTopic\":\"%s\"}", - msgPayload, contentTopic); + WAKU_CALL(waku_content_topic(ctx, + handle_content_topic, + userData, + "appName", + 1, + "contentTopicName", + "encoding")); + snprintf(jsonWakuMsg, + MAX_MSG_SIZE, + "{\"payload\":\"%s\",\"contentTopic\":\"%s\"}", + msgPayload, contentTopic); - free(msgPayload); + free(msgPayload); - WAKU_CALL( waku_relay_publish(ctx, - "/waku/2/rs/16/32", - jsonWakuMsg, - 10000 /*timeout ms*/, - event_handler, - userData) ); + WAKU_CALL(waku_relay_publish(ctx, + event_handler, + userData, + "/waku/2/rs/16/32", + jsonWakuMsg, + 10000 /*timeout ms*/)); } -void show_help_and_exit() { - printf("Wrong parameters\n"); - exit(1); +void show_help_and_exit() +{ + printf("Wrong parameters\n"); + exit(1); } -void print_default_pubsub_topic(int callerRet, const char* msg, size_t len, void* userData) { - printf("Default pubsub topic: %s\n", msg); - signal_cond(); +void print_default_pubsub_topic(int callerRet, const char *msg, size_t len, void *userData) +{ + printf("Default pubsub topic: %s\n", msg); + signal_cond(); } -void print_waku_version(int callerRet, const char* msg, size_t len, void* userData) { - printf("Git Version: %s\n", msg); - signal_cond(); +void print_waku_version(int callerRet, const char *msg, size_t len, void *userData) +{ + printf("Git Version: %s\n", msg); + signal_cond(); } // Beginning of UI program logic -enum PROGRAM_STATE { - MAIN_MENU, - SUBSCRIBE_TOPIC_MENU, - CONNECT_TO_OTHER_NODE_MENU, - PUBLISH_MESSAGE_MENU +enum PROGRAM_STATE +{ + MAIN_MENU, + SUBSCRIBE_TOPIC_MENU, + CONNECT_TO_OTHER_NODE_MENU, + PUBLISH_MESSAGE_MENU }; enum PROGRAM_STATE current_state = MAIN_MENU; -void show_main_menu() { - printf("\nPlease, select an option:\n"); - printf("\t1.) Subscribe to topic\n"); - printf("\t2.) Connect to other node\n"); - printf("\t3.) Publish a message\n"); +void show_main_menu() +{ + printf("\nPlease, select an option:\n"); + printf("\t1.) Subscribe to topic\n"); + printf("\t2.) Connect to other node\n"); + printf("\t3.) Publish a message\n"); } -void handle_user_input() { - char cmd[1024]; - memset(cmd, 0, 1024); - int numRead = read(0, cmd, 1024); - if (numRead <= 0) { - return; - } +void handle_user_input() +{ + char cmd[1024]; + memset(cmd, 0, 1024); + int numRead = read(0, cmd, 1024); + if (numRead <= 0) + { + return; + } - switch (atoi(cmd)) - { - case SUBSCRIBE_TOPIC_MENU: - { - printf("Indicate the Pubsubtopic to subscribe:\n"); - char pubsubTopic[128]; - scanf("%127s", pubsubTopic); + switch (atoi(cmd)) + { + case SUBSCRIBE_TOPIC_MENU: + { + printf("Indicate the Pubsubtopic to subscribe:\n"); + char pubsubTopic[128]; + scanf("%127s", pubsubTopic); - WAKU_CALL( waku_relay_subscribe(ctx, - pubsubTopic, - event_handler, - userData) ); - printf("The subscription went well\n"); + WAKU_CALL(waku_relay_subscribe(ctx, + event_handler, + userData, + pubsubTopic)); + printf("The subscription went well\n"); - show_main_menu(); - } + show_main_menu(); + } + break; + + case CONNECT_TO_OTHER_NODE_MENU: + // printf("Connecting to a node. Please indicate the peer Multiaddress:\n"); + // printf("e.g.: /ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\n"); + // char peerAddr[512]; + // scanf("%511s", peerAddr); + // WAKU_CALL(waku_connect(ctx, peerAddr, 10000 /* timeoutMs */, event_handler, userData)); + show_main_menu(); break; - case CONNECT_TO_OTHER_NODE_MENU: - printf("Connecting to a node. Please indicate the peer Multiaddress:\n"); - printf("e.g.: /ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\n"); - char peerAddr[512]; - scanf("%511s", peerAddr); - WAKU_CALL(waku_connect(ctx, peerAddr, 10000 /* timeoutMs */, event_handler, userData)); - show_main_menu(); + case PUBLISH_MESSAGE_MENU: + { + printf("Type the message to publish:\n"); + char msg[1024]; + scanf("%1023s", msg); + + publish_message(msg); + + show_main_menu(); + } + break; + + case MAIN_MENU: break; - - case PUBLISH_MESSAGE_MENU: - { - printf("Type the message to publish:\n"); - char msg[1024]; - scanf("%1023s", msg); - - publish_message(msg); - - show_main_menu(); - } - break; - - case MAIN_MENU: - break; - } + } } // End of UI program logic -int main(int argc, char** argv) { - struct ConfigNode cfgNode; - // default values - snprintf(cfgNode.host, 128, "0.0.0.0"); - cfgNode.port = 60000; - cfgNode.relay = 1; +int main(int argc, char **argv) +{ + struct ConfigNode cfgNode; + // default values + snprintf(cfgNode.host, 128, "0.0.0.0"); + cfgNode.port = 60000; + cfgNode.relay = 1; - cfgNode.store = 0; - snprintf(cfgNode.storeNode, 2048, ""); - snprintf(cfgNode.storeRetentionPolicy, 64, "time:6000000"); - snprintf(cfgNode.storeDbUrl, 256, "postgres://postgres:test123@localhost:5432/postgres"); - cfgNode.storeVacuum = 0; - cfgNode.storeDbMigration = 0; - cfgNode.storeMaxNumDbConnections = 30; + cfgNode.store = 0; + snprintf(cfgNode.storeNode, 2048, ""); + snprintf(cfgNode.storeRetentionPolicy, 64, "time:6000000"); + snprintf(cfgNode.storeDbUrl, 256, "postgres://postgres:test123@localhost:5432/postgres"); + cfgNode.storeVacuum = 0; + cfgNode.storeDbMigration = 0; + cfgNode.storeMaxNumDbConnections = 30; - if (argp_parse(&argp, argc, argv, 0, 0, &cfgNode) - == ARGP_ERR_UNKNOWN) { - show_help_and_exit(); - } + if (argp_parse(&argp, argc, argv, 0, 0, &cfgNode) == ARGP_ERR_UNKNOWN) + { + show_help_and_exit(); + } - char jsonConfig[5000]; - snprintf(jsonConfig, 5000, "{ \ + char jsonConfig[5000]; + snprintf(jsonConfig, 5000, "{ \ \"clusterId\": 16, \ \"shards\": [ 1, 32, 64, 128, 256 ], \ \"numShardsInNetwork\": 257, \ @@ -313,54 +339,56 @@ int main(int argc, char** argv) { \"discv5UdpPort\": 9999, \ \"dnsDiscoveryUrl\": \"enrtree://AMOJVZX4V6EXP7NTJPMAYJYST2QP6AJXYW76IU6VGJS7UVSNDYZG4@boot.prod.status.nodes.status.im\", \ \"dnsDiscoveryNameServers\": [\"8.8.8.8\", \"1.0.0.1\"] \ - }", cfgNode.host, - cfgNode.port, - cfgNode.relay ? "true":"false", - cfgNode.store ? "true":"false", - cfgNode.storeDbUrl, - cfgNode.storeRetentionPolicy, - cfgNode.storeMaxNumDbConnections); + }", + cfgNode.host, + cfgNode.port, + cfgNode.relay ? "true" : "false", + cfgNode.store ? "true" : "false", + cfgNode.storeDbUrl, + cfgNode.storeRetentionPolicy, + cfgNode.storeMaxNumDbConnections); - ctx = waku_new(jsonConfig, event_handler, userData); - waitForCallback(); + ctx = waku_new(jsonConfig, event_handler, userData); + waitForCallback(); - WAKU_CALL( waku_default_pubsub_topic(ctx, print_default_pubsub_topic, userData) ); - WAKU_CALL( waku_version(ctx, print_waku_version, userData) ); + WAKU_CALL(waku_default_pubsub_topic(ctx, print_default_pubsub_topic, userData)); + WAKU_CALL(waku_version(ctx, print_waku_version, userData)); - printf("Bind addr: %s:%u\n", cfgNode.host, cfgNode.port); - printf("Waku Relay enabled: %s\n", cfgNode.relay == 1 ? "YES": "NO"); + printf("Bind addr: %s:%u\n", cfgNode.host, cfgNode.port); + printf("Waku Relay enabled: %s\n", cfgNode.relay == 1 ? "YES" : "NO"); - waku_set_event_callback(ctx, on_event_received, userData); + set_event_callback(ctx, on_event_received, userData); - waku_start(ctx, event_handler, userData); - waitForCallback(); + waku_start(ctx, event_handler, userData); + waitForCallback(); - WAKU_CALL( waku_listen_addresses(ctx, event_handler, userData) ); + WAKU_CALL(waku_listen_addresses(ctx, event_handler, userData)); - WAKU_CALL( waku_relay_subscribe(ctx, - "/waku/2/rs/0/0", - event_handler, - userData) ); + WAKU_CALL(waku_relay_subscribe(ctx, + event_handler, + userData, + "/waku/2/rs/16/32")); - WAKU_CALL( waku_discv5_update_bootnodes(ctx, - "[\"enr:-QEkuEBIkb8q8_mrorHndoXH9t5N6ZfD-jehQCrYeoJDPHqT0l0wyaONa2-piRQsi3oVKAzDShDVeoQhy0uwN1xbZfPZAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQKnGt-GSgqPSf3IAPM7bFgTlpczpMZZLF3geeoNNsxzSoN0Y3CCdl-DdWRwgiMohXdha3UyDw\",\"enr:-QEkuEB3WHNS-xA3RDpfu9A2Qycr3bN3u7VoArMEiDIFZJ66F1EB3d4wxZN1hcdcOX-RfuXB-MQauhJGQbpz3qUofOtLAYJpZIJ2NIJpcIQI2SVcim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQPK35Nnz0cWUtSAhBp7zvHEhyU_AqeQUlqzLiLxfP2L4oN0Y3CCdl-DdWRwgiMohXdha3UyDw\"]", - event_handler, - userData) ); + WAKU_CALL(waku_discv5_update_bootnodes(ctx, + event_handler, + userData, + "[\"enr:-QEkuEBIkb8q8_mrorHndoXH9t5N6ZfD-jehQCrYeoJDPHqT0l0wyaONa2-piRQsi3oVKAzDShDVeoQhy0uwN1xbZfPZAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQKnGt-GSgqPSf3IAPM7bFgTlpczpMZZLF3geeoNNsxzSoN0Y3CCdl-DdWRwgiMohXdha3UyDw\",\"enr:-QEkuEB3WHNS-xA3RDpfu9A2Qycr3bN3u7VoArMEiDIFZJ66F1EB3d4wxZN1hcdcOX-RfuXB-MQauhJGQbpz3qUofOtLAYJpZIJ2NIJpcIQI2SVcim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQPK35Nnz0cWUtSAhBp7zvHEhyU_AqeQUlqzLiLxfP2L4oN0Y3CCdl-DdWRwgiMohXdha3UyDw\"]")); - WAKU_CALL( waku_get_peerids_from_peerstore(ctx, - event_handler, - userData) ); + WAKU_CALL(waku_get_peerids_from_peerstore(ctx, + event_handler, + userData)); - show_main_menu(); - while(1) { - handle_user_input(); + show_main_menu(); + while (1) + { + handle_user_input(); - // Uncomment the following if need to test the metrics retrieval - // WAKU_CALL( waku_get_metrics(ctx, - // event_handler, - // userData) ); - } + // Uncomment the following if need to test the metrics retrieval + // WAKU_CALL( waku_get_metrics(ctx, + // event_handler, + // userData) ); + } - pthread_mutex_destroy(&mutex); - pthread_cond_destroy(&cond); + pthread_mutex_destroy(&mutex); + pthread_cond_destroy(&cond); } diff --git a/examples/cpp/waku.cpp b/examples/cpp/waku.cpp index c47877d02..2824f8e53 100644 --- a/examples/cpp/waku.cpp +++ b/examples/cpp/waku.cpp @@ -21,37 +21,43 @@ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int callback_executed = 0; -void waitForCallback() { +void waitForCallback() +{ pthread_mutex_lock(&mutex); - while (!callback_executed) { + while (!callback_executed) + { pthread_cond_wait(&cond, &mutex); } callback_executed = 0; pthread_mutex_unlock(&mutex); } -void signal_cond() { +void signal_cond() +{ pthread_mutex_lock(&mutex); callback_executed = 1; pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); } -#define WAKU_CALL(call) \ -do { \ - int ret = call; \ - if (ret != 0) { \ - std::cout << "Failed the call to: " << #call << ". Code: " << ret << "\n"; \ - } \ - waitForCallback(); \ -} while (0) +#define WAKU_CALL(call) \ + do \ + { \ + int ret = call; \ + if (ret != 0) \ + { \ + std::cout << "Failed the call to: " << #call << ". Code: " << ret << "\n"; \ + } \ + waitForCallback(); \ + } while (0) -struct ConfigNode { - char host[128]; - int port; - char key[128]; - int relay; - char peers[2048]; +struct ConfigNode +{ + char host[128]; + int port; + char key[128]; + int relay; + char peers[2048]; }; // Arguments parsing @@ -59,70 +65,76 @@ static char doc[] = "\nC example that shows how to use the waku library."; static char args_doc[] = ""; static struct argp_option options[] = { - { "host", 'h', "HOST", 0, "IP to listen for for LibP2P traffic. (default: \"0.0.0.0\")"}, - { "port", 'p', "PORT", 0, "TCP listening port. (default: \"60000\")"}, - { "key", 'k', "KEY", 0, "P2P node private key as 64 char hex string."}, - { "relay", 'r', "RELAY", 0, "Enable relay protocol: 1 or 0. (default: 1)"}, - { "peers", 'a', "PEERS", 0, "Comma-separated list of peer-multiaddress to connect\ + {"host", 'h', "HOST", 0, "IP to listen for for LibP2P traffic. (default: \"0.0.0.0\")"}, + {"port", 'p', "PORT", 0, "TCP listening port. (default: \"60000\")"}, + {"key", 'k', "KEY", 0, "P2P node private key as 64 char hex string."}, + {"relay", 'r', "RELAY", 0, "Enable relay protocol: 1 or 0. (default: 1)"}, + {"peers", 'a', "PEERS", 0, "Comma-separated list of peer-multiaddress to connect\ to. (default: \"\") e.g. \"/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\""}, - { 0 } -}; + {0}}; -static error_t parse_opt(int key, char *arg, struct argp_state *state) { +static error_t parse_opt(int key, char *arg, struct argp_state *state) +{ - struct ConfigNode *cfgNode = (ConfigNode *) state->input; - switch (key) { - case 'h': - snprintf(cfgNode->host, 128, "%s", arg); - break; - case 'p': - cfgNode->port = atoi(arg); - break; - case 'k': - snprintf(cfgNode->key, 128, "%s", arg); - break; - case 'r': - cfgNode->relay = atoi(arg); - break; - case 'a': - snprintf(cfgNode->peers, 2048, "%s", arg); - break; - case ARGP_KEY_ARG: - if (state->arg_num >= 1) /* Too many arguments. */ + struct ConfigNode *cfgNode = (ConfigNode *)state->input; + switch (key) + { + case 'h': + snprintf(cfgNode->host, 128, "%s", arg); + break; + case 'p': + cfgNode->port = atoi(arg); + break; + case 'k': + snprintf(cfgNode->key, 128, "%s", arg); + break; + case 'r': + cfgNode->relay = atoi(arg); + break; + case 'a': + snprintf(cfgNode->peers, 2048, "%s", arg); + break; + case ARGP_KEY_ARG: + if (state->arg_num >= 1) /* Too many arguments. */ argp_usage(state); - break; - case ARGP_KEY_END: - break; - default: - return ARGP_ERR_UNKNOWN; - } + break; + case ARGP_KEY_END: + break; + default: + return ARGP_ERR_UNKNOWN; + } return 0; } -void event_handler(const char* msg, size_t len) { +void event_handler(const char *msg, size_t len) +{ printf("Receiving event: %s\n", msg); } -void handle_error(const char* msg, size_t len) { +void handle_error(const char *msg, size_t len) +{ printf("handle_error: %s\n", msg); exit(1); } template -auto cify(F&& f) { - static F fn = std::forward(f); - return [](int callerRet, const char* msg, size_t len, void* userData) { - signal_cond(); - return fn(msg, len); - }; +auto cify(F &&f) +{ + static F fn = std::forward(f); + return [](int callerRet, const char *msg, size_t len, void *userData) + { + signal_cond(); + return fn(msg, len); + }; } -static struct argp argp = { options, parse_opt, args_doc, doc, 0, 0, 0 }; +static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0}; // Beginning of UI program logic -enum PROGRAM_STATE { +enum PROGRAM_STATE +{ MAIN_MENU, SUBSCRIBE_TOPIC_MENU, CONNECT_TO_OTHER_NODE_MENU, @@ -131,18 +143,21 @@ enum PROGRAM_STATE { enum PROGRAM_STATE current_state = MAIN_MENU; -void show_main_menu() { +void show_main_menu() +{ printf("\nPlease, select an option:\n"); printf("\t1.) Subscribe to topic\n"); printf("\t2.) Connect to other node\n"); printf("\t3.) Publish a message\n"); } -void handle_user_input(void* ctx) { +void handle_user_input(void *ctx) +{ char cmd[1024]; memset(cmd, 0, 1024); int numRead = read(0, cmd, 1024); - if (numRead <= 0) { + if (numRead <= 0) + { return; } @@ -154,12 +169,11 @@ void handle_user_input(void* ctx) { char pubsubTopic[128]; scanf("%127s", pubsubTopic); - WAKU_CALL( waku_relay_subscribe(ctx, - pubsubTopic, - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr) ); + WAKU_CALL(waku_relay_subscribe(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr, + pubsubTopic)); printf("The subscription went well\n"); show_main_menu(); @@ -171,15 +185,14 @@ void handle_user_input(void* ctx) { printf("e.g.: /ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\n"); char peerAddr[512]; scanf("%511s", peerAddr); - WAKU_CALL( waku_connect(ctx, - peerAddr, - 10000 /* timeoutMs */, - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr)); + WAKU_CALL(waku_connect(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr, + peerAddr, + 10000 /* timeoutMs */)); show_main_menu(); - break; + break; case PUBLISH_MESSAGE_MENU: { @@ -193,28 +206,26 @@ void handle_user_input(void* ctx) { std::string contentTopic; waku_content_topic(ctx, + cify([&contentTopic](const char *msg, size_t len) + { contentTopic = msg; }), + nullptr, "appName", - 1, - "contentTopicName", - "encoding", - cify([&contentTopic](const char* msg, size_t len) { - contentTopic = msg; - }), - nullptr); + 1, + "contentTopicName", + "encoding"); snprintf(jsonWakuMsg, 2048, "{\"payload\":\"%s\",\"contentTopic\":\"%s\"}", msgPayload.data(), contentTopic.c_str()); - WAKU_CALL( waku_relay_publish(ctx, - "/waku/2/rs/16/32", - jsonWakuMsg, - 10000 /*timeout ms*/, - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr) ); + WAKU_CALL(waku_relay_publish(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr, + "/waku/2/rs/16/32", + jsonWakuMsg, + 10000 /*timeout ms*/)); show_main_menu(); } @@ -227,12 +238,14 @@ void handle_user_input(void* ctx) { // End of UI program logic -void show_help_and_exit() { +void show_help_and_exit() +{ printf("Wrong parameters\n"); exit(1); } -int main(int argc, char** argv) { +int main(int argc, char **argv) +{ struct ConfigNode cfgNode; // default values snprintf(cfgNode.host, 128, "0.0.0.0"); @@ -241,8 +254,8 @@ int main(int argc, char** argv) { cfgNode.port = 60000; cfgNode.relay = 1; - if (argp_parse(&argp, argc, argv, 0, 0, &cfgNode) - == ARGP_ERR_UNKNOWN) { + if (argp_parse(&argp, argc, argv, 0, 0, &cfgNode) == ARGP_ERR_UNKNOWN) + { show_help_and_exit(); } @@ -260,72 +273,64 @@ int main(int argc, char** argv) { \"discv5UdpPort\": 9999, \ \"dnsDiscoveryUrl\": \"enrtree://AMOJVZX4V6EXP7NTJPMAYJYST2QP6AJXYW76IU6VGJS7UVSNDYZG4@boot.prod.status.nodes.status.im\", \ \"dnsDiscoveryNameServers\": [\"8.8.8.8\", \"1.0.0.1\"] \ - }", cfgNode.host, - cfgNode.port); + }", + cfgNode.host, + cfgNode.port); - void* ctx = + void *ctx = waku_new(jsonConfig, - cify([](const char* msg, size_t len) { - std::cout << "waku_new feedback: " << msg << std::endl; - } - ), - nullptr - ); + cify([](const char *msg, size_t len) + { std::cout << "waku_new feedback: " << msg << std::endl; }), + nullptr); waitForCallback(); // example on how to retrieve a value from the `libwaku` callback. std::string defaultPubsubTopic; WAKU_CALL( waku_default_pubsub_topic( - ctx, - cify([&defaultPubsubTopic](const char* msg, size_t len) { - defaultPubsubTopic = msg; - } - ), - nullptr)); + ctx, + cify([&defaultPubsubTopic](const char *msg, size_t len) + { defaultPubsubTopic = msg; }), + nullptr)); std::cout << "Default pubsub topic: " << defaultPubsubTopic << std::endl; - WAKU_CALL(waku_version(ctx, - cify([&](const char* msg, size_t len) { - std::cout << "Git Version: " << msg << std::endl; - }), + WAKU_CALL(waku_version(ctx, + cify([&](const char *msg, size_t len) + { std::cout << "Git Version: " << msg << std::endl; }), nullptr)); printf("Bind addr: %s:%u\n", cfgNode.host, cfgNode.port); - printf("Waku Relay enabled: %s\n", cfgNode.relay == 1 ? "YES": "NO"); + printf("Waku Relay enabled: %s\n", cfgNode.relay == 1 ? "YES" : "NO"); std::string pubsubTopic; - WAKU_CALL(waku_pubsub_topic(ctx, - "example", - cify([&](const char* msg, size_t len) { - pubsubTopic = msg; - }), - nullptr)); + WAKU_CALL(waku_pubsub_topic(ctx, + cify([&](const char *msg, size_t len) + { pubsubTopic = msg; }), + nullptr, + "example")); std::cout << "Custom pubsub topic: " << pubsubTopic << std::endl; - waku_set_event_callback(ctx, - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr); + set_event_callback(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr); - WAKU_CALL( waku_start(ctx, - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr)); + WAKU_CALL(waku_start(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr)); - WAKU_CALL( waku_relay_subscribe(ctx, - defaultPubsubTopic.c_str(), - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr) ); + WAKU_CALL(waku_relay_subscribe(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr, + defaultPubsubTopic.c_str())); show_main_menu(); - while(1) { + while (1) + { handle_user_input(ctx); } } diff --git a/examples/golang/waku.go b/examples/golang/waku.go index 846362dfe..e205ecd09 100644 --- a/examples/golang/waku.go +++ b/examples/golang/waku.go @@ -71,32 +71,32 @@ package main static void* cGoWakuNew(const char* configJson, void* resp) { // We pass NULL because we are not interested in retrieving data from this callback - void* ret = waku_new(configJson, (WakuCallBack) callback, resp); + void* ret = waku_new(configJson, (FFICallBack) callback, resp); return ret; } static void cGoWakuStart(void* wakuCtx, void* resp) { - WAKU_CALL(waku_start(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_start(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuStop(void* wakuCtx, void* resp) { - WAKU_CALL(waku_stop(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_stop(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuDestroy(void* wakuCtx, void* resp) { - WAKU_CALL(waku_destroy(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_destroy(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuStartDiscV5(void* wakuCtx, void* resp) { - WAKU_CALL(waku_start_discv5(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_start_discv5(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuStopDiscV5(void* wakuCtx, void* resp) { - WAKU_CALL(waku_stop_discv5(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_stop_discv5(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuVersion(void* wakuCtx, void* resp) { - WAKU_CALL(waku_version(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_version(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuSetEventCallback(void* wakuCtx) { @@ -112,7 +112,7 @@ package main // This technique is needed because cgo only allows to export Go functions and not methods. - waku_set_event_callback(wakuCtx, (WakuCallBack) globalEventCallback, wakuCtx); + set_event_callback(wakuCtx, (FFICallBack) globalEventCallback, wakuCtx); } static void cGoWakuContentTopic(void* wakuCtx, @@ -123,20 +123,21 @@ package main void* resp) { WAKU_CALL( waku_content_topic(wakuCtx, + (FFICallBack) callback, + resp, appName, appVersion, contentTopicName, - encoding, - (WakuCallBack) callback, - resp) ); + encoding + ) ); } static void cGoWakuPubsubTopic(void* wakuCtx, char* topicName, void* resp) { - WAKU_CALL( waku_pubsub_topic(wakuCtx, topicName, (WakuCallBack) callback, resp) ); + WAKU_CALL( waku_pubsub_topic(wakuCtx, (FFICallBack) callback, resp, topicName) ); } static void cGoWakuDefaultPubsubTopic(void* wakuCtx, void* resp) { - WAKU_CALL (waku_default_pubsub_topic(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL (waku_default_pubsub_topic(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuRelayPublish(void* wakuCtx, @@ -146,34 +147,36 @@ package main void* resp) { WAKU_CALL (waku_relay_publish(wakuCtx, + (FFICallBack) callback, + resp, pubSubTopic, jsonWakuMessage, - timeoutMs, - (WakuCallBack) callback, - resp)); + timeoutMs + )); } static void cGoWakuRelaySubscribe(void* wakuCtx, char* pubSubTopic, void* resp) { WAKU_CALL ( waku_relay_subscribe(wakuCtx, - pubSubTopic, - (WakuCallBack) callback, - resp) ); + (FFICallBack) callback, + resp, + pubSubTopic) ); } static void cGoWakuRelayUnsubscribe(void* wakuCtx, char* pubSubTopic, void* resp) { WAKU_CALL ( waku_relay_unsubscribe(wakuCtx, - pubSubTopic, - (WakuCallBack) callback, - resp) ); + (FFICallBack) callback, + resp, + pubSubTopic) ); } static void cGoWakuConnect(void* wakuCtx, char* peerMultiAddr, int timeoutMs, void* resp) { WAKU_CALL( waku_connect(wakuCtx, + (FFICallBack) callback, + resp, peerMultiAddr, - timeoutMs, - (WakuCallBack) callback, - resp) ); + timeoutMs + ) ); } static void cGoWakuDialPeerById(void* wakuCtx, @@ -183,42 +186,44 @@ package main void* resp) { WAKU_CALL( waku_dial_peer_by_id(wakuCtx, + (FFICallBack) callback, + resp, peerId, protocol, - timeoutMs, - (WakuCallBack) callback, - resp) ); + timeoutMs + ) ); } static void cGoWakuDisconnectPeerById(void* wakuCtx, char* peerId, void* resp) { WAKU_CALL( waku_disconnect_peer_by_id(wakuCtx, - peerId, - (WakuCallBack) callback, - resp) ); + (FFICallBack) callback, + resp, + peerId + ) ); } static void cGoWakuListenAddresses(void* wakuCtx, void* resp) { - WAKU_CALL (waku_listen_addresses(wakuCtx, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_listen_addresses(wakuCtx, (FFICallBack) callback, resp) ); } static void cGoWakuGetMyENR(void* ctx, void* resp) { - WAKU_CALL (waku_get_my_enr(ctx, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_get_my_enr(ctx, (FFICallBack) callback, resp) ); } static void cGoWakuGetMyPeerId(void* ctx, void* resp) { - WAKU_CALL (waku_get_my_peerid(ctx, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_get_my_peerid(ctx, (FFICallBack) callback, resp) ); } static void cGoWakuListPeersInMesh(void* ctx, char* pubSubTopic, void* resp) { - WAKU_CALL (waku_relay_get_num_peers_in_mesh(ctx, pubSubTopic, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_relay_get_num_peers_in_mesh(ctx, (FFICallBack) callback, resp, pubSubTopic) ); } static void cGoWakuGetNumConnectedPeers(void* ctx, char* pubSubTopic, void* resp) { - WAKU_CALL (waku_relay_get_num_connected_peers(ctx, pubSubTopic, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_relay_get_num_connected_peers(ctx, (FFICallBack) callback, resp, pubSubTopic) ); } static void cGoWakuGetPeerIdsFromPeerStore(void* wakuCtx, void* resp) { - WAKU_CALL (waku_get_peerids_from_peerstore(wakuCtx, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_get_peerids_from_peerstore(wakuCtx, (FFICallBack) callback, resp) ); } static void cGoWakuLightpushPublish(void* wakuCtx, @@ -227,10 +232,11 @@ package main void* resp) { WAKU_CALL (waku_lightpush_publish(wakuCtx, + (FFICallBack) callback, + resp, pubSubTopic, - jsonWakuMessage, - (WakuCallBack) callback, - resp)); + jsonWakuMessage + )); } static void cGoWakuStoreQuery(void* wakuCtx, @@ -240,11 +246,12 @@ package main void* resp) { WAKU_CALL (waku_store_query(wakuCtx, + (FFICallBack) callback, + resp, jsonQuery, peerAddr, - timeoutMs, - (WakuCallBack) callback, - resp)); + timeoutMs + )); } static void cGoWakuPeerExchangeQuery(void* wakuCtx, @@ -252,9 +259,10 @@ package main void* resp) { WAKU_CALL (waku_peer_exchange_request(wakuCtx, - numPeers, - (WakuCallBack) callback, - resp)); + (FFICallBack) callback, + resp, + numPeers + )); } static void cGoWakuGetPeerIdsByProtocol(void* wakuCtx, @@ -262,9 +270,10 @@ package main void* resp) { WAKU_CALL (waku_get_peerids_by_protocol(wakuCtx, - protocol, - (WakuCallBack) callback, - resp)); + (FFICallBack) callback, + resp, + protocol + )); } */ diff --git a/examples/python/waku.py b/examples/python/waku.py index 4d5f5643e..65eb5d750 100644 --- a/examples/python/waku.py +++ b/examples/python/waku.py @@ -102,8 +102,8 @@ print("Waku Relay enabled: {}".format(args.relay)) # Set the event callback callback = callback_type(handle_event) # This line is important so that the callback is not gc'ed -libwaku.waku_set_event_callback.argtypes = [callback_type, ctypes.c_void_p] -libwaku.waku_set_event_callback(callback, ctypes.c_void_p(0)) +libwaku.set_event_callback.argtypes = [callback_type, ctypes.c_void_p] +libwaku.set_event_callback(callback, ctypes.c_void_p(0)) # Start the node libwaku.waku_start.argtypes = [ctypes.c_void_p, @@ -117,32 +117,32 @@ libwaku.waku_start(ctx, # Subscribe to the default pubsub topic libwaku.waku_relay_subscribe.argtypes = [ctypes.c_void_p, - ctypes.c_char_p, callback_type, - ctypes.c_void_p] + ctypes.c_void_p, + ctypes.c_char_p] libwaku.waku_relay_subscribe(ctx, - default_pubsub_topic.encode('utf-8'), callback_type( #onErrCb lambda ret, msg, len: print("Error calling waku_relay_subscribe: %s" % msg.decode('utf-8')) ), - ctypes.c_void_p(0)) + ctypes.c_void_p(0), + default_pubsub_topic.encode('utf-8')) libwaku.waku_connect.argtypes = [ctypes.c_void_p, - ctypes.c_char_p, - ctypes.c_int, callback_type, - ctypes.c_void_p] + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_int] libwaku.waku_connect(ctx, - args.peer.encode('utf-8'), - 10000, # onErrCb callback_type( lambda ret, msg, len: print("Error calling waku_connect: %s" % msg.decode('utf-8'))), - ctypes.c_void_p(0)) + ctypes.c_void_p(0), + args.peer.encode('utf-8'), + 10000) # app = Flask(__name__) # @app.route("/") diff --git a/examples/qt/waku_handler.h b/examples/qt/waku_handler.h index 161a17c82..2fb3ce3b7 100644 --- a/examples/qt/waku_handler.h +++ b/examples/qt/waku_handler.h @@ -27,7 +27,7 @@ public: void initialize(const QString& jsonConfig, WakuCallBack event_handler, void* userData) { ctx = waku_new(jsonConfig.toUtf8().constData(), WakuCallBack(event_handler), userData); - waku_set_event_callback(ctx, on_event_received, userData); + set_event_callback(ctx, on_event_received, userData); qDebug() << "Waku context initialized, ready to start."; } diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs index 926d0e3b0..d26e9627e 100644 --- a/examples/rust/src/main.rs +++ b/examples/rust/src/main.rs @@ -3,22 +3,22 @@ use std::ffi::CString; use std::os::raw::{c_char, c_int, c_void}; use std::{slice, thread, time}; -pub type WakuCallback = unsafe extern "C" fn(c_int, *const c_char, usize, *const c_void); +pub type FFICallBack = unsafe extern "C" fn(c_int, *const c_char, usize, *const c_void); extern "C" { pub fn waku_new( config_json: *const u8, - cb: WakuCallback, + cb: FFICallBack, user_data: *const c_void, ) -> *mut c_void; - pub fn waku_version(ctx: *const c_void, cb: WakuCallback, user_data: *const c_void) -> c_int; + pub fn waku_version(ctx: *const c_void, cb: FFICallBack, user_data: *const c_void) -> c_int; - pub fn waku_start(ctx: *const c_void, cb: WakuCallback, user_data: *const c_void) -> c_int; + pub fn waku_start(ctx: *const c_void, cb: FFICallBack, user_data: *const c_void) -> c_int; pub fn waku_default_pubsub_topic( ctx: *mut c_void, - cb: WakuCallback, + cb: FFICallBack, user_data: *const c_void, ) -> *mut c_void; } @@ -40,7 +40,7 @@ pub unsafe extern "C" fn trampoline( closure(return_val, &buffer_utf8); } -pub fn get_trampoline(_closure: &C) -> WakuCallback +pub fn get_trampoline(_closure: &C) -> FFICallBack where C: FnMut(i32, &str), { diff --git a/library/alloc.nim b/library/alloc.nim deleted file mode 100644 index 1a6f118b5..000000000 --- a/library/alloc.nim +++ /dev/null @@ -1,42 +0,0 @@ -## Can be shared safely between threads -type SharedSeq*[T] = tuple[data: ptr UncheckedArray[T], len: int] - -proc alloc*(str: cstring): cstring = - # Byte allocation from the given address. - # There should be the corresponding manual deallocation with deallocShared ! - if str.isNil(): - var ret = cast[cstring](allocShared(1)) # Allocate memory for the null terminator - ret[0] = '\0' # Set the null terminator - return ret - - let ret = cast[cstring](allocShared(len(str) + 1)) - copyMem(ret, str, len(str) + 1) - return ret - -proc alloc*(str: string): cstring = - ## Byte allocation from the given address. - ## There should be the corresponding manual deallocation with deallocShared ! - var ret = cast[cstring](allocShared(str.len + 1)) - let s = cast[seq[char]](str) - for i in 0 ..< str.len: - ret[i] = s[i] - ret[str.len] = '\0' - return ret - -proc allocSharedSeq*[T](s: seq[T]): SharedSeq[T] = - let data = allocShared(sizeof(T) * s.len) - if s.len != 0: - copyMem(data, unsafeAddr s[0], s.len) - return (cast[ptr UncheckedArray[T]](data), s.len) - -proc deallocSharedSeq*[T](s: var SharedSeq[T]) = - deallocShared(s.data) - s.len = 0 - -proc toSeq*[T](s: SharedSeq[T]): seq[T] = - ## Creates a seq[T] from a SharedSeq[T]. No explicit dealloc is required - ## as req[T] is a GC managed type. - var ret = newSeq[T]() - for i in 0 ..< s.len: - ret.add(s.data[i]) - return ret diff --git a/library/declare_lib.nim b/library/declare_lib.nim new file mode 100644 index 000000000..188de8549 --- /dev/null +++ b/library/declare_lib.nim @@ -0,0 +1,10 @@ +import ffi +import waku/factory/waku + +declareLibrary("waku") + +proc set_event_callback( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.dynlib, exportc, cdecl.} = + ctx[].eventCallback = cast[pointer](callback) + ctx[].eventUserData = userData diff --git a/library/events/json_waku_not_responding_event.nim b/library/events/json_waku_not_responding_event.nim deleted file mode 100644 index 1e1d5fcc5..000000000 --- a/library/events/json_waku_not_responding_event.nim +++ /dev/null @@ -1,9 +0,0 @@ -import system, std/json, ./json_base_event - -type JsonWakuNotRespondingEvent* = ref object of JsonEvent - -proc new*(T: type JsonWakuNotRespondingEvent): T = - return JsonWakuNotRespondingEvent(eventType: "waku_not_responding") - -method `$`*(event: JsonWakuNotRespondingEvent): string = - $(%*event) diff --git a/library/ffi_types.nim b/library/ffi_types.nim deleted file mode 100644 index a5eeb9711..000000000 --- a/library/ffi_types.nim +++ /dev/null @@ -1,30 +0,0 @@ -################################################################################ -### Exported types - -type WakuCallBack* = proc( - callerRet: cint, msg: ptr cchar, len: csize_t, userData: pointer -) {.cdecl, gcsafe, raises: [].} - -const RET_OK*: cint = 0 -const RET_ERR*: cint = 1 -const RET_MISSING_CALLBACK*: cint = 2 - -### End of exported types -################################################################################ - -################################################################################ -### FFI utils - -template foreignThreadGc*(body: untyped) = - when declared(setupForeignThreadGc): - setupForeignThreadGc() - - body - - when declared(tearDownForeignThreadGc): - tearDownForeignThreadGc() - -type onDone* = proc() - -### End of FFI utils -################################################################################ diff --git a/library/kernel_api/debug_node_api.nim b/library/kernel_api/debug_node_api.nim new file mode 100644 index 000000000..98f5332b4 --- /dev/null +++ b/library/kernel_api/debug_node_api.nim @@ -0,0 +1,49 @@ +import std/json +import + chronicles, + chronos, + results, + eth/p2p/discoveryv5/enr, + strutils, + libp2p/peerid, + metrics, + ffi +import waku/factory/waku, waku/node/waku_node, waku/node/health_monitor, library/declare_lib + +proc getMultiaddresses(node: WakuNode): seq[string] = + return node.info().listenAddresses + +proc getMetrics(): string = + {.gcsafe.}: + return defaultRegistry.toText() ## defaultRegistry is {.global.} in metrics module + +proc waku_version( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + return ok(WakuNodeVersionString) + +proc waku_listen_addresses( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## returns a comma-separated string of the listen addresses + return ok(ctx.myLib[].node.getMultiaddresses().join(",")) + +proc waku_get_my_enr( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + return ok(ctx.myLib[].node.enr.toURI()) + +proc waku_get_my_peerid( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + return ok($ctx.myLib[].node.peerId()) + +proc waku_get_metrics( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + return ok(getMetrics()) + +proc waku_is_online( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + return ok($ctx.myLib[].healthMonitor.onlineMonitor.amIOnline()) diff --git a/library/kernel_api/discovery_api.nim b/library/kernel_api/discovery_api.nim new file mode 100644 index 000000000..f61b7bad1 --- /dev/null +++ b/library/kernel_api/discovery_api.nim @@ -0,0 +1,96 @@ +import std/json +import chronos, chronicles, results, strutils, libp2p/multiaddress, ffi +import + waku/factory/waku, + waku/discovery/waku_dnsdisc, + waku/discovery/waku_discv5, + waku/waku_core/peers, + waku/node/waku_node, + waku/node/kernel_api, + library/declare_lib + +proc retrieveBootstrapNodes( + enrTreeUrl: string, ipDnsServer: string +): Future[Result[seq[string], string]] {.async.} = + let dnsNameServers = @[parseIpAddress(ipDnsServer)] + let discoveredPeers: seq[RemotePeerInfo] = ( + await retrieveDynamicBootstrapNodes(enrTreeUrl, dnsNameServers) + ).valueOr: + return err("failed discovering peers from DNS: " & $error) + + var multiAddresses = newSeq[string]() + + for discPeer in discoveredPeers: + for address in discPeer.addrs: + multiAddresses.add($address & "/p2p/" & $discPeer) + + return ok(multiAddresses) + +proc updateDiscv5BootstrapNodes(nodes: string, waku: Waku): Result[void, string] = + waku.wakuDiscv5.updateBootstrapRecords(nodes).isOkOr: + return err("error in updateDiscv5BootstrapNodes: " & $error) + return ok() + +proc performPeerExchangeRequestTo*( + numPeers: uint64, waku: Waku +): Future[Result[int, string]] {.async.} = + let numPeersRecv = (await waku.node.fetchPeerExchangePeers(numPeers)).valueOr: + return err($error) + return ok(numPeersRecv) + +proc waku_discv5_update_bootnodes( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + bootnodes: cstring, +) {.ffi.} = + ## Updates the bootnode list used for discovering new peers via DiscoveryV5 + ## bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]` + + updateDiscv5BootstrapNodes($bootnodes, ctx.myLib[]).isOkOr: + error "UPDATE_DISCV5_BOOTSTRAP_NODES failed", error = error + return err($error) + + return ok("discovery request processed correctly") + +proc waku_dns_discovery( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + enrTreeUrl: cstring, + nameDnsServer: cstring, + timeoutMs: cint, +) {.ffi.} = + let nodes = (await retrieveBootstrapNodes($enrTreeUrl, $nameDnsServer)).valueOr: + error "GET_BOOTSTRAP_NODES failed", error = error + return err($error) + + ## returns a comma-separated string of bootstrap nodes' multiaddresses + return ok(nodes.join(",")) + +proc waku_start_discv5( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + (await ctx.myLib[].wakuDiscv5.start()).isOkOr: + error "START_DISCV5 failed", error = error + return err("error starting discv5: " & $error) + + return ok("discv5 started correctly") + +proc waku_stop_discv5( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + await ctx.myLib[].wakuDiscv5.stop() + return ok("discv5 stopped correctly") + +proc waku_peer_exchange_request( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + numPeers: uint64, +) {.ffi.} = + let numValidPeers = (await performPeerExchangeRequestTo(numPeers, ctx.myLib[])).valueOr: + error "waku_peer_exchange_request failed", error = error + return err("failed peer exchange: " & $error) + + return ok($numValidPeers) diff --git a/library/waku_thread_requests/requests/node_lifecycle_request.nim b/library/kernel_api/node_lifecycle_api.nim similarity index 60% rename from library/waku_thread_requests/requests/node_lifecycle_request.nim rename to library/kernel_api/node_lifecycle_api.nim index aa71ac6bb..a2bb25609 100644 --- a/library/waku_thread_requests/requests/node_lifecycle_request.nim +++ b/library/kernel_api/node_lifecycle_api.nim @@ -1,43 +1,14 @@ import std/[options, json, strutils, net] -import chronos, chronicles, results, confutils, confutils/std/net +import chronos, chronicles, results, confutils, confutils/std/net, ffi import waku/node/peer_manager/peer_manager, tools/confutils/cli_args, waku/factory/waku, waku/factory/node_factory, - waku/factory/networks_config, waku/factory/app_callbacks, - waku/rest_api/endpoint/builder - -import - ../../alloc - -type NodeLifecycleMsgType* = enum - CREATE_NODE - START_NODE - STOP_NODE - -type NodeLifecycleRequest* = object - operation: NodeLifecycleMsgType - configJson: cstring ## Only used in 'CREATE_NODE' operation - appCallbacks: AppCallbacks - -proc createShared*( - T: type NodeLifecycleRequest, - op: NodeLifecycleMsgType, - configJson: cstring = "", - appCallbacks: AppCallbacks = nil, -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].appCallbacks = appCallbacks - ret[].configJson = configJson.alloc() - return ret - -proc destroyShared(self: ptr NodeLifecycleRequest) = - deallocShared(self[].configJson) - deallocShared(self) + waku/rest_api/endpoint/builder, + library/declare_lib proc createWaku( configJson: cstring, appCallbacks: AppCallbacks = nil @@ -87,26 +58,30 @@ proc createWaku( return ok(wakuRes) -proc process*( - self: ptr NodeLifecycleRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - case self.operation - of CREATE_NODE: - waku[] = (await createWaku(self.configJson, self.appCallbacks)).valueOr: - error "CREATE_NODE failed", error = error +registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): + proc( + configJson: cstring, appCallbacks: AppCallbacks + ): Future[Result[string, string]] {.async.} = + ctx.myLib[] = (await createWaku(configJson, cast[AppCallbacks](appCallbacks))).valueOr: + error "CreateNodeRequest failed", error = error return err($error) - of START_NODE: - (await waku.startWaku()).isOkOr: - error "START_NODE failed", error = error - return err($error) - of STOP_NODE: - try: - await waku[].stop() - except Exception: - error "STOP_NODE failed", error = getCurrentExceptionMsg() - return err(getCurrentExceptionMsg()) + return ok("") + +proc waku_start( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + (await startWaku(ctx[].myLib)).isOkOr: + error "START_NODE failed", error = error + return err("failed to start: " & $error) + return ok("") + +proc waku_stop( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + try: + await ctx.myLib[].stop() + except Exception as exc: + error "STOP_NODE failed", error = exc.msg + return err("failed to stop: " & exc.msg) return ok("") diff --git a/library/kernel_api/peer_manager_api.nim b/library/kernel_api/peer_manager_api.nim new file mode 100644 index 000000000..f0ae37f00 --- /dev/null +++ b/library/kernel_api/peer_manager_api.nim @@ -0,0 +1,123 @@ +import std/[sequtils, strutils, tables] +import chronicles, chronos, results, options, json, ffi +import waku/factory/waku, waku/node/waku_node, waku/node/peer_manager, ../declare_lib + +type PeerInfo = object + protocols: seq[string] + addresses: seq[string] + +proc waku_get_peerids_from_peerstore( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## returns a comma-separated string of peerIDs + let peerIDs = + ctx.myLib[].node.peerManager.switch.peerStore.peers().mapIt($it.peerId).join(",") + return ok(peerIDs) + +proc waku_connect( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + peerMultiAddr: cstring, + timeoutMs: cuint, +) {.ffi.} = + let peers = ($peerMultiAddr).split(",").mapIt(strip(it)) + await ctx.myLib[].node.connectToNodes(peers, source = "static") + return ok("") + +proc waku_disconnect_peer_by_id( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer, peerId: cstring +) {.ffi.} = + let pId = PeerId.init($peerId).valueOr: + error "DISCONNECT_PEER_BY_ID failed", error = $error + return err($error) + await ctx.myLib[].node.peerManager.disconnectNode(pId) + return ok("") + +proc waku_disconnect_all_peers( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + await ctx.myLib[].node.peerManager.disconnectAllPeers() + return ok("") + +proc waku_dial_peer( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + peerMultiAddr: cstring, + protocol: cstring, + timeoutMs: cuint, +) {.ffi.} = + let remotePeerInfo = parsePeerInfo($peerMultiAddr).valueOr: + error "DIAL_PEER failed", error = $error + return err($error) + let conn = await ctx.myLib[].node.peerManager.dialPeer(remotePeerInfo, $protocol) + if conn.isNone(): + let msg = "failed dialing peer" + error "DIAL_PEER failed", error = msg, peerId = $remotePeerInfo.peerId + return err(msg) + return ok("") + +proc waku_dial_peer_by_id( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + peerId: cstring, + protocol: cstring, + timeoutMs: cuint, +) {.ffi.} = + let pId = PeerId.init($peerId).valueOr: + error "DIAL_PEER_BY_ID failed", error = $error + return err($error) + let conn = await ctx.myLib[].node.peerManager.dialPeer(pId, $protocol) + if conn.isNone(): + let msg = "failed dialing peer" + error "DIAL_PEER_BY_ID failed", error = msg, peerId = $peerId + return err(msg) + + return ok("") + +proc waku_get_connected_peers_info( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## returns a JSON string mapping peerIDs to objects with protocols and addresses + + var peersMap = initTable[string, PeerInfo]() + let peers = ctx.myLib[].node.peerManager.switch.peerStore.peers().filterIt( + it.connectedness == Connected + ) + + # Build a map of peer IDs to peer info objects + for peer in peers: + let peerIdStr = $peer.peerId + peersMap[peerIdStr] = + PeerInfo(protocols: peer.protocols, addresses: peer.addrs.mapIt($it)) + + # Convert the map to JSON string + let jsonObj = %*peersMap + let jsonStr = $jsonObj + return ok(jsonStr) + +proc waku_get_connected_peers( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## returns a comma-separated string of peerIDs + let + (inPeerIds, outPeerIds) = ctx.myLib[].node.peerManager.connectedPeers() + connectedPeerids = concat(inPeerIds, outPeerIds) + + return ok(connectedPeerids.mapIt($it).join(",")) + +proc waku_get_peerids_by_protocol( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + protocol: cstring, +) {.ffi.} = + ## returns a comma-separated string of peerIDs that mount the given protocol + let connectedPeers = ctx.myLib[].node.peerManager.switch.peerStore + .peers($protocol) + .filterIt(it.connectedness == Connected) + .mapIt($it.peerId) + .join(",") + return ok(connectedPeers) diff --git a/library/kernel_api/ping_api.nim b/library/kernel_api/ping_api.nim new file mode 100644 index 000000000..4f10dcf59 --- /dev/null +++ b/library/kernel_api/ping_api.nim @@ -0,0 +1,43 @@ +import std/[json, strutils] +import chronos, results, ffi +import libp2p/[protocols/ping, switch, multiaddress, multicodec] +import waku/[factory/waku, waku_core/peers, node/waku_node], library/declare_lib + +proc waku_ping_peer( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + peerAddr: cstring, + timeoutMs: cuint, +) {.ffi.} = + let peerInfo = peers.parsePeerInfo(($peerAddr).split(",")).valueOr: + return err("PingRequest failed to parse peer addr: " & $error) + + let timeout = chronos.milliseconds(timeoutMs) + proc ping(): Future[Result[Duration, string]] {.async, gcsafe.} = + try: + let conn = + await ctx.myLib[].node.switch.dial(peerInfo.peerId, peerInfo.addrs, PingCodec) + defer: + await conn.close() + + let pingRTT = await ctx.myLib[].node.libp2pPing.ping(conn) + if pingRTT == 0.nanos: + return err("could not ping peer: rtt-0") + return ok(pingRTT) + except CatchableError as exc: + return err("could not ping peer: " & exc.msg) + + let pingFuture = ping() + let pingRTT: Duration = + if timeout == chronos.milliseconds(0): # No timeout expected + (await pingFuture).valueOr: + return err("ping failed, no timeout expected: " & error) + else: + let timedOut = not (await pingFuture.withTimeout(timeout)) + if timedOut: + return err("ping timed out") + pingFuture.read().valueOr: + return err("failed to read ping future: " & error) + + return ok($(pingRTT.nanos)) diff --git a/library/kernel_api/protocols/filter_api.nim b/library/kernel_api/protocols/filter_api.nim new file mode 100644 index 000000000..c4f99510a --- /dev/null +++ b/library/kernel_api/protocols/filter_api.nim @@ -0,0 +1,109 @@ +import options, std/[strutils, sequtils] +import chronicles, chronos, results, ffi +import + waku/waku_filter_v2/client, + waku/waku_core/message/message, + waku/factory/waku, + waku/waku_relay, + waku/waku_filter_v2/common, + waku/waku_core/subscription/push_handler, + waku/node/peer_manager/peer_manager, + waku/node/waku_node, + waku/node/kernel_api, + waku/waku_core/topics/pubsub_topic, + waku/waku_core/topics/content_topic, + library/events/json_message_event, + library/declare_lib + +const FilterOpTimeout = 5.seconds + +proc checkFilterClientMounted(waku: Waku): Result[string, string] = + if waku.node.wakuFilterClient.isNil(): + let errorMsg = "wakuFilterClient is not mounted" + error "fail filter process", error = errorMsg + return err(errorMsg) + return ok("") + +proc waku_filter_subscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, + contentTopics: cstring, +) {.ffi.} = + proc onReceivedMessage(ctx: ptr FFIContext): WakuRelayHandler = + return proc(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async.} = + callEventCallback(ctx, "onReceivedMessage"): + $JsonMessageEvent.new(pubsubTopic, msg) + + checkFilterClientMounted(ctx.myLib[]).isOkOr: + return err($error) + + var filterPushEventCallback = FilterPushHandler(onReceivedMessage(ctx)) + ctx.myLib[].node.wakuFilterClient.registerPushHandler(filterPushEventCallback) + + let peer = ctx.myLib[].node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: + let errorMsg = "could not find peer with WakuFilterSubscribeCodec when subscribing" + error "fail filter subscribe", error = errorMsg + return err(errorMsg) + + let subFut = ctx.myLib[].node.filterSubscribe( + some(PubsubTopic($pubsubTopic)), + ($contentTopics).split(",").mapIt(ContentTopic(it)), + peer, + ) + if not await subFut.withTimeout(FilterOpTimeout): + let errorMsg = "filter subscription timed out" + error "fail filter unsubscribe", error = errorMsg + + return err(errorMsg) + + return ok("") + +proc waku_filter_unsubscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, + contentTopics: cstring, +) {.ffi.} = + checkFilterClientMounted(ctx.myLib[]).isOkOr: + return err($error) + + let peer = ctx.myLib[].node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: + let errorMsg = + "could not find peer with WakuFilterSubscribeCodec when unsubscribing" + error "fail filter process", error = errorMsg + return err(errorMsg) + + let subFut = ctx.myLib[].node.filterUnsubscribe( + some(PubsubTopic($pubsubTopic)), + ($contentTopics).split(",").mapIt(ContentTopic(it)), + peer, + ) + if not await subFut.withTimeout(FilterOpTimeout): + let errorMsg = "filter un-subscription timed out" + error "fail filter unsubscribe", error = errorMsg + return err(errorMsg) + return ok("") + +proc waku_filter_unsubscribe_all( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + checkFilterClientMounted(ctx.myLib[]).isOkOr: + return err($error) + + let peer = ctx.myLib[].node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: + let errorMsg = + "could not find peer with WakuFilterSubscribeCodec when unsubscribing all" + error "fail filter unsubscribe all", error = errorMsg + return err(errorMsg) + + let unsubFut = ctx.myLib[].node.filterUnsubscribeAll(peer) + + if not await unsubFut.withTimeout(FilterOpTimeout): + let errorMsg = "filter un-subscription all timed out" + error "fail filter unsubscribe all", error = errorMsg + + return err(errorMsg) + return ok("") diff --git a/library/kernel_api/protocols/lightpush_api.nim b/library/kernel_api/protocols/lightpush_api.nim new file mode 100644 index 000000000..e9251a3f3 --- /dev/null +++ b/library/kernel_api/protocols/lightpush_api.nim @@ -0,0 +1,51 @@ +import options, std/[json, strformat] +import chronicles, chronos, results, ffi +import + waku/waku_core/message/message, + waku/waku_core/codecs, + waku/factory/waku, + waku/waku_core/message, + waku/waku_core/topics/pubsub_topic, + waku/waku_lightpush_legacy/client, + waku/node/peer_manager/peer_manager, + library/events/json_message_event, + library/declare_lib + +proc waku_lightpush_publish( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, + jsonWakuMessage: cstring, +) {.ffi.} = + if ctx.myLib[].node.wakuLightpushClient.isNil(): + let errorMsg = "LightpushRequest waku.node.wakuLightpushClient is nil" + error "PUBLISH failed", error = errorMsg + return err(errorMsg) + + var jsonMessage: JsonMessage + try: + let jsonContent = parseJson($jsonWakuMessage) + jsonMessage = JsonMessage.fromJsonNode(jsonContent).valueOr: + raise newException(JsonParsingError, $error) + except JsonParsingError as exc: + return err(fmt"Error parsing json message: {exc.msg}") + + let msg = json_message_event.toWakuMessage(jsonMessage).valueOr: + return err("Problem building the WakuMessage: " & $error) + + let peerOpt = ctx.myLib[].node.peerManager.selectPeer(WakuLightPushCodec) + if peerOpt.isNone(): + let errorMsg = "failed to lightpublish message, no suitable remote peers" + error "PUBLISH failed", error = errorMsg + return err(errorMsg) + + let msgHashHex = ( + await ctx.myLib[].node.wakuLegacyLightpushClient.publish( + $pubsubTopic, msg, peer = peerOpt.get() + ) + ).valueOr: + error "PUBLISH failed", error = error + return err($error) + + return ok(msgHashHex) diff --git a/library/kernel_api/protocols/relay_api.nim b/library/kernel_api/protocols/relay_api.nim new file mode 100644 index 000000000..b184d6011 --- /dev/null +++ b/library/kernel_api/protocols/relay_api.nim @@ -0,0 +1,171 @@ +import std/[net, sequtils, strutils, json], strformat +import chronicles, chronos, stew/byteutils, results, ffi +import + waku/waku_core/message/message, + waku/factory/[validator_signed, waku], + tools/confutils/cli_args, + waku/waku_core/message, + waku/waku_core/topics/pubsub_topic, + waku/waku_core/topics, + waku/node/kernel_api/relay, + waku/waku_relay/protocol, + waku/node/peer_manager, + library/events/json_message_event, + library/declare_lib + +proc waku_relay_get_peers_in_mesh( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + let meshPeers = ctx.myLib[].node.wakuRelay.getPeersInMesh($pubsubTopic).valueOr: + error "LIST_MESH_PEERS failed", error = error + return err($error) + ## returns a comma-separated string of peerIDs + return ok(meshPeers.mapIt($it).join(",")) + +proc waku_relay_get_num_peers_in_mesh( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + let numPeersInMesh = ctx.myLib[].node.wakuRelay.getNumPeersInMesh($pubsubTopic).valueOr: + error "NUM_MESH_PEERS failed", error = error + return err($error) + return ok($numPeersInMesh) + +proc waku_relay_get_connected_peers( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + ## Returns the list of all connected peers to an specific pubsub topic + let connPeers = ctx.myLib[].node.wakuRelay.getConnectedPeers($pubsubTopic).valueOr: + error "LIST_CONNECTED_PEERS failed", error = error + return err($error) + ## returns a comma-separated string of peerIDs + return ok(connPeers.mapIt($it).join(",")) + +proc waku_relay_get_num_connected_peers( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + let numConnPeers = ctx.myLib[].node.wakuRelay.getNumConnectedPeers($pubsubTopic).valueOr: + error "NUM_CONNECTED_PEERS failed", error = error + return err($error) + return ok($numConnPeers) + +proc waku_relay_add_protected_shard( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + clusterId: cint, + shardId: cint, + publicKey: cstring, +) {.ffi.} = + ## Protects a shard with a public key + try: + let relayShard = RelayShard(clusterId: uint16(clusterId), shardId: uint16(shardId)) + let protectedShard = ProtectedShard.parseCmdArg($relayShard & ":" & $publicKey) + ctx.myLib[].node.wakuRelay.addSignedShardsValidator( + @[protectedShard], uint16(clusterId) + ) + except ValueError as exc: + return err("ERROR in waku_relay_add_protected_shard: " & exc.msg) + + return ok("") + +proc waku_relay_subscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + echo "Subscribing to topic: " & $pubSubTopic & " ..." + proc onReceivedMessage(ctx: ptr FFIContext[Waku]): WakuRelayHandler = + return proc(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async.} = + callEventCallback(ctx, "onReceivedMessage"): + $JsonMessageEvent.new(pubsubTopic, msg) + + var cb = onReceivedMessage(ctx) + + ctx.myLib[].node.subscribe( + (kind: SubscriptionKind.PubsubSub, topic: $pubsubTopic), + handler = WakuRelayHandler(cb), + ).isOkOr: + error "SUBSCRIBE failed", error = error + return err($error) + return ok("") + +proc waku_relay_unsubscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + ctx.myLib[].node.unsubscribe((kind: SubscriptionKind.PubsubSub, topic: $pubsubTopic)).isOkOr: + error "UNSUBSCRIBE failed", error = error + return err($error) + + return ok("") + +proc waku_relay_publish( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, + jsonWakuMessage: cstring, + timeoutMs: cuint, +) {.ffi.} = + var + # https://rfc.vac.dev/spec/36/#extern-char-waku_relay_publishchar-messagejson-char-pubsubtopic-int-timeoutms + jsonMessage: JsonMessage + try: + let jsonContent = parseJson($jsonWakuMessage) + jsonMessage = JsonMessage.fromJsonNode(jsonContent).valueOr: + raise newException(JsonParsingError, $error) + except JsonParsingError as exc: + return err(fmt"Error parsing json message: {exc.msg}") + + let msg = json_message_event.toWakuMessage(jsonMessage).valueOr: + return err("Problem building the WakuMessage: " & $error) + + (await ctx.myLib[].node.wakuRelay.publish($pubsubTopic, msg)).isOkOr: + error "PUBLISH failed", error = error + return err($error) + + let msgHash = computeMessageHash($pubSubTopic, msg).to0xHex + return ok(msgHash) + +proc waku_default_pubsub_topic( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + # https://rfc.vac.dev/spec/36/#extern-char-waku_default_pubsub_topic + return ok(DefaultPubsubTopic) + +proc waku_content_topic( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + appName: cstring, + appVersion: cuint, + contentTopicName: cstring, + encoding: cstring, +) {.ffi.} = + # https://rfc.vac.dev/spec/36/#extern-char-waku_content_topicchar-applicationname-unsigned-int-applicationversion-char-contenttopicname-char-encoding + + return ok(fmt"/{$appName}/{$appVersion}/{$contentTopicName}/{$encoding}") + +proc waku_pubsub_topic( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + topicName: cstring, +) {.ffi.} = + # https://rfc.vac.dev/spec/36/#extern-char-waku_pubsub_topicchar-name-char-encoding + return ok(fmt"/waku/2/{$topicName}") diff --git a/library/waku_thread_requests/requests/protocols/store_request.nim b/library/kernel_api/protocols/store_api.nim similarity index 57% rename from library/waku_thread_requests/requests/protocols/store_request.nim rename to library/kernel_api/protocols/store_api.nim index 3fe1e2f13..0df4d9b1f 100644 --- a/library/waku_thread_requests/requests/protocols/store_request.nim +++ b/library/kernel_api/protocols/store_api.nim @@ -1,28 +1,16 @@ import std/[json, sugar, strutils, options] -import chronos, chronicles, results, stew/byteutils +import chronos, chronicles, results, stew/byteutils, ffi import - ../../../../waku/factory/waku, - ../../../alloc, - ../../../utils, - ../../../../waku/waku_core/peers, - ../../../../waku/waku_core/time, - ../../../../waku/waku_core/message/digest, - ../../../../waku/waku_store/common, - ../../../../waku/waku_store/client, - ../../../../waku/common/paging + waku/factory/waku, + library/utils, + waku/waku_core/peers, + waku/waku_core/message/digest, + waku/waku_store/common, + waku/waku_store/client, + waku/common/paging, + library/declare_lib -type StoreReqType* = enum - REMOTE_QUERY ## to perform a query to another Store node - -type StoreRequest* = object - operation: StoreReqType - jsonQuery: cstring - peerAddr: cstring - timeoutMs: cint - -func fromJsonNode( - T: type StoreRequest, jsonContent: JsonNode -): Result[StoreQueryRequest, string] = +func fromJsonNode(jsonContent: JsonNode): Result[StoreQueryRequest, string] = var contentTopics: seq[string] if jsonContent.contains("contentTopics"): contentTopics = collect(newSeq): @@ -78,54 +66,29 @@ func fromJsonNode( ) ) -proc createShared*( - T: type StoreRequest, - op: StoreReqType, +proc waku_store_query( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, jsonQuery: cstring, peerAddr: cstring, timeoutMs: cint, -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].timeoutMs = timeoutMs - ret[].jsonQuery = jsonQuery.alloc() - ret[].peerAddr = peerAddr.alloc() - return ret - -proc destroyShared(self: ptr StoreRequest) = - deallocShared(self[].jsonQuery) - deallocShared(self[].peerAddr) - deallocShared(self) - -proc process_remote_query( - self: ptr StoreRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = +) {.ffi.} = let jsonContentRes = catch: - parseJson($self[].jsonQuery) + parseJson($jsonQuery) if jsonContentRes.isErr(): return err("StoreRequest failed parsing store request: " & jsonContentRes.error.msg) - let storeQueryRequest = ?StoreRequest.fromJsonNode(jsonContentRes.get()) + let storeQueryRequest = ?fromJsonNode(jsonContentRes.get()) - let peer = peers.parsePeerInfo(($self[].peerAddr).split(",")).valueOr: + let peer = peers.parsePeerInfo(($peerAddr).split(",")).valueOr: return err("StoreRequest failed to parse peer addr: " & $error) - let queryResponse = (await waku.node.wakuStoreClient.query(storeQueryRequest, peer)).valueOr: + let queryResponse = ( + await ctx.myLib[].node.wakuStoreClient.query(storeQueryRequest, peer) + ).valueOr: return err("StoreRequest failed store query: " & $error) let res = $(%*(queryResponse.toHex())) return ok(res) ## returning the response in json format - -proc process*( - self: ptr StoreRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - deallocShared(self) - - case self.operation - of REMOTE_QUERY: - return await self.process_remote_query(waku) - - error "store request not handled at all" - return err("store request not handled at all") diff --git a/library/libwaku.h b/library/libwaku.h index b5d6c9bab..67c89c7c2 100644 --- a/library/libwaku.h +++ b/library/libwaku.h @@ -10,241 +10,242 @@ #include // The possible returned values for the functions that return int -#define RET_OK 0 -#define RET_ERR 1 -#define RET_MISSING_CALLBACK 2 +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif -typedef void (*WakuCallBack) (int callerRet, const char* msg, size_t len, void* userData); + typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); -// Creates a new instance of the waku node. -// Sets up the waku node from the given configuration. -// Returns a pointer to the Context needed by the rest of the API functions. -void* waku_new( - const char* configJson, - WakuCallBack callback, - void* userData); + // Creates a new instance of the waku node. + // Sets up the waku node from the given configuration. + // Returns a pointer to the Context needed by the rest of the API functions. + void *waku_new( + const char *configJson, + FFICallBack callback, + void *userData); -int waku_start(void* ctx, - WakuCallBack callback, - void* userData); + int waku_start(void *ctx, + FFICallBack callback, + void *userData); -int waku_stop(void* ctx, - WakuCallBack callback, - void* userData); + int waku_stop(void *ctx, + FFICallBack callback, + void *userData); -// Destroys an instance of a waku node created with waku_new -int waku_destroy(void* ctx, - WakuCallBack callback, - void* userData); + // Destroys an instance of a waku node created with waku_new + int waku_destroy(void *ctx, + FFICallBack callback, + void *userData); -int waku_version(void* ctx, - WakuCallBack callback, - void* userData); + int waku_version(void *ctx, + FFICallBack callback, + void *userData); -// Sets a callback that will be invoked whenever an event occurs. -// It is crucial that the passed callback is fast, non-blocking and potentially thread-safe. -void waku_set_event_callback(void* ctx, - WakuCallBack callback, - void* userData); + // Sets a callback that will be invoked whenever an event occurs. + // It is crucial that the passed callback is fast, non-blocking and potentially thread-safe. + void set_event_callback(void *ctx, + FFICallBack callback, + void *userData); -int waku_content_topic(void* ctx, - const char* appName, - unsigned int appVersion, - const char* contentTopicName, - const char* encoding, - WakuCallBack callback, - void* userData); + int waku_content_topic(void *ctx, + FFICallBack callback, + void *userData, + const char *appName, + unsigned int appVersion, + const char *contentTopicName, + const char *encoding); -int waku_pubsub_topic(void* ctx, - const char* topicName, - WakuCallBack callback, - void* userData); + int waku_pubsub_topic(void *ctx, + FFICallBack callback, + void *userData, + const char *topicName); -int waku_default_pubsub_topic(void* ctx, - WakuCallBack callback, - void* userData); + int waku_default_pubsub_topic(void *ctx, + FFICallBack callback, + void *userData); -int waku_relay_publish(void* ctx, - const char* pubSubTopic, - const char* jsonWakuMessage, - unsigned int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_relay_publish(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic, + const char *jsonWakuMessage, + unsigned int timeoutMs); -int waku_lightpush_publish(void* ctx, - const char* pubSubTopic, - const char* jsonWakuMessage, - WakuCallBack callback, - void* userData); + int waku_lightpush_publish(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic, + const char *jsonWakuMessage); -int waku_relay_subscribe(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_subscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_relay_add_protected_shard(void* ctx, - int clusterId, - int shardId, - char* publicKey, - WakuCallBack callback, - void* userData); + int waku_relay_add_protected_shard(void *ctx, + FFICallBack callback, + void *userData, + int clusterId, + int shardId, + char *publicKey); -int waku_relay_unsubscribe(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_unsubscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_filter_subscribe(void* ctx, - const char* pubSubTopic, - const char* contentTopics, - WakuCallBack callback, - void* userData); + int waku_filter_subscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic, + const char *contentTopics); -int waku_filter_unsubscribe(void* ctx, - const char* pubSubTopic, - const char* contentTopics, - WakuCallBack callback, - void* userData); + int waku_filter_unsubscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic, + const char *contentTopics); -int waku_filter_unsubscribe_all(void* ctx, - WakuCallBack callback, - void* userData); + int waku_filter_unsubscribe_all(void *ctx, + FFICallBack callback, + void *userData); -int waku_relay_get_num_connected_peers(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_get_num_connected_peers(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_relay_get_connected_peers(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_get_connected_peers(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_relay_get_num_peers_in_mesh(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_get_num_peers_in_mesh(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_relay_get_peers_in_mesh(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_get_peers_in_mesh(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_store_query(void* ctx, - const char* jsonQuery, - const char* peerAddr, - int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_store_query(void *ctx, + FFICallBack callback, + void *userData, + const char *jsonQuery, + const char *peerAddr, + int timeoutMs); -int waku_connect(void* ctx, - const char* peerMultiAddr, - unsigned int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_connect(void *ctx, + FFICallBack callback, + void *userData, + const char *peerMultiAddr, + unsigned int timeoutMs); -int waku_disconnect_peer_by_id(void* ctx, - const char* peerId, - WakuCallBack callback, - void* userData); + int waku_disconnect_peer_by_id(void *ctx, + FFICallBack callback, + void *userData, + const char *peerId); -int waku_disconnect_all_peers(void* ctx, - WakuCallBack callback, - void* userData); + int waku_disconnect_all_peers(void *ctx, + FFICallBack callback, + void *userData); -int waku_dial_peer(void* ctx, - const char* peerMultiAddr, - const char* protocol, - int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_dial_peer(void *ctx, + FFICallBack callback, + void *userData, + const char *peerMultiAddr, + const char *protocol, + int timeoutMs); -int waku_dial_peer_by_id(void* ctx, - const char* peerId, - const char* protocol, - int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_dial_peer_by_id(void *ctx, + FFICallBack callback, + void *userData, + const char *peerId, + const char *protocol, + int timeoutMs); -int waku_get_peerids_from_peerstore(void* ctx, - WakuCallBack callback, - void* userData); + int waku_get_peerids_from_peerstore(void *ctx, + FFICallBack callback, + void *userData); -int waku_get_connected_peers_info(void* ctx, - WakuCallBack callback, - void* userData); + int waku_get_connected_peers_info(void *ctx, + FFICallBack callback, + void *userData); -int waku_get_peerids_by_protocol(void* ctx, - const char* protocol, - WakuCallBack callback, - void* userData); + int waku_get_peerids_by_protocol(void *ctx, + FFICallBack callback, + void *userData, + const char *protocol); -int waku_listen_addresses(void* ctx, - WakuCallBack callback, - void* userData); + int waku_listen_addresses(void *ctx, + FFICallBack callback, + void *userData); -int waku_get_connected_peers(void* ctx, - WakuCallBack callback, - void* userData); + int waku_get_connected_peers(void *ctx, + FFICallBack callback, + void *userData); -// Returns a list of multiaddress given a url to a DNS discoverable ENR tree -// Parameters -// char* entTreeUrl: URL containing a discoverable ENR tree -// char* nameDnsServer: The nameserver to resolve the ENR tree url. -// int timeoutMs: Timeout value in milliseconds to execute the call. -int waku_dns_discovery(void* ctx, - const char* entTreeUrl, - const char* nameDnsServer, - int timeoutMs, - WakuCallBack callback, - void* userData); + // Returns a list of multiaddress given a url to a DNS discoverable ENR tree + // Parameters + // char* entTreeUrl: URL containing a discoverable ENR tree + // char* nameDnsServer: The nameserver to resolve the ENR tree url. + // int timeoutMs: Timeout value in milliseconds to execute the call. + int waku_dns_discovery(void *ctx, + FFICallBack callback, + void *userData, + const char *entTreeUrl, + const char *nameDnsServer, + int timeoutMs); -// Updates the bootnode list used for discovering new peers via DiscoveryV5 -// bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]` -int waku_discv5_update_bootnodes(void* ctx, - char* bootnodes, - WakuCallBack callback, - void* userData); + // Updates the bootnode list used for discovering new peers via DiscoveryV5 + // bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]` + int waku_discv5_update_bootnodes(void *ctx, + FFICallBack callback, + void *userData, + char *bootnodes); -int waku_start_discv5(void* ctx, - WakuCallBack callback, - void* userData); + int waku_start_discv5(void *ctx, + FFICallBack callback, + void *userData); -int waku_stop_discv5(void* ctx, - WakuCallBack callback, - void* userData); + int waku_stop_discv5(void *ctx, + FFICallBack callback, + void *userData); -// Retrieves the ENR information -int waku_get_my_enr(void* ctx, - WakuCallBack callback, - void* userData); + // Retrieves the ENR information + int waku_get_my_enr(void *ctx, + FFICallBack callback, + void *userData); -int waku_get_my_peerid(void* ctx, - WakuCallBack callback, - void* userData); + int waku_get_my_peerid(void *ctx, + FFICallBack callback, + void *userData); -int waku_get_metrics(void* ctx, - WakuCallBack callback, - void* userData); + int waku_get_metrics(void *ctx, + FFICallBack callback, + void *userData); -int waku_peer_exchange_request(void* ctx, - int numPeers, - WakuCallBack callback, - void* userData); + int waku_peer_exchange_request(void *ctx, + FFICallBack callback, + void *userData, + int numPeers); -int waku_ping_peer(void* ctx, - const char* peerAddr, - int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_ping_peer(void *ctx, + FFICallBack callback, + void *userData, + const char *peerAddr, + int timeoutMs); -int waku_is_online(void* ctx, - WakuCallBack callback, - void* userData); + int waku_is_online(void *ctx, + FFICallBack callback, + void *userData); #ifdef __cplusplus } diff --git a/library/libwaku.nim b/library/libwaku.nim index ad3afa134..c71e823d6 100644 --- a/library/libwaku.nim +++ b/library/libwaku.nim @@ -1,107 +1,35 @@ -{.pragma: exported, exportc, cdecl, raises: [].} -{.pragma: callback, cdecl, raises: [], gcsafe.} -{.passc: "-fPIC".} - -when defined(linux): - {.passl: "-Wl,-soname,libwaku.so".} - -import std/[json, atomics, strformat, options, atomics] -import chronicles, chronos, chronos/threadsync +import std/[atomics, options, atomics, macros] +import chronicles, chronos, chronos/threadsync, ffi import - waku/common/base64, waku/waku_core/message/message, - waku/node/waku_node, - waku/node/peer_manager, waku/waku_core/topics/pubsub_topic, - waku/waku_core/subscription/push_handler, waku/waku_relay, ./events/json_message_event, - ./waku_context, - ./waku_thread_requests/requests/node_lifecycle_request, - ./waku_thread_requests/requests/peer_manager_request, - ./waku_thread_requests/requests/protocols/relay_request, - ./waku_thread_requests/requests/protocols/store_request, - ./waku_thread_requests/requests/protocols/lightpush_request, - ./waku_thread_requests/requests/protocols/filter_request, - ./waku_thread_requests/requests/debug_node_request, - ./waku_thread_requests/requests/discovery_request, - ./waku_thread_requests/requests/ping_request, - ./waku_thread_requests/waku_thread_request, - ./alloc, - ./ffi_types, - ../waku/factory/app_callbacks + ./events/json_topic_health_change_event, + ./events/json_connection_change_event, + ../waku/factory/app_callbacks, + waku/factory/waku, + waku/node/waku_node, + ./declare_lib ################################################################################ -### Wrapper around the waku node -################################################################################ - -################################################################################ -### Not-exported components - -template checkLibwakuParams*( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -) = - if not isNil(ctx): - ctx[].userData = userData - - if isNil(callback): - return RET_MISSING_CALLBACK - -proc handleRequest( - ctx: ptr WakuContext, - requestType: RequestType, - content: pointer, - callback: WakuCallBack, - userData: pointer, -): cint = - waku_context.sendRequestToWakuThread(ctx, requestType, content, callback, userData).isOkOr: - let msg = "libwaku error: " & $error - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - return RET_OK - -### End of not-exported components -################################################################################ - -################################################################################ -### Library setup - -# Every Nim library must have this function called - the name is derived from -# the `--nimMainPrefix` command line option -proc libwakuNimMain() {.importc.} - -# To control when the library has been initialized -var initialized: Atomic[bool] - -if defined(android): - # Redirect chronicles to Android System logs - when compiles(defaultChroniclesStream.outputs[0].writer): - defaultChroniclesStream.outputs[0].writer = proc( - logLevel: LogLevel, msg: LogOutputStr - ) {.raises: [].} = - echo logLevel, msg - -proc initializeLibrary() {.exported.} = - if not initialized.exchange(true): - ## Every Nim library needs to call `NimMain` once exactly, to initialize the Nim runtime. - ## Being `` the value given in the optional compilation flag --nimMainPrefix:yourprefix - libwakuNimMain() - when declared(setupForeignThreadGc): - setupForeignThreadGc() - when declared(nimGC_setStackBottom): - var locals {.volatile, noinit.}: pointer - locals = addr(locals) - nimGC_setStackBottom(locals) - -### End of library setup -################################################################################ +## Include different APIs, i.e. all procs with {.ffi.} pragma +include + ./kernel_api/peer_manager_api, + ./kernel_api/discovery_api, + ./kernel_api/node_lifecycle_api, + ./kernel_api/debug_node_api, + ./kernel_api/ping_api, + ./kernel_api/protocols/relay_api, + ./kernel_api/protocols/store_api, + ./kernel_api/protocols/lightpush_api, + ./kernel_api/protocols/filter_api ################################################################################ ### Exported procs proc waku_new( - configJson: cstring, callback: WakuCallback, userData: pointer + configJson: cstring, callback: FFICallback, userData: pointer ): pointer {.dynlib, exportc, cdecl.} = initializeLibrary() @@ -111,41 +39,50 @@ proc waku_new( return nil ## Create the Waku thread that will keep waiting for req from the main thread. - var ctx = waku_context.createWakuContext().valueOr: - let msg = "Error in createWakuContext: " & $error + var ctx = ffi.createFFIContext[Waku]().valueOr: + let msg = "Error in createFFIContext: " & $error callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) return nil ctx.userData = userData + proc onReceivedMessage(ctx: ptr FFIContext): WakuRelayHandler = + return proc(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async.} = + callEventCallback(ctx, "onReceivedMessage"): + $JsonMessageEvent.new(pubsubTopic, msg) + + proc onTopicHealthChange(ctx: ptr FFIContext): TopicHealthChangeHandler = + return proc(pubsubTopic: PubsubTopic, topicHealth: TopicHealth) {.async.} = + callEventCallback(ctx, "onTopicHealthChange"): + $JsonTopicHealthChangeEvent.new(pubsubTopic, topicHealth) + + proc onConnectionChange(ctx: ptr FFIContext): ConnectionChangeHandler = + return proc(peerId: PeerId, peerEvent: PeerEventKind) {.async.} = + callEventCallback(ctx, "onConnectionChange"): + $JsonConnectionChangeEvent.new($peerId, peerEvent) + let appCallbacks = AppCallbacks( relayHandler: onReceivedMessage(ctx), topicHealthChangeHandler: onTopicHealthChange(ctx), connectionChangeHandler: onConnectionChange(ctx), ) - let retCode = handleRequest( - ctx, - RequestType.LIFECYCLE, - NodeLifecycleRequest.createShared( - NodeLifecycleMsgType.CREATE_NODE, configJson, appCallbacks - ), - callback, - userData, - ) - - if retCode == RET_ERR: + ffi.sendRequestToFFIThread( + ctx, CreateNodeRequest.ffiNewReq(callback, userData, configJson, appCallbacks) + ).isOkOr: + let msg = "error in sendRequestToFFIThread: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) return nil return ctx proc waku_destroy( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +): cint {.dynlib, exportc, cdecl.} = initializeLibrary() - checkLibwakuParams(ctx, callback, userData) + checkParams(ctx, callback, userData) - waku_context.destroyWakuContext(ctx).isOkOr: + ffi.destroyFFIContext(ctx).isOkOr: let msg = "libwaku error: " & $error callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) return RET_ERR @@ -155,699 +92,5 @@ proc waku_destroy( return RET_OK -proc waku_version( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - callback( - RET_OK, - cast[ptr cchar](WakuNodeVersionString), - cast[csize_t](len(WakuNodeVersionString)), - userData, - ) - - return RET_OK - -proc waku_set_event_callback( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -) {.dynlib, exportc.} = - initializeLibrary() - ctx[].eventCallback = cast[pointer](callback) - ctx[].eventUserData = userData - -proc waku_content_topic( - ctx: ptr WakuContext, - appName: cstring, - appVersion: cuint, - contentTopicName: cstring, - encoding: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - # https://rfc.vac.dev/spec/36/#extern-char-waku_content_topicchar-applicationname-unsigned-int-applicationversion-char-contenttopicname-char-encoding - - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - let contentTopic = fmt"/{$appName}/{$appVersion}/{$contentTopicName}/{$encoding}" - callback( - RET_OK, unsafeAddr contentTopic[0], cast[csize_t](len(contentTopic)), userData - ) - - return RET_OK - -proc waku_pubsub_topic( - ctx: ptr WakuContext, topicName: cstring, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc, cdecl.} = - # https://rfc.vac.dev/spec/36/#extern-char-waku_pubsub_topicchar-name-char-encoding - - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - let outPubsubTopic = fmt"/waku/2/{$topicName}" - callback( - RET_OK, unsafeAddr outPubsubTopic[0], cast[csize_t](len(outPubsubTopic)), userData - ) - - return RET_OK - -proc waku_default_pubsub_topic( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - # https://rfc.vac.dev/spec/36/#extern-char-waku_default_pubsub_topic - - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - callback( - RET_OK, - cast[ptr cchar](DefaultPubsubTopic), - cast[csize_t](len(DefaultPubsubTopic)), - userData, - ) - - return RET_OK - -proc waku_relay_publish( - ctx: ptr WakuContext, - pubSubTopic: cstring, - jsonWakuMessage: cstring, - timeoutMs: cuint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - # https://rfc.vac.dev/spec/36/#extern-char-waku_relay_publishchar-messagejson-char-pubsubtopic-int-timeoutms - - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - var jsonMessage: JsonMessage - try: - let jsonContent = parseJson($jsonWakuMessage) - jsonMessage = JsonMessage.fromJsonNode(jsonContent).valueOr: - raise newException(JsonParsingError, $error) - except JsonParsingError: - let msg = fmt"Error parsing json message: {getCurrentExceptionMsg()}" - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - let wakuMessage = jsonMessage.toWakuMessage().valueOr: - let msg = "Problem building the WakuMessage: " & $error - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.PUBLISH, pubSubTopic, nil, wakuMessage), - callback, - userData, - ) - -proc waku_start( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - handleRequest( - ctx, - RequestType.LIFECYCLE, - NodeLifecycleRequest.createShared(NodeLifecycleMsgType.START_NODE), - callback, - userData, - ) - -proc waku_stop( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - handleRequest( - ctx, - RequestType.LIFECYCLE, - NodeLifecycleRequest.createShared(NodeLifecycleMsgType.STOP_NODE), - callback, - userData, - ) - -proc waku_relay_subscribe( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - var cb = onReceivedMessage(ctx) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.SUBSCRIBE, pubSubTopic, WakuRelayHandler(cb)), - callback, - userData, - ) - -proc waku_relay_add_protected_shard( - ctx: ptr WakuContext, - clusterId: cint, - shardId: cint, - publicKey: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared( - RelayMsgType.ADD_PROTECTED_SHARD, - clusterId = clusterId, - shardId = shardId, - publicKey = publicKey, - ), - callback, - userData, - ) - -proc waku_relay_unsubscribe( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared( - RelayMsgType.UNSUBSCRIBE, pubSubTopic, WakuRelayHandler(onReceivedMessage(ctx)) - ), - callback, - userData, - ) - -proc waku_relay_get_num_connected_peers( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.NUM_CONNECTED_PEERS, pubSubTopic), - callback, - userData, - ) - -proc waku_relay_get_connected_peers( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.LIST_CONNECTED_PEERS, pubSubTopic), - callback, - userData, - ) - -proc waku_relay_get_num_peers_in_mesh( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.NUM_MESH_PEERS, pubSubTopic), - callback, - userData, - ) - -proc waku_relay_get_peers_in_mesh( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.LIST_MESH_PEERS, pubSubTopic), - callback, - userData, - ) - -proc waku_filter_subscribe( - ctx: ptr WakuContext, - pubSubTopic: cstring, - contentTopics: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.FILTER, - FilterRequest.createShared( - FilterMsgType.SUBSCRIBE, - pubSubTopic, - contentTopics, - FilterPushHandler(onReceivedMessage(ctx)), - ), - callback, - userData, - ) - -proc waku_filter_unsubscribe( - ctx: ptr WakuContext, - pubSubTopic: cstring, - contentTopics: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.FILTER, - FilterRequest.createShared(FilterMsgType.UNSUBSCRIBE, pubSubTopic, contentTopics), - callback, - userData, - ) - -proc waku_filter_unsubscribe_all( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.FILTER, - FilterRequest.createShared(FilterMsgType.UNSUBSCRIBE_ALL), - callback, - userData, - ) - -proc waku_lightpush_publish( - ctx: ptr WakuContext, - pubSubTopic: cstring, - jsonWakuMessage: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - var jsonMessage: JsonMessage - try: - let jsonContent = parseJson($jsonWakuMessage) - jsonMessage = JsonMessage.fromJsonNode(jsonContent).valueOr: - raise newException(JsonParsingError, $error) - except JsonParsingError: - let msg = fmt"Error parsing json message: {getCurrentExceptionMsg()}" - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - let wakuMessage = jsonMessage.toWakuMessage().valueOr: - let msg = "Problem building the WakuMessage: " & $error - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - handleRequest( - ctx, - RequestType.LIGHTPUSH, - LightpushRequest.createShared(LightpushMsgType.PUBLISH, pubSubTopic, wakuMessage), - callback, - userData, - ) - -proc waku_connect( - ctx: ptr WakuContext, - peerMultiAddr: cstring, - timeoutMs: cuint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared( - PeerManagementMsgType.CONNECT_TO, $peerMultiAddr, chronos.milliseconds(timeoutMs) - ), - callback, - userData, - ) - -proc waku_disconnect_peer_by_id( - ctx: ptr WakuContext, peerId: cstring, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared( - op = PeerManagementMsgType.DISCONNECT_PEER_BY_ID, peerId = $peerId - ), - callback, - userData, - ) - -proc waku_disconnect_all_peers( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared(op = PeerManagementMsgType.DISCONNECT_ALL_PEERS), - callback, - userData, - ) - -proc waku_dial_peer( - ctx: ptr WakuContext, - peerMultiAddr: cstring, - protocol: cstring, - timeoutMs: cuint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared( - op = PeerManagementMsgType.DIAL_PEER, - peerMultiAddr = $peerMultiAddr, - protocol = $protocol, - ), - callback, - userData, - ) - -proc waku_dial_peer_by_id( - ctx: ptr WakuContext, - peerId: cstring, - protocol: cstring, - timeoutMs: cuint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared( - op = PeerManagementMsgType.DIAL_PEER_BY_ID, peerId = $peerId, protocol = $protocol - ), - callback, - userData, - ) - -proc waku_get_peerids_from_peerstore( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared(PeerManagementMsgType.GET_ALL_PEER_IDS), - callback, - userData, - ) - -proc waku_get_connected_peers_info( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared(PeerManagementMsgType.GET_CONNECTED_PEERS_INFO), - callback, - userData, - ) - -proc waku_get_connected_peers( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared(PeerManagementMsgType.GET_CONNECTED_PEERS), - callback, - userData, - ) - -proc waku_get_peerids_by_protocol( - ctx: ptr WakuContext, protocol: cstring, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared( - op = PeerManagementMsgType.GET_PEER_IDS_BY_PROTOCOL, protocol = $protocol - ), - callback, - userData, - ) - -proc waku_store_query( - ctx: ptr WakuContext, - jsonQuery: cstring, - peerAddr: cstring, - timeoutMs: cint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.STORE, - StoreRequest.createShared(StoreReqType.REMOTE_QUERY, jsonQuery, peerAddr, timeoutMs), - callback, - userData, - ) - -proc waku_listen_addresses( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.RETRIEVE_LISTENING_ADDRESSES), - callback, - userData, - ) - -proc waku_dns_discovery( - ctx: ptr WakuContext, - entTreeUrl: cstring, - nameDnsServer: cstring, - timeoutMs: cint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DISCOVERY, - DiscoveryRequest.createRetrieveBootstrapNodesRequest( - DiscoveryMsgType.GET_BOOTSTRAP_NODES, entTreeUrl, nameDnsServer, timeoutMs - ), - callback, - userData, - ) - -proc waku_discv5_update_bootnodes( - ctx: ptr WakuContext, bootnodes: cstring, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - ## Updates the bootnode list used for discovering new peers via DiscoveryV5 - ## bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]` - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DISCOVERY, - DiscoveryRequest.createUpdateBootstrapNodesRequest( - DiscoveryMsgType.UPDATE_DISCV5_BOOTSTRAP_NODES, bootnodes - ), - callback, - userData, - ) - -proc waku_get_my_enr( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.RETRIEVE_MY_ENR), - callback, - userData, - ) - -proc waku_get_my_peerid( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.RETRIEVE_MY_PEER_ID), - callback, - userData, - ) - -proc waku_get_metrics( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.RETRIEVE_METRICS), - callback, - userData, - ) - -proc waku_start_discv5( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DISCOVERY, - DiscoveryRequest.createDiscV5StartRequest(), - callback, - userData, - ) - -proc waku_stop_discv5( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DISCOVERY, - DiscoveryRequest.createDiscV5StopRequest(), - callback, - userData, - ) - -proc waku_peer_exchange_request( - ctx: ptr WakuContext, numPeers: uint64, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DISCOVERY, - DiscoveryRequest.createPeerExchangeRequest(numPeers), - callback, - userData, - ) - -proc waku_ping_peer( - ctx: ptr WakuContext, - peerAddr: cstring, - timeoutMs: cuint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PING, - PingRequest.createShared(peerAddr, chronos.milliseconds(timeoutMs)), - callback, - userData, - ) - -proc waku_is_online( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.RETRIEVE_ONLINE_STATE), - callback, - userData, - ) - -### End of exported procs -################################################################################ +# ### End of exported procs +# ################################################################################ diff --git a/library/waku_context.nim b/library/waku_context.nim deleted file mode 100644 index ab4b996af..000000000 --- a/library/waku_context.nim +++ /dev/null @@ -1,223 +0,0 @@ -{.pragma: exported, exportc, cdecl, raises: [].} -{.pragma: callback, cdecl, raises: [], gcsafe.} -{.passc: "-fPIC".} - -import std/[options, atomics, os, net, locks] -import chronicles, chronos, chronos/threadsync, taskpools/channels_spsc_single, results -import - waku/common/logging, - waku/factory/waku, - waku/node/peer_manager, - waku/waku_relay/[protocol, topic_health], - waku/waku_core/[topics/pubsub_topic, message], - ./waku_thread_requests/[waku_thread_request, requests/debug_node_request], - ./ffi_types, - ./events/[ - json_message_event, json_topic_health_change_event, json_connection_change_event, - json_waku_not_responding_event, - ] - -type WakuContext* = object - wakuThread: Thread[(ptr WakuContext)] - watchdogThread: Thread[(ptr WakuContext)] - # monitors the Waku thread and notifies the Waku SDK consumer if it hangs - lock: Lock - reqChannel: ChannelSPSCSingle[ptr WakuThreadRequest] - reqSignal: ThreadSignalPtr - # to inform The Waku Thread (a.k.a TWT) that a new request is sent - reqReceivedSignal: ThreadSignalPtr - # to inform the main thread that the request is rx by TWT - userData*: pointer - eventCallback*: pointer - eventUserdata*: pointer - running: Atomic[bool] # To control when the threads are running - -const git_version* {.strdefine.} = "n/a" -const versionString = "version / git commit hash: " & waku.git_version - -template callEventCallback(ctx: ptr WakuContext, eventName: string, body: untyped) = - if isNil(ctx[].eventCallback): - error eventName & " - eventCallback is nil" - return - - foreignThreadGc: - try: - let event = body - cast[WakuCallBack](ctx[].eventCallback)( - RET_OK, unsafeAddr event[0], cast[csize_t](len(event)), ctx[].eventUserData - ) - except Exception, CatchableError: - let msg = - "Exception " & eventName & " when calling 'eventCallBack': " & - getCurrentExceptionMsg() - cast[WakuCallBack](ctx[].eventCallback)( - RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), ctx[].eventUserData - ) - -proc onConnectionChange*(ctx: ptr WakuContext): ConnectionChangeHandler = - return proc(peerId: PeerId, peerEvent: PeerEventKind) {.async.} = - callEventCallback(ctx, "onConnectionChange"): - $JsonConnectionChangeEvent.new($peerId, peerEvent) - -proc onReceivedMessage*(ctx: ptr WakuContext): WakuRelayHandler = - return proc(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async.} = - callEventCallback(ctx, "onReceivedMessage"): - $JsonMessageEvent.new(pubsubTopic, msg) - -proc onTopicHealthChange*(ctx: ptr WakuContext): TopicHealthChangeHandler = - return proc(pubsubTopic: PubsubTopic, topicHealth: TopicHealth) {.async.} = - callEventCallback(ctx, "onTopicHealthChange"): - $JsonTopicHealthChangeEvent.new(pubsubTopic, topicHealth) - -proc onWakuNotResponding*(ctx: ptr WakuContext) = - callEventCallback(ctx, "onWakuNotResponsive"): - $JsonWakuNotRespondingEvent.new() - -proc sendRequestToWakuThread*( - ctx: ptr WakuContext, - reqType: RequestType, - reqContent: pointer, - callback: WakuCallBack, - userData: pointer, - timeout = InfiniteDuration, -): Result[void, string] = - ctx.lock.acquire() - # This lock is only necessary while we use a SP Channel and while the signalling - # between threads assumes that there aren't concurrent requests. - # Rearchitecting the signaling + migrating to a MP Channel will allow us to receive - # requests concurrently and spare us the need of locks - defer: - ctx.lock.release() - - let req = WakuThreadRequest.createShared(reqType, reqContent, callback, userData) - ## Sending the request - let sentOk = ctx.reqChannel.trySend(req) - if not sentOk: - deallocShared(req) - return err("Couldn't send a request to the waku thread: " & $req[]) - - let fireSync = ctx.reqSignal.fireSync().valueOr: - deallocShared(req) - return err("failed fireSync: " & $error) - - if not fireSync: - deallocShared(req) - return err("Couldn't fireSync in time") - - ## wait until the Waku Thread properly received the request - ctx.reqReceivedSignal.waitSync(timeout).isOkOr: - deallocShared(req) - return err("Couldn't receive reqReceivedSignal signal") - - ## Notice that in case of "ok", the deallocShared(req) is performed by the Waku Thread in the - ## process proc. See the 'waku_thread_request.nim' module for more details. - ok() - -proc watchdogThreadBody(ctx: ptr WakuContext) {.thread.} = - ## Watchdog thread that monitors the Waku thread and notifies the library user if it hangs. - - let watchdogRun = proc(ctx: ptr WakuContext) {.async.} = - const WatchdogStartDelay = 10.seconds - const WatchdogTimeinterval = 1.seconds - const WakuNotRespondingTimeout = 3.seconds - - # Give time for the node to be created and up before sending watchdog requests - await sleepAsync(WatchdogStartDelay) - while true: - await sleepAsync(WatchdogTimeinterval) - - if ctx.running.load == false: - info "Watchdog thread exiting because WakuContext is not running" - break - - let wakuCallback = proc( - callerRet: cint, msg: ptr cchar, len: csize_t, userData: pointer - ) {.cdecl, gcsafe, raises: [].} = - discard ## Don't do anything. Just respecting the callback signature. - const nilUserData = nil - - trace "Sending watchdog request to Waku thread" - - sendRequestToWakuThread( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.CHECK_WAKU_NOT_BLOCKED), - wakuCallback, - nilUserData, - WakuNotRespondingTimeout, - ).isOkOr: - error "Failed to send watchdog request to Waku thread", error = $error - onWakuNotResponding(ctx) - - waitFor watchdogRun(ctx) - -proc wakuThreadBody(ctx: ptr WakuContext) {.thread.} = - ## Waku thread that attends library user requests (stop, connect_to, etc.) - - logging.setupLog(logging.LogLevel.DEBUG, logging.LogFormat.TEXT) - - let wakuRun = proc(ctx: ptr WakuContext) {.async.} = - var waku: Waku - while true: - await ctx.reqSignal.wait() - - if ctx.running.load == false: - break - - ## Trying to get a request from the libwaku requestor thread - var request: ptr WakuThreadRequest - let recvOk = ctx.reqChannel.tryRecv(request) - if not recvOk: - error "waku thread could not receive a request" - continue - - ## Handle the request - asyncSpawn WakuThreadRequest.process(request, addr waku) - - ctx.reqReceivedSignal.fireSync().isOkOr: - error "could not fireSync back to requester thread", error = error - - waitFor wakuRun(ctx) - -proc createWakuContext*(): Result[ptr WakuContext, string] = - ## This proc is called from the main thread and it creates - ## the Waku working thread. - var ctx = createShared(WakuContext, 1) - ctx.reqSignal = ThreadSignalPtr.new().valueOr: - return err("couldn't create reqSignal ThreadSignalPtr") - ctx.reqReceivedSignal = ThreadSignalPtr.new().valueOr: - return err("couldn't create reqReceivedSignal ThreadSignalPtr") - ctx.lock.initLock() - - ctx.running.store(true) - - try: - createThread(ctx.wakuThread, wakuThreadBody, ctx) - except ValueError, ResourceExhaustedError: - freeShared(ctx) - return err("failed to create the Waku thread: " & getCurrentExceptionMsg()) - - try: - createThread(ctx.watchdogThread, watchdogThreadBody, ctx) - except ValueError, ResourceExhaustedError: - freeShared(ctx) - return err("failed to create the watchdog thread: " & getCurrentExceptionMsg()) - - return ok(ctx) - -proc destroyWakuContext*(ctx: ptr WakuContext): Result[void, string] = - ctx.running.store(false) - - let signaledOnTime = ctx.reqSignal.fireSync().valueOr: - return err("error in destroyWakuContext: " & $error) - if not signaledOnTime: - return err("failed to signal reqSignal on time in destroyWakuContext") - - joinThread(ctx.wakuThread) - joinThread(ctx.watchdogThread) - ctx.lock.deinitLock() - ?ctx.reqSignal.close() - ?ctx.reqReceivedSignal.close() - freeShared(ctx) - - return ok() diff --git a/library/waku_thread_requests/requests/debug_node_request.nim b/library/waku_thread_requests/requests/debug_node_request.nim deleted file mode 100644 index c9aa5a743..000000000 --- a/library/waku_thread_requests/requests/debug_node_request.nim +++ /dev/null @@ -1,63 +0,0 @@ -import std/json -import - chronicles, - chronos, - results, - eth/p2p/discoveryv5/enr, - strutils, - libp2p/peerid, - metrics -import - ../../../waku/factory/waku, - ../../../waku/node/waku_node, - ../../../waku/node/health_monitor - -type DebugNodeMsgType* = enum - RETRIEVE_LISTENING_ADDRESSES - RETRIEVE_MY_ENR - RETRIEVE_MY_PEER_ID - RETRIEVE_METRICS - RETRIEVE_ONLINE_STATE - CHECK_WAKU_NOT_BLOCKED - -type DebugNodeRequest* = object - operation: DebugNodeMsgType - -proc createShared*(T: type DebugNodeRequest, op: DebugNodeMsgType): ptr type T = - var ret = createShared(T) - ret[].operation = op - return ret - -proc destroyShared(self: ptr DebugNodeRequest) = - deallocShared(self) - -proc getMultiaddresses(node: WakuNode): seq[string] = - return node.info().listenAddresses - -proc getMetrics(): string = - {.gcsafe.}: - return defaultRegistry.toText() ## defaultRegistry is {.global.} in metrics module - -proc process*( - self: ptr DebugNodeRequest, waku: Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - case self.operation - of RETRIEVE_LISTENING_ADDRESSES: - ## returns a comma-separated string of the listen addresses - return ok(waku.node.getMultiaddresses().join(",")) - of RETRIEVE_MY_ENR: - return ok(waku.node.enr.toURI()) - of RETRIEVE_MY_PEER_ID: - return ok($waku.node.peerId()) - of RETRIEVE_METRICS: - return ok(getMetrics()) - of RETRIEVE_ONLINE_STATE: - return ok($waku.healthMonitor.onlineMonitor.amIOnline()) - of CHECK_WAKU_NOT_BLOCKED: - return ok("waku thread is not blocked") - - error "unsupported operation in DebugNodeRequest" - return err("unsupported operation in DebugNodeRequest") diff --git a/library/waku_thread_requests/requests/discovery_request.nim b/library/waku_thread_requests/requests/discovery_request.nim deleted file mode 100644 index 405483a46..000000000 --- a/library/waku_thread_requests/requests/discovery_request.nim +++ /dev/null @@ -1,151 +0,0 @@ -import std/json -import chronos, chronicles, results, strutils, libp2p/multiaddress -import - ../../../waku/factory/waku, - ../../../waku/discovery/waku_dnsdisc, - ../../../waku/discovery/waku_discv5, - ../../../waku/waku_core/peers, - ../../../waku/node/waku_node, - ../../../waku/node/kernel_api, - ../../alloc - -type DiscoveryMsgType* = enum - GET_BOOTSTRAP_NODES - UPDATE_DISCV5_BOOTSTRAP_NODES - START_DISCV5 - STOP_DISCV5 - PEER_EXCHANGE - -type DiscoveryRequest* = object - operation: DiscoveryMsgType - - ## used in GET_BOOTSTRAP_NODES - enrTreeUrl: cstring - nameDnsServer: cstring - timeoutMs: cint - - ## used in UPDATE_DISCV5_BOOTSTRAP_NODES - nodes: cstring - - ## used in PEER_EXCHANGE - numPeers: uint64 - -proc createShared( - T: type DiscoveryRequest, - op: DiscoveryMsgType, - enrTreeUrl: cstring, - nameDnsServer: cstring, - timeoutMs: cint, - nodes: cstring, - numPeers: uint64, -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].enrTreeUrl = enrTreeUrl.alloc() - ret[].nameDnsServer = nameDnsServer.alloc() - ret[].timeoutMs = timeoutMs - ret[].nodes = nodes.alloc() - ret[].numPeers = numPeers - return ret - -proc createRetrieveBootstrapNodesRequest*( - T: type DiscoveryRequest, - op: DiscoveryMsgType, - enrTreeUrl: cstring, - nameDnsServer: cstring, - timeoutMs: cint, -): ptr type T = - return T.createShared(op, enrTreeUrl, nameDnsServer, timeoutMs, "", 0) - -proc createUpdateBootstrapNodesRequest*( - T: type DiscoveryRequest, op: DiscoveryMsgType, nodes: cstring -): ptr type T = - return T.createShared(op, "", "", 0, nodes, 0) - -proc createDiscV5StartRequest*(T: type DiscoveryRequest): ptr type T = - return T.createShared(START_DISCV5, "", "", 0, "", 0) - -proc createDiscV5StopRequest*(T: type DiscoveryRequest): ptr type T = - return T.createShared(STOP_DISCV5, "", "", 0, "", 0) - -proc createPeerExchangeRequest*( - T: type DiscoveryRequest, numPeers: uint64 -): ptr type T = - return T.createShared(PEER_EXCHANGE, "", "", 0, "", numPeers) - -proc destroyShared(self: ptr DiscoveryRequest) = - deallocShared(self[].enrTreeUrl) - deallocShared(self[].nameDnsServer) - deallocShared(self[].nodes) - deallocShared(self) - -proc retrieveBootstrapNodes( - enrTreeUrl: string, ipDnsServer: string -): Future[Result[seq[string], string]] {.async.} = - let dnsNameServers = @[parseIpAddress(ipDnsServer)] - let discoveredPeers: seq[RemotePeerInfo] = ( - await retrieveDynamicBootstrapNodes(enrTreeUrl, dnsNameServers) - ).valueOr: - return err("failed discovering peers from DNS: " & $error) - - var multiAddresses = newSeq[string]() - - for discPeer in discoveredPeers: - for address in discPeer.addrs: - multiAddresses.add($address & "/p2p/" & $discPeer) - - return ok(multiAddresses) - -proc updateDiscv5BootstrapNodes(nodes: string, waku: ptr Waku): Result[void, string] = - waku.wakuDiscv5.updateBootstrapRecords(nodes).isOkOr: - return err("error in updateDiscv5BootstrapNodes: " & $error) - return ok() - -proc performPeerExchangeRequestTo( - numPeers: uint64, waku: ptr Waku -): Future[Result[int, string]] {.async.} = - let numPeersRecv = (await waku.node.fetchPeerExchangePeers(numPeers)).valueOr: - return err($error) - return ok(numPeersRecv) - -proc process*( - self: ptr DiscoveryRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - case self.operation - of START_DISCV5: - let res = await waku.wakuDiscv5.start() - res.isOkOr: - error "START_DISCV5 failed", error = error - return err($error) - - return ok("discv5 started correctly") - of STOP_DISCV5: - await waku.wakuDiscv5.stop() - - return ok("discv5 stopped correctly") - of GET_BOOTSTRAP_NODES: - let nodes = ( - await retrieveBootstrapNodes($self[].enrTreeUrl, $self[].nameDnsServer) - ).valueOr: - error "GET_BOOTSTRAP_NODES failed", error = error - return err($error) - - ## returns a comma-separated string of bootstrap nodes' multiaddresses - return ok(nodes.join(",")) - of UPDATE_DISCV5_BOOTSTRAP_NODES: - updateDiscv5BootstrapNodes($self[].nodes, waku).isOkOr: - error "UPDATE_DISCV5_BOOTSTRAP_NODES failed", error = error - return err($error) - - return ok("discovery request processed correctly") - of PEER_EXCHANGE: - let numValidPeers = (await performPeerExchangeRequestTo(self[].numPeers, waku)).valueOr: - error "PEER_EXCHANGE failed", error = error - return err($error) - return ok($numValidPeers) - - error "discovery request not handled" - return err("discovery request not handled") diff --git a/library/waku_thread_requests/requests/peer_manager_request.nim b/library/waku_thread_requests/requests/peer_manager_request.nim deleted file mode 100644 index cac5ca30e..000000000 --- a/library/waku_thread_requests/requests/peer_manager_request.nim +++ /dev/null @@ -1,135 +0,0 @@ -import std/[sequtils, strutils, tables] -import chronicles, chronos, results, options, json -import - ../../../waku/factory/waku, - ../../../waku/node/waku_node, - ../../alloc, - ../../../waku/node/peer_manager - -type PeerManagementMsgType* {.pure.} = enum - CONNECT_TO - GET_ALL_PEER_IDS - GET_CONNECTED_PEERS_INFO - GET_PEER_IDS_BY_PROTOCOL - DISCONNECT_PEER_BY_ID - DISCONNECT_ALL_PEERS - DIAL_PEER - DIAL_PEER_BY_ID - GET_CONNECTED_PEERS - -type PeerManagementRequest* = object - operation: PeerManagementMsgType - peerMultiAddr: cstring - dialTimeout: Duration - protocol: cstring - peerId: cstring - -type PeerInfo = object - protocols: seq[string] - addresses: seq[string] - -proc createShared*( - T: type PeerManagementRequest, - op: PeerManagementMsgType, - peerMultiAddr = "", - dialTimeout = chronos.milliseconds(0), ## arbitrary Duration as not all ops needs dialTimeout - peerId = "", - protocol = "", -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].peerMultiAddr = peerMultiAddr.alloc() - ret[].peerId = peerId.alloc() - ret[].protocol = protocol.alloc() - ret[].dialTimeout = dialTimeout - return ret - -proc destroyShared(self: ptr PeerManagementRequest) = - if not isNil(self[].peerMultiAddr): - deallocShared(self[].peerMultiAddr) - - if not isNil(self[].peerId): - deallocShared(self[].peerId) - - if not isNil(self[].protocol): - deallocShared(self[].protocol) - - deallocShared(self) - -proc process*( - self: ptr PeerManagementRequest, waku: Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - case self.operation - of CONNECT_TO: - let peers = ($self[].peerMultiAddr).split(",").mapIt(strip(it)) - await waku.node.connectToNodes(peers, source = "static") - return ok("") - of GET_ALL_PEER_IDS: - ## returns a comma-separated string of peerIDs - let peerIDs = - waku.node.peerManager.switch.peerStore.peers().mapIt($it.peerId).join(",") - return ok(peerIDs) - of GET_CONNECTED_PEERS_INFO: - ## returns a JSON string mapping peerIDs to objects with protocols and addresses - - var peersMap = initTable[string, PeerInfo]() - let peers = waku.node.peerManager.switch.peerStore.peers().filterIt( - it.connectedness == Connected - ) - - # Build a map of peer IDs to peer info objects - for peer in peers: - let peerIdStr = $peer.peerId - peersMap[peerIdStr] = - PeerInfo(protocols: peer.protocols, addresses: peer.addrs.mapIt($it)) - - # Convert the map to JSON string - let jsonObj = %*peersMap - let jsonStr = $jsonObj - return ok(jsonStr) - of GET_PEER_IDS_BY_PROTOCOL: - ## returns a comma-separated string of peerIDs that mount the given protocol - let connectedPeers = waku.node.peerManager.switch.peerStore - .peers($self[].protocol) - .filterIt(it.connectedness == Connected) - .mapIt($it.peerId) - .join(",") - return ok(connectedPeers) - of DISCONNECT_PEER_BY_ID: - let peerId = PeerId.init($self[].peerId).valueOr: - error "DISCONNECT_PEER_BY_ID failed", error = $error - return err($error) - await waku.node.peerManager.disconnectNode(peerId) - return ok("") - of DISCONNECT_ALL_PEERS: - await waku.node.peerManager.disconnectAllPeers() - return ok("") - of DIAL_PEER: - let remotePeerInfo = parsePeerInfo($self[].peerMultiAddr).valueOr: - error "DIAL_PEER failed", error = $error - return err($error) - let conn = await waku.node.peerManager.dialPeer(remotePeerInfo, $self[].protocol) - if conn.isNone(): - let msg = "failed dialing peer" - error "DIAL_PEER failed", error = msg, peerId = $remotePeerInfo.peerId - return err(msg) - of DIAL_PEER_BY_ID: - let peerId = PeerId.init($self[].peerId).valueOr: - error "DIAL_PEER_BY_ID failed", error = $error - return err($error) - let conn = await waku.node.peerManager.dialPeer(peerId, $self[].protocol) - if conn.isNone(): - let msg = "failed dialing peer" - error "DIAL_PEER_BY_ID failed", error = msg, peerId = $peerId - return err(msg) - of GET_CONNECTED_PEERS: - ## returns a comma-separated string of peerIDs - let - (inPeerIds, outPeerIds) = waku.node.peerManager.connectedPeers() - connectedPeerids = concat(inPeerIds, outPeerIds) - return ok(connectedPeerids.mapIt($it).join(",")) - - return ok("") diff --git a/library/waku_thread_requests/requests/ping_request.nim b/library/waku_thread_requests/requests/ping_request.nim deleted file mode 100644 index 716b9ed68..000000000 --- a/library/waku_thread_requests/requests/ping_request.nim +++ /dev/null @@ -1,54 +0,0 @@ -import std/[json, strutils] -import chronos, results -import libp2p/[protocols/ping, switch, multiaddress, multicodec] -import ../../../waku/[factory/waku, waku_core/peers, node/waku_node], ../../alloc - -type PingRequest* = object - peerAddr: cstring - timeout: Duration - -proc createShared*( - T: type PingRequest, peerAddr: cstring, timeout: Duration -): ptr type T = - var ret = createShared(T) - ret[].peerAddr = peerAddr.alloc() - ret[].timeout = timeout - return ret - -proc destroyShared(self: ptr PingRequest) = - deallocShared(self[].peerAddr) - deallocShared(self) - -proc process*( - self: ptr PingRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - let peerInfo = peers.parsePeerInfo(($self[].peerAddr).split(",")).valueOr: - return err("PingRequest failed to parse peer addr: " & $error) - - proc ping(): Future[Result[Duration, string]] {.async, gcsafe.} = - try: - let conn = await waku.node.switch.dial(peerInfo.peerId, peerInfo.addrs, PingCodec) - defer: - await conn.close() - - let pingRTT = await waku.node.libp2pPing.ping(conn) - if pingRTT == 0.nanos: - return err("could not ping peer: rtt-0") - return ok(pingRTT) - except CatchableError: - return err("could not ping peer: " & getCurrentExceptionMsg()) - - let pingFuture = ping() - let pingRTT: Duration = - if self[].timeout == chronos.milliseconds(0): # No timeout expected - ?(await pingFuture) - else: - let timedOut = not (await pingFuture.withTimeout(self[].timeout)) - if timedOut: - return err("ping timed out") - ?(pingFuture.read()) - - ok($(pingRTT.nanos)) diff --git a/library/waku_thread_requests/requests/protocols/filter_request.nim b/library/waku_thread_requests/requests/protocols/filter_request.nim deleted file mode 100644 index cd401d443..000000000 --- a/library/waku_thread_requests/requests/protocols/filter_request.nim +++ /dev/null @@ -1,106 +0,0 @@ -import options, std/[strutils, sequtils] -import chronicles, chronos, results -import - ../../../../waku/waku_filter_v2/client, - ../../../../waku/waku_core/message/message, - ../../../../waku/factory/waku, - ../../../../waku/waku_filter_v2/common, - ../../../../waku/waku_core/subscription/push_handler, - ../../../../waku/node/peer_manager/peer_manager, - ../../../../waku/node/waku_node, - ../../../../waku/node/kernel_api, - ../../../../waku/waku_core/topics/pubsub_topic, - ../../../../waku/waku_core/topics/content_topic, - ../../../alloc - -type FilterMsgType* = enum - SUBSCRIBE - UNSUBSCRIBE - UNSUBSCRIBE_ALL - -type FilterRequest* = object - operation: FilterMsgType - pubsubTopic: cstring - contentTopics: cstring ## comma-separated list of content-topics - filterPushEventCallback: FilterPushHandler ## handles incoming filter pushed msgs - -proc createShared*( - T: type FilterRequest, - op: FilterMsgType, - pubsubTopic: cstring = "", - contentTopics: cstring = "", - filterPushEventCallback: FilterPushHandler = nil, -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].pubsubTopic = pubsubTopic.alloc() - ret[].contentTopics = contentTopics.alloc() - ret[].filterPushEventCallback = filterPushEventCallback - - return ret - -proc destroyShared(self: ptr FilterRequest) = - deallocShared(self[].pubsubTopic) - deallocShared(self[].contentTopics) - deallocShared(self) - -proc process*( - self: ptr FilterRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - const FilterOpTimeout = 5.seconds - if waku.node.wakuFilterClient.isNil(): - let errorMsg = "FilterRequest waku.node.wakuFilterClient is nil" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - - case self.operation - of SUBSCRIBE: - waku.node.wakuFilterClient.registerPushHandler(self.filterPushEventCallback) - - let peer = waku.node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: - let errorMsg = - "could not find peer with WakuFilterSubscribeCodec when subscribing" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - - let pubsubTopic = some(PubsubTopic($self[].pubsubTopic)) - let contentTopics = ($(self[].contentTopics)).split(",").mapIt(ContentTopic(it)) - - let subFut = waku.node.filterSubscribe(pubsubTopic, contentTopics, peer) - if not await subFut.withTimeout(FilterOpTimeout): - let errorMsg = "filter subscription timed out" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - of UNSUBSCRIBE: - let peer = waku.node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: - let errorMsg = - "could not find peer with WakuFilterSubscribeCodec when unsubscribing" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - - let pubsubTopic = some(PubsubTopic($self[].pubsubTopic)) - let contentTopics = ($(self[].contentTopics)).split(",").mapIt(ContentTopic(it)) - - let subFut = waku.node.filterUnsubscribe(pubsubTopic, contentTopics, peer) - if not await subFut.withTimeout(FilterOpTimeout): - let errorMsg = "filter un-subscription timed out" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - of UNSUBSCRIBE_ALL: - let peer = waku.node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: - let errorMsg = - "could not find peer with WakuFilterSubscribeCodec when unsubscribing all" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - - let unsubFut = waku.node.filterUnsubscribeAll(peer) - - if not await unsubFut.withTimeout(FilterOpTimeout): - let errorMsg = "filter un-subscription all timed out" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - - return ok("") diff --git a/library/waku_thread_requests/requests/protocols/lightpush_request.nim b/library/waku_thread_requests/requests/protocols/lightpush_request.nim deleted file mode 100644 index bc3d9de2c..000000000 --- a/library/waku_thread_requests/requests/protocols/lightpush_request.nim +++ /dev/null @@ -1,109 +0,0 @@ -import options -import chronicles, chronos, results -import - ../../../../waku/waku_core/message/message, - ../../../../waku/waku_core/codecs, - ../../../../waku/factory/waku, - ../../../../waku/waku_core/message, - ../../../../waku/waku_core/time, # Timestamp - ../../../../waku/waku_core/topics/pubsub_topic, - ../../../../waku/waku_lightpush_legacy/client, - ../../../../waku/waku_lightpush_legacy/common, - ../../../../waku/node/peer_manager/peer_manager, - ../../../alloc - -type LightpushMsgType* = enum - PUBLISH - -type ThreadSafeWakuMessage* = object - payload: SharedSeq[byte] - contentTopic: cstring - meta: SharedSeq[byte] - version: uint32 - timestamp: Timestamp - ephemeral: bool - when defined(rln): - proof: SharedSeq[byte] - -type LightpushRequest* = object - operation: LightpushMsgType - pubsubTopic: cstring - message: ThreadSafeWakuMessage # only used in 'PUBLISH' requests - -proc createShared*( - T: type LightpushRequest, - op: LightpushMsgType, - pubsubTopic: cstring, - m = WakuMessage(), -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].pubsubTopic = pubsubTopic.alloc() - ret[].message = ThreadSafeWakuMessage( - payload: allocSharedSeq(m.payload), - contentTopic: m.contentTopic.alloc(), - meta: allocSharedSeq(m.meta), - version: m.version, - timestamp: m.timestamp, - ephemeral: m.ephemeral, - ) - when defined(rln): - ret[].message.proof = allocSharedSeq(m.proof) - - return ret - -proc destroyShared(self: ptr LightpushRequest) = - deallocSharedSeq(self[].message.payload) - deallocShared(self[].message.contentTopic) - deallocSharedSeq(self[].message.meta) - when defined(rln): - deallocSharedSeq(self[].message.proof) - - deallocShared(self) - -proc toWakuMessage(m: ThreadSafeWakuMessage): WakuMessage = - var wakuMessage = WakuMessage() - - wakuMessage.payload = m.payload.toSeq() - wakuMessage.contentTopic = $m.contentTopic - wakuMessage.meta = m.meta.toSeq() - wakuMessage.version = m.version - wakuMessage.timestamp = m.timestamp - wakuMessage.ephemeral = m.ephemeral - - when defined(rln): - wakuMessage.proof = m.proof - - return wakuMessage - -proc process*( - self: ptr LightpushRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - case self.operation - of PUBLISH: - let msg = self.message.toWakuMessage() - let pubsubTopic = $self.pubsubTopic - - if waku.node.wakuLightpushClient.isNil(): - let errorMsg = "LightpushRequest waku.node.wakuLightpushClient is nil" - error "PUBLISH failed", error = errorMsg - return err(errorMsg) - - let peerOpt = waku.node.peerManager.selectPeer(WakuLightPushCodec) - if peerOpt.isNone(): - let errorMsg = "failed to lightpublish message, no suitable remote peers" - error "PUBLISH failed", error = errorMsg - return err(errorMsg) - - let msgHashHex = ( - await waku.node.wakuLegacyLightpushClient.publish( - pubsubTopic, msg, peer = peerOpt.get() - ) - ).valueOr: - error "PUBLISH failed", error = error - return err($error) - - return ok(msgHashHex) diff --git a/library/waku_thread_requests/requests/protocols/relay_request.nim b/library/waku_thread_requests/requests/protocols/relay_request.nim deleted file mode 100644 index e110f689e..000000000 --- a/library/waku_thread_requests/requests/protocols/relay_request.nim +++ /dev/null @@ -1,168 +0,0 @@ -import std/[net, sequtils, strutils] -import chronicles, chronos, stew/byteutils, results -import - waku/waku_core/message/message, - waku/factory/[validator_signed, waku], - tools/confutils/cli_args, - waku/waku_node, - waku/waku_core/message, - waku/waku_core/time, # Timestamp - waku/waku_core/topics/pubsub_topic, - waku/waku_core/topics, - waku/waku_relay/protocol, - waku/node/peer_manager - -import - ../../../alloc - -type RelayMsgType* = enum - SUBSCRIBE - UNSUBSCRIBE - PUBLISH - NUM_CONNECTED_PEERS - LIST_CONNECTED_PEERS - ## to return the list of all connected peers to an specific pubsub topic - NUM_MESH_PEERS - LIST_MESH_PEERS - ## to return the list of only the peers that conform the mesh for a particular pubsub topic - ADD_PROTECTED_SHARD ## Protects a shard with a public key - -type ThreadSafeWakuMessage* = object - payload: SharedSeq[byte] - contentTopic: cstring - meta: SharedSeq[byte] - version: uint32 - timestamp: Timestamp - ephemeral: bool - when defined(rln): - proof: SharedSeq[byte] - -type RelayRequest* = object - operation: RelayMsgType - pubsubTopic: cstring - relayEventCallback: WakuRelayHandler # not used in 'PUBLISH' requests - message: ThreadSafeWakuMessage # only used in 'PUBLISH' requests - clusterId: cint # only used in 'ADD_PROTECTED_SHARD' requests - shardId: cint # only used in 'ADD_PROTECTED_SHARD' requests - publicKey: cstring # only used in 'ADD_PROTECTED_SHARD' requests - -proc createShared*( - T: type RelayRequest, - op: RelayMsgType, - pubsubTopic: cstring = nil, - relayEventCallback: WakuRelayHandler = nil, - m = WakuMessage(), - clusterId: cint = 0, - shardId: cint = 0, - publicKey: cstring = nil, -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].pubsubTopic = pubsubTopic.alloc() - ret[].clusterId = clusterId - ret[].shardId = shardId - ret[].publicKey = publicKey.alloc() - ret[].relayEventCallback = relayEventCallback - ret[].message = ThreadSafeWakuMessage( - payload: allocSharedSeq(m.payload), - contentTopic: m.contentTopic.alloc(), - meta: allocSharedSeq(m.meta), - version: m.version, - timestamp: m.timestamp, - ephemeral: m.ephemeral, - ) - when defined(rln): - ret[].message.proof = allocSharedSeq(m.proof) - - return ret - -proc destroyShared(self: ptr RelayRequest) = - deallocSharedSeq(self[].message.payload) - deallocShared(self[].message.contentTopic) - deallocSharedSeq(self[].message.meta) - when defined(rln): - deallocSharedSeq(self[].message.proof) - deallocShared(self[].pubsubTopic) - deallocShared(self[].publicKey) - deallocShared(self) - -proc toWakuMessage(m: ThreadSafeWakuMessage): WakuMessage = - var wakuMessage = WakuMessage() - - wakuMessage.payload = m.payload.toSeq() - wakuMessage.contentTopic = $m.contentTopic - wakuMessage.meta = m.meta.toSeq() - wakuMessage.version = m.version - wakuMessage.timestamp = m.timestamp - wakuMessage.ephemeral = m.ephemeral - - when defined(rln): - wakuMessage.proof = m.proof - - return wakuMessage - -proc process*( - self: ptr RelayRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - if waku.node.wakuRelay.isNil(): - return err("Operation not supported without Waku Relay enabled.") - - case self.operation - of SUBSCRIBE: - waku.node.subscribe( - (kind: SubscriptionKind.PubsubSub, topic: $self.pubsubTopic), - handler = self.relayEventCallback, - ).isOkOr: - error "SUBSCRIBE failed", error - return err($error) - of UNSUBSCRIBE: - waku.node.unsubscribe((kind: SubscriptionKind.PubsubSub, topic: $self.pubsubTopic)).isOkOr: - error "UNSUBSCRIBE failed", error - return err($error) - of PUBLISH: - let msg = self.message.toWakuMessage() - let pubsubTopic = $self.pubsubTopic - - (await waku.node.wakuRelay.publish(pubsubTopic, msg)).isOkOr: - error "PUBLISH failed", error - return err($error) - - let msgHash = computeMessageHash(pubSubTopic, msg).to0xHex - return ok(msgHash) - of NUM_CONNECTED_PEERS: - let numConnPeers = waku.node.wakuRelay.getNumConnectedPeers($self.pubsubTopic).valueOr: - error "NUM_CONNECTED_PEERS failed", error - return err($error) - return ok($numConnPeers) - of LIST_CONNECTED_PEERS: - let connPeers = waku.node.wakuRelay.getConnectedPeers($self.pubsubTopic).valueOr: - error "LIST_CONNECTED_PEERS failed", error = error - return err($error) - ## returns a comma-separated string of peerIDs - return ok(connPeers.mapIt($it).join(",")) - of NUM_MESH_PEERS: - let numPeersInMesh = waku.node.wakuRelay.getNumPeersInMesh($self.pubsubTopic).valueOr: - error "NUM_MESH_PEERS failed", error = error - return err($error) - return ok($numPeersInMesh) - of LIST_MESH_PEERS: - let meshPeers = waku.node.wakuRelay.getPeersInMesh($self.pubsubTopic).valueOr: - error "LIST_MESH_PEERS failed", error = error - return err($error) - ## returns a comma-separated string of peerIDs - return ok(meshPeers.mapIt($it).join(",")) - of ADD_PROTECTED_SHARD: - try: - let relayShard = - RelayShard(clusterId: uint16(self.clusterId), shardId: uint16(self.shardId)) - let protectedShard = - ProtectedShard.parseCmdArg($relayShard & ":" & $self.publicKey) - waku.node.wakuRelay.addSignedShardsValidator( - @[protectedShard], uint16(self.clusterId) - ) - except ValueError: - return err(getCurrentExceptionMsg()) - return ok("") diff --git a/library/waku_thread_requests/waku_thread_request.nim b/library/waku_thread_requests/waku_thread_request.nim deleted file mode 100644 index 50462fba7..000000000 --- a/library/waku_thread_requests/waku_thread_request.nim +++ /dev/null @@ -1,104 +0,0 @@ -## This file contains the base message request type that will be handled. -## The requests are created by the main thread and processed by -## the Waku Thread. - -import std/json, results -import chronos, chronos/threadsync -import - ../../waku/factory/waku, - ../ffi_types, - ./requests/node_lifecycle_request, - ./requests/peer_manager_request, - ./requests/protocols/relay_request, - ./requests/protocols/store_request, - ./requests/protocols/lightpush_request, - ./requests/protocols/filter_request, - ./requests/debug_node_request, - ./requests/discovery_request, - ./requests/ping_request - -type RequestType* {.pure.} = enum - LIFECYCLE - PEER_MANAGER - PING - RELAY - STORE - DEBUG - DISCOVERY - LIGHTPUSH - FILTER - -type WakuThreadRequest* = object - reqType: RequestType - reqContent: pointer - callback: WakuCallBack - userData: pointer - -proc createShared*( - T: type WakuThreadRequest, - reqType: RequestType, - reqContent: pointer, - callback: WakuCallBack, - userData: pointer, -): ptr type T = - var ret = createShared(T) - ret[].reqType = reqType - ret[].reqContent = reqContent - ret[].callback = callback - ret[].userData = userData - return ret - -proc handleRes[T: string | void]( - res: Result[T, string], request: ptr WakuThreadRequest -) = - ## Handles the Result responses, which can either be Result[string, string] or - ## Result[void, string]. - - defer: - deallocShared(request) - - if res.isErr(): - foreignThreadGc: - let msg = "libwaku error: handleRes fireSyncRes error: " & $res.error - request[].callback( - RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData - ) - return - - foreignThreadGc: - var msg: cstring = "" - when T is string: - msg = res.get().cstring() - request[].callback( - RET_OK, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData - ) - return - -proc process*( - T: type WakuThreadRequest, request: ptr WakuThreadRequest, waku: ptr Waku -) {.async.} = - let retFut = - case request[].reqType - of LIFECYCLE: - cast[ptr NodeLifecycleRequest](request[].reqContent).process(waku) - of PEER_MANAGER: - cast[ptr PeerManagementRequest](request[].reqContent).process(waku[]) - of PING: - cast[ptr PingRequest](request[].reqContent).process(waku) - of RELAY: - cast[ptr RelayRequest](request[].reqContent).process(waku) - of STORE: - cast[ptr StoreRequest](request[].reqContent).process(waku) - of DEBUG: - cast[ptr DebugNodeRequest](request[].reqContent).process(waku[]) - of DISCOVERY: - cast[ptr DiscoveryRequest](request[].reqContent).process(waku) - of LIGHTPUSH: - cast[ptr LightpushRequest](request[].reqContent).process(waku) - of FILTER: - cast[ptr FilterRequest](request[].reqContent).process(waku) - - handleRes(await retFut, request) - -proc `$`*(self: WakuThreadRequest): string = - return $self.reqType diff --git a/vendor/nim-ffi b/vendor/nim-ffi new file mode 160000 index 000000000..d7a549212 --- /dev/null +++ b/vendor/nim-ffi @@ -0,0 +1 @@ +Subproject commit d7a5492121aad190cf549436836e2fa42e34ff9b diff --git a/waku.nimble b/waku.nimble index 09ff48969..7bfdfab12 100644 --- a/waku.nimble +++ b/waku.nimble @@ -30,7 +30,8 @@ requires "nim >= 2.2.4", "regex", "results", "db_connector", - "minilru" + "minilru", + "ffi" ### Helper functions proc buildModule(filePath, params = "", lang = "c"): bool = From 96196ab8bc05f31b09dac2403f9d5de3bc05f31b Mon Sep 17 00:00:00 2001 From: Pablo Lopez Date: Mon, 22 Dec 2025 15:40:09 +0200 Subject: [PATCH 039/155] feat: compilation for iOS WIP (#3668) * feat: compilation for iOS WIP * fix: nim ios version 18 --- .gitignore | 10 + Makefile | 45 ++ .../ios/WakuExample.xcodeproj/project.pbxproj | 331 ++++++++ .../contents.xcworkspacedata | 7 + examples/ios/WakuExample/ContentView.swift | 229 ++++++ examples/ios/WakuExample/Info.plist | 36 + .../WakuExample/WakuExample-Bridging-Header.h | 15 + examples/ios/WakuExample/WakuExampleApp.swift | 19 + examples/ios/WakuExample/WakuNode.swift | 739 ++++++++++++++++++ examples/ios/WakuExample/libwaku.h | 253 ++++++ examples/ios/project.yml | 47 ++ library/ios_bearssl_stubs.c | 32 + library/ios_natpmp_stubs.c | 14 + waku.nimble | 179 +++++ 14 files changed, 1956 insertions(+) create mode 100644 examples/ios/WakuExample.xcodeproj/project.pbxproj create mode 100644 examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 examples/ios/WakuExample/ContentView.swift create mode 100644 examples/ios/WakuExample/Info.plist create mode 100644 examples/ios/WakuExample/WakuExample-Bridging-Header.h create mode 100644 examples/ios/WakuExample/WakuExampleApp.swift create mode 100644 examples/ios/WakuExample/WakuNode.swift create mode 100644 examples/ios/WakuExample/libwaku.h create mode 100644 examples/ios/project.yml create mode 100644 library/ios_bearssl_stubs.c create mode 100644 library/ios_natpmp_stubs.c diff --git a/.gitignore b/.gitignore index 7430c3e99..f03c4ebaf 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,10 @@ nimbus-build-system.paths /examples/nodejs/build/ /examples/rust/target/ +# Xcode user data +xcuserdata/ +*.xcuserstate + # Coverage coverage_html_report/ @@ -79,3 +83,9 @@ waku_handler.moc.cpp # Nix build result result + +# llms +AGENTS.md +nimble.develop +nimble.paths +nimbledeps diff --git a/Makefile b/Makefile index 35c107d2d..87bd7bc74 100644 --- a/Makefile +++ b/Makefile @@ -517,6 +517,51 @@ libwaku-android: # It's likely this architecture is not used so we might just not support it. # $(MAKE) libwaku-android-arm +################# +## iOS Bindings # +################# +.PHONY: libwaku-ios-precheck \ + libwaku-ios-device \ + libwaku-ios-simulator \ + libwaku-ios + +IOS_DEPLOYMENT_TARGET ?= 18.0 + +# Get SDK paths dynamically using xcrun +define get_ios_sdk_path +$(shell xcrun --sdk $(1) --show-sdk-path 2>/dev/null) +endef + +libwaku-ios-precheck: +ifeq ($(detected_OS),Darwin) + @command -v xcrun >/dev/null 2>&1 || { echo "Error: Xcode command line tools not installed"; exit 1; } +else + $(error iOS builds are only supported on macOS) +endif + +# Build for iOS architecture +build-libwaku-for-ios-arch: + IOS_SDK=$(IOS_SDK) IOS_ARCH=$(IOS_ARCH) IOS_SDK_PATH=$(IOS_SDK_PATH) $(ENV_SCRIPT) nim libWakuIOS $(NIM_PARAMS) waku.nims + +# iOS device (arm64) +libwaku-ios-device: IOS_ARCH=arm64 +libwaku-ios-device: IOS_SDK=iphoneos +libwaku-ios-device: IOS_SDK_PATH=$(call get_ios_sdk_path,iphoneos) +libwaku-ios-device: | libwaku-ios-precheck build deps + $(MAKE) build-libwaku-for-ios-arch IOS_ARCH=$(IOS_ARCH) IOS_SDK=$(IOS_SDK) IOS_SDK_PATH=$(IOS_SDK_PATH) + +# iOS simulator (arm64 - Apple Silicon Macs) +libwaku-ios-simulator: IOS_ARCH=arm64 +libwaku-ios-simulator: IOS_SDK=iphonesimulator +libwaku-ios-simulator: IOS_SDK_PATH=$(call get_ios_sdk_path,iphonesimulator) +libwaku-ios-simulator: | libwaku-ios-precheck build deps + $(MAKE) build-libwaku-for-ios-arch IOS_ARCH=$(IOS_ARCH) IOS_SDK=$(IOS_SDK) IOS_SDK_PATH=$(IOS_SDK_PATH) + +# Build all iOS targets +libwaku-ios: + $(MAKE) libwaku-ios-device + $(MAKE) libwaku-ios-simulator + cwaku_example: | build libwaku echo -e $(BUILD_MSG) "build/$@" && \ cc -o "build/$@" \ diff --git a/examples/ios/WakuExample.xcodeproj/project.pbxproj b/examples/ios/WakuExample.xcodeproj/project.pbxproj new file mode 100644 index 000000000..b7ce1dce7 --- /dev/null +++ b/examples/ios/WakuExample.xcodeproj/project.pbxproj @@ -0,0 +1,331 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 63; + objects = { + +/* Begin PBXBuildFile section */ + 45714AF6D1D12AF5C36694FB /* WakuExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0671AF6DCB0D788B0C1E9C8B /* WakuExampleApp.swift */; }; + 6468FA3F5F760D3FCAD6CDBF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D8744E36DADC11F38A1CC99 /* ContentView.swift */; }; + C4EA202B782038F96336401F /* WakuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A565C495A63CFF7396FBC /* WakuNode.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0671AF6DCB0D788B0C1E9C8B /* WakuExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WakuExampleApp.swift; sourceTree = ""; }; + 31BE20DB2755A11000723420 /* libwaku.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libwaku.h; sourceTree = ""; }; + 5C5AAC91E0166D28BFA986DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 638A565C495A63CFF7396FBC /* WakuNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WakuNode.swift; sourceTree = ""; }; + 7D8744E36DADC11F38A1CC99 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A8655016B3DF9B0877631CE5 /* WakuExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WakuExample-Bridging-Header.h"; sourceTree = ""; }; + CFBE844B6E18ACB81C65F83B /* WakuExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WakuExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 34547A6259485BD047D6375C /* Products */ = { + isa = PBXGroup; + children = ( + CFBE844B6E18ACB81C65F83B /* WakuExample.app */, + ); + name = Products; + sourceTree = ""; + }; + 4F76CB85EC44E951B8E75522 /* WakuExample */ = { + isa = PBXGroup; + children = ( + 7D8744E36DADC11F38A1CC99 /* ContentView.swift */, + 5C5AAC91E0166D28BFA986DB /* Info.plist */, + 31BE20DB2755A11000723420 /* libwaku.h */, + A8655016B3DF9B0877631CE5 /* WakuExample-Bridging-Header.h */, + 0671AF6DCB0D788B0C1E9C8B /* WakuExampleApp.swift */, + 638A565C495A63CFF7396FBC /* WakuNode.swift */, + ); + path = WakuExample; + sourceTree = ""; + }; + D40CD2446F177CAABB0A747A = { + isa = PBXGroup; + children = ( + 4F76CB85EC44E951B8E75522 /* WakuExample */, + 34547A6259485BD047D6375C /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F751EF8294AD21F713D47FDA /* WakuExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 757FA0123629BD63CB254113 /* Build configuration list for PBXNativeTarget "WakuExample" */; + buildPhases = ( + D3AFD8C4DA68BF5C4F7D8E10 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WakuExample; + packageProductDependencies = ( + ); + productName = WakuExample; + productReference = CFBE844B6E18ACB81C65F83B /* WakuExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4FF82F0F4AF8E1E34728F150 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1500; + }; + buildConfigurationList = B3A4F48294254543E79767C4 /* Build configuration list for PBXProject "WakuExample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = D40CD2446F177CAABB0A747A; + minimizedProjectReferenceProxies = 1; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F751EF8294AD21F713D47FDA /* WakuExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + D3AFD8C4DA68BF5C4F7D8E10 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6468FA3F5F760D3FCAD6CDBF /* ContentView.swift in Sources */, + 45714AF6D1D12AF5C36694FB /* WakuExampleApp.swift in Sources */, + C4EA202B782038F96336401F /* WakuNode.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 36939122077C66DD94082311 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 2Q52K2W84K; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/WakuExample"; + INFOPLIST_FILE = WakuExample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/../../build/ios/iphonesimulator-arm64"; + MACOSX_DEPLOYMENT_TARGET = 15.6; + OTHER_LDFLAGS = ( + "-lc++", + "-force_load", + "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64/libwaku.a", + "-lsqlite3", + "-lz", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.waku.example; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "WakuExample/WakuExample-Bridging-Header.h"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 9BA833A09EEDB4B3FCCD8F8E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + A59ABFB792FED8974231E5AC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + AF5ADDAA865B1F6BD4E70A79 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 2Q52K2W84K; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/WakuExample"; + INFOPLIST_FILE = WakuExample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/../../build/ios/iphonesimulator-arm64"; + MACOSX_DEPLOYMENT_TARGET = 15.6; + OTHER_LDFLAGS = ( + "-lc++", + "-force_load", + "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64/libwaku.a", + "-lsqlite3", + "-lz", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.waku.example; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "WakuExample/WakuExample-Bridging-Header.h"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 757FA0123629BD63CB254113 /* Build configuration list for PBXNativeTarget "WakuExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AF5ADDAA865B1F6BD4E70A79 /* Debug */, + 36939122077C66DD94082311 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + B3A4F48294254543E79767C4 /* Build configuration list for PBXProject "WakuExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A59ABFB792FED8974231E5AC /* Debug */, + 9BA833A09EEDB4B3FCCD8F8E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4FF82F0F4AF8E1E34728F150 /* Project object */; +} diff --git a/examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/ios/WakuExample/ContentView.swift b/examples/ios/WakuExample/ContentView.swift new file mode 100644 index 000000000..14bb4ee1d --- /dev/null +++ b/examples/ios/WakuExample/ContentView.swift @@ -0,0 +1,229 @@ +// +// ContentView.swift +// WakuExample +// +// Minimal chat PoC using libwaku on iOS +// + +import SwiftUI + +struct ContentView: View { + @StateObject private var wakuNode = WakuNode() + @State private var messageText = "" + + var body: some View { + ZStack { + // Main content + VStack(spacing: 0) { + // Header with status + HStack { + Circle() + .fill(statusColor) + .frame(width: 10, height: 10) + VStack(alignment: .leading, spacing: 2) { + Text(wakuNode.status.rawValue) + .font(.caption) + if wakuNode.status == .running { + HStack(spacing: 4) { + Text(wakuNode.isConnected ? "Connected" : "Discovering...") + Text("•") + filterStatusView + } + .font(.caption2) + .foregroundColor(.secondary) + + // Subscription maintenance status + if wakuNode.subscriptionMaintenanceActive { + HStack(spacing: 4) { + Image(systemName: "arrow.triangle.2.circlepath") + .foregroundColor(.blue) + Text("Maintenance active") + if wakuNode.failedSubscribeAttempts > 0 { + Text("(\(wakuNode.failedSubscribeAttempts) retries)") + .foregroundColor(.orange) + } + } + .font(.caption2) + .foregroundColor(.secondary) + } + } + } + Spacer() + if wakuNode.status == .stopped { + Button("Start") { + wakuNode.start() + } + .buttonStyle(.borderedProminent) + .controlSize(.small) + } else if wakuNode.status == .running { + if !wakuNode.filterSubscribed { + Button("Resub") { + wakuNode.resubscribe() + } + .buttonStyle(.bordered) + .controlSize(.small) + } + Button("Stop") { + wakuNode.stop() + } + .buttonStyle(.bordered) + .controlSize(.small) + } + } + .padding() + .background(Color.gray.opacity(0.1)) + + // Messages list + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 8) { + ForEach(wakuNode.receivedMessages.reversed()) { message in + MessageBubble(message: message) + .id(message.id) + } + } + .padding() + } + .onChange(of: wakuNode.receivedMessages.count) { _, newCount in + if let lastMessage = wakuNode.receivedMessages.first { + withAnimation { + proxy.scrollTo(lastMessage.id, anchor: .bottom) + } + } + } + } + + Divider() + + // Message input + HStack(spacing: 12) { + TextField("Message", text: $messageText) + .textFieldStyle(.roundedBorder) + .disabled(wakuNode.status != .running) + + Button(action: sendMessage) { + Image(systemName: "paperplane.fill") + .foregroundColor(.white) + .padding(10) + .background(canSend ? Color.blue : Color.gray) + .clipShape(Circle()) + } + .disabled(!canSend) + } + .padding() + .background(Color.gray.opacity(0.1)) + } + + // Toast overlay for errors + VStack { + ForEach(wakuNode.errorQueue) { error in + ToastView(error: error) { + wakuNode.dismissError(error) + } + .transition(.asymmetric( + insertion: .move(edge: .top).combined(with: .opacity), + removal: .opacity + )) + } + Spacer() + } + .padding(.top, 8) + .animation(.easeInOut(duration: 0.3), value: wakuNode.errorQueue) + } + } + + private var statusColor: Color { + switch wakuNode.status { + case .stopped: return .gray + case .starting: return .yellow + case .running: return .green + case .error: return .red + } + } + + @ViewBuilder + private var filterStatusView: some View { + if wakuNode.filterSubscribed { + Text("Filter OK") + .foregroundColor(.green) + } else if wakuNode.failedSubscribeAttempts > 0 { + Text("Filter retrying (\(wakuNode.failedSubscribeAttempts))") + .foregroundColor(.orange) + } else { + Text("Filter pending") + .foregroundColor(.orange) + } + } + + private var canSend: Bool { + wakuNode.status == .running && wakuNode.isConnected && !messageText.trimmingCharacters(in: .whitespaces).isEmpty + } + + private func sendMessage() { + let text = messageText.trimmingCharacters(in: .whitespaces) + guard !text.isEmpty else { return } + + wakuNode.publish(message: text) + messageText = "" + } +} + +// MARK: - Toast View + +struct ToastView: View { + let error: TimestampedError + let onDismiss: () -> Void + + var body: some View { + HStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.white) + + Text(error.message) + .font(.subheadline) + .foregroundColor(.white) + .lineLimit(2) + + Spacer() + + Button(action: onDismiss) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.white.opacity(0.8)) + .font(.title3) + } + .buttonStyle(.plain) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color.red.opacity(0.9)) + .shadow(color: .black.opacity(0.2), radius: 8, x: 0, y: 4) + ) + .padding(.horizontal, 16) + .padding(.vertical, 4) + } +} + +// MARK: - Message Bubble + +struct MessageBubble: View { + let message: WakuMessage + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(message.payload) + .padding(10) + .background(Color.blue.opacity(0.1)) + .cornerRadius(12) + + Text(message.timestamp, style: .time) + .font(.caption2) + .foregroundColor(.secondary) + } + } +} + +#Preview { + ContentView() +} diff --git a/examples/ios/WakuExample/Info.plist b/examples/ios/WakuExample/Info.plist new file mode 100644 index 000000000..a9222555a --- /dev/null +++ b/examples/ios/WakuExample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Waku Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.waku.example + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + WakuExample + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + + diff --git a/examples/ios/WakuExample/WakuExample-Bridging-Header.h b/examples/ios/WakuExample/WakuExample-Bridging-Header.h new file mode 100644 index 000000000..50595450e --- /dev/null +++ b/examples/ios/WakuExample/WakuExample-Bridging-Header.h @@ -0,0 +1,15 @@ +// +// WakuExample-Bridging-Header.h +// WakuExample +// +// Bridging header to expose libwaku C functions to Swift +// + +#ifndef WakuExample_Bridging_Header_h +#define WakuExample_Bridging_Header_h + +#import "libwaku.h" + +#endif /* WakuExample_Bridging_Header_h */ + + diff --git a/examples/ios/WakuExample/WakuExampleApp.swift b/examples/ios/WakuExample/WakuExampleApp.swift new file mode 100644 index 000000000..fb99785aa --- /dev/null +++ b/examples/ios/WakuExample/WakuExampleApp.swift @@ -0,0 +1,19 @@ +// +// WakuExampleApp.swift +// WakuExample +// +// SwiftUI app entry point for Waku iOS example +// + +import SwiftUI + +@main +struct WakuExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} + + diff --git a/examples/ios/WakuExample/WakuNode.swift b/examples/ios/WakuExample/WakuNode.swift new file mode 100644 index 000000000..245529a2f --- /dev/null +++ b/examples/ios/WakuExample/WakuNode.swift @@ -0,0 +1,739 @@ +// +// WakuNode.swift +// WakuExample +// +// Swift wrapper around libwaku C API for edge mode (lightpush + filter) +// Uses Swift actors for thread safety and UI responsiveness +// + +import Foundation + +// MARK: - Data Types + +/// Message received from Waku network +struct WakuMessage: Identifiable, Equatable, Sendable { + let id: String // messageHash from Waku - unique identifier for deduplication + let payload: String + let contentTopic: String + let timestamp: Date +} + +/// Waku node status +enum WakuNodeStatus: String, Sendable { + case stopped = "Stopped" + case starting = "Starting..." + case running = "Running" + case error = "Error" +} + +/// Status updates from WakuActor to WakuNode +enum WakuStatusUpdate: Sendable { + case statusChanged(WakuNodeStatus) + case connectionChanged(isConnected: Bool) + case filterSubscriptionChanged(subscribed: Bool, failedAttempts: Int) + case maintenanceChanged(active: Bool) + case error(String) +} + +/// Error with timestamp for toast queue +struct TimestampedError: Identifiable, Equatable { + let id = UUID() + let message: String + let timestamp: Date + + static func == (lhs: TimestampedError, rhs: TimestampedError) -> Bool { + lhs.id == rhs.id + } +} + +// MARK: - Callback Context for C API + +private final class CallbackContext: @unchecked Sendable { + private let lock = NSLock() + private var _continuation: CheckedContinuation<(success: Bool, result: String?), Never>? + private var _resumed = false + var success: Bool = false + var result: String? + + var continuation: CheckedContinuation<(success: Bool, result: String?), Never>? { + get { + lock.lock() + defer { lock.unlock() } + return _continuation + } + set { + lock.lock() + defer { lock.unlock() } + _continuation = newValue + } + } + + /// Thread-safe resume - ensures continuation is only resumed once + /// Returns true if this call actually resumed, false if already resumed + @discardableResult + func resumeOnce(returning value: (success: Bool, result: String?)) -> Bool { + lock.lock() + defer { lock.unlock() } + + guard !_resumed, let cont = _continuation else { + return false + } + + _resumed = true + _continuation = nil + cont.resume(returning: value) + return true + } +} + +// MARK: - WakuActor + +/// Actor that isolates all Waku operations from the main thread +/// All C API calls and mutable state are contained here +actor WakuActor { + + // MARK: - State + + private var ctx: UnsafeMutableRawPointer? + private var seenMessageHashes: Set = [] + private var isSubscribed: Bool = false + private var isSubscribing: Bool = false + private var hasPeers: Bool = false + private var maintenanceTask: Task? + private var eventProcessingTask: Task? + + // Stream continuations for communicating with UI + private var messageContinuation: AsyncStream.Continuation? + private var statusContinuation: AsyncStream.Continuation? + + // Event stream from C callbacks + private var eventContinuation: AsyncStream.Continuation? + + // Configuration + let defaultPubsubTopic = "/waku/2/rs/1/0" + let defaultContentTopic = "/waku-ios-example/1/chat/proto" + private let staticPeer = "/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ" + + // Subscription maintenance settings + private let maxFailedSubscribes = 3 + private let retryWaitSeconds: UInt64 = 2_000_000_000 // 2 seconds in nanoseconds + private let maintenanceIntervalSeconds: UInt64 = 30_000_000_000 // 30 seconds in nanoseconds + private let maxSeenHashes = 1000 + + // MARK: - Static callback storage (for C callbacks) + + // We need a way for C callbacks to reach the actor + // Using a simple static reference (safe because we only have one instance) + private static var sharedEventContinuation: AsyncStream.Continuation? + + private static let eventCallback: WakuCallBack = { ret, msg, len, userData in + guard ret == RET_OK, let msg = msg else { return } + let str = String(cString: msg) + WakuActor.sharedEventContinuation?.yield(str) + } + + private static let syncCallback: WakuCallBack = { ret, msg, len, userData in + guard let userData = userData else { return } + let context = Unmanaged.fromOpaque(userData).takeUnretainedValue() + let success = (ret == RET_OK) + var resultStr: String? = nil + if let msg = msg { + resultStr = String(cString: msg) + } + context.resumeOnce(returning: (success, resultStr)) + } + + // MARK: - Stream Setup + + func setMessageContinuation(_ continuation: AsyncStream.Continuation?) { + self.messageContinuation = continuation + } + + func setStatusContinuation(_ continuation: AsyncStream.Continuation?) { + self.statusContinuation = continuation + } + + // MARK: - Public API + + var isRunning: Bool { + ctx != nil + } + + var hasConnectedPeers: Bool { + hasPeers + } + + func start() async { + guard ctx == nil else { + print("[WakuActor] Already started") + return + } + + statusContinuation?.yield(.statusChanged(.starting)) + + // Create event stream for C callbacks + let eventStream = AsyncStream { continuation in + self.eventContinuation = continuation + WakuActor.sharedEventContinuation = continuation + } + + // Start event processing task + eventProcessingTask = Task { [weak self] in + for await eventJson in eventStream { + await self?.handleEvent(eventJson) + } + } + + // Initialize the node + let success = await initializeNode() + + if success { + statusContinuation?.yield(.statusChanged(.running)) + + // Connect to peer + let connected = await connectToPeer() + if connected { + hasPeers = true + statusContinuation?.yield(.connectionChanged(isConnected: true)) + + // Start maintenance loop + startMaintenanceLoop() + } else { + statusContinuation?.yield(.error("Failed to connect to service peer")) + } + } + } + + func stop() async { + guard let context = ctx else { return } + + // Stop maintenance loop + maintenanceTask?.cancel() + maintenanceTask = nil + + // Stop event processing + eventProcessingTask?.cancel() + eventProcessingTask = nil + + // Close event stream + eventContinuation?.finish() + eventContinuation = nil + WakuActor.sharedEventContinuation = nil + + statusContinuation?.yield(.statusChanged(.stopped)) + statusContinuation?.yield(.connectionChanged(isConnected: false)) + statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: 0)) + statusContinuation?.yield(.maintenanceChanged(active: false)) + + // Reset state + let ctxToStop = context + ctx = nil + isSubscribed = false + isSubscribing = false + hasPeers = false + seenMessageHashes.removeAll() + + // Unsubscribe and stop in background (fire and forget) + Task.detached { + // Unsubscribe + _ = await self.callWakuSync { waku_filter_unsubscribe_all(ctxToStop, WakuActor.syncCallback, $0) } + print("[WakuActor] Unsubscribed from filter") + + // Stop + _ = await self.callWakuSync { waku_stop(ctxToStop, WakuActor.syncCallback, $0) } + print("[WakuActor] Node stopped") + + // Destroy + _ = await self.callWakuSync { waku_destroy(ctxToStop, WakuActor.syncCallback, $0) } + print("[WakuActor] Node destroyed") + } + } + + func publish(message: String, contentTopic: String? = nil) async { + guard let context = ctx else { + print("[WakuActor] Node not started") + return + } + + guard hasPeers else { + print("[WakuActor] No peers connected yet") + statusContinuation?.yield(.error("No peers connected yet. Please wait...")) + return + } + + let topic = contentTopic ?? defaultContentTopic + guard let payloadData = message.data(using: .utf8) else { return } + let payloadBase64 = payloadData.base64EncodedString() + let timestamp = Int64(Date().timeIntervalSince1970 * 1_000_000_000) + let jsonMessage = """ + {"payload":"\(payloadBase64)","contentTopic":"\(topic)","timestamp":\(timestamp)} + """ + + let result = await callWakuSync { userData in + waku_lightpush_publish( + context, + self.defaultPubsubTopic, + jsonMessage, + WakuActor.syncCallback, + userData + ) + } + + if result.success { + print("[WakuActor] Published message") + } else { + print("[WakuActor] Publish error: \(result.result ?? "unknown")") + statusContinuation?.yield(.error("Failed to send message")) + } + } + + func resubscribe() async { + print("[WakuActor] Force resubscribe requested") + isSubscribed = false + isSubscribing = false + statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: 0)) + _ = await subscribe() + } + + // MARK: - Private Methods + + private func initializeNode() async -> Bool { + let config = """ + { + "tcpPort": 60000, + "clusterId": 1, + "shards": [0], + "relay": false, + "lightpush": true, + "filter": true, + "logLevel": "DEBUG", + "discv5Discovery": true, + "discv5BootstrapNodes": [ + "enr:-QESuEB4Dchgjn7gfAvwB00CxTA-nGiyk-aALI-H4dYSZD3rUk7bZHmP8d2U6xDiQ2vZffpo45Jp7zKNdnwDUx6g4o6XAYJpZIJ2NIJpcIRA4VDAim11bHRpYWRkcnO4XAArNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwAtNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOvD3S3jUNICsrOILlmhENiWAMmMVlAl6-Q8wRB7hidY4N0Y3CCdl-DdWRwgiMohXdha3UyDw", + "enr:-QEkuEBIkb8q8_mrorHndoXH9t5N6ZfD-jehQCrYeoJDPHqT0l0wyaONa2-piRQsi3oVKAzDShDVeoQhy0uwN1xbZfPZAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQKnGt-GSgqPSf3IAPM7bFgTlpczpMZZLF3geeoNNsxzSoN0Y3CCdl-DdWRwgiMohXdha3UyDw" + ], + "discv5UdpPort": 9999, + "dnsDiscovery": true, + "dnsDiscoveryUrl": "enrtree://AOGYWMBYOUIMOENHXCHILPKY3ZRFEULMFI4DOM442QSZ73TT2A7VI@test.waku.nodes.status.im", + "dnsDiscoveryNameServers": ["8.8.8.8", "1.0.0.1"] + } + """ + + // Create node - waku_new is special, it returns the context directly + let createResult = await withCheckedContinuation { (continuation: CheckedContinuation<(ctx: UnsafeMutableRawPointer?, success: Bool, result: String?), Never>) in + let callbackCtx = CallbackContext() + let userDataPtr = Unmanaged.passRetained(callbackCtx).toOpaque() + + // Set up a simple callback for waku_new + let newCtx = waku_new(config, { ret, msg, len, userData in + guard let userData = userData else { return } + let context = Unmanaged.fromOpaque(userData).takeUnretainedValue() + context.success = (ret == RET_OK) + if let msg = msg { + context.result = String(cString: msg) + } + }, userDataPtr) + + // Small delay to ensure callback completes + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + Unmanaged.fromOpaque(userDataPtr).release() + continuation.resume(returning: (newCtx, callbackCtx.success, callbackCtx.result)) + } + } + + guard createResult.ctx != nil else { + statusContinuation?.yield(.statusChanged(.error)) + statusContinuation?.yield(.error("Failed to create node: \(createResult.result ?? "unknown")")) + return false + } + + ctx = createResult.ctx + + // Set event callback + waku_set_event_callback(ctx, WakuActor.eventCallback, nil) + + // Start node + let startResult = await callWakuSync { userData in + waku_start(self.ctx, WakuActor.syncCallback, userData) + } + + guard startResult.success else { + statusContinuation?.yield(.statusChanged(.error)) + statusContinuation?.yield(.error("Failed to start node: \(startResult.result ?? "unknown")")) + ctx = nil + return false + } + + print("[WakuActor] Node started") + return true + } + + private func connectToPeer() async -> Bool { + guard let context = ctx else { return false } + + print("[WakuActor] Connecting to static peer...") + + let result = await callWakuSync { userData in + waku_connect(context, self.staticPeer, 10000, WakuActor.syncCallback, userData) + } + + if result.success { + print("[WakuActor] Connected to peer successfully") + return true + } else { + print("[WakuActor] Failed to connect: \(result.result ?? "unknown")") + return false + } + } + + private func subscribe(contentTopic: String? = nil) async -> Bool { + guard let context = ctx else { return false } + guard !isSubscribed && !isSubscribing else { return isSubscribed } + + isSubscribing = true + let topic = contentTopic ?? defaultContentTopic + + let result = await callWakuSync { userData in + waku_filter_subscribe( + context, + self.defaultPubsubTopic, + topic, + WakuActor.syncCallback, + userData + ) + } + + isSubscribing = false + + if result.success { + print("[WakuActor] Subscribe request successful to \(topic)") + isSubscribed = true + statusContinuation?.yield(.filterSubscriptionChanged(subscribed: true, failedAttempts: 0)) + return true + } else { + print("[WakuActor] Subscribe error: \(result.result ?? "unknown")") + isSubscribed = false + return false + } + } + + private func pingFilterPeer() async -> Bool { + guard let context = ctx else { return false } + + let result = await callWakuSync { userData in + waku_ping_peer( + context, + self.staticPeer, + 10000, + WakuActor.syncCallback, + userData + ) + } + + return result.success + } + + // MARK: - Subscription Maintenance + + private func startMaintenanceLoop() { + guard maintenanceTask == nil else { + print("[WakuActor] Maintenance loop already running") + return + } + + statusContinuation?.yield(.maintenanceChanged(active: true)) + print("[WakuActor] Starting subscription maintenance loop") + + maintenanceTask = Task { [weak self] in + guard let self = self else { return } + + var failedSubscribes = 0 + var isFirstPingOnConnection = true + + while !Task.isCancelled { + guard await self.isRunning else { break } + + print("[WakuActor] Maintaining subscription...") + + let pingSuccess = await self.pingFilterPeer() + let currentlySubscribed = await self.isSubscribed + + if pingSuccess && currentlySubscribed { + print("[WakuActor] Subscription is live, waiting 30s") + try? await Task.sleep(nanoseconds: self.maintenanceIntervalSeconds) + continue + } + + if !isFirstPingOnConnection && !pingSuccess { + print("[WakuActor] Ping failed - subscription may be lost") + await self.statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: failedSubscribes)) + } + isFirstPingOnConnection = false + + print("[WakuActor] No active subscription found. Sending subscribe request...") + + await self.resetSubscriptionState() + let subscribeSuccess = await self.subscribe() + + if subscribeSuccess { + print("[WakuActor] Subscribe request successful") + failedSubscribes = 0 + try? await Task.sleep(nanoseconds: self.maintenanceIntervalSeconds) + continue + } + + failedSubscribes += 1 + await self.statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: failedSubscribes)) + print("[WakuActor] Subscribe request failed. Attempt \(failedSubscribes)/\(self.maxFailedSubscribes)") + + if failedSubscribes < self.maxFailedSubscribes { + print("[WakuActor] Retrying in 2s...") + try? await Task.sleep(nanoseconds: self.retryWaitSeconds) + } else { + print("[WakuActor] Max subscribe failures reached") + await self.statusContinuation?.yield(.error("Filter subscription failed after \(self.maxFailedSubscribes) attempts")) + failedSubscribes = 0 + try? await Task.sleep(nanoseconds: self.maintenanceIntervalSeconds) + } + } + + print("[WakuActor] Subscription maintenance loop stopped") + await self.statusContinuation?.yield(.maintenanceChanged(active: false)) + } + } + + private func resetSubscriptionState() { + isSubscribed = false + isSubscribing = false + } + + // MARK: - Event Handling + + private func handleEvent(_ eventJson: String) { + guard let data = eventJson.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let eventType = json["eventType"] as? String else { + return + } + + if eventType == "connection_change" { + handleConnectionChange(json) + } else if eventType == "message" { + handleMessage(json) + } + } + + private func handleConnectionChange(_ json: [String: Any]) { + guard let peerEvent = json["peerEvent"] as? String else { return } + + if peerEvent == "Joined" || peerEvent == "Identified" { + hasPeers = true + statusContinuation?.yield(.connectionChanged(isConnected: true)) + } else if peerEvent == "Left" { + statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: 0)) + } + } + + private func handleMessage(_ json: [String: Any]) { + guard let messageHash = json["messageHash"] as? String, + let wakuMessage = json["wakuMessage"] as? [String: Any], + let payloadBase64 = wakuMessage["payload"] as? String, + let contentTopic = wakuMessage["contentTopic"] as? String, + let payloadData = Data(base64Encoded: payloadBase64), + let payloadString = String(data: payloadData, encoding: .utf8) else { + return + } + + // Deduplicate + guard !seenMessageHashes.contains(messageHash) else { + return + } + + seenMessageHashes.insert(messageHash) + + // Limit memory usage + if seenMessageHashes.count > maxSeenHashes { + seenMessageHashes.removeAll() + } + + let message = WakuMessage( + id: messageHash, + payload: payloadString, + contentTopic: contentTopic, + timestamp: Date() + ) + + messageContinuation?.yield(message) + } + + // MARK: - Helper for synchronous C calls + + private func callWakuSync(_ work: @escaping (UnsafeMutableRawPointer) -> Void) async -> (success: Bool, result: String?) { + await withCheckedContinuation { continuation in + let context = CallbackContext() + context.continuation = continuation + let userDataPtr = Unmanaged.passRetained(context).toOpaque() + + work(userDataPtr) + + // Set a timeout to avoid hanging forever + DispatchQueue.global().asyncAfter(deadline: .now() + 15) { + // Try to resume with timeout - will be ignored if callback already resumed + let didTimeout = context.resumeOnce(returning: (false, "Timeout")) + if didTimeout { + print("[WakuActor] Call timed out") + } + Unmanaged.fromOpaque(userDataPtr).release() + } + } + } +} + +// MARK: - WakuNode (MainActor UI Wrapper) + +/// Main-thread UI wrapper that consumes updates from WakuActor via AsyncStreams +@MainActor +class WakuNode: ObservableObject { + + // MARK: - Published Properties (UI State) + + @Published var status: WakuNodeStatus = .stopped + @Published var receivedMessages: [WakuMessage] = [] + @Published var errorQueue: [TimestampedError] = [] + @Published var isConnected: Bool = false + @Published var filterSubscribed: Bool = false + @Published var subscriptionMaintenanceActive: Bool = false + @Published var failedSubscribeAttempts: Int = 0 + + // Topics (read-only access to actor's config) + var defaultPubsubTopic: String { "/waku/2/rs/1/0" } + var defaultContentTopic: String { "/waku-ios-example/1/chat/proto" } + + // MARK: - Private Properties + + private let actor = WakuActor() + private var messageTask: Task? + private var statusTask: Task? + + // MARK: - Initialization + + init() {} + + deinit { + messageTask?.cancel() + statusTask?.cancel() + } + + // MARK: - Public API + + func start() { + guard status == .stopped || status == .error else { + print("[WakuNode] Already started or starting") + return + } + + // Create message stream + let messageStream = AsyncStream { continuation in + Task { + await self.actor.setMessageContinuation(continuation) + } + } + + // Create status stream + let statusStream = AsyncStream { continuation in + Task { + await self.actor.setStatusContinuation(continuation) + } + } + + // Start consuming messages + messageTask = Task { @MainActor in + for await message in messageStream { + self.receivedMessages.insert(message, at: 0) + if self.receivedMessages.count > 100 { + self.receivedMessages.removeLast() + } + } + } + + // Start consuming status updates + statusTask = Task { @MainActor in + for await update in statusStream { + self.handleStatusUpdate(update) + } + } + + // Start the actor + Task { + await actor.start() + } + } + + func stop() { + messageTask?.cancel() + messageTask = nil + statusTask?.cancel() + statusTask = nil + + Task { + await actor.stop() + } + + // Immediate UI update + status = .stopped + isConnected = false + filterSubscribed = false + subscriptionMaintenanceActive = false + failedSubscribeAttempts = 0 + } + + func publish(message: String, contentTopic: String? = nil) { + Task { + await actor.publish(message: message, contentTopic: contentTopic) + } + } + + func resubscribe() { + Task { + await actor.resubscribe() + } + } + + func dismissError(_ error: TimestampedError) { + errorQueue.removeAll { $0.id == error.id } + } + + func dismissAllErrors() { + errorQueue.removeAll() + } + + // MARK: - Private Methods + + private func handleStatusUpdate(_ update: WakuStatusUpdate) { + switch update { + case .statusChanged(let newStatus): + status = newStatus + + case .connectionChanged(let connected): + isConnected = connected + + case .filterSubscriptionChanged(let subscribed, let attempts): + filterSubscribed = subscribed + failedSubscribeAttempts = attempts + + case .maintenanceChanged(let active): + subscriptionMaintenanceActive = active + + case .error(let message): + let error = TimestampedError(message: message, timestamp: Date()) + errorQueue.append(error) + + // Schedule auto-dismiss after 10 seconds + let errorId = error.id + Task { @MainActor in + try? await Task.sleep(nanoseconds: 10_000_000_000) + self.errorQueue.removeAll { $0.id == errorId } + } + } + } +} diff --git a/examples/ios/WakuExample/libwaku.h b/examples/ios/WakuExample/libwaku.h new file mode 100644 index 000000000..b5d6c9bab --- /dev/null +++ b/examples/ios/WakuExample/libwaku.h @@ -0,0 +1,253 @@ + +// Generated manually and inspired by the one generated by the Nim Compiler. +// In order to see the header file generated by Nim just run `make libwaku` +// from the root repo folder and the header should be created in +// nimcache/release/libwaku/libwaku.h +#ifndef __libwaku__ +#define __libwaku__ + +#include +#include + +// The possible returned values for the functions that return int +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*WakuCallBack) (int callerRet, const char* msg, size_t len, void* userData); + +// Creates a new instance of the waku node. +// Sets up the waku node from the given configuration. +// Returns a pointer to the Context needed by the rest of the API functions. +void* waku_new( + const char* configJson, + WakuCallBack callback, + void* userData); + +int waku_start(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_stop(void* ctx, + WakuCallBack callback, + void* userData); + +// Destroys an instance of a waku node created with waku_new +int waku_destroy(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_version(void* ctx, + WakuCallBack callback, + void* userData); + +// Sets a callback that will be invoked whenever an event occurs. +// It is crucial that the passed callback is fast, non-blocking and potentially thread-safe. +void waku_set_event_callback(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_content_topic(void* ctx, + const char* appName, + unsigned int appVersion, + const char* contentTopicName, + const char* encoding, + WakuCallBack callback, + void* userData); + +int waku_pubsub_topic(void* ctx, + const char* topicName, + WakuCallBack callback, + void* userData); + +int waku_default_pubsub_topic(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_relay_publish(void* ctx, + const char* pubSubTopic, + const char* jsonWakuMessage, + unsigned int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_lightpush_publish(void* ctx, + const char* pubSubTopic, + const char* jsonWakuMessage, + WakuCallBack callback, + void* userData); + +int waku_relay_subscribe(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_relay_add_protected_shard(void* ctx, + int clusterId, + int shardId, + char* publicKey, + WakuCallBack callback, + void* userData); + +int waku_relay_unsubscribe(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_filter_subscribe(void* ctx, + const char* pubSubTopic, + const char* contentTopics, + WakuCallBack callback, + void* userData); + +int waku_filter_unsubscribe(void* ctx, + const char* pubSubTopic, + const char* contentTopics, + WakuCallBack callback, + void* userData); + +int waku_filter_unsubscribe_all(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_relay_get_num_connected_peers(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_relay_get_connected_peers(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_relay_get_num_peers_in_mesh(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_relay_get_peers_in_mesh(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_store_query(void* ctx, + const char* jsonQuery, + const char* peerAddr, + int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_connect(void* ctx, + const char* peerMultiAddr, + unsigned int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_disconnect_peer_by_id(void* ctx, + const char* peerId, + WakuCallBack callback, + void* userData); + +int waku_disconnect_all_peers(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_dial_peer(void* ctx, + const char* peerMultiAddr, + const char* protocol, + int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_dial_peer_by_id(void* ctx, + const char* peerId, + const char* protocol, + int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_get_peerids_from_peerstore(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_get_connected_peers_info(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_get_peerids_by_protocol(void* ctx, + const char* protocol, + WakuCallBack callback, + void* userData); + +int waku_listen_addresses(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_get_connected_peers(void* ctx, + WakuCallBack callback, + void* userData); + +// Returns a list of multiaddress given a url to a DNS discoverable ENR tree +// Parameters +// char* entTreeUrl: URL containing a discoverable ENR tree +// char* nameDnsServer: The nameserver to resolve the ENR tree url. +// int timeoutMs: Timeout value in milliseconds to execute the call. +int waku_dns_discovery(void* ctx, + const char* entTreeUrl, + const char* nameDnsServer, + int timeoutMs, + WakuCallBack callback, + void* userData); + +// Updates the bootnode list used for discovering new peers via DiscoveryV5 +// bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]` +int waku_discv5_update_bootnodes(void* ctx, + char* bootnodes, + WakuCallBack callback, + void* userData); + +int waku_start_discv5(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_stop_discv5(void* ctx, + WakuCallBack callback, + void* userData); + +// Retrieves the ENR information +int waku_get_my_enr(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_get_my_peerid(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_get_metrics(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_peer_exchange_request(void* ctx, + int numPeers, + WakuCallBack callback, + void* userData); + +int waku_ping_peer(void* ctx, + const char* peerAddr, + int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_is_online(void* ctx, + WakuCallBack callback, + void* userData); + +#ifdef __cplusplus +} +#endif + +#endif /* __libwaku__ */ diff --git a/examples/ios/project.yml b/examples/ios/project.yml new file mode 100644 index 000000000..9519e8b9e --- /dev/null +++ b/examples/ios/project.yml @@ -0,0 +1,47 @@ +name: WakuExample +options: + bundleIdPrefix: org.waku + deploymentTarget: + iOS: "14.0" + xcodeVersion: "15.0" + +settings: + SWIFT_VERSION: "5.0" + SUPPORTED_PLATFORMS: "iphoneos iphonesimulator" + SUPPORTS_MACCATALYST: "NO" + +targets: + WakuExample: + type: application + platform: iOS + supportedDestinations: [iOS] + sources: + - WakuExample + settings: + INFOPLIST_FILE: WakuExample/Info.plist + PRODUCT_BUNDLE_IDENTIFIER: org.waku.example + SWIFT_OBJC_BRIDGING_HEADER: WakuExample/WakuExample-Bridging-Header.h + HEADER_SEARCH_PATHS: + - "$(PROJECT_DIR)/WakuExample" + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]": + - "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64" + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]": + - "$(PROJECT_DIR)/../../build/ios/iphonesimulator-arm64" + OTHER_LDFLAGS: + - "-lc++" + - "-lwaku" + IPHONEOS_DEPLOYMENT_TARGET: "14.0" + info: + path: WakuExample/Info.plist + properties: + CFBundleName: WakuExample + CFBundleDisplayName: Waku Example + CFBundleIdentifier: org.waku.example + CFBundleVersion: "1" + CFBundleShortVersionString: "1.0" + UILaunchScreen: {} + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + NSAppTransportSecurity: + NSAllowsArbitraryLoads: true + diff --git a/library/ios_bearssl_stubs.c b/library/ios_bearssl_stubs.c new file mode 100644 index 000000000..a028cdf25 --- /dev/null +++ b/library/ios_bearssl_stubs.c @@ -0,0 +1,32 @@ +/** + * iOS stubs for BearSSL tools functions not normally included in the library. + * These are typically from the BearSSL tools/ directory which is for CLI tools. + */ + +#include + +/* x509_noanchor context - simplified stub */ +typedef struct { + void *vtable; + void *inner; +} x509_noanchor_context; + +/* Stub for x509_noanchor_init - used to skip anchor validation */ +void x509_noanchor_init(x509_noanchor_context *xwc, const void **inner) { + if (xwc && inner) { + xwc->inner = (void*)*inner; + xwc->vtable = NULL; + } +} + +/* TAs (Trust Anchors) - empty array stub */ +/* This is typically defined by applications with their CA certificates */ +typedef struct { + void *dn; + size_t dn_len; + unsigned flags; + void *pkey; +} br_x509_trust_anchor; + +const br_x509_trust_anchor TAs[1] = {{0}}; +const size_t TAs_NUM = 0; diff --git a/library/ios_natpmp_stubs.c b/library/ios_natpmp_stubs.c new file mode 100644 index 000000000..ef635db10 --- /dev/null +++ b/library/ios_natpmp_stubs.c @@ -0,0 +1,14 @@ +/** + * iOS stub for getgateway.c functions. + * iOS doesn't have net/route.h, so we provide a stub that returns failure. + * NAT-PMP functionality won't work but the library will link. + */ + +#include +#include + +/* getdefaultgateway - returns -1 (failure) on iOS */ +int getdefaultgateway(in_addr_t *addr) { + (void)addr; /* unused */ + return -1; /* failure - not supported on iOS */ +} diff --git a/waku.nimble b/waku.nimble index 7bfdfab12..5c5c09763 100644 --- a/waku.nimble +++ b/waku.nimble @@ -213,3 +213,182 @@ task libWakuAndroid, "Build the mobile bindings for Android": let srcDir = "./library" let extraParams = "-d:chronicles_log_level=ERROR" buildMobileAndroid srcDir, extraParams + +### Mobile iOS +import std/sequtils + +proc buildMobileIOS(srcDir = ".", params = "") = + echo "Building iOS libwaku library" + + let iosArch = getEnv("IOS_ARCH") + let iosSdk = getEnv("IOS_SDK") + let sdkPath = getEnv("IOS_SDK_PATH") + + if sdkPath.len == 0: + quit "Error: IOS_SDK_PATH not set. Set it to the path of the iOS SDK" + + # Use SDK name in path to differentiate device vs simulator + let outDir = "build/ios/" & iosSdk & "-" & iosArch + if not dirExists outDir: + mkDir outDir + + var extra_params = params + for i in 2 ..< paramCount(): + extra_params &= " " & paramStr(i) + + let cpu = if iosArch == "arm64": "arm64" else: "amd64" + + # The output static library + let nimcacheDir = outDir & "/nimcache" + let objDir = outDir & "/obj" + let vendorObjDir = outDir & "/vendor_obj" + let aFile = outDir & "/libwaku.a" + + if not dirExists objDir: + mkDir objDir + if not dirExists vendorObjDir: + mkDir vendorObjDir + + let clangBase = "clang -arch " & iosArch & " -isysroot " & sdkPath & + " -mios-version-min=18.0 -fembed-bitcode -fPIC -O2" + + # Generate C sources from Nim (no linking) + exec "nim c" & + " --nimcache:" & nimcacheDir & + " --os:ios --cpu:" & cpu & + " --compileOnly:on" & + " --noMain --mm:refc" & + " --threads:on --opt:size --header" & + " -d:metrics -d:discv5_protocol_id=d5waku" & + " --nimMainPrefix:libwaku --skipParentCfg:on" & + " --cc:clang" & + " " & extra_params & + " " & srcDir & "/libwaku.nim" + + # Compile vendor C libraries for iOS + + # --- BearSSL --- + echo "Compiling BearSSL for iOS..." + let bearSslSrcDir = "./vendor/nim-bearssl/bearssl/csources/src" + let bearSslIncDir = "./vendor/nim-bearssl/bearssl/csources/inc" + for path in walkDirRec(bearSslSrcDir): + if path.endsWith(".c"): + let relPath = path.replace(bearSslSrcDir & "/", "").replace("/", "_") + let baseName = relPath.changeFileExt("o") + let oFile = vendorObjDir / ("bearssl_" & baseName) + if not fileExists(oFile): + exec clangBase & " -I" & bearSslIncDir & " -I" & bearSslSrcDir & " -c " & path & " -o " & oFile + + # --- secp256k1 --- + echo "Compiling secp256k1 for iOS..." + let secp256k1Dir = "./vendor/nim-secp256k1/vendor/secp256k1" + let secp256k1Flags = " -I" & secp256k1Dir & "/include" & + " -I" & secp256k1Dir & "/src" & + " -I" & secp256k1Dir & + " -DENABLE_MODULE_RECOVERY=1" & + " -DENABLE_MODULE_ECDH=1" & + " -DECMULT_WINDOW_SIZE=15" & + " -DECMULT_GEN_PREC_BITS=4" + + # Main secp256k1 source + let secp256k1Obj = vendorObjDir / "secp256k1.o" + if not fileExists(secp256k1Obj): + exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/secp256k1.c -o " & secp256k1Obj + + # Precomputed tables (required for ecmult operations) + let secp256k1PreEcmultObj = vendorObjDir / "secp256k1_precomputed_ecmult.o" + if not fileExists(secp256k1PreEcmultObj): + exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/precomputed_ecmult.c -o " & secp256k1PreEcmultObj + + let secp256k1PreEcmultGenObj = vendorObjDir / "secp256k1_precomputed_ecmult_gen.o" + if not fileExists(secp256k1PreEcmultGenObj): + exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/precomputed_ecmult_gen.c -o " & secp256k1PreEcmultGenObj + + # --- miniupnpc --- + echo "Compiling miniupnpc for iOS..." + let miniupnpcSrcDir = "./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/src" + let miniupnpcIncDir = "./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/include" + let miniupnpcBuildDir = "./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/build" + let miniupnpcFiles = @[ + "addr_is_reserved.c", "connecthostport.c", "igd_desc_parse.c", + "minisoap.c", "minissdpc.c", "miniupnpc.c", "miniwget.c", + "minixml.c", "portlistingparse.c", "receivedata.c", "upnpcommands.c", + "upnpdev.c", "upnperrors.c", "upnpreplyparse.c" + ] + for fileName in miniupnpcFiles: + let srcPath = miniupnpcSrcDir / fileName + let oFile = vendorObjDir / ("miniupnpc_" & fileName.changeFileExt("o")) + if fileExists(srcPath) and not fileExists(oFile): + exec clangBase & + " -I" & miniupnpcIncDir & + " -I" & miniupnpcSrcDir & + " -I" & miniupnpcBuildDir & + " -DMINIUPNPC_SET_SOCKET_TIMEOUT" & + " -D_BSD_SOURCE -D_DEFAULT_SOURCE" & + " -c " & srcPath & " -o " & oFile + + # --- libnatpmp --- + echo "Compiling libnatpmp for iOS..." + let natpmpSrcDir = "./vendor/nim-nat-traversal/vendor/libnatpmp-upstream" + # Only compile natpmp.c - getgateway.c uses net/route.h which is not available on iOS + let natpmpObj = vendorObjDir / "natpmp_natpmp.o" + if not fileExists(natpmpObj): + exec clangBase & + " -I" & natpmpSrcDir & + " -DENABLE_STRNATPMPERR" & + " -c " & natpmpSrcDir & "/natpmp.c -o " & natpmpObj + + # Use iOS-specific stub for getgateway + let getgatewayStubSrc = "./library/ios_natpmp_stubs.c" + let getgatewayStubObj = vendorObjDir / "natpmp_getgateway_stub.o" + if fileExists(getgatewayStubSrc) and not fileExists(getgatewayStubObj): + exec clangBase & " -c " & getgatewayStubSrc & " -o " & getgatewayStubObj + + # --- BearSSL stubs (for tools functions not in main library) --- + echo "Compiling BearSSL stubs for iOS..." + let bearSslStubsSrc = "./library/ios_bearssl_stubs.c" + let bearSslStubsObj = vendorObjDir / "bearssl_stubs.o" + if fileExists(bearSslStubsSrc) and not fileExists(bearSslStubsObj): + exec clangBase & " -c " & bearSslStubsSrc & " -o " & bearSslStubsObj + + # Compile all Nim-generated C files to object files + echo "Compiling Nim-generated C files for iOS..." + var cFiles: seq[string] = @[] + for kind, path in walkDir(nimcacheDir): + if kind == pcFile and path.endsWith(".c"): + cFiles.add(path) + + for cFile in cFiles: + let baseName = extractFilename(cFile).changeFileExt("o") + let oFile = objDir / baseName + exec clangBase & + " -DENABLE_STRNATPMPERR" & + " -I./vendor/nimbus-build-system/vendor/Nim/lib/" & + " -I./vendor/nim-bearssl/bearssl/csources/inc/" & + " -I./vendor/nim-bearssl/bearssl/csources/tools/" & + " -I./vendor/nim-bearssl/bearssl/abi/" & + " -I./vendor/nim-secp256k1/vendor/secp256k1/include/" & + " -I./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/include/" & + " -I./vendor/nim-nat-traversal/vendor/libnatpmp-upstream/" & + " -I" & nimcacheDir & + " -c " & cFile & + " -o " & oFile + + # Create static library from all object files + echo "Creating static library..." + var objFiles: seq[string] = @[] + for kind, path in walkDir(objDir): + if kind == pcFile and path.endsWith(".o"): + objFiles.add(path) + for kind, path in walkDir(vendorObjDir): + if kind == pcFile and path.endsWith(".o"): + objFiles.add(path) + + exec "libtool -static -o " & aFile & " " & objFiles.join(" ") + + echo "✔ iOS library created: " & aFile + +task libWakuIOS, "Build the mobile bindings for iOS": + let srcDir = "./library" + let extraParams = "-d:chronicles_log_level=ERROR" + buildMobileIOS srcDir, extraParams From dafdee9f5ffc0460f45307c61fbd8e9832fc3ecd Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Mon, 29 Dec 2025 23:04:24 +0100 Subject: [PATCH 040/155] small refactor README to start using Logos Messaging Nim term --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ce352d6f5..c64479738 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,21 @@ -# Nwaku +# Logos Messaging Nim ## Introduction -The nwaku repository implements Waku, and provides tools related to it. +The logos-messaging-nim, a.k.a. lmn or nwaku, repository implements a set of libp2p protocols aimed to bring +private communications. -- A Nim implementation of the [Waku (v2) protocol](https://specs.vac.dev/specs/waku/v2/waku-v2.html). -- CLI application `wakunode2` that allows you to run a Waku node. -- Examples of Waku usage. +- Nim implementation of [these specs](https://github.com/vacp2p/rfc-index/tree/main/waku). +- C library that exposes the implemented protocols. +- CLI application that allows you to run an lmn node. +- Examples. - Various tests of above. For more details see the [source code](waku/README.md) ## How to Build & Run ( Linux, MacOS & WSL ) -These instructions are generic. For more detailed instructions, see the Waku source code above. +These instructions are generic. For more detailed instructions, see the source code above. ### Prerequisites From a865ff72c86a17bdafad6779391d4d8994d7c0dc Mon Sep 17 00:00:00 2001 From: Sasha <118575614+weboko@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:19:37 +0100 Subject: [PATCH 041/155] update js-waku repo reference (#3684) --- .github/ISSUE_TEMPLATE/prepare_beta_release.md | 2 +- .github/ISSUE_TEMPLATE/prepare_full_release.md | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/pre-release.yml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/prepare_beta_release.md b/.github/ISSUE_TEMPLATE/prepare_beta_release.md index 9afaefbd1..383d9018c 100644 --- a/.github/ISSUE_TEMPLATE/prepare_beta_release.md +++ b/.github/ISSUE_TEMPLATE/prepare_beta_release.md @@ -22,7 +22,7 @@ All items below are to be completed by the owner of the given release. - [ ] Generate and edit release notes in CHANGELOG.md. - [ ] **Waku test and fleets validation** - - [ ] Ensure all the unit tests (specifically js-waku tests) are green against the release candidate. + - [ ] Ensure all the unit tests (specifically logos-messaging-js tests) are green against the release candidate. - [ ] Deploy the release candidate to `waku.test` only through [deploy-waku-test job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-test/) and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). - After completion, disable [deployment job](https://ci.infra.status.im/job/nim-waku/) so that its version is not updated on every merge to master. - Verify the deployed version at https://fleets.waku.org/. diff --git a/.github/ISSUE_TEMPLATE/prepare_full_release.md b/.github/ISSUE_TEMPLATE/prepare_full_release.md index 314146f60..d7458a8e3 100644 --- a/.github/ISSUE_TEMPLATE/prepare_full_release.md +++ b/.github/ISSUE_TEMPLATE/prepare_full_release.md @@ -24,7 +24,7 @@ All items below are to be completed by the owner of the given release. - [ ] **Validation of release candidate** - [ ] **Automated testing** - - [ ] Ensure all the unit tests (specifically js-waku tests) are green against the release candidate. + - [ ] Ensure all the unit tests (specifically logos-messaging-js tests) are green against the release candidate. - [ ] Ask Vac-QA and Vac-DST to perform the available tests against the release candidate. - [ ] Vac-DST (an additional report is needed; see [this](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f)) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b12a5109..da8383e43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,14 +151,14 @@ jobs: js-waku-node: needs: build-docker-image - uses: logos-messaging/js-waku/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-messaging-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node js-waku-node-optional: needs: build-docker-image - uses: logos-messaging/js-waku/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-messaging-js/.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 380ec755f..faded198b 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -98,7 +98,7 @@ jobs: js-waku-node: needs: build-docker-image - uses: logos-messaging/js-waku/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-messaging-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node @@ -106,7 +106,7 @@ jobs: js-waku-node-optional: needs: build-docker-image - uses: logos-messaging/js-waku/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-messaging-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node-optional From a4e44dbe05347197ba367f967aa99078814a80fc Mon Sep 17 00:00:00 2001 From: Tanya S <120410716+stubbsta@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:35:16 +0200 Subject: [PATCH 042/155] chore: Update anvil config (#3662) * Use anvil config disable-min-priority-fee to prevent gas price doubling * remove gas limit set in utils->deployContract --- tests/waku_rln_relay/utils.nim | 1 - tests/waku_rln_relay/utils_onchain.nim | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/waku_rln_relay/utils.nim b/tests/waku_rln_relay/utils.nim index a4247ab44..8aed18f9b 100644 --- a/tests/waku_rln_relay/utils.nim +++ b/tests/waku_rln_relay/utils.nim @@ -24,7 +24,6 @@ proc deployContract*( tr.`from` = Opt.some(web3.defaultAccount) let sData = code & contractInput tr.data = Opt.some(hexToSeqByte(sData)) - tr.gas = Opt.some(Quantity(3000000000000)) if gasPrice != 0: tr.gasPrice = Opt.some(gasPrice.Quantity) diff --git a/tests/waku_rln_relay/utils_onchain.nim b/tests/waku_rln_relay/utils_onchain.nim index d8bb13a62..9f1048097 100644 --- a/tests/waku_rln_relay/utils_onchain.nim +++ b/tests/waku_rln_relay/utils_onchain.nim @@ -529,6 +529,7 @@ proc runAnvil*( # --chain-id Chain ID of the network. # --load-state Initialize the chain from a previously saved state snapshot (read-only) # --dump-state Dump the state on exit to the given file (write-only) + # Values used are representative of Linea Sepolia testnet # See anvil documentation https://book.getfoundry.sh/reference/anvil/ for more details try: let anvilPath = getAnvilPath() @@ -539,11 +540,16 @@ proc runAnvil*( "--port", $port, "--gas-limit", - "300000000000000", + "30000000", + "--gas-price", + "7", + "--base-fee", + "7", "--balance", - "1000000000", + "10000000000", "--chain-id", $chainId, + "--disable-min-priority-fee", ] # Add state file argument if provided From 284a0816ccd1d7709f6e36acf37e8ad583502f6c Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:48:19 +0100 Subject: [PATCH 043/155] chore: use chronos' TokenBucket (#3670) * Adapt using chronos' TokenBucket. Removed TokenBucket and test. bump nim-chronos -> nim-libp2p/nim-lsquic/nim-jwt -> adapt to latest libp2p changes * Fix libp2p/utility reports unlisted exception can occure from close of socket in waitForService - -d:ssl compile flag caused it * Adapt request_limiter to new chronos' TokenBucket replenish algorithm to keep original intent of use * Fix filter dos protection test * Fix peer manager tests due change caused by new libp2p * Adjust store test rate limit to eliminate CI test flakyness of timing * Adjust store test rate limit to eliminate CI test flakyness of timing - lightpush/legacy_lightpush/filter * Rework filter dos protection test to avoid CI crazy timing causing flakyness in test results compared to local runs * Rework lightpush dos protection test to avoid CI crazy timing causing flakyness in test results compared to local runs * Rework lightpush and legacy lightpush rate limit tests to eliminate timing effect in CI that cause longer awaits thus result in minting new tokens unlike local runs --- .gitmodules | 6 + tests/common/test_all.nim | 1 - tests/common/test_tokenbucket.nim | 69 ------- tests/test_peer_manager.nim | 5 + .../test_waku_filter_dos_protection.nim | 98 +++++++--- tests/waku_lightpush/test_ratelimit.nim | 66 +++---- .../waku_lightpush_legacy/test_ratelimit.nim | 64 +++--- tests/waku_store/test_wakunode_store.nim | 2 +- vendor/nim-chronos | 2 +- vendor/nim-jwt | 1 + vendor/nim-libp2p | 2 +- vendor/nim-lsquic | 1 + waku.nimble | 8 +- waku/common/rate_limit/per_peer_limiter.nim | 2 +- waku/common/rate_limit/request_limiter.nim | 78 ++++++-- .../rate_limit/single_token_limiter.nim | 16 +- waku/common/rate_limit/token_bucket.nim | 182 ------------------ waku/factory/builder.nim | 1 + waku/factory/waku.nim | 1 - waku/node/peer_manager/peer_manager.nim | 7 +- waku/waku_rendezvous/protocol.nim | 1 - 21 files changed, 241 insertions(+), 372 deletions(-) delete mode 100644 tests/common/test_tokenbucket.nim create mode 160000 vendor/nim-jwt create mode 160000 vendor/nim-lsquic delete mode 100644 waku/common/rate_limit/token_bucket.nim diff --git a/.gitmodules b/.gitmodules index 4d56c4333..6a63491e3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -184,6 +184,12 @@ url = https://github.com/logos-messaging/waku-rlnv2-contract.git ignore = untracked branch = master +[submodule "vendor/nim-lsquic"] + path = vendor/nim-lsquic + url = https://github.com/vacp2p/nim-lsquic +[submodule "vendor/nim-jwt"] + path = vendor/nim-jwt + url = https://github.com/vacp2p/nim-jwt.git [submodule "vendor/nim-ffi"] path = vendor/nim-ffi url = https://github.com/logos-messaging/nim-ffi/ diff --git a/tests/common/test_all.nim b/tests/common/test_all.nim index 7495c7c9e..d597a7424 100644 --- a/tests/common/test_all.nim +++ b/tests/common/test_all.nim @@ -6,7 +6,6 @@ import ./test_protobuf_validation, ./test_sqlite_migrations, ./test_parse_size, - ./test_tokenbucket, ./test_requestratelimiter, ./test_ratelimit_setting, ./test_timed_map, diff --git a/tests/common/test_tokenbucket.nim b/tests/common/test_tokenbucket.nim deleted file mode 100644 index 5bc1a0583..000000000 --- a/tests/common/test_tokenbucket.nim +++ /dev/null @@ -1,69 +0,0 @@ -# Chronos Test Suite -# (c) Copyright 2022-Present -# Status Research & Development GmbH -# -# Licensed under either of -# Apache License, version 2.0, (LICENSE-APACHEv2) -# MIT license (LICENSE-MIT) - -{.used.} - -import testutils/unittests -import chronos -import ../../waku/common/rate_limit/token_bucket - -suite "Token Bucket": - test "TokenBucket Sync test - strict": - var bucket = TokenBucket.newStrict(1000, 1.milliseconds) - let - start = Moment.now() - fullTime = start + 1.milliseconds - check: - bucket.tryConsume(800, start) == true - bucket.tryConsume(200, start) == true - # Out of budget - bucket.tryConsume(100, start) == false - bucket.tryConsume(800, fullTime) == true - bucket.tryConsume(200, fullTime) == true - # Out of budget - bucket.tryConsume(100, fullTime) == false - - test "TokenBucket Sync test - compensating": - var bucket = TokenBucket.new(1000, 1.milliseconds) - let - start = Moment.now() - fullTime = start + 1.milliseconds - check: - bucket.tryConsume(800, start) == true - bucket.tryConsume(200, start) == true - # Out of budget - bucket.tryConsume(100, start) == false - bucket.tryConsume(800, fullTime) == true - bucket.tryConsume(200, fullTime) == true - # Due not using the bucket for a full period the compensation will satisfy this request - bucket.tryConsume(100, fullTime) == true - - test "TokenBucket Max compensation": - var bucket = TokenBucket.new(1000, 1.minutes) - var reqTime = Moment.now() - - check bucket.tryConsume(1000, reqTime) - check bucket.tryConsume(1, reqTime) == false - reqTime += 1.minutes - check bucket.tryConsume(500, reqTime) == true - reqTime += 1.minutes - check bucket.tryConsume(1000, reqTime) == true - reqTime += 10.seconds - # max compensation is 25% so try to consume 250 more - check bucket.tryConsume(250, reqTime) == true - reqTime += 49.seconds - # out of budget within the same period - check bucket.tryConsume(1, reqTime) == false - - test "TokenBucket Short replenish": - var bucket = TokenBucket.new(15000, 1.milliseconds) - let start = Moment.now() - check bucket.tryConsume(15000, start) - check bucket.tryConsume(1, start) == false - - check bucket.tryConsume(15000, start + 1.milliseconds) == true diff --git a/tests/test_peer_manager.nim b/tests/test_peer_manager.nim index 1369f3f88..97df39582 100644 --- a/tests/test_peer_manager.nim +++ b/tests/test_peer_manager.nim @@ -997,6 +997,7 @@ procSuite "Peer Manager": .build(), maxFailedAttempts = 1, storage = nil, + maxConnections = 20, ) # Create 30 peers and add them to the peerstore @@ -1063,6 +1064,7 @@ procSuite "Peer Manager": backoffFactor = 2, maxFailedAttempts = 10, storage = nil, + maxConnections = 20, ) var p1: PeerId require p1.init("QmeuZJbXrszW2jdT7GdduSjQskPU3S7vvGWKtKgDfkDvW" & "1") @@ -1116,6 +1118,7 @@ procSuite "Peer Manager": .build(), maxFailedAttempts = 150, storage = nil, + maxConnections = 20, ) # Should result in backoff > 1 week @@ -1131,6 +1134,7 @@ procSuite "Peer Manager": .build(), maxFailedAttempts = 10, storage = nil, + maxConnections = 20, ) let pm = PeerManager.new( @@ -1144,6 +1148,7 @@ procSuite "Peer Manager": .build(), maxFailedAttempts = 5, storage = nil, + maxConnections = 20, ) asyncTest "colocationLimit is enforced by pruneConnsByIp()": diff --git a/tests/waku_filter_v2/test_waku_filter_dos_protection.nim b/tests/waku_filter_v2/test_waku_filter_dos_protection.nim index 7c8c640ba..fd3d8c837 100644 --- a/tests/waku_filter_v2/test_waku_filter_dos_protection.nim +++ b/tests/waku_filter_v2/test_waku_filter_dos_protection.nim @@ -122,24 +122,51 @@ suite "Waku Filter - DOS protection": check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) - await sleepAsync(20.milliseconds) - check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == - none(FilterSubscribeErrorKind) + # Avoid using tiny sleeps to control refill behavior: CI scheduling can + # oversleep and mint additional tokens. Instead, issue a small burst of + # subscribe requests and require at least one TOO_MANY_REQUESTS. + var c1SubscribeFutures = newSeq[Future[FilterSubscribeResult]]() + for i in 0 ..< 6: + c1SubscribeFutures.add( + client1.wakuFilterClient.subscribe( + serverRemotePeerInfo, pubsubTopic, contentTopicSeq + ) + ) + + let c1Finished = await allFinished(c1SubscribeFutures) + var c1GotTooMany = false + for fut in c1Finished: + check not fut.failed() + let res = fut.read() + if res.isErr() and res.error().kind == FilterSubscribeErrorKind.TOO_MANY_REQUESTS: + c1GotTooMany = true + break + check c1GotTooMany + + # Ensure the other client is not affected by client1's rate limit. check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) - await sleepAsync(20.milliseconds) - check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == - none(FilterSubscribeErrorKind) - await sleepAsync(20.milliseconds) - check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == - some(FilterSubscribeErrorKind.TOO_MANY_REQUESTS) - check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == - none(FilterSubscribeErrorKind) - check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == - some(FilterSubscribeErrorKind.TOO_MANY_REQUESTS) + + var c2SubscribeFutures = newSeq[Future[FilterSubscribeResult]]() + for i in 0 ..< 6: + c2SubscribeFutures.add( + client2.wakuFilterClient.subscribe( + serverRemotePeerInfo, pubsubTopic, contentTopicSeq + ) + ) + + let c2Finished = await allFinished(c2SubscribeFutures) + var c2GotTooMany = false + for fut in c2Finished: + check not fut.failed() + let res = fut.read() + if res.isErr() and res.error().kind == FilterSubscribeErrorKind.TOO_MANY_REQUESTS: + c2GotTooMany = true + break + check c2GotTooMany # ensure period of time has passed and clients can again use the service - await sleepAsync(1000.milliseconds) + await sleepAsync(1100.milliseconds) check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == @@ -147,29 +174,54 @@ suite "Waku Filter - DOS protection": asyncTest "Ensure normal usage allowed": # Given + # Rate limit setting is (3 requests / 1000ms) per peer. + # In a token-bucket model this means: + # - capacity = 3 tokens + # - refill rate = 3 tokens / second => ~1 token every ~333ms + # - each request consumes 1 token (including UNSUBSCRIBE) check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) check wakuFilter.subscriptions.isSubscribed(client1.clientPeerId) - await sleepAsync(500.milliseconds) - check client1.ping(serverRemotePeerInfo) == none(FilterSubscribeErrorKind) - check wakuFilter.subscriptions.isSubscribed(client1.clientPeerId) + # Expected remaining tokens (approx): 2 await sleepAsync(500.milliseconds) check client1.ping(serverRemotePeerInfo) == none(FilterSubscribeErrorKind) check wakuFilter.subscriptions.isSubscribed(client1.clientPeerId) - await sleepAsync(50.milliseconds) + # After ~500ms, ~1 token refilled; PING consumes 1 => expected remaining: 2 + + await sleepAsync(500.milliseconds) + check client1.ping(serverRemotePeerInfo) == none(FilterSubscribeErrorKind) + check wakuFilter.subscriptions.isSubscribed(client1.clientPeerId) + + # After another ~500ms, ~1 token refilled; PING consumes 1 => expected remaining: 2 + check client1.unsubscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) check wakuFilter.subscriptions.isSubscribed(client1.clientPeerId) == false - await sleepAsync(50.milliseconds) check client1.ping(serverRemotePeerInfo) == some(FilterSubscribeErrorKind.NOT_FOUND) - check client1.ping(serverRemotePeerInfo) == some(FilterSubscribeErrorKind.NOT_FOUND) - await sleepAsync(50.milliseconds) - check client1.ping(serverRemotePeerInfo) == - some(FilterSubscribeErrorKind.TOO_MANY_REQUESTS) + # After unsubscribing, PING is expected to return NOT_FOUND while still + # counting towards the rate limit. + + # CI can oversleep / schedule slowly, which can mint extra tokens between + # requests. To make the test robust, issue a small burst of pings and + # require at least one TOO_MANY_REQUESTS response. + var pingFutures = newSeq[Future[FilterSubscribeResult]]() + for i in 0 ..< 9: + pingFutures.add(client1.wakuFilterClient.ping(serverRemotePeerInfo)) + + let finished = await allFinished(pingFutures) + var gotTooMany = false + for fut in finished: + check not fut.failed() + let pingRes = fut.read() + if pingRes.isErr() and pingRes.error().kind == FilterSubscribeErrorKind.TOO_MANY_REQUESTS: + gotTooMany = true + break + + check gotTooMany check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) diff --git a/tests/waku_lightpush/test_ratelimit.nim b/tests/waku_lightpush/test_ratelimit.nim index 7420a4e56..bdab3f074 100644 --- a/tests/waku_lightpush/test_ratelimit.nim +++ b/tests/waku_lightpush/test_ratelimit.nim @@ -80,11 +80,12 @@ suite "Rate limited push service": await allFutures(serverSwitch.start(), clientSwitch.start()) ## Given - var handlerFuture = newFuture[(string, WakuMessage)]() + # Don't rely on per-request timing assumptions or a single shared Future. + # CI can be slow enough that sequential requests accidentally refill tokens. + # Instead we issue a small burst and assert we observe at least one rejection. let handler = proc( peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = - handlerFuture.complete((pubsubTopic, message)) return lightpushSuccessResult(1) let @@ -93,45 +94,38 @@ suite "Rate limited push service": client = newTestWakuLightpushClient(clientSwitch) let serverPeerId = serverSwitch.peerInfo.toRemotePeerInfo() - let topic = DefaultPubsubTopic + let tokenPeriod = 500.millis - let successProc = proc(): Future[void] {.async.} = + # Fire a burst of requests; require at least one success and one rejection. + var publishFutures = newSeq[Future[WakuLightPushResult]]() + for i in 0 ..< 10: let message = fakeWakuMessage() - handlerFuture = newFuture[(string, WakuMessage)]() - let requestRes = - await client.publish(some(DefaultPubsubTopic), message, serverPeerId) - discard await handlerFuture.withTimeout(10.millis) + publishFutures.add( + client.publish(some(DefaultPubsubTopic), message, serverPeerId) + ) - check: - requestRes.isOk() - handlerFuture.finished() - let (handledMessagePubsubTopic, handledMessage) = handlerFuture.read() - check: - handledMessagePubsubTopic == DefaultPubsubTopic - handledMessage == message + let finished = await allFinished(publishFutures) + var gotOk = false + var gotTooMany = false + for fut in finished: + check not fut.failed() + let res = fut.read() + if res.isOk(): + gotOk = true + else: + check res.error.code == LightPushErrorCode.TOO_MANY_REQUESTS + check res.error.desc == some(TooManyRequestsMessage) + gotTooMany = true - let rejectProc = proc(): Future[void] {.async.} = - let message = fakeWakuMessage() - handlerFuture = newFuture[(string, WakuMessage)]() - let requestRes = - await client.publish(some(DefaultPubsubTopic), message, serverPeerId) - discard await handlerFuture.withTimeout(10.millis) + check gotOk + check gotTooMany - check: - requestRes.isErr() - requestRes.error.code == LightPushErrorCode.TOO_MANY_REQUESTS - requestRes.error.desc == some(TooManyRequestsMessage) - - for testCnt in 0 .. 2: - await successProc() - await sleepAsync(20.millis) - - await rejectProc() - - await sleepAsync(500.millis) - - ## next one shall succeed due to the rate limit time window has passed - await successProc() + # ensure period of time has passed and the client can again use the service + await sleepAsync(tokenPeriod + 100.millis) + let recoveryRes = await client.publish( + some(DefaultPubsubTopic), fakeWakuMessage(), serverPeerId + ) + check recoveryRes.isOk() ## Cleanup await allFutures(clientSwitch.stop(), serverSwitch.stop()) diff --git a/tests/waku_lightpush_legacy/test_ratelimit.nim b/tests/waku_lightpush_legacy/test_ratelimit.nim index 3df8d369d..37c43a066 100644 --- a/tests/waku_lightpush_legacy/test_ratelimit.nim +++ b/tests/waku_lightpush_legacy/test_ratelimit.nim @@ -86,58 +86,52 @@ suite "Rate limited push service": await allFutures(serverSwitch.start(), clientSwitch.start()) ## Given - var handlerFuture = newFuture[(string, WakuMessage)]() let handler = proc( peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = - handlerFuture.complete((pubsubTopic, message)) return ok() let + tokenPeriod = 500.millis server = await newTestWakuLegacyLightpushNode( - serverSwitch, handler, some((3, 500.millis)) + serverSwitch, handler, some((3, tokenPeriod)) ) client = newTestWakuLegacyLightpushClient(clientSwitch) let serverPeerId = serverSwitch.peerInfo.toRemotePeerInfo() - let topic = DefaultPubsubTopic - let successProc = proc(): Future[void] {.async.} = - let message = fakeWakuMessage() - handlerFuture = newFuture[(string, WakuMessage)]() - let requestRes = - await client.publish(DefaultPubsubTopic, message, peer = serverPeerId) - discard await handlerFuture.withTimeout(10.millis) + # Avoid assuming the exact Nth request will be rejected. With Chronos TokenBucket + # minting semantics and real network latency, CI timing can allow refills. + # Instead, send a short burst and require that we observe at least one rejection. + let burstSize = 10 + var publishFutures: seq[Future[WakuLightPushResult[string]]] = @[] + for _ in 0 ..< burstSize: + publishFutures.add( + client.publish(DefaultPubsubTopic, fakeWakuMessage(), peer = serverPeerId) + ) - check: - requestRes.isOk() - handlerFuture.finished() - let (handledMessagePubsubTopic, handledMessage) = handlerFuture.read() - check: - handledMessagePubsubTopic == DefaultPubsubTopic - handledMessage == message + let finished = await allFinished(publishFutures) + var gotOk = false + var gotTooMany = false + for fut in finished: + check not fut.failed() + let res = fut.read() + if res.isOk(): + gotOk = true + elif res.error == "TOO_MANY_REQUESTS": + gotTooMany = true - let rejectProc = proc(): Future[void] {.async.} = - let message = fakeWakuMessage() - handlerFuture = newFuture[(string, WakuMessage)]() - let requestRes = - await client.publish(DefaultPubsubTopic, message, peer = serverPeerId) - discard await handlerFuture.withTimeout(10.millis) + check: + gotOk + gotTooMany - check: - requestRes.isErr() - requestRes.error == "TOO_MANY_REQUESTS" - - for testCnt in 0 .. 2: - await successProc() - await sleepAsync(20.millis) - - await rejectProc() - - await sleepAsync(500.millis) + await sleepAsync(tokenPeriod + 100.millis) ## next one shall succeed due to the rate limit time window has passed - await successProc() + let afterCooldownRes = + await client.publish(DefaultPubsubTopic, fakeWakuMessage(), peer = serverPeerId) + check: + afterCooldownRes.isOk() ## Cleanup await allFutures(clientSwitch.stop(), serverSwitch.stop()) diff --git a/tests/waku_store/test_wakunode_store.nim b/tests/waku_store/test_wakunode_store.nim index b20309079..7d1a44ecc 100644 --- a/tests/waku_store/test_wakunode_store.nim +++ b/tests/waku_store/test_wakunode_store.nim @@ -413,7 +413,7 @@ procSuite "WakuNode - Store": for count in 0 ..< 3: waitFor successProc() - waitFor sleepAsync(20.millis) + waitFor sleepAsync(5.millis) waitFor failsProc() diff --git a/vendor/nim-chronos b/vendor/nim-chronos index 0646c444f..85af4db76 160000 --- a/vendor/nim-chronos +++ b/vendor/nim-chronos @@ -1 +1 @@ -Subproject commit 0646c444fce7c7ed08ef6f2c9a7abfd172ffe655 +Subproject commit 85af4db764ecd3573c4704139560df3943216cf1 diff --git a/vendor/nim-jwt b/vendor/nim-jwt new file mode 160000 index 000000000..18f8378de --- /dev/null +++ b/vendor/nim-jwt @@ -0,0 +1 @@ +Subproject commit 18f8378de52b241f321c1f9ea905456e89b95c6f diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index e82080f7b..eb7e6ff89 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit e82080f7b1aa61c6d35fa5311b873f41eff4bb52 +Subproject commit eb7e6ff89889e41b57515f891ba82986c54809fb diff --git a/vendor/nim-lsquic b/vendor/nim-lsquic new file mode 160000 index 000000000..f3fe33462 --- /dev/null +++ b/vendor/nim-lsquic @@ -0,0 +1 @@ +Subproject commit f3fe33462601ea34eb2e8e9c357c92e61f8d121b diff --git a/waku.nimble b/waku.nimble index 5c5c09763..afc0ad634 100644 --- a/waku.nimble +++ b/waku.nimble @@ -31,6 +31,8 @@ requires "nim >= 2.2.4", "results", "db_connector", "minilru", + "lsquic", + "jwt", "ffi" ### Helper functions @@ -148,7 +150,8 @@ task chat2, "Build example Waku chat usage": let name = "chat2" buildBinary name, "apps/chat2/", - "-d:chronicles_sinks=textlines[file] -d:ssl -d:chronicles_log_level='TRACE' " + "-d:chronicles_sinks=textlines[file] -d:chronicles_log_level='TRACE' " + # -d:ssl - cause unlisted exception error in libp2p/utility... task chat2mix, "Build example Waku chat mix usage": # NOTE For debugging, set debug level. For chat usage we want minimal log @@ -158,7 +161,8 @@ task chat2mix, "Build example Waku chat mix usage": let name = "chat2mix" buildBinary name, "apps/chat2mix/", - "-d:chronicles_sinks=textlines[file] -d:ssl -d:chronicles_log_level='TRACE' " + "-d:chronicles_sinks=textlines[file] -d:chronicles_log_level='TRACE' " + # -d:ssl - cause unlisted exception error in libp2p/utility... task chat2bridge, "Build chat2bridge": let name = "chat2bridge" diff --git a/waku/common/rate_limit/per_peer_limiter.nim b/waku/common/rate_limit/per_peer_limiter.nim index 5cb96a2d1..16b6bf065 100644 --- a/waku/common/rate_limit/per_peer_limiter.nim +++ b/waku/common/rate_limit/per_peer_limiter.nim @@ -20,7 +20,7 @@ proc mgetOrPut( perPeerRateLimiter: var PerPeerRateLimiter, peerId: PeerId ): var Option[TokenBucket] = return perPeerRateLimiter.peerBucket.mgetOrPut( - peerId, newTokenBucket(perPeerRateLimiter.setting, ReplenishMode.Compensating) + peerId, newTokenBucket(perPeerRateLimiter.setting, ReplenishMode.Continuous) ) template checkUsageLimit*( diff --git a/waku/common/rate_limit/request_limiter.nim b/waku/common/rate_limit/request_limiter.nim index 0ede20be4..bc318e151 100644 --- a/waku/common/rate_limit/request_limiter.nim +++ b/waku/common/rate_limit/request_limiter.nim @@ -39,38 +39,82 @@ const SECONDS_RATIO = 3 const MINUTES_RATIO = 2 type RequestRateLimiter* = ref object of RootObj - tokenBucket: Option[TokenBucket] + tokenBucket: TokenBucket setting*: Option[RateLimitSetting] + mainBucketSetting: RateLimitSetting + ratio: int peerBucketSetting*: RateLimitSetting peerUsage: TimedMap[PeerId, TokenBucket] + checkUsageImpl: proc( + t: var RequestRateLimiter, proto: string, conn: Connection, now: Moment + ): bool {.gcsafe, raises: [].} + +proc newMainTokenBucket( + setting: RateLimitSetting, ratio: int, startTime: Moment +): TokenBucket = + ## RequestRateLimiter's global bucket should keep the *rate* of the configured + ## setting while allowing a larger burst window. We achieve this by scaling + ## both capacity and fillDuration by the same ratio. + ## + ## This matches previous behavior where unused tokens could effectively + ## accumulate across multiple periods. + let burstCapacity = setting.volume * ratio + var bucket = TokenBucket.new( + capacity = burstCapacity, + fillDuration = setting.period * ratio, + startTime = startTime, + mode = Continuous, + ) + + # Start with the configured volume (not the burst capacity) so that the + # initial burst behavior matches the raw setting, while still allowing + # accumulation up to `burstCapacity` over time. + let excess = burstCapacity - setting.volume + if excess > 0: + discard bucket.tryConsume(excess, startTime) + + return bucket proc mgetOrPut( - requestRateLimiter: var RequestRateLimiter, peerId: PeerId + requestRateLimiter: var RequestRateLimiter, peerId: PeerId, now: Moment ): var TokenBucket = - let bucketForNew = newTokenBucket(some(requestRateLimiter.peerBucketSetting)).valueOr: + let bucketForNew = newTokenBucket( + some(requestRateLimiter.peerBucketSetting), Discrete, now + ).valueOr: raiseAssert "This branch is not allowed to be reached as it will not be called if the setting is None." return requestRateLimiter.peerUsage.mgetOrPut(peerId, bucketForNew) -proc checkUsage*( - t: var RequestRateLimiter, proto: string, conn: Connection, now = Moment.now() -): bool {.raises: [].} = - if t.tokenBucket.isNone(): - return true +proc checkUsageUnlimited( + t: var RequestRateLimiter, proto: string, conn: Connection, now: Moment +): bool {.gcsafe, raises: [].} = + true - let peerBucket = t.mgetOrPut(conn.peerId) +proc checkUsageLimited( + t: var RequestRateLimiter, proto: string, conn: Connection, now: Moment +): bool {.gcsafe, raises: [].} = + # Lazy-init the main bucket using the first observed request time. This makes + # refill behavior deterministic under tests where `now` is controlled. + if isNil(t.tokenBucket): + t.tokenBucket = newMainTokenBucket(t.mainBucketSetting, t.ratio, now) + + let peerBucket = t.mgetOrPut(conn.peerId, now) ## check requesting peer's usage is not over the calculated ratio and let that peer go which not requested much/or this time... if not peerBucket.tryConsume(1, now): trace "peer usage limit reached", peer = conn.peerId return false # Ok if the peer can consume, check the overall budget we have left - let tokenBucket = t.tokenBucket.get() - if not tokenBucket.tryConsume(1, now): + if not t.tokenBucket.tryConsume(1, now): return false return true +proc checkUsage*( + t: var RequestRateLimiter, proto: string, conn: Connection, now = Moment.now() +): bool {.raises: [].} = + t.checkUsageImpl(t, proto, conn, now) + template checkUsageLimit*( t: var RequestRateLimiter, proto: string, @@ -135,9 +179,19 @@ func calcPeerTokenSetting( proc newRequestRateLimiter*(setting: Option[RateLimitSetting]): RequestRateLimiter = let ratio = calcPeriodRatio(setting) + let isLimited = setting.isSome() and not setting.get().isUnlimited() + let mainBucketSetting = + if isLimited: + setting.get() + else: + (0, 0.minutes) + return RequestRateLimiter( - tokenBucket: newTokenBucket(setting), + tokenBucket: nil, setting: setting, + mainBucketSetting: mainBucketSetting, + ratio: ratio, peerBucketSetting: calcPeerTokenSetting(setting, ratio), peerUsage: init(TimedMap[PeerId, TokenBucket], calcCacheTimeout(setting, ratio)), + checkUsageImpl: (if isLimited: checkUsageLimited else: checkUsageUnlimited), ) diff --git a/waku/common/rate_limit/single_token_limiter.nim b/waku/common/rate_limit/single_token_limiter.nim index 50fb2d64c..fc4b0acd5 100644 --- a/waku/common/rate_limit/single_token_limiter.nim +++ b/waku/common/rate_limit/single_token_limiter.nim @@ -6,12 +6,15 @@ import std/[options], chronos/timer, libp2p/stream/connection, libp2p/utility import std/times except TimeInterval, Duration -import ./[token_bucket, setting, service_metrics] +import chronos/ratelimit as token_bucket + +import ./[setting, service_metrics] export token_bucket, setting, service_metrics proc newTokenBucket*( setting: Option[RateLimitSetting], - replenishMode: ReplenishMode = ReplenishMode.Compensating, + replenishMode: static[ReplenishMode] = ReplenishMode.Continuous, + startTime: Moment = Moment.now(), ): Option[TokenBucket] = if setting.isNone(): return none[TokenBucket]() @@ -19,7 +22,14 @@ proc newTokenBucket*( if setting.get().isUnlimited(): return none[TokenBucket]() - return some(TokenBucket.new(setting.get().volume, setting.get().period)) + return some( + TokenBucket.new( + capacity = setting.get().volume, + fillDuration = setting.get().period, + startTime = startTime, + mode = replenishMode, + ) + ) proc checkUsage( t: var TokenBucket, proto: string, now = Moment.now() diff --git a/waku/common/rate_limit/token_bucket.nim b/waku/common/rate_limit/token_bucket.nim deleted file mode 100644 index 799817ebd..000000000 --- a/waku/common/rate_limit/token_bucket.nim +++ /dev/null @@ -1,182 +0,0 @@ -{.push raises: [].} - -import chronos, std/math, std/options - -const BUDGET_COMPENSATION_LIMIT_PERCENT = 0.25 - -## This is an extract from chronos/rate_limit.nim due to the found bug in the original implementation. -## Unfortunately that bug cannot be solved without harm the original features of TokenBucket class. -## So, this current shortcut is used to enable move ahead with nwaku rate limiter implementation. -## ref: https://github.com/status-im/nim-chronos/issues/500 -## -## This version of TokenBucket is different from the original one in chronos/rate_limit.nim in many ways: -## - It has a new mode called `Compensating` which is the default mode. -## Compensation is calculated as the not used bucket capacity in the last measured period(s) in average. -## or up until maximum the allowed compansation treshold (Currently it is const 25%). -## Also compensation takes care of the proper time period calculation to avoid non-usage periods that can lead to -## overcompensation. -## - Strict mode is also available which will only replenish when time period is over but also will fill -## the bucket to the max capacity. - -type - ReplenishMode* = enum - Strict - Compensating - - TokenBucket* = ref object - budget: int ## Current number of tokens in the bucket - budgetCap: int ## Bucket capacity - lastTimeFull: Moment - ## This timer measures the proper periodizaiton of the bucket refilling - fillDuration: Duration ## Refill period - case replenishMode*: ReplenishMode - of Strict: - ## In strict mode, the bucket is refilled only till the budgetCap - discard - of Compensating: - ## This is the default mode. - maxCompensation: float - -func periodDistance(bucket: TokenBucket, currentTime: Moment): float = - ## notice fillDuration cannot be zero by design - ## period distance is a float number representing the calculated period time - ## since the last time bucket was refilled. - return - nanoseconds(currentTime - bucket.lastTimeFull).float / - nanoseconds(bucket.fillDuration).float - -func getUsageAverageSince(bucket: TokenBucket, distance: float): float = - if distance == 0.float: - ## in case there is zero time difference than the usage percentage is 100% - return 1.0 - - ## budgetCap can never be zero - ## usage average is calculated as a percentage of total capacity available over - ## the measured period - return bucket.budget.float / bucket.budgetCap.float / distance - -proc calcCompensation(bucket: TokenBucket, averageUsage: float): int = - # if we already fully used or even overused the tokens, there is no place for compensation - if averageUsage >= 1.0: - return 0 - - ## compensation is the not used bucket capacity in the last measured period(s) in average. - ## or maximum the allowed compansation treshold - let compensationPercent = - min((1.0 - averageUsage) * bucket.budgetCap.float, bucket.maxCompensation) - return trunc(compensationPercent).int - -func periodElapsed(bucket: TokenBucket, currentTime: Moment): bool = - return currentTime - bucket.lastTimeFull >= bucket.fillDuration - -## Update will take place if bucket is empty and trying to consume tokens. -## It checks if the bucket can be replenished as refill duration is passed or not. -## - strict mode: -proc updateStrict(bucket: TokenBucket, currentTime: Moment) = - if bucket.fillDuration == default(Duration): - bucket.budget = min(bucket.budgetCap, bucket.budget) - return - - if not periodElapsed(bucket, currentTime): - return - - bucket.budget = bucket.budgetCap - bucket.lastTimeFull = currentTime - -## - compensating - ballancing load: -## - between updates we calculate average load (current bucket capacity / number of periods till last update) -## - gives the percentage load used recently -## - with this we can replenish bucket up to 100% + calculated leftover from previous period (caped with max treshold) -proc updateWithCompensation(bucket: TokenBucket, currentTime: Moment) = - if bucket.fillDuration == default(Duration): - bucket.budget = min(bucket.budgetCap, bucket.budget) - return - - # do not replenish within the same period - if not periodElapsed(bucket, currentTime): - return - - let distance = bucket.periodDistance(currentTime) - let recentAvgUsage = bucket.getUsageAverageSince(distance) - let compensation = bucket.calcCompensation(recentAvgUsage) - - bucket.budget = bucket.budgetCap + compensation - bucket.lastTimeFull = currentTime - -proc update(bucket: TokenBucket, currentTime: Moment) = - if bucket.replenishMode == ReplenishMode.Compensating: - updateWithCompensation(bucket, currentTime) - else: - updateStrict(bucket, currentTime) - -proc tryConsume*(bucket: TokenBucket, tokens: int, now = Moment.now()): bool = - ## If `tokens` are available, consume them, - ## Otherwhise, return false. - - if bucket.budget >= bucket.budgetCap: - bucket.lastTimeFull = now - - if bucket.budget >= tokens: - bucket.budget -= tokens - return true - - bucket.update(now) - - if bucket.budget >= tokens: - bucket.budget -= tokens - return true - else: - return false - -proc replenish*(bucket: TokenBucket, tokens: int, now = Moment.now()) = - ## Add `tokens` to the budget (capped to the bucket capacity) - bucket.budget += tokens - bucket.update(now) - -proc new*( - T: type[TokenBucket], - budgetCap: int, - fillDuration: Duration = 1.seconds, - mode: ReplenishMode = ReplenishMode.Compensating, -): T = - assert not isZero(fillDuration) - assert budgetCap != 0 - - ## Create different mode TokenBucket - case mode - of ReplenishMode.Strict: - return T( - budget: budgetCap, - budgetCap: budgetCap, - fillDuration: fillDuration, - lastTimeFull: Moment.now(), - replenishMode: mode, - ) - of ReplenishMode.Compensating: - T( - budget: budgetCap, - budgetCap: budgetCap, - fillDuration: fillDuration, - lastTimeFull: Moment.now(), - replenishMode: mode, - maxCompensation: budgetCap.float * BUDGET_COMPENSATION_LIMIT_PERCENT, - ) - -proc newStrict*(T: type[TokenBucket], capacity: int, period: Duration): TokenBucket = - T.new(capacity, period, ReplenishMode.Strict) - -proc newCompensating*( - T: type[TokenBucket], capacity: int, period: Duration -): TokenBucket = - T.new(capacity, period, ReplenishMode.Compensating) - -func `$`*(b: TokenBucket): string {.inline.} = - if isNil(b): - return "nil" - return $b.budgetCap & "/" & $b.fillDuration - -func `$`*(ob: Option[TokenBucket]): string {.inline.} = - if ob.isNone(): - return "no-limit" - - return $ob.get() diff --git a/waku/factory/builder.nim b/waku/factory/builder.nim index 772cfbffd..f379f92bb 100644 --- a/waku/factory/builder.nim +++ b/waku/factory/builder.nim @@ -209,6 +209,7 @@ proc build*(builder: WakuNodeBuilder): Result[WakuNode, string] = maxServicePeers = some(builder.maxServicePeers), colocationLimit = builder.colocationLimit, shardedPeerManagement = builder.shardAware, + maxConnections = builder.switchMaxConnections.get(builders.MaxConnections), ) var node: WakuNode diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index c0380ccc9..d55206f97 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -13,7 +13,6 @@ import libp2p/services/autorelayservice, libp2p/services/hpservice, libp2p/peerid, - libp2p/discovery/rendezvousinterface, eth/keys, eth/p2p/discoveryv5/enr, presto, diff --git a/waku/node/peer_manager/peer_manager.nim b/waku/node/peer_manager/peer_manager.nim index 1abcc1ac0..487d3894d 100644 --- a/waku/node/peer_manager/peer_manager.nim +++ b/waku/node/peer_manager/peer_manager.nim @@ -103,6 +103,7 @@ type PeerManager* = ref object of RootObj onConnectionChange*: ConnectionChangeHandler online: bool ## state managed by online_monitor module getShards: GetShards + maxConnections: int #~~~~~~~~~~~~~~~~~~~# # Helper Functions # @@ -748,7 +749,6 @@ proc logAndMetrics(pm: PeerManager) {.async.} = var peerStore = pm.switch.peerStore # log metrics let (inRelayPeers, outRelayPeers) = pm.connectedPeers(WakuRelayCodec) - let maxConnections = pm.switch.connManager.inSema.size let notConnectedPeers = peerStore.getDisconnectedPeers().mapIt(RemotePeerInfo.init(it.peerId, it.addrs)) let outsideBackoffPeers = notConnectedPeers.filterIt(pm.canBeConnected(it.peerId)) @@ -758,7 +758,7 @@ proc logAndMetrics(pm: PeerManager) {.async.} = info "Relay peer connections", inRelayConns = $inRelayPeers.len & "/" & $pm.inRelayPeersTarget, outRelayConns = $outRelayPeers.len & "/" & $pm.outRelayPeersTarget, - totalConnections = $totalConnections & "/" & $maxConnections, + totalConnections = $totalConnections & "/" & $pm.maxConnections, notConnectedPeers = notConnectedPeers.len, outsideBackoffPeers = outsideBackoffPeers.len @@ -1048,9 +1048,9 @@ proc new*( maxFailedAttempts = MaxFailedAttempts, colocationLimit = DefaultColocationLimit, shardedPeerManagement = false, + maxConnections: int = MaxConnections, ): PeerManager {.gcsafe.} = let capacity = switch.peerStore.capacity - let maxConnections = switch.connManager.inSema.size if maxConnections > capacity: error "Max number of connections can't be greater than PeerManager capacity", capacity = capacity, maxConnections = maxConnections @@ -1099,6 +1099,7 @@ proc new*( colocationLimit: colocationLimit, shardedPeerManagement: shardedPeerManagement, online: true, + maxConnections: maxConnections, ) proc peerHook( diff --git a/waku/waku_rendezvous/protocol.nim b/waku/waku_rendezvous/protocol.nim index 7b97375ff..00b5f1a5c 100644 --- a/waku/waku_rendezvous/protocol.nim +++ b/waku/waku_rendezvous/protocol.nim @@ -8,7 +8,6 @@ import stew/byteutils, libp2p/protocols/rendezvous, libp2p/protocols/rendezvous/protobuf, - libp2p/discovery/discoverymngr, libp2p/utils/semaphore, libp2p/utils/offsettedseq, libp2p/crypto/curve25519, From c27405b19c62bd06ccf5a322590fc55ffa172ea3 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:55:25 +0100 Subject: [PATCH 044/155] chore: context aware brokers (#3674) * Refactor RequestBroker to support context aware use - introduction of BrokerContext * Context aware extension for EventBroker, EventBoker support for native or external types * Enhance MultiRequestBroker - similar to RequestBroker and EventBroker - with support for native and external types and context aware execution. * Move duplicated and common code into broker_utils from event- request- and multi_request_brokers * Change BrokerContext from random number to counter * Apply suggestion from @Ivansete-status Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> * Adjust naming in broker tests * Follow up adjustment from send_api use --------- Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> --- tests/common/test_event_broker.nim | 76 +++ tests/common/test_multi_request_broker.nim | 111 +++- tests/common/test_request_broker.nim | 163 ++++++ waku/common/broker/broker_context.nim | 68 +++ waku/common/broker/event_broker.nim | 346 +++++++----- waku/common/broker/helper/broker_utils.nim | 163 ++++++ waku/common/broker/multi_request_broker.nim | 562 +++++++++++++------- waku/common/broker/request_broker.nim | 451 +++++++++++----- 8 files changed, 1477 insertions(+), 463 deletions(-) create mode 100644 waku/common/broker/broker_context.nim diff --git a/tests/common/test_event_broker.nim b/tests/common/test_event_broker.nim index cead1277f..bcd081f4f 100644 --- a/tests/common/test_event_broker.nim +++ b/tests/common/test_event_broker.nim @@ -4,6 +4,15 @@ import testutils/unittests import waku/common/broker/event_broker +type ExternalDefinedEventType = object + label*: string + +EventBroker: + type IntEvent = int + +EventBroker: + type ExternalAliasEvent = distinct ExternalDefinedEventType + EventBroker: type SampleEvent = object value*: int @@ -123,3 +132,70 @@ suite "EventBroker": check counter == 21 # 1+2+3 + 4+5+6 RefEvent.dropAllListeners() + + test "supports BrokerContext-scoped listeners": + SampleEvent.dropAllListeners() + + let ctxA = NewBrokerContext() + let ctxB = NewBrokerContext() + + var seenA: seq[int] = @[] + var seenB: seq[int] = @[] + + discard SampleEvent.listen( + ctxA, + proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = + seenA.add(evt.value), + ) + + discard SampleEvent.listen( + ctxB, + proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = + seenB.add(evt.value), + ) + + SampleEvent.emit(ctxA, SampleEvent(value: 1, label: "a")) + SampleEvent.emit(ctxB, SampleEvent(value: 2, label: "b")) + waitForListeners() + + check seenA == @[1] + check seenB == @[2] + + SampleEvent.dropAllListeners(ctxA) + SampleEvent.emit(ctxA, SampleEvent(value: 3, label: "a2")) + SampleEvent.emit(ctxB, SampleEvent(value: 4, label: "b2")) + waitForListeners() + + check seenA == @[1] + check seenB == @[2, 4] + + SampleEvent.dropAllListeners(ctxB) + + test "supports non-object event types (auto-distinct)": + var seen: seq[int] = @[] + + discard IntEvent.listen( + proc(evt: IntEvent): Future[void] {.async: (raises: []).} = + seen.add(int(evt)) + ) + + IntEvent.emit(IntEvent(42)) + waitForListeners() + + check seen == @[42] + IntEvent.dropAllListeners() + + test "supports externally-defined type aliases (auto-distinct)": + var seen: seq[string] = @[] + + discard ExternalAliasEvent.listen( + proc(evt: ExternalAliasEvent): Future[void] {.async: (raises: []).} = + let base = ExternalDefinedEventType(evt) + seen.add(base.label) + ) + + ExternalAliasEvent.emit(ExternalAliasEvent(ExternalDefinedEventType(label: "x"))) + waitForListeners() + + check seen == @["x"] + ExternalAliasEvent.dropAllListeners() diff --git a/tests/common/test_multi_request_broker.nim b/tests/common/test_multi_request_broker.nim index 3bf10a54d..39ed90eea 100644 --- a/tests/common/test_multi_request_broker.nim +++ b/tests/common/test_multi_request_broker.nim @@ -31,6 +31,23 @@ MultiRequestBroker: suffix: string ): Future[Result[DualResponse, string]] {.async.} +type ExternalBaseType = string + +MultiRequestBroker: + type NativeIntResponse = int + + proc signatureFetch*(): Future[Result[NativeIntResponse, string]] {.async.} + +MultiRequestBroker: + type ExternalAliasResponse = ExternalBaseType + + proc signatureFetch*(): Future[Result[ExternalAliasResponse, string]] {.async.} + +MultiRequestBroker: + type AlreadyDistinctResponse = distinct int + + proc signatureFetch*(): Future[Result[AlreadyDistinctResponse, string]] {.async.} + suite "MultiRequestBroker": test "aggregates zero-argument providers": discard NoArgResponse.setProvider( @@ -194,7 +211,6 @@ suite "MultiRequestBroker": let firstHandler = NoArgResponse.setProvider( proc(): Future[Result[NoArgResponse, string]] {.async.} = raise newException(ValueError, "first handler raised") - ok(NoArgResponse(label: "any")) ) discard NoArgResponse.setProvider( @@ -211,6 +227,99 @@ suite "MultiRequestBroker": test "ref providers returning nil fail request": DualResponse.clearProviders() + test "supports native request types": + NativeIntResponse.clearProviders() + + discard NativeIntResponse.setProvider( + proc(): Future[Result[NativeIntResponse, string]] {.async.} = + ok(NativeIntResponse(1)) + ) + + discard NativeIntResponse.setProvider( + proc(): Future[Result[NativeIntResponse, string]] {.async.} = + ok(NativeIntResponse(2)) + ) + + let res = waitFor NativeIntResponse.request() + check res.isOk() + check res.get().len == 2 + check res.get().anyIt(int(it) == 1) + check res.get().anyIt(int(it) == 2) + + NativeIntResponse.clearProviders() + + test "supports external request types": + ExternalAliasResponse.clearProviders() + + discard ExternalAliasResponse.setProvider( + proc(): Future[Result[ExternalAliasResponse, string]] {.async.} = + ok(ExternalAliasResponse("hello")) + ) + + let res = waitFor ExternalAliasResponse.request() + check res.isOk() + check res.get().len == 1 + check ExternalBaseType(res.get()[0]) == "hello" + + ExternalAliasResponse.clearProviders() + + test "supports already-distinct request types": + AlreadyDistinctResponse.clearProviders() + + discard AlreadyDistinctResponse.setProvider( + proc(): Future[Result[AlreadyDistinctResponse, string]] {.async.} = + ok(AlreadyDistinctResponse(7)) + ) + + let res = waitFor AlreadyDistinctResponse.request() + check res.isOk() + check res.get().len == 1 + check int(res.get()[0]) == 7 + + AlreadyDistinctResponse.clearProviders() + + test "context-aware providers are isolated": + NoArgResponse.clearProviders() + let ctxA = NewBrokerContext() + let ctxB = NewBrokerContext() + + discard NoArgResponse.setProvider( + ctxA, + proc(): Future[Result[NoArgResponse, string]] {.async.} = + ok(NoArgResponse(label: "a")), + ) + discard NoArgResponse.setProvider( + ctxB, + proc(): Future[Result[NoArgResponse, string]] {.async.} = + ok(NoArgResponse(label: "b")), + ) + + let resA = waitFor NoArgResponse.request(ctxA) + check resA.isOk() + check resA.get().len == 1 + check resA.get()[0].label == "a" + + let resB = waitFor NoArgResponse.request(ctxB) + check resB.isOk() + check resB.get().len == 1 + check resB.get()[0].label == "b" + + let resDefault = waitFor NoArgResponse.request() + check resDefault.isOk() + check resDefault.get().len == 0 + + NoArgResponse.clearProviders(ctxA) + let clearedA = waitFor NoArgResponse.request(ctxA) + check clearedA.isOk() + check clearedA.get().len == 0 + + let stillB = waitFor NoArgResponse.request(ctxB) + check stillB.isOk() + check stillB.get().len == 1 + check stillB.get()[0].label == "b" + + NoArgResponse.clearProviders(ctxB) + discard DualResponse.setProvider( proc(): Future[Result[DualResponse, string]] {.async.} = let nilResponse: DualResponse = nil diff --git a/tests/common/test_request_broker.nim b/tests/common/test_request_broker.nim index a534216dc..87065a916 100644 --- a/tests/common/test_request_broker.nim +++ b/tests/common/test_request_broker.nim @@ -203,6 +203,104 @@ suite "RequestBroker macro (async mode)": DualResponse.clearProvider() + test "supports keyed providers (async, zero-arg)": + SimpleResponse.clearProvider() + + check SimpleResponse + .setProvider( + proc(): Future[Result[SimpleResponse, string]] {.async.} = + ok(SimpleResponse(value: "default")) + ) + .isOk() + + check SimpleResponse + .setProvider( + BrokerContext(0x11111111'u32), + proc(): Future[Result[SimpleResponse, string]] {.async.} = + ok(SimpleResponse(value: "one")), + ) + .isOk() + + check SimpleResponse + .setProvider( + BrokerContext(0x22222222'u32), + proc(): Future[Result[SimpleResponse, string]] {.async.} = + ok(SimpleResponse(value: "two")), + ) + .isOk() + + let defaultRes = waitFor SimpleResponse.request() + check defaultRes.isOk() + check defaultRes.value.value == "default" + + let res1 = waitFor SimpleResponse.request(BrokerContext(0x11111111'u32)) + check res1.isOk() + check res1.value.value == "one" + + let res2 = waitFor SimpleResponse.request(BrokerContext(0x22222222'u32)) + check res2.isOk() + check res2.value.value == "two" + + let missing = waitFor SimpleResponse.request(BrokerContext(0x33333333'u32)) + check missing.isErr() + check missing.error.contains("no provider registered for broker context") + + check SimpleResponse + .setProvider( + BrokerContext(0x11111111'u32), + proc(): Future[Result[SimpleResponse, string]] {.async.} = + ok(SimpleResponse(value: "dup")), + ) + .isErr() + + SimpleResponse.clearProvider() + + test "supports keyed providers (async, with args)": + KeyedResponse.clearProvider() + + check KeyedResponse + .setProvider( + proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = + ok(KeyedResponse(key: "default-" & key, payload: $subKey)) + ) + .isOk() + + check KeyedResponse + .setProvider( + BrokerContext(0xABCDEF01'u32), + proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = + ok(KeyedResponse(key: "k1-" & key, payload: "p" & $subKey)), + ) + .isOk() + + check KeyedResponse + .setProvider( + BrokerContext(0xABCDEF02'u32), + proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = + ok(KeyedResponse(key: "k2-" & key, payload: "q" & $subKey)), + ) + .isOk() + + let d = waitFor KeyedResponse.request("topic", 7) + check d.isOk() + check d.value.key == "default-topic" + + let k1 = waitFor KeyedResponse.request(BrokerContext(0xABCDEF01'u32), "topic", 7) + check k1.isOk() + check k1.value.key == "k1-topic" + check k1.value.payload == "p7" + + let k2 = waitFor KeyedResponse.request(BrokerContext(0xABCDEF02'u32), "topic", 7) + check k2.isOk() + check k2.value.key == "k2-topic" + check k2.value.payload == "q7" + + let miss = waitFor KeyedResponse.request(BrokerContext(0xDEADBEEF'u32), "topic", 7) + check miss.isErr() + check miss.error.contains("no provider registered for broker context") + + KeyedResponse.clearProvider() + ## --------------------------------------------------------------------------- ## Sync-mode brokers + tests ## --------------------------------------------------------------------------- @@ -370,6 +468,71 @@ suite "RequestBroker macro (sync mode)": ImplicitResponseSync.clearProvider() + test "supports keyed providers (sync, zero-arg)": + SimpleResponseSync.clearProvider() + + check SimpleResponseSync + .setProvider( + proc(): Result[SimpleResponseSync, string] = + ok(SimpleResponseSync(value: "default")) + ) + .isOk() + + check SimpleResponseSync + .setProvider( + BrokerContext(0x10101010'u32), + proc(): Result[SimpleResponseSync, string] = + ok(SimpleResponseSync(value: "ten")), + ) + .isOk() + + let defaultRes = SimpleResponseSync.request() + check defaultRes.isOk() + check defaultRes.value.value == "default" + + let keyedRes = SimpleResponseSync.request(BrokerContext(0x10101010'u32)) + check keyedRes.isOk() + check keyedRes.value.value == "ten" + + let miss = SimpleResponseSync.request(BrokerContext(0x20202020'u32)) + check miss.isErr() + check miss.error.contains("no provider registered for broker context") + + SimpleResponseSync.clearProvider() + + test "supports keyed providers (sync, with args)": + KeyedResponseSync.clearProvider() + + check KeyedResponseSync + .setProvider( + proc(key: string, subKey: int): Result[KeyedResponseSync, string] = + ok(KeyedResponseSync(key: "default-" & key, payload: $subKey)) + ) + .isOk() + + check KeyedResponseSync + .setProvider( + BrokerContext(0xA0A0A0A0'u32), + proc(key: string, subKey: int): Result[KeyedResponseSync, string] = + ok(KeyedResponseSync(key: "k-" & key, payload: "p" & $subKey)), + ) + .isOk() + + let d = KeyedResponseSync.request("topic", 2) + check d.isOk() + check d.value.key == "default-topic" + + let keyed = KeyedResponseSync.request(BrokerContext(0xA0A0A0A0'u32), "topic", 2) + check keyed.isOk() + check keyed.value.key == "k-topic" + check keyed.value.payload == "p2" + + let miss = KeyedResponseSync.request(BrokerContext(0xB0B0B0B0'u32), "topic", 2) + check miss.isErr() + check miss.error.contains("no provider registered for broker context") + + KeyedResponseSync.clearProvider() + ## --------------------------------------------------------------------------- ## POD / external type brokers + tests (distinct/alias behavior) ## --------------------------------------------------------------------------- diff --git a/waku/common/broker/broker_context.nim b/waku/common/broker/broker_context.nim new file mode 100644 index 000000000..483a2e3a7 --- /dev/null +++ b/waku/common/broker/broker_context.nim @@ -0,0 +1,68 @@ +{.push raises: [].} + +import std/[strutils, concurrency/atomics], chronos + +type BrokerContext* = distinct uint32 + +func `==`*(a, b: BrokerContext): bool = + uint32(a) == uint32(b) + +func `!=`*(a, b: BrokerContext): bool = + uint32(a) != uint32(b) + +func `$`*(bc: BrokerContext): string = + toHex(uint32(bc), 8) + +const DefaultBrokerContext* = BrokerContext(0xCAFFE14E'u32) + +# Global broker context accessor. +# +# NOTE: This intentionally creates a *single* active BrokerContext per process +# (per event loop thread). Use only if you accept serialization of all broker +# context usage through the lock. +var globalBrokerContextLock {.threadvar.}: AsyncLock +globalBrokerContextLock = newAsyncLock() +var globalBrokerContextValue {.threadvar.}: BrokerContext +globalBrokerContextValue = DefaultBrokerContext +proc globalBrokerContext*(): BrokerContext = + ## Returns the currently active global broker context. + ## + ## This is intentionally lock-free; callers should use it inside + ## `withNewGlobalBrokerContext` / `withGlobalBrokerContext`. + globalBrokerContextValue + +var gContextCounter: Atomic[uint32] + +proc NewBrokerContext*(): BrokerContext = + var nextId = gContextCounter.fetchAdd(1, moRelaxed) + if nextId == uint32(DefaultBrokerContext): + nextId = gContextCounter.fetchAdd(1, moRelaxed) + return BrokerContext(nextId) + +template lockGlobalBrokerContext*(brokerCtx: BrokerContext, body: untyped): untyped = + ## Runs `body` while holding the global broker context lock with the provided + ## `brokerCtx` installed as the globally accessible context. + ## + ## This template is intended for use from within `chronos` async procs. + block: + await noCancel(globalBrokerContextLock.acquire()) + let previousBrokerCtx = globalBrokerContextValue + globalBrokerContextValue = brokerCtx + try: + body + finally: + globalBrokerContextValue = previousBrokerCtx + try: + globalBrokerContextLock.release() + except AsyncLockError: + doAssert false, "globalBrokerContextLock.release(): lock not held" + +template lockNewGlobalBrokerContext*(body: untyped): untyped = + ## Runs `body` while holding the global broker context lock with a freshly + ## generated broker context installed as the global accessor. + ## + ## The previous global broker context (if any) is restored on exit. + lockGlobalBrokerContext(NewBrokerContext()): + body + +{.pop.} diff --git a/waku/common/broker/event_broker.nim b/waku/common/broker/event_broker.nim index 05d7b50ab..779689f88 100644 --- a/waku/common/broker/event_broker.nim +++ b/waku/common/broker/event_broker.nim @@ -5,10 +5,35 @@ ## need for direct dependencies in between emitters and listeners. ## Worth considering using it in a single or many emitters to many listeners scenario. ## -## Generates a standalone, type-safe event broker for the declared object type. +## Generates a standalone, type-safe event broker for the declared type. ## The macro exports the value type itself plus a broker companion that manages ## listeners via thread-local storage. ## +## Type definitions: +## - Inline `object` / `ref object` definitions are supported. +## - Native types, aliases, and externally-defined types are also supported. +## In that case, EventBroker will automatically wrap the declared RHS type in +## `distinct` unless you already used `distinct`. +## This keeps event types unique even when multiple brokers share the same +## underlying base type. +## +## Default vs. context aware use: +## Every generated broker is a thread-local global instance. This means EventBroker +## enables decoupled event exchange threadwise. +## +## Sometimes we use brokers inside a context (e.g. within a component that has many +## modules or subsystems). If you instantiate multiple such components in a single +## thread, and each component must have its own listener set for the same EventBroker +## type, you can use context-aware EventBroker. +## +## Context awareness is supported through the `BrokerContext` argument for +## `listen`, `emit`, `dropListener`, and `dropAllListeners`. +## Listener stores are kept separate per broker context. +## +## Default broker context is defined as `DefaultBrokerContext`. If you don't need +## context awareness, you can keep using the interfaces without the context +## argument, which operate on `DefaultBrokerContext`. +## ## Usage: ## Declare your desired event type inside an `EventBroker` macro, add any number of fields.: ## ```nim @@ -47,87 +72,46 @@ ## GreetingEvent.dropListener(handle) ## ``` +## Example (non-object event type): +## ```nim +## EventBroker: +## type CounterEvent = int # exported as: `distinct int` +## +## discard CounterEvent.listen( +## proc(evt: CounterEvent): Future[void] {.async.} = +## echo int(evt) +## ) +## CounterEvent.emit(CounterEvent(42)) +## ``` + import std/[macros, tables] import chronos, chronicles, results -import ./helper/broker_utils +import ./helper/broker_utils, broker_context -export chronicles, results, chronos +export chronicles, results, chronos, broker_context macro EventBroker*(body: untyped): untyped = when defined(eventBrokerDebug): echo body.treeRepr - var typeIdent: NimNode = nil - var objectDef: NimNode = nil - var fieldNames: seq[NimNode] = @[] - var fieldTypes: seq[NimNode] = @[] - var isRefObject = false - for stmt in body: - if stmt.kind == nnkTypeSection: - for def in stmt: - if def.kind != nnkTypeDef: - continue - let rhs = def[2] - var objectType: NimNode - case rhs.kind - of nnkObjectTy: - objectType = rhs - of nnkRefTy: - isRefObject = true - if rhs.len != 1 or rhs[0].kind != nnkObjectTy: - error("EventBroker ref object must wrap a concrete object definition", rhs) - objectType = rhs[0] - else: - continue - if not typeIdent.isNil(): - error("Only one object type may be declared inside EventBroker", def) - typeIdent = baseTypeIdent(def[0]) - let recList = objectType[2] - if recList.kind != nnkRecList: - error("EventBroker object must declare a standard field list", objectType) - var exportedRecList = newTree(nnkRecList) - for field in recList: - case field.kind - of nnkIdentDefs: - ensureFieldDef(field) - let fieldTypeNode = field[field.len - 2] - for i in 0 ..< field.len - 2: - let baseFieldIdent = baseTypeIdent(field[i]) - fieldNames.add(copyNimTree(baseFieldIdent)) - fieldTypes.add(copyNimTree(fieldTypeNode)) - var cloned = copyNimTree(field) - for i in 0 ..< cloned.len - 2: - cloned[i] = exportIdentNode(cloned[i]) - exportedRecList.add(cloned) - of nnkEmpty: - discard - else: - error( - "EventBroker object definition only supports simple field declarations", - field, - ) - let exportedObjectType = newTree( - nnkObjectTy, - copyNimTree(objectType[0]), - copyNimTree(objectType[1]), - exportedRecList, - ) - if isRefObject: - objectDef = newTree(nnkRefTy, exportedObjectType) - else: - objectDef = exportedObjectType - if typeIdent.isNil(): - error("EventBroker body must declare exactly one object type", body) + let parsed = parseSingleTypeDef(body, "EventBroker", collectFieldInfo = true) + let typeIdent = parsed.typeIdent + let objectDef = parsed.objectDef + let fieldNames = parsed.fieldNames + let fieldTypes = parsed.fieldTypes + let hasInlineFields = parsed.hasInlineFields let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*") let sanitized = sanitizeIdentName(typeIdent) let typeNameLit = newLit($typeIdent) - let isRefObjectLit = newLit(isRefObject) let handlerProcIdent = ident(sanitized & "ListenerProc") let listenerHandleIdent = ident(sanitized & "Listener") let brokerTypeIdent = ident(sanitized & "Broker") let exportedHandlerProcIdent = postfix(copyNimTree(handlerProcIdent), "*") let exportedListenerHandleIdent = postfix(copyNimTree(listenerHandleIdent), "*") let exportedBrokerTypeIdent = postfix(copyNimTree(brokerTypeIdent), "*") + let bucketTypeIdent = ident(sanitized & "CtxBucket") + let findBucketIdxIdent = ident(sanitized & "FindBucketIdx") + let getOrCreateBucketIdxIdent = ident(sanitized & "GetOrCreateBucketIdx") let accessProcIdent = ident("access" & sanitized & "Broker") let globalVarIdent = ident("g" & sanitized & "Broker") let listenImplIdent = ident("register" & sanitized & "Listener") @@ -147,10 +131,14 @@ macro EventBroker*(body: untyped): untyped = `exportedHandlerProcIdent` = proc(event: `typeIdent`): Future[void] {.async: (raises: []), gcsafe.} - `exportedBrokerTypeIdent` = ref object + `bucketTypeIdent` = object + brokerCtx: BrokerContext listeners: Table[uint64, `handlerProcIdent`] nextId: uint64 + `exportedBrokerTypeIdent` = ref object + buckets: seq[`bucketTypeIdent`] + ) result.add( @@ -163,49 +151,102 @@ macro EventBroker*(body: untyped): untyped = proc `accessProcIdent`(): `brokerTypeIdent` = if `globalVarIdent`.isNil(): new(`globalVarIdent`) - `globalVarIdent`.listeners = initTable[uint64, `handlerProcIdent`]() + `globalVarIdent`.buckets = + @[ + `bucketTypeIdent`( + brokerCtx: DefaultBrokerContext, + listeners: initTable[uint64, `handlerProcIdent`](), + nextId: 1'u64, + ) + ] `globalVarIdent` ) result.add( quote do: + proc `findBucketIdxIdent`( + broker: `brokerTypeIdent`, brokerCtx: BrokerContext + ): int = + if brokerCtx == DefaultBrokerContext: + return 0 + for i in 1 ..< broker.buckets.len: + if broker.buckets[i].brokerCtx == brokerCtx: + return i + return -1 + + proc `getOrCreateBucketIdxIdent`( + broker: `brokerTypeIdent`, brokerCtx: BrokerContext + ): int = + let idx = `findBucketIdxIdent`(broker, brokerCtx) + if idx >= 0: + return idx + broker.buckets.add( + `bucketTypeIdent`( + brokerCtx: brokerCtx, + listeners: initTable[uint64, `handlerProcIdent`](), + nextId: 1'u64, + ) + ) + return broker.buckets.high + proc `listenImplIdent`( - handler: `handlerProcIdent` + brokerCtx: BrokerContext, handler: `handlerProcIdent` ): Result[`listenerHandleIdent`, string] = if handler.isNil(): return err("Must provide a non-nil event handler") var broker = `accessProcIdent`() - if broker.nextId == 0'u64: - broker.nextId = 1'u64 - if broker.nextId == high(uint64): - error "Cannot add more listeners: ID space exhausted", nextId = $broker.nextId + + let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx) + if broker.buckets[bucketIdx].nextId == 0'u64: + broker.buckets[bucketIdx].nextId = 1'u64 + + if broker.buckets[bucketIdx].nextId == high(uint64): + error "Cannot add more listeners: ID space exhausted", + nextId = $broker.buckets[bucketIdx].nextId return err("Cannot add more listeners, listener ID space exhausted") - let newId = broker.nextId - inc broker.nextId - broker.listeners[newId] = handler + + let newId = broker.buckets[bucketIdx].nextId + inc broker.buckets[bucketIdx].nextId + broker.buckets[bucketIdx].listeners[newId] = handler return ok(`listenerHandleIdent`(id: newId)) ) result.add( quote do: - proc `dropListenerImplIdent`(handle: `listenerHandleIdent`) = + proc `dropListenerImplIdent`( + brokerCtx: BrokerContext, handle: `listenerHandleIdent` + ) = if handle.id == 0'u64: return var broker = `accessProcIdent`() - if broker.listeners.len == 0: + + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: return - broker.listeners.del(handle.id) + + if broker.buckets[bucketIdx].listeners.len == 0: + return + broker.buckets[bucketIdx].listeners.del(handle.id) + if brokerCtx != DefaultBrokerContext and + broker.buckets[bucketIdx].listeners.len == 0: + broker.buckets.delete(bucketIdx) ) result.add( quote do: - proc `dropAllListenersImplIdent`() = + proc `dropAllListenersImplIdent`(brokerCtx: BrokerContext) = var broker = `accessProcIdent`() - if broker.listeners.len > 0: - broker.listeners.clear() + + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: + return + if broker.buckets[bucketIdx].listeners.len > 0: + broker.buckets[bucketIdx].listeners.clear() + if brokerCtx != DefaultBrokerContext: + broker.buckets.delete(bucketIdx) ) @@ -214,17 +255,34 @@ macro EventBroker*(body: untyped): untyped = proc listen*( _: typedesc[`typeIdent`], handler: `handlerProcIdent` ): Result[`listenerHandleIdent`, string] = - return `listenImplIdent`(handler) + return `listenImplIdent`(DefaultBrokerContext, handler) + + proc listen*( + _: typedesc[`typeIdent`], + brokerCtx: BrokerContext, + handler: `handlerProcIdent`, + ): Result[`listenerHandleIdent`, string] = + return `listenImplIdent`(brokerCtx, handler) ) result.add( quote do: proc dropListener*(_: typedesc[`typeIdent`], handle: `listenerHandleIdent`) = - `dropListenerImplIdent`(handle) + `dropListenerImplIdent`(DefaultBrokerContext, handle) + + proc dropListener*( + _: typedesc[`typeIdent`], + brokerCtx: BrokerContext, + handle: `listenerHandleIdent`, + ) = + `dropListenerImplIdent`(brokerCtx, handle) proc dropAllListeners*(_: typedesc[`typeIdent`]) = - `dropAllListenersImplIdent`() + `dropAllListenersImplIdent`(DefaultBrokerContext) + + proc dropAllListeners*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) = + `dropAllListenersImplIdent`(brokerCtx) ) @@ -241,68 +299,114 @@ macro EventBroker*(body: untyped): untyped = error "Failed to execute event listener", error = getCurrentExceptionMsg() proc `emitImplIdent`( - event: `typeIdent` + brokerCtx: BrokerContext, event: `typeIdent` ): Future[void] {.async: (raises: []), gcsafe.} = - when `isRefObjectLit`: + when compiles(event.isNil()): if event.isNil(): error "Cannot emit uninitialized event object", eventType = `typeNameLit` return let broker = `accessProcIdent`() - if broker.listeners.len == 0: + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: # nothing to do as nobody is listening return + if broker.buckets[bucketIdx].listeners.len == 0: + return var callbacks: seq[`handlerProcIdent`] = @[] - for cb in broker.listeners.values: + for cb in broker.buckets[bucketIdx].listeners.values: callbacks.add(cb) for cb in callbacks: asyncSpawn `listenerTaskIdent`(cb, event) proc emit*(event: `typeIdent`) = - asyncSpawn `emitImplIdent`(event) + asyncSpawn `emitImplIdent`(DefaultBrokerContext, event) proc emit*(_: typedesc[`typeIdent`], event: `typeIdent`) = - asyncSpawn `emitImplIdent`(event) + asyncSpawn `emitImplIdent`(DefaultBrokerContext, event) + + proc emit*( + _: typedesc[`typeIdent`], brokerCtx: BrokerContext, event: `typeIdent` + ) = + asyncSpawn `emitImplIdent`(brokerCtx, event) ) - var emitCtorParams = newTree(nnkFormalParams, newEmptyNode()) - let typedescParamType = - newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)) - emitCtorParams.add( - newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode()) - ) - for i in 0 ..< fieldNames.len: + if hasInlineFields: + # Typedesc emit constructor overloads for inline object/ref object types. + var emitCtorParams = newTree(nnkFormalParams, newEmptyNode()) + let typedescParamType = + newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)) emitCtorParams.add( - newTree( - nnkIdentDefs, - copyNimTree(fieldNames[i]), - copyNimTree(fieldTypes[i]), - newEmptyNode(), + newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode()) + ) + for i in 0 ..< fieldNames.len: + emitCtorParams.add( + newTree( + nnkIdentDefs, + copyNimTree(fieldNames[i]), + copyNimTree(fieldTypes[i]), + newEmptyNode(), + ) ) + + var emitCtorExpr = newTree(nnkObjConstr, copyNimTree(typeIdent)) + for i in 0 ..< fieldNames.len: + emitCtorExpr.add( + newTree( + nnkExprColonExpr, copyNimTree(fieldNames[i]), copyNimTree(fieldNames[i]) + ) + ) + + let emitCtorCallDefault = + newCall(copyNimTree(emitImplIdent), ident("DefaultBrokerContext"), emitCtorExpr) + let emitCtorBodyDefault = quote: + asyncSpawn `emitCtorCallDefault` + + let typedescEmitProcDefault = newTree( + nnkProcDef, + postfix(ident("emit"), "*"), + newEmptyNode(), + newEmptyNode(), + emitCtorParams, + newEmptyNode(), + newEmptyNode(), + emitCtorBodyDefault, ) + result.add(typedescEmitProcDefault) - var emitCtorExpr = newTree(nnkObjConstr, copyNimTree(typeIdent)) - for i in 0 ..< fieldNames.len: - emitCtorExpr.add( - newTree(nnkExprColonExpr, copyNimTree(fieldNames[i]), copyNimTree(fieldNames[i])) + var emitCtorParamsCtx = newTree(nnkFormalParams, newEmptyNode()) + emitCtorParamsCtx.add( + newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode()) ) + emitCtorParamsCtx.add( + newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()) + ) + for i in 0 ..< fieldNames.len: + emitCtorParamsCtx.add( + newTree( + nnkIdentDefs, + copyNimTree(fieldNames[i]), + copyNimTree(fieldTypes[i]), + newEmptyNode(), + ) + ) - let emitCtorCall = newCall(copyNimTree(emitImplIdent), emitCtorExpr) - let emitCtorBody = quote: - asyncSpawn `emitCtorCall` + let emitCtorCallCtx = + newCall(copyNimTree(emitImplIdent), ident("brokerCtx"), copyNimTree(emitCtorExpr)) + let emitCtorBodyCtx = quote: + asyncSpawn `emitCtorCallCtx` - let typedescEmitProc = newTree( - nnkProcDef, - postfix(ident("emit"), "*"), - newEmptyNode(), - newEmptyNode(), - emitCtorParams, - newEmptyNode(), - newEmptyNode(), - emitCtorBody, - ) - - result.add(typedescEmitProc) + let typedescEmitProcCtx = newTree( + nnkProcDef, + postfix(ident("emit"), "*"), + newEmptyNode(), + newEmptyNode(), + emitCtorParamsCtx, + newEmptyNode(), + newEmptyNode(), + emitCtorBodyCtx, + ) + result.add(typedescEmitProcCtx) when defined(eventBrokerDebug): echo result.repr diff --git a/waku/common/broker/helper/broker_utils.nim b/waku/common/broker/helper/broker_utils.nim index ea9f85750..90f2055d3 100644 --- a/waku/common/broker/helper/broker_utils.nim +++ b/waku/common/broker/helper/broker_utils.nim @@ -1,5 +1,21 @@ import std/macros +type ParsedBrokerType* = object + ## Result of parsing the single `type` definition inside a broker macro body. + ## + ## - `typeIdent`: base identifier for the declared type name + ## - `objectDef`: exported type definition RHS (inline object fields exported; + ## non-object types wrapped in `distinct` unless already distinct) + ## - `isRefObject`: true only for inline `ref object` definitions + ## - `hasInlineFields`: true for inline `object` / `ref object` + ## - `fieldNames`/`fieldTypes`: populated only when `collectFieldInfo = true` + typeIdent*: NimNode + objectDef*: NimNode + isRefObject*: bool + hasInlineFields*: bool + fieldNames*: seq[NimNode] + fieldTypes*: seq[NimNode] + proc sanitizeIdentName*(node: NimNode): string = var raw = $node var sanitizedName = newStringOfCap(raw.len) @@ -41,3 +57,150 @@ proc baseTypeIdent*(defName: NimNode): NimNode = baseTypeIdent(defName[0]) else: error("Unsupported type name in broker definition", defName) + +proc ensureDistinctType*(rhs: NimNode): NimNode = + ## For PODs / aliases / externally-defined types, wrap in `distinct` unless + ## it's already distinct. + if rhs.kind == nnkDistinctTy: + return copyNimTree(rhs) + newTree(nnkDistinctTy, copyNimTree(rhs)) + +proc cloneParams*(params: seq[NimNode]): seq[NimNode] = + ## Deep copy parameter definitions so they can be inserted in multiple places. + result = @[] + for param in params: + result.add(copyNimTree(param)) + +proc collectParamNames*(params: seq[NimNode]): seq[NimNode] = + ## Extract all identifier symbols declared across IdentDefs nodes. + result = @[] + for param in params: + assert param.kind == nnkIdentDefs + for i in 0 ..< param.len - 2: + let nameNode = param[i] + if nameNode.kind == nnkEmpty: + continue + result.add(ident($nameNode)) + +proc parseSingleTypeDef*( + body: NimNode, + macroName: string, + allowRefToNonObject = false, + collectFieldInfo = false, +): ParsedBrokerType = + ## Parses exactly one `type` definition from a broker macro body. + ## + ## Supported RHS: + ## - inline `object` / `ref object` (fields are auto-exported) + ## - non-object types / aliases / externally-defined types (wrapped in `distinct`) + ## - optionally: `ref SomeType` when `allowRefToNonObject = true` + var typeIdent: NimNode = nil + var objectDef: NimNode = nil + var isRefObject = false + var hasInlineFields = false + var fieldNames: seq[NimNode] = @[] + var fieldTypes: seq[NimNode] = @[] + + for stmt in body: + if stmt.kind != nnkTypeSection: + continue + for def in stmt: + if def.kind != nnkTypeDef: + continue + if not typeIdent.isNil(): + error("Only one type may be declared inside " & macroName, def) + typeIdent = baseTypeIdent(def[0]) + let rhs = def[2] + + case rhs.kind + of nnkObjectTy: + let recList = rhs[2] + if recList.kind != nnkRecList: + error(macroName & " object must declare a standard field list", rhs) + var exportedRecList = newTree(nnkRecList) + for field in recList: + case field.kind + of nnkIdentDefs: + ensureFieldDef(field) + if collectFieldInfo: + let fieldTypeNode = field[field.len - 2] + for i in 0 ..< field.len - 2: + let baseFieldIdent = baseTypeIdent(field[i]) + fieldNames.add(copyNimTree(baseFieldIdent)) + fieldTypes.add(copyNimTree(fieldTypeNode)) + var cloned = copyNimTree(field) + for i in 0 ..< cloned.len - 2: + cloned[i] = exportIdentNode(cloned[i]) + exportedRecList.add(cloned) + of nnkEmpty: + discard + else: + error( + macroName & " object definition only supports simple field declarations", + field, + ) + objectDef = newTree( + nnkObjectTy, copyNimTree(rhs[0]), copyNimTree(rhs[1]), exportedRecList + ) + isRefObject = false + hasInlineFields = true + of nnkRefTy: + if rhs.len != 1: + error(macroName & " ref type must have a single base", rhs) + if rhs[0].kind == nnkObjectTy: + let obj = rhs[0] + let recList = obj[2] + if recList.kind != nnkRecList: + error(macroName & " object must declare a standard field list", obj) + var exportedRecList = newTree(nnkRecList) + for field in recList: + case field.kind + of nnkIdentDefs: + ensureFieldDef(field) + if collectFieldInfo: + let fieldTypeNode = field[field.len - 2] + for i in 0 ..< field.len - 2: + let baseFieldIdent = baseTypeIdent(field[i]) + fieldNames.add(copyNimTree(baseFieldIdent)) + fieldTypes.add(copyNimTree(fieldTypeNode)) + var cloned = copyNimTree(field) + for i in 0 ..< cloned.len - 2: + cloned[i] = exportIdentNode(cloned[i]) + exportedRecList.add(cloned) + of nnkEmpty: + discard + else: + error( + macroName & " object definition only supports simple field declarations", + field, + ) + let exportedObjectType = newTree( + nnkObjectTy, copyNimTree(obj[0]), copyNimTree(obj[1]), exportedRecList + ) + objectDef = newTree(nnkRefTy, exportedObjectType) + isRefObject = true + hasInlineFields = true + elif allowRefToNonObject: + ## `ref SomeType` (SomeType can be defined elsewhere) + objectDef = ensureDistinctType(rhs) + isRefObject = false + hasInlineFields = false + else: + error(macroName & " ref object must wrap a concrete object definition", rhs) + else: + ## Non-object type / alias. + objectDef = ensureDistinctType(rhs) + isRefObject = false + hasInlineFields = false + + if typeIdent.isNil(): + error(macroName & " body must declare exactly one type", body) + + result = ParsedBrokerType( + typeIdent: typeIdent, + objectDef: objectDef, + isRefObject: isRefObject, + hasInlineFields: hasInlineFields, + fieldNames: fieldNames, + fieldTypes: fieldTypes, + ) diff --git a/waku/common/broker/multi_request_broker.nim b/waku/common/broker/multi_request_broker.nim index 7f4161f5a..2baa19940 100644 --- a/waku/common/broker/multi_request_broker.nim +++ b/waku/common/broker/multi_request_broker.nim @@ -5,12 +5,35 @@ ## need for direct dependencies in between. ## Worth considering using it for use cases where you need to collect data from multiple providers. ## -## Provides a declarative way to define an immutable value type together with a -## thread-local broker that can register multiple asynchronous providers, dispatch -## typed requests, and clear handlers. Unlike `RequestBroker`, -## every call to `request` fan-outs to every registered provider and returns with -## collected responses. -## Request succeeds if all providers succeed, otherwise fails with an error. +## Generates a standalone, type-safe request broker for the declared type. +## The macro exports the value type itself plus a broker companion that manages +## providers via thread-local storage. +## +## Unlike `RequestBroker`, every call to `request` fan-outs to every registered +## provider and returns all collected responses. +## The request succeeds only if all providers succeed, otherwise it fails. +## +## Type definitions: +## - Inline `object` / `ref object` definitions are supported. +## - Native types, aliases, and externally-defined types are also supported. +## In that case, MultiRequestBroker will automatically wrap the declared RHS +## type in `distinct` unless you already used `distinct`. +## This keeps request types unique even when multiple brokers share the same +## underlying base type. +## +## Default vs. context aware use: +## Every generated broker is a thread-local global instance. +## Sometimes you want multiple independent provider sets for the same request +## type within the same thread (e.g. multiple components). For that, you can use +## context-aware MultiRequestBroker. +## +## Context awareness is supported through the `BrokerContext` argument for +## `setProvider`, `request`, `removeProvider`, and `clearProviders`. +## Provider stores are kept separate per broker context. +## +## Default broker context is defined as `DefaultBrokerContext`. If you don't +## need context awareness, you can keep using the interfaces without the context +## argument, which operate on `DefaultBrokerContext`. ## ## Usage: ## @@ -29,14 +52,17 @@ ## ## ``` ## -## You regiser request processor (proveder) at any place of the code without the need to know of who ever may request. -## Respectively to the defined signatures register provider functions with `TypeName.setProvider(...)`. -## Providers are async procs or lambdas that return with a Future[Result[seq[TypeName], string]]. -## Notice MultiRequestBroker's `setProvider` return with a handler that can be used to remove the provider later (or error). +## You can register a request processor (provider) anywhere without the need to +## know who will request. +## Register provider functions with `TypeName.setProvider(...)`. +## Providers are async procs or lambdas that return `Future[Result[TypeName, string]]`. +## `setProvider` returns a handle (or an error) that can later be used to remove +## the provider. -## Requests can be made from anywhere with no direct dependency on the provider(s) by -## calling `TypeName.request()` - with arguments respecting the signature(s). -## This will asynchronously call the registered provider and return the collected data, in form of `Future[Result[seq[TypeName], string]]`. +## Requests can be made from anywhere with no direct dependency on the provider(s) +## by calling `TypeName.request()` (with arguments respecting the declared signature). +## This will asynchronously call all registered providers and return the collected +## responses as `Future[Result[seq[TypeName], string]]`. ## ## Whenever you don't want to process requests anymore (or your object instance that provides the request goes out of scope), ## you can remove it from the broker with `TypeName.removeProvider(handle)`. @@ -77,8 +103,9 @@ import std/[macros, strutils, tables, sugar] import chronos import results import ./helper/broker_utils +import ./broker_context -export results, chronos +export results, chronos, broker_context proc isReturnTypeValid(returnType, typeIdent: NimNode): bool = ## Accept Future[Result[TypeIdent, string]] as the contract. @@ -95,23 +122,6 @@ proc isReturnTypeValid(returnType, typeIdent: NimNode): bool = return false inner[2].kind == nnkIdent and inner[2].eqIdent("string") -proc cloneParams(params: seq[NimNode]): seq[NimNode] = - ## Deep copy parameter definitions so they can be reused in generated nodes. - result = @[] - for param in params: - result.add(copyNimTree(param)) - -proc collectParamNames(params: seq[NimNode]): seq[NimNode] = - ## Extract identifiers declared in parameter definitions. - result = @[] - for param in params: - assert param.kind == nnkIdentDefs - for i in 0 ..< param.len - 2: - let nameNode = param[i] - if nameNode.kind == nnkEmpty: - continue - result.add(ident($nameNode)) - proc makeProcType(returnType: NimNode, params: seq[NimNode]): NimNode = var formal = newTree(nnkFormalParams) formal.add(returnType) @@ -126,65 +136,10 @@ proc makeProcType(returnType: NimNode, params: seq[NimNode]): NimNode = macro MultiRequestBroker*(body: untyped): untyped = when defined(requestBrokerDebug): echo body.treeRepr - var typeIdent: NimNode = nil - var objectDef: NimNode = nil - var isRefObject = false - for stmt in body: - if stmt.kind == nnkTypeSection: - for def in stmt: - if def.kind != nnkTypeDef: - continue - let rhs = def[2] - var objectType: NimNode - case rhs.kind - of nnkObjectTy: - objectType = rhs - of nnkRefTy: - isRefObject = true - if rhs.len != 1 or rhs[0].kind != nnkObjectTy: - error( - "MultiRequestBroker ref object must wrap a concrete object definition", - rhs, - ) - objectType = rhs[0] - else: - continue - if not typeIdent.isNil(): - error("Only one object type may be declared inside MultiRequestBroker", def) - typeIdent = baseTypeIdent(def[0]) - let recList = objectType[2] - if recList.kind != nnkRecList: - error( - "MultiRequestBroker object must declare a standard field list", objectType - ) - var exportedRecList = newTree(nnkRecList) - for field in recList: - case field.kind - of nnkIdentDefs: - ensureFieldDef(field) - var cloned = copyNimTree(field) - for i in 0 ..< cloned.len - 2: - cloned[i] = exportIdentNode(cloned[i]) - exportedRecList.add(cloned) - of nnkEmpty: - discard - else: - error( - "MultiRequestBroker object definition only supports simple field declarations", - field, - ) - let exportedObjectType = newTree( - nnkObjectTy, - copyNimTree(objectType[0]), - copyNimTree(objectType[1]), - exportedRecList, - ) - if isRefObject: - objectDef = newTree(nnkRefTy, exportedObjectType) - else: - objectDef = exportedObjectType - if typeIdent.isNil(): - error("MultiRequestBroker body must declare exactly one object type", body) + let parsed = parseSingleTypeDef(body, "MultiRequestBroker") + let typeIdent = parsed.typeIdent + let objectDef = parsed.objectDef + let isRefObject = parsed.isRefObject when defined(requestBrokerDebug): echo "MultiRequestBroker generating type: ", $typeIdent @@ -193,12 +148,13 @@ macro MultiRequestBroker*(body: untyped): untyped = let sanitized = sanitizeIdentName(typeIdent) let typeNameLit = newLit($typeIdent) let isRefObjectLit = newLit(isRefObject) - let tableSym = bindSym"Table" - let initTableSym = bindSym"initTable" let uint64Ident = ident("uint64") let providerKindIdent = ident(sanitized & "ProviderKind") let providerHandleIdent = ident(sanitized & "ProviderHandle") let exportedProviderHandleIdent = postfix(copyNimTree(providerHandleIdent), "*") + let bucketTypeIdent = ident(sanitized & "CtxBucket") + let findBucketIdxIdent = ident(sanitized & "FindBucketIdx") + let getOrCreateBucketIdxIdent = ident(sanitized & "GetOrCreateBucketIdx") let zeroKindIdent = ident("pk" & sanitized & "NoArgs") let argKindIdent = ident("pk" & sanitized & "WithArgs") var zeroArgSig: NimNode = nil @@ -306,63 +262,90 @@ macro MultiRequestBroker*(body: untyped): untyped = let procType = makeProcType(returnType, cloneParams(argParams)) typeSection.add(newTree(nnkTypeDef, argProviderName, newEmptyNode(), procType)) - var brokerRecList = newTree(nnkRecList) + var bucketRecList = newTree(nnkRecList) + bucketRecList.add( + newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()) + ) if not zeroArgSig.isNil(): - brokerRecList.add( + bucketRecList.add( newTree( nnkIdentDefs, zeroArgFieldName, - newTree(nnkBracketExpr, tableSym, uint64Ident, zeroArgProviderName), + newTree(nnkBracketExpr, ident("seq"), zeroArgProviderName), newEmptyNode(), ) ) if not argSig.isNil(): - brokerRecList.add( + bucketRecList.add( newTree( nnkIdentDefs, argFieldName, - newTree(nnkBracketExpr, tableSym, uint64Ident, argProviderName), + newTree(nnkBracketExpr, ident("seq"), argProviderName), newEmptyNode(), ) ) - brokerRecList.add(newTree(nnkIdentDefs, ident("nextId"), uint64Ident, newEmptyNode())) - let brokerTypeIdent = ident(sanitizeIdentName(typeIdent) & "Broker") - let brokerTypeDef = newTree( - nnkTypeDef, - brokerTypeIdent, - newEmptyNode(), + typeSection.add( newTree( - nnkRefTy, newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), brokerRecList) - ), + nnkTypeDef, + bucketTypeIdent, + newEmptyNode(), + newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), bucketRecList), + ) + ) + + var brokerRecList = newTree(nnkRecList) + brokerRecList.add( + newTree( + nnkIdentDefs, + ident("buckets"), + newTree(nnkBracketExpr, ident("seq"), bucketTypeIdent), + newEmptyNode(), + ) + ) + let brokerTypeIdent = ident(sanitizeIdentName(typeIdent) & "Broker") + typeSection.add( + newTree( + nnkTypeDef, + brokerTypeIdent, + newEmptyNode(), + newTree( + nnkRefTy, newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), brokerRecList) + ), + ) ) - typeSection.add(brokerTypeDef) result = newStmtList() result.add(typeSection) let globalVarIdent = ident("g" & sanitizeIdentName(typeIdent) & "Broker") let accessProcIdent = ident("access" & sanitizeIdentName(typeIdent) & "Broker") - var initStatements = newStmtList() - if not zeroArgSig.isNil(): - initStatements.add( - quote do: - `globalVarIdent`.`zeroArgFieldName` = - `initTableSym`[`uint64Ident`, `zeroArgProviderName`]() - ) - if not argSig.isNil(): - initStatements.add( - quote do: - `globalVarIdent`.`argFieldName` = - `initTableSym`[`uint64Ident`, `argProviderName`]() - ) result.add( quote do: var `globalVarIdent` {.threadvar.}: `brokerTypeIdent` + proc `findBucketIdxIdent`( + broker: `brokerTypeIdent`, brokerCtx: BrokerContext + ): int = + if brokerCtx == DefaultBrokerContext: + return 0 + for i in 1 ..< broker.buckets.len: + if broker.buckets[i].brokerCtx == brokerCtx: + return i + return -1 + + proc `getOrCreateBucketIdxIdent`( + broker: `brokerTypeIdent`, brokerCtx: BrokerContext + ): int = + let idx = `findBucketIdxIdent`(broker, brokerCtx) + if idx >= 0: + return idx + broker.buckets.add(`bucketTypeIdent`(brokerCtx: brokerCtx)) + return broker.buckets.high + proc `accessProcIdent`(): `brokerTypeIdent` = if `globalVarIdent`.isNil(): new(`globalVarIdent`) - `globalVarIdent`.nextId = 1'u64 - `initStatements` + `globalVarIdent`.buckets = + @[`bucketTypeIdent`(brokerCtx: DefaultBrokerContext)] return `globalVarIdent` ) @@ -372,40 +355,47 @@ macro MultiRequestBroker*(body: untyped): untyped = result.add( quote do: proc setProvider*( - _: typedesc[`typeIdent`], handler: `zeroArgProviderName` + _: typedesc[`typeIdent`], + brokerCtx: BrokerContext, + handler: `zeroArgProviderName`, ): Result[`providerHandleIdent`, string] = if handler.isNil(): return err("Provider handler must be provided") let broker = `accessProcIdent`() - if broker.nextId == 0'u64: - broker.nextId = 1'u64 - for existingId, existing in broker.`zeroArgFieldName`.pairs: - if existing == handler: - return ok(`providerHandleIdent`(id: existingId, kind: `zeroKindIdent`)) - let newId = broker.nextId - inc broker.nextId - broker.`zeroArgFieldName`[newId] = handler - return ok(`providerHandleIdent`(id: newId, kind: `zeroKindIdent`)) + let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx) + for i, existing in broker.buckets[bucketIdx].`zeroArgFieldName`: + if not existing.isNil() and existing == handler: + return ok(`providerHandleIdent`(id: uint64(i + 1), kind: `zeroKindIdent`)) + broker.buckets[bucketIdx].`zeroArgFieldName`.add(handler) + return ok( + `providerHandleIdent`( + id: uint64(broker.buckets[bucketIdx].`zeroArgFieldName`.len), + kind: `zeroKindIdent`, + ) + ) + + proc setProvider*( + _: typedesc[`typeIdent`], handler: `zeroArgProviderName` + ): Result[`providerHandleIdent`, string] = + return setProvider(`typeIdent`, DefaultBrokerContext, handler) - ) - clearBody.add( - quote do: - let broker = `accessProcIdent`() - if not broker.isNil() and broker.`zeroArgFieldName`.len > 0: - broker.`zeroArgFieldName`.clear() ) result.add( quote do: proc request*( - _: typedesc[`typeIdent`] + _: typedesc[`typeIdent`], brokerCtx: BrokerContext ): Future[Result[seq[`typeIdent`], string]] {.async: (raises: []), gcsafe.} = var aggregated: seq[`typeIdent`] = @[] - let providers = `accessProcIdent`().`zeroArgFieldName` + let broker = `accessProcIdent`() + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: + return ok(aggregated) + let providers = broker.buckets[bucketIdx].`zeroArgFieldName` if providers.len == 0: return ok(aggregated) # var providersFut: seq[Future[Result[`typeIdent`, string]]] = collect: var providersFut = collect(newSeq): - for provider in providers.values: + for provider in providers: if provider.isNil(): continue provider() @@ -435,32 +425,40 @@ macro MultiRequestBroker*(body: untyped): untyped = return ok(aggregated) + proc request*( + _: typedesc[`typeIdent`] + ): Future[Result[seq[`typeIdent`], string]] = + return request(`typeIdent`, DefaultBrokerContext) + ) if not argSig.isNil(): result.add( quote do: proc setProvider*( - _: typedesc[`typeIdent`], handler: `argProviderName` + _: typedesc[`typeIdent`], + brokerCtx: BrokerContext, + handler: `argProviderName`, ): Result[`providerHandleIdent`, string] = if handler.isNil(): return err("Provider handler must be provided") let broker = `accessProcIdent`() - if broker.nextId == 0'u64: - broker.nextId = 1'u64 - for existingId, existing in broker.`argFieldName`.pairs: - if existing == handler: - return ok(`providerHandleIdent`(id: existingId, kind: `argKindIdent`)) - let newId = broker.nextId - inc broker.nextId - broker.`argFieldName`[newId] = handler - return ok(`providerHandleIdent`(id: newId, kind: `argKindIdent`)) + let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx) + for i, existing in broker.buckets[bucketIdx].`argFieldName`: + if not existing.isNil() and existing == handler: + return ok(`providerHandleIdent`(id: uint64(i + 1), kind: `argKindIdent`)) + broker.buckets[bucketIdx].`argFieldName`.add(handler) + return ok( + `providerHandleIdent`( + id: uint64(broker.buckets[bucketIdx].`argFieldName`.len), + kind: `argKindIdent`, + ) + ) + + proc setProvider*( + _: typedesc[`typeIdent`], handler: `argProviderName` + ): Result[`providerHandleIdent`, string] = + return setProvider(`typeIdent`, DefaultBrokerContext, handler) - ) - clearBody.add( - quote do: - let broker = `accessProcIdent`() - if not broker.isNil() and broker.`argFieldName`.len > 0: - broker.`argFieldName`.clear() ) let requestParamDefs = cloneParams(argParams) let argNameIdents = collectParamNames(requestParamDefs) @@ -481,17 +479,24 @@ macro MultiRequestBroker*(body: untyped): untyped = newEmptyNode(), ) ) + formalParams.add( + newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()) + ) for paramDef in requestParamDefs: formalParams.add(paramDef) let requestPragmas = quote: {.async: (raises: []), gcsafe.} let requestBody = quote: var aggregated: seq[`typeIdent`] = @[] - let providers = `accessProcIdent`().`argFieldName` + let broker = `accessProcIdent`() + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: + return ok(aggregated) + let providers = broker.buckets[bucketIdx].`argFieldName` if providers.len == 0: return ok(aggregated) var providersFut = collect(newSeq): - for provider in providers.values: + for provider in providers: if provider.isNil(): continue let `providerSym` = provider @@ -531,53 +536,208 @@ macro MultiRequestBroker*(body: untyped): untyped = ) ) - result.add( - quote do: - proc clearProviders*(_: typedesc[`typeIdent`]) = - `clearBody` - let broker = `accessProcIdent`() - if not broker.isNil(): - broker.nextId = 1'u64 - - ) - - let removeHandleSym = genSym(nskParam, "handle") - let removeBrokerSym = genSym(nskLet, "broker") - var removeBody = newStmtList() - removeBody.add( - quote do: - if `removeHandleSym`.id == 0'u64: - return - let `removeBrokerSym` = `accessProcIdent`() - if `removeBrokerSym`.isNil(): - return - ) - if not zeroArgSig.isNil(): - removeBody.add( + # Backward-compatible default-context overload (no brokerCtx parameter). + var formalParamsDefault = newTree(nnkFormalParams) + formalParamsDefault.add( quote do: - if `removeHandleSym`.kind == `zeroKindIdent`: - `removeBrokerSym`.`zeroArgFieldName`.del(`removeHandleSym`.id) - return + Future[Result[seq[`typeIdent`], string]] ) - if not argSig.isNil(): - removeBody.add( - quote do: - if `removeHandleSym`.kind == `argKindIdent`: - `removeBrokerSym`.`argFieldName`.del(`removeHandleSym`.id) - return + formalParamsDefault.add( + newTree( + nnkIdentDefs, + ident("_"), + newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)), + newEmptyNode(), + ) ) - removeBody.add( - quote do: - discard - ) - result.add( - quote do: - proc removeProvider*( - _: typedesc[`typeIdent`], `removeHandleSym`: `providerHandleIdent` - ) = - `removeBody` + for paramDef in requestParamDefs: + formalParamsDefault.add(copyNimTree(paramDef)) - ) + var wrapperCall = newCall(ident("request")) + wrapperCall.add(copyNimTree(typeIdent)) + wrapperCall.add(ident("DefaultBrokerContext")) + for argName in argNameIdents: + wrapperCall.add(copyNimTree(argName)) + + result.add( + newTree( + nnkProcDef, + postfix(ident("request"), "*"), + newEmptyNode(), + newEmptyNode(), + formalParamsDefault, + newEmptyNode(), + newEmptyNode(), + newStmtList(newTree(nnkReturnStmt, wrapperCall)), + ) + ) + let removeHandleCtxSym = genSym(nskParam, "handle") + let removeHandleDefaultSym = genSym(nskParam, "handle") + + when true: + # Generate clearProviders / removeProvider with macro-time knowledge about which + # provider lists exist (zero-arg and/or arg providers). + if not zeroArgSig.isNil() and not argSig.isNil(): + result.add( + quote do: + proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) = + let broker = `accessProcIdent`() + if broker.isNil(): + return + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: + return + broker.buckets[bucketIdx].`zeroArgFieldName`.setLen(0) + broker.buckets[bucketIdx].`argFieldName`.setLen(0) + if brokerCtx != DefaultBrokerContext: + broker.buckets.delete(bucketIdx) + + proc clearProviders*(_: typedesc[`typeIdent`]) = + clearProviders(`typeIdent`, DefaultBrokerContext) + + proc removeProvider*( + _: typedesc[`typeIdent`], + brokerCtx: BrokerContext, + `removeHandleCtxSym`: `providerHandleIdent`, + ) = + if `removeHandleCtxSym`.id == 0'u64: + return + let broker = `accessProcIdent`() + if broker.isNil(): + return + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: + return + + if `removeHandleCtxSym`.kind == `zeroKindIdent`: + let idx = int(`removeHandleCtxSym`.id) - 1 + if idx >= 0 and idx < broker.buckets[bucketIdx].`zeroArgFieldName`.len: + broker.buckets[bucketIdx].`zeroArgFieldName`[idx] = nil + elif `removeHandleCtxSym`.kind == `argKindIdent`: + let idx = int(`removeHandleCtxSym`.id) - 1 + if idx >= 0 and idx < broker.buckets[bucketIdx].`argFieldName`.len: + broker.buckets[bucketIdx].`argFieldName`[idx] = nil + + if brokerCtx != DefaultBrokerContext: + var hasAny = false + for p in broker.buckets[bucketIdx].`zeroArgFieldName`: + if not p.isNil(): + hasAny = true + break + if not hasAny: + for p in broker.buckets[bucketIdx].`argFieldName`: + if not p.isNil(): + hasAny = true + break + if not hasAny: + broker.buckets.delete(bucketIdx) + + proc removeProvider*( + _: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent` + ) = + removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`) + + ) + elif not zeroArgSig.isNil(): + result.add( + quote do: + proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) = + let broker = `accessProcIdent`() + if broker.isNil(): + return + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: + return + broker.buckets[bucketIdx].`zeroArgFieldName`.setLen(0) + if brokerCtx != DefaultBrokerContext: + broker.buckets.delete(bucketIdx) + + proc clearProviders*(_: typedesc[`typeIdent`]) = + clearProviders(`typeIdent`, DefaultBrokerContext) + + proc removeProvider*( + _: typedesc[`typeIdent`], + brokerCtx: BrokerContext, + `removeHandleCtxSym`: `providerHandleIdent`, + ) = + if `removeHandleCtxSym`.id == 0'u64: + return + let broker = `accessProcIdent`() + if broker.isNil(): + return + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: + return + if `removeHandleCtxSym`.kind != `zeroKindIdent`: + return + let idx = int(`removeHandleCtxSym`.id) - 1 + if idx >= 0 and idx < broker.buckets[bucketIdx].`zeroArgFieldName`.len: + broker.buckets[bucketIdx].`zeroArgFieldName`[idx] = nil + if brokerCtx != DefaultBrokerContext: + var hasAny = false + for p in broker.buckets[bucketIdx].`zeroArgFieldName`: + if not p.isNil(): + hasAny = true + break + if not hasAny: + broker.buckets.delete(bucketIdx) + + proc removeProvider*( + _: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent` + ) = + removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`) + + ) + else: + result.add( + quote do: + proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) = + let broker = `accessProcIdent`() + if broker.isNil(): + return + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: + return + broker.buckets[bucketIdx].`argFieldName`.setLen(0) + if brokerCtx != DefaultBrokerContext: + broker.buckets.delete(bucketIdx) + + proc clearProviders*(_: typedesc[`typeIdent`]) = + clearProviders(`typeIdent`, DefaultBrokerContext) + + proc removeProvider*( + _: typedesc[`typeIdent`], + brokerCtx: BrokerContext, + `removeHandleCtxSym`: `providerHandleIdent`, + ) = + if `removeHandleCtxSym`.id == 0'u64: + return + let broker = `accessProcIdent`() + if broker.isNil(): + return + let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) + if bucketIdx < 0: + return + if `removeHandleCtxSym`.kind != `argKindIdent`: + return + let idx = int(`removeHandleCtxSym`.id) - 1 + if idx >= 0 and idx < broker.buckets[bucketIdx].`argFieldName`.len: + broker.buckets[bucketIdx].`argFieldName`[idx] = nil + if brokerCtx != DefaultBrokerContext: + var hasAny = false + for p in broker.buckets[bucketIdx].`argFieldName`: + if not p.isNil(): + hasAny = true + break + if not hasAny: + broker.buckets.delete(bucketIdx) + + proc removeProvider*( + _: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent` + ) = + removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`) + + ) when defined(requestBrokerDebug): echo result.repr diff --git a/waku/common/broker/request_broker.nim b/waku/common/broker/request_broker.nim index dece77381..46f7d7d16 100644 --- a/waku/common/broker/request_broker.nim +++ b/waku/common/broker/request_broker.nim @@ -16,6 +16,18 @@ ## `async` mode is better to be used when you request date that may involve some long IO operation ## or action. ## +## Default vs. context aware use: +## Every generated broker is a thread-local global instance. This means each RequestBroker enables decoupled +## data exchange threadwise. Sometimes we use brokers inside a context - like inside a component that has many modules or subsystems. +## In case you would instantiate multiple such components in a single thread, and each component must has its own provider for the same RequestBroker type, +## in order to avoid provider collision, you can use context aware RequestBroker. +## Context awareness is supported through the `BrokerContext` argument for `setProvider`, `request`, `clearProvider` interfaces. +## Suce use requires generating a new unique `BrokerContext` value per component instance, and spread it to all modules using the brokers. +## Example, store the `BrokerContext` as a field inside the top level component instance, and spread around at initialization of the subcomponents.. +## +## Default broker context is defined as `DefaultBrokerContext` constant. But if you don't need context awareness, you can use the +## interfaces without context argument. +## ## Usage: ## Declare your desired request type inside a `RequestBroker` macro, add any number of fields. ## Define the provider signature, that is enforced at compile time. @@ -89,7 +101,13 @@ ## After this, you can register a provider anywhere in your code with ## `TypeName.setProvider(...)`, which returns error if already having a provider. ## Providers are async procs/lambdas in default mode and sync procs in sync mode. -## Only one provider can be registered at a time per signature type (zero arg and/or multi arg). +## +## Providers are stored as a broker-context keyed list: +## - the default provider is always stored at index 0 (reserved broker context: 0) +## - additional providers can be registered under arbitrary non-zero broker contexts +## +## The original `setProvider(handler)` / `request(...)` APIs continue to operate +## on the default provider (broker context 0) for backward compatibility. ## ## Requests can be made from anywhere with no direct dependency on the provider by ## calling `TypeName.request()` - with arguments respecting the signature(s). @@ -139,11 +157,12 @@ ## automatically, so the caller only needs to provide the type definition. import std/[macros, strutils] +from std/sequtils import keepItIf import chronos import results -import ./helper/broker_utils +import ./helper/broker_utils, broker_context -export results, chronos +export results, chronos, keepItIf, broker_context proc errorFuture[T](message: string): Future[Result[T, string]] {.inline.} = ## Build a future that is already completed with an error result. @@ -187,23 +206,6 @@ proc isReturnTypeValid(returnType, typeIdent: NimNode, mode: RequestBrokerMode): of rbSync: isSyncReturnTypeValid(returnType, typeIdent) -proc cloneParams(params: seq[NimNode]): seq[NimNode] = - ## Deep copy parameter definitions so they can be inserted in multiple places. - result = @[] - for param in params: - result.add(copyNimTree(param)) - -proc collectParamNames(params: seq[NimNode]): seq[NimNode] = - ## Extract all identifier symbols declared across IdentDefs nodes. - result = @[] - for param in params: - assert param.kind == nnkIdentDefs - for i in 0 ..< param.len - 2: - let nameNode = param[i] - if nameNode.kind == nnkEmpty: - continue - result.add(ident($nameNode)) - proc makeProcType( returnType: NimNode, params: seq[NimNode], mode: RequestBrokerMode ): NimNode = @@ -234,92 +236,13 @@ proc parseMode(modeNode: NimNode): RequestBrokerMode = else: error("RequestBroker mode must be `sync` or `async` (default is async)", modeNode) -proc ensureDistinctType(rhs: NimNode): NimNode = - ## For PODs / aliases / externally-defined types, wrap in `distinct` unless - ## it's already distinct. - if rhs.kind == nnkDistinctTy: - return copyNimTree(rhs) - newTree(nnkDistinctTy, copyNimTree(rhs)) - proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = when defined(requestBrokerDebug): echo body.treeRepr echo "RequestBroker mode: ", $mode - var typeIdent: NimNode = nil - var objectDef: NimNode = nil - for stmt in body: - if stmt.kind == nnkTypeSection: - for def in stmt: - if def.kind != nnkTypeDef: - continue - if not typeIdent.isNil(): - error("Only one type may be declared inside RequestBroker", def) - - typeIdent = baseTypeIdent(def[0]) - let rhs = def[2] - - ## Support inline object types (fields are auto-exported) - ## AND non-object types / aliases (e.g. `string`, `int`, `OtherType`). - case rhs.kind - of nnkObjectTy: - let recList = rhs[2] - if recList.kind != nnkRecList: - error("RequestBroker object must declare a standard field list", rhs) - var exportedRecList = newTree(nnkRecList) - for field in recList: - case field.kind - of nnkIdentDefs: - ensureFieldDef(field) - var cloned = copyNimTree(field) - for i in 0 ..< cloned.len - 2: - cloned[i] = exportIdentNode(cloned[i]) - exportedRecList.add(cloned) - of nnkEmpty: - discard - else: - error( - "RequestBroker object definition only supports simple field declarations", - field, - ) - objectDef = newTree( - nnkObjectTy, copyNimTree(rhs[0]), copyNimTree(rhs[1]), exportedRecList - ) - of nnkRefTy: - if rhs.len != 1: - error("RequestBroker ref type must have a single base", rhs) - if rhs[0].kind == nnkObjectTy: - let obj = rhs[0] - let recList = obj[2] - if recList.kind != nnkRecList: - error("RequestBroker object must declare a standard field list", obj) - var exportedRecList = newTree(nnkRecList) - for field in recList: - case field.kind - of nnkIdentDefs: - ensureFieldDef(field) - var cloned = copyNimTree(field) - for i in 0 ..< cloned.len - 2: - cloned[i] = exportIdentNode(cloned[i]) - exportedRecList.add(cloned) - of nnkEmpty: - discard - else: - error( - "RequestBroker object definition only supports simple field declarations", - field, - ) - let exportedObjectType = newTree( - nnkObjectTy, copyNimTree(obj[0]), copyNimTree(obj[1]), exportedRecList - ) - objectDef = newTree(nnkRefTy, exportedObjectType) - else: - ## `ref SomeType` (SomeType can be defined elsewhere) - objectDef = ensureDistinctType(rhs) - else: - ## Non-object type / alias (e.g. `string`, `int`, `SomeExternalType`). - objectDef = ensureDistinctType(rhs) - if typeIdent.isNil(): - error("RequestBroker body must declare exactly one type", body) + let parsed = parseSingleTypeDef(body, "RequestBroker", allowRefToNonObject = true) + let typeIdent = parsed.typeIdent + let objectDef = parsed.objectDef when defined(requestBrokerDebug): echo "RequestBroker generating type: ", $typeIdent @@ -329,11 +252,9 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = let typeNameLit = newLit(typeDisplayName) var zeroArgSig: NimNode = nil var zeroArgProviderName: NimNode = nil - var zeroArgFieldName: NimNode = nil var argSig: NimNode = nil var argParams: seq[NimNode] = @[] var argProviderName: NimNode = nil - var argFieldName: NimNode = nil for stmt in body: case stmt.kind @@ -368,7 +289,6 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = error("Only one zero-argument signature is allowed", stmt) zeroArgSig = stmt zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs") - zeroArgFieldName = ident("providerNoArgs") elif paramCount >= 1: if argSig != nil: error("Only one argument-based signature is allowed", stmt) @@ -391,7 +311,6 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = error("Signature parameter must declare a name", paramDef) argParams.add(copyNimTree(paramDef)) argProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderWithArgs") - argFieldName = ident("providerWithArgs") of nnkTypeSection, nnkEmpty: discard else: @@ -400,7 +319,6 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = if zeroArgSig.isNil() and argSig.isNil(): zeroArgSig = newEmptyNode() zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs") - zeroArgFieldName = ident("providerNoArgs") var typeSection = newTree(nnkTypeSection) typeSection.add(newTree(nnkTypeDef, exportedTypeIdent, newEmptyNode(), objectDef)) @@ -423,12 +341,29 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = var brokerRecList = newTree(nnkRecList) if not zeroArgSig.isNil(): + let zeroArgProvidersFieldName = ident("providersNoArgs") + let zeroArgProvidersTupleTy = newTree( + nnkTupleTy, + newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()), + newTree(nnkIdentDefs, ident("handler"), zeroArgProviderName, newEmptyNode()), + ) + let zeroArgProvidersSeqTy = + newTree(nnkBracketExpr, ident("seq"), zeroArgProvidersTupleTy) brokerRecList.add( - newTree(nnkIdentDefs, zeroArgFieldName, zeroArgProviderName, newEmptyNode()) + newTree( + nnkIdentDefs, zeroArgProvidersFieldName, zeroArgProvidersSeqTy, newEmptyNode() + ) ) if not argSig.isNil(): + let argProvidersFieldName = ident("providersWithArgs") + let argProvidersTupleTy = newTree( + nnkTupleTy, + newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()), + newTree(nnkIdentDefs, ident("handler"), argProviderName, newEmptyNode()), + ) + let argProvidersSeqTy = newTree(nnkBracketExpr, ident("seq"), argProvidersTupleTy) brokerRecList.add( - newTree(nnkIdentDefs, argFieldName, argProviderName, newEmptyNode()) + newTree(nnkIdentDefs, argProvidersFieldName, argProvidersSeqTy, newEmptyNode()) ) let brokerTypeIdent = ident(sanitizeIdentName(typeIdent) & "Broker") let brokerTypeDef = newTree( @@ -443,31 +378,97 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = let globalVarIdent = ident("g" & sanitizeIdentName(typeIdent) & "Broker") let accessProcIdent = ident("access" & sanitizeIdentName(typeIdent) & "Broker") + + var brokerNewBody = newStmtList() + if not zeroArgSig.isNil(): + brokerNewBody.add( + quote do: + result.providersNoArgs = + @[(brokerCtx: DefaultBrokerContext, handler: default(`zeroArgProviderName`))] + ) + if not argSig.isNil(): + brokerNewBody.add( + quote do: + result.providersWithArgs = + @[(brokerCtx: DefaultBrokerContext, handler: default(`argProviderName`))] + ) + + var brokerInitChecks = newStmtList() + if not zeroArgSig.isNil(): + brokerInitChecks.add( + quote do: + if `globalVarIdent`.providersNoArgs.len == 0: + `globalVarIdent` = `brokerTypeIdent`.new() + ) + if not argSig.isNil(): + brokerInitChecks.add( + quote do: + if `globalVarIdent`.providersWithArgs.len == 0: + `globalVarIdent` = `brokerTypeIdent`.new() + ) + result.add( quote do: var `globalVarIdent` {.threadvar.}: `brokerTypeIdent` + proc new(_: type `brokerTypeIdent`): `brokerTypeIdent` = + result = `brokerTypeIdent`() + `brokerNewBody` + proc `accessProcIdent`(): var `brokerTypeIdent` = + `brokerInitChecks` `globalVarIdent` ) - var clearBody = newStmtList() + var clearBodyKeyed = newStmtList() + let brokerCtxParamIdent = ident("brokerCtx") if not zeroArgSig.isNil(): + let zeroArgProvidersFieldName = ident("providersNoArgs") result.add( quote do: proc setProvider*( _: typedesc[`typeIdent`], handler: `zeroArgProviderName` ): Result[void, string] = - if not `accessProcIdent`().`zeroArgFieldName`.isNil(): + if not `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler.isNil(): return err("Zero-arg provider already set") - `accessProcIdent`().`zeroArgFieldName` = handler + `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler = handler return ok() ) - clearBody.add( + + result.add( quote do: - `accessProcIdent`().`zeroArgFieldName` = nil + proc setProvider*( + _: typedesc[`typeIdent`], + brokerCtx: BrokerContext, + handler: `zeroArgProviderName`, + ): Result[void, string] = + if brokerCtx == DefaultBrokerContext: + return setProvider(`typeIdent`, handler) + + for entry in `accessProcIdent`().`zeroArgProvidersFieldName`: + if entry.brokerCtx == brokerCtx: + return err( + "RequestBroker(" & `typeNameLit` & + "): provider already set for broker context " & $brokerCtx + ) + + `accessProcIdent`().`zeroArgProvidersFieldName`.add( + (brokerCtx: brokerCtx, handler: handler) + ) + return ok() + + ) + clearBodyKeyed.add( + quote do: + if `brokerCtxParamIdent` == DefaultBrokerContext: + `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler = + default(`zeroArgProviderName`) + else: + `accessProcIdent`().`zeroArgProvidersFieldName`.keepItIf( + it.brokerCtx != `brokerCtxParamIdent` + ) ) case mode of rbAsync: @@ -476,11 +477,34 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = proc request*( _: typedesc[`typeIdent`] ): Future[Result[`typeIdent`, string]] {.async: (raises: []).} = - let provider = `accessProcIdent`().`zeroArgFieldName` + return await request(`typeIdent`, DefaultBrokerContext) + + ) + + result.add( + quote do: + proc request*( + _: typedesc[`typeIdent`], brokerCtx: BrokerContext + ): Future[Result[`typeIdent`, string]] {.async: (raises: []).} = + var provider: `zeroArgProviderName` + if brokerCtx == DefaultBrokerContext: + provider = `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler + else: + for entry in `accessProcIdent`().`zeroArgProvidersFieldName`: + if entry.brokerCtx == brokerCtx: + provider = entry.handler + break + if provider.isNil(): + if brokerCtx == DefaultBrokerContext: + return err( + "RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered" + ) return err( - "RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered" + "RequestBroker(" & `typeNameLit` & + "): no provider registered for broker context " & $brokerCtx ) + let catchedRes = catch: await provider() @@ -507,10 +531,32 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = proc request*( _: typedesc[`typeIdent`] ): Result[`typeIdent`, string] {.gcsafe, raises: [].} = - let provider = `accessProcIdent`().`zeroArgFieldName` + return request(`typeIdent`, DefaultBrokerContext) + + ) + + result.add( + quote do: + proc request*( + _: typedesc[`typeIdent`], brokerCtx: BrokerContext + ): Result[`typeIdent`, string] {.gcsafe, raises: [].} = + var provider: `zeroArgProviderName` + if brokerCtx == DefaultBrokerContext: + provider = `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler + else: + for entry in `accessProcIdent`().`zeroArgProvidersFieldName`: + if entry.brokerCtx == brokerCtx: + provider = entry.handler + break + if provider.isNil(): + if brokerCtx == DefaultBrokerContext: + return err( + "RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered" + ) return err( - "RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered" + "RequestBroker(" & `typeNameLit` & + "): no provider registered for broker context " & $brokerCtx ) var providerRes: Result[`typeIdent`, string] @@ -533,24 +579,54 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = ) if not argSig.isNil(): + let argProvidersFieldName = ident("providersWithArgs") result.add( quote do: proc setProvider*( _: typedesc[`typeIdent`], handler: `argProviderName` ): Result[void, string] = - if not `accessProcIdent`().`argFieldName`.isNil(): + if not `accessProcIdent`().`argProvidersFieldName`[0].handler.isNil(): return err("Provider already set") - `accessProcIdent`().`argFieldName` = handler + `accessProcIdent`().`argProvidersFieldName`[0].handler = handler return ok() ) - clearBody.add( + + result.add( quote do: - `accessProcIdent`().`argFieldName` = nil + proc setProvider*( + _: typedesc[`typeIdent`], + brokerCtx: BrokerContext, + handler: `argProviderName`, + ): Result[void, string] = + if brokerCtx == DefaultBrokerContext: + return setProvider(`typeIdent`, handler) + + for entry in `accessProcIdent`().`argProvidersFieldName`: + if entry.brokerCtx == brokerCtx: + return err( + "RequestBroker(" & `typeNameLit` & + "): provider already set for broker context " & $brokerCtx + ) + + `accessProcIdent`().`argProvidersFieldName`.add( + (brokerCtx: brokerCtx, handler: handler) + ) + return ok() + + ) + clearBodyKeyed.add( + quote do: + if `brokerCtxParamIdent` == DefaultBrokerContext: + `accessProcIdent`().`argProvidersFieldName`[0].handler = + default(`argProviderName`) + else: + `accessProcIdent`().`argProvidersFieldName`.keepItIf( + it.brokerCtx != `brokerCtxParamIdent` + ) ) let requestParamDefs = cloneParams(argParams) let argNameIdents = collectParamNames(requestParamDefs) - let providerSym = genSym(nskLet, "provider") var formalParams = newTree(nnkFormalParams) formalParams.add(copyNimTree(returnType)) formalParams.add( @@ -572,29 +648,96 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = of rbSync: quote: {.gcsafe, raises: [].} - var providerCall = newCall(providerSym) + + var forwardCall = newCall(ident("request")) + forwardCall.add(copyNimTree(typeIdent)) + forwardCall.add(ident("DefaultBrokerContext")) for argName in argNameIdents: - providerCall.add(argName) + forwardCall.add(argName) + var requestBody = newStmtList() - requestBody.add( - quote do: - let `providerSym` = `accessProcIdent`().`argFieldName` + case mode + of rbAsync: + requestBody.add( + quote do: + return await `forwardCall` + ) + of rbSync: + requestBody.add( + quote do: + return `forwardCall` + ) + + result.add( + newTree( + nnkProcDef, + postfix(ident("request"), "*"), + newEmptyNode(), + newEmptyNode(), + formalParams, + requestPragmas, + newEmptyNode(), + requestBody, + ) ) - requestBody.add( + + # Keyed request variant for the argument-based signature. + let requestParamDefsKeyed = cloneParams(argParams) + let argNameIdentsKeyed = collectParamNames(requestParamDefsKeyed) + let providerSymKeyed = genSym(nskVar, "provider") + var formalParamsKeyed = newTree(nnkFormalParams) + formalParamsKeyed.add(copyNimTree(returnType)) + formalParamsKeyed.add( + newTree( + nnkIdentDefs, + ident("_"), + newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)), + newEmptyNode(), + ) + ) + formalParamsKeyed.add( + newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()) + ) + for paramDef in requestParamDefsKeyed: + formalParamsKeyed.add(paramDef) + + let requestPragmasKeyed = requestPragmas + var providerCallKeyed = newCall(providerSymKeyed) + for argName in argNameIdentsKeyed: + providerCallKeyed.add(argName) + + var requestBodyKeyed = newStmtList() + requestBodyKeyed.add( quote do: - if `providerSym`.isNil(): + var `providerSymKeyed`: `argProviderName` + if brokerCtx == DefaultBrokerContext: + `providerSymKeyed` = `accessProcIdent`().`argProvidersFieldName`[0].handler + else: + for entry in `accessProcIdent`().`argProvidersFieldName`: + if entry.brokerCtx == brokerCtx: + `providerSymKeyed` = entry.handler + break + ) + requestBodyKeyed.add( + quote do: + if `providerSymKeyed`.isNil(): + if brokerCtx == DefaultBrokerContext: + return err( + "RequestBroker(" & `typeNameLit` & + "): no provider registered for input signature" + ) return err( "RequestBroker(" & `typeNameLit` & - "): no provider registered for input signature" + "): no provider registered for broker context " & $brokerCtx ) ) case mode of rbAsync: - requestBody.add( + requestBodyKeyed.add( quote do: let catchedRes = catch: - await `providerCall` + await `providerCallKeyed` if catchedRes.isErr(): return err( "RequestBroker(" & `typeNameLit` & "): provider threw exception: " & @@ -612,11 +755,11 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = return providerRes ) of rbSync: - requestBody.add( + requestBodyKeyed.add( quote do: var providerRes: Result[`typeIdent`, string] try: - providerRes = `providerCall` + providerRes = `providerCallKeyed` except CatchableError as e: return err( "RequestBroker(" & `typeNameLit` & "): provider threw exception: " & e.msg @@ -631,24 +774,52 @@ proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = ) return providerRes ) - # requestBody.add(providerCall) + result.add( newTree( nnkProcDef, postfix(ident("request"), "*"), newEmptyNode(), newEmptyNode(), - formalParams, - requestPragmas, + formalParamsKeyed, + requestPragmasKeyed, newEmptyNode(), - requestBody, + requestBodyKeyed, + ) + ) + + block: + var formalParamsClearKeyed = newTree(nnkFormalParams) + formalParamsClearKeyed.add(newEmptyNode()) + formalParamsClearKeyed.add( + newTree( + nnkIdentDefs, + ident("_"), + newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)), + newEmptyNode(), + ) + ) + formalParamsClearKeyed.add( + newTree(nnkIdentDefs, brokerCtxParamIdent, ident("BrokerContext"), newEmptyNode()) + ) + + result.add( + newTree( + nnkProcDef, + postfix(ident("clearProvider"), "*"), + newEmptyNode(), + newEmptyNode(), + formalParamsClearKeyed, + newEmptyNode(), + newEmptyNode(), + clearBodyKeyed, ) ) result.add( quote do: proc clearProvider*(_: typedesc[`typeIdent`]) = - `clearBody` + clearProvider(`typeIdent`, DefaultBrokerContext) ) From 91b4c5f52ee37f0db7ea2714ea591bcf4f26b5fe Mon Sep 17 00:00:00 2001 From: Darshan <35736874+darshankabariya@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:05:25 +0530 Subject: [PATCH 045/155] fix: store protocol issue in v0.37.0 (#3657) --- tests/testlib/wakunode.nim | 4 +- tools/confutils/cli_args.nim | 18 +++------ .../conf_builder/waku_conf_builder.nim | 16 +++++--- waku/node/peer_manager/peer_manager.nim | 2 +- waku/waku_archive/archive.nim | 10 +++++ waku/waku_store/client.nim | 39 +++++++++++++++---- 6 files changed, 59 insertions(+), 30 deletions(-) diff --git a/tests/testlib/wakunode.nim b/tests/testlib/wakunode.nim index ef6ba2b24..36aacce03 100644 --- a/tests/testlib/wakunode.nim +++ b/tests/testlib/wakunode.nim @@ -34,8 +34,8 @@ proc defaultTestWakuConfBuilder*(): WakuConfBuilder = @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")] ) builder.withNatStrategy("any") - builder.withMaxConnections(50) - builder.withRelayServiceRatio("60:40") + builder.withMaxConnections(150) + builder.withRelayServiceRatio("50:50") builder.withMaxMessageSize("1024 KiB") builder.withClusterId(DefaultClusterId) builder.withSubscribeShards(@[DefaultShardId]) diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index e6b3fc97d..6811e335f 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -206,22 +206,17 @@ type WakuNodeConf* = object .}: bool maxConnections* {. - desc: "Maximum allowed number of libp2p connections.", - defaultValue: 50, + desc: + "Maximum allowed number of libp2p connections. (Default: 150) that's recommended value for better connectivity", + defaultValue: 150, name: "max-connections" .}: int - maxRelayPeers* {. - desc: - "Deprecated. Use relay-service-ratio instead. It represents the maximum allowed number of relay peers.", - name: "max-relay-peers" - .}: Option[int] - relayServiceRatio* {. desc: "This percentage ratio represents the relay peers to service peers. For example, 60:40, tells that 60% of the max-connections will be used for relay protocol and the other 40% of max-connections will be reserved for other service protocols (e.g., filter, lightpush, store, metadata, etc.)", - name: "relay-service-ratio", - defaultValue: "60:40" # 60:40 ratio of relay to service peers + defaultValue: "50:50", + name: "relay-service-ratio" .}: string colocationLimit* {. @@ -957,9 +952,6 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.withExtMultiAddrsOnly(n.extMultiAddrsOnly) b.withMaxConnections(n.maxConnections) - if n.maxRelayPeers.isSome(): - b.withMaxRelayPeers(n.maxRelayPeers.get()) - if n.relayServiceRatio != "": b.withRelayServiceRatio(n.relayServiceRatio) b.withColocationLimit(n.colocationLimit) diff --git a/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index f3f942ecc..b952e711e 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -30,6 +30,8 @@ import logScope: topics = "waku conf builder" +const DefaultMaxConnections* = 150 + type MaxMessageSizeKind* = enum mmskNone mmskStr @@ -248,9 +250,6 @@ proc withAgentString*(b: var WakuConfBuilder, agentString: string) = proc withColocationLimit*(b: var WakuConfBuilder, colocationLimit: int) = b.colocationLimit = some(colocationLimit) -proc withMaxRelayPeers*(b: var WakuConfBuilder, maxRelayPeers: int) = - b.maxRelayPeers = some(maxRelayPeers) - proc withRelayServiceRatio*(b: var WakuConfBuilder, relayServiceRatio: string) = b.relayServiceRatio = some(relayServiceRatio) @@ -592,8 +591,13 @@ proc build*( if builder.maxConnections.isSome(): builder.maxConnections.get() else: - warn "Max Connections was not specified, defaulting to 300" - 300 + warn "Max connections not specified, defaulting to DefaultMaxConnections", + default = DefaultMaxConnections + DefaultMaxConnections + + if maxConnections < DefaultMaxConnections: + warn "max-connections less than DefaultMaxConnections; we suggest using DefaultMaxConnections or more for better connectivity", + provided = maxConnections, recommended = DefaultMaxConnections # TODO: Do the git version thing here let agentString = builder.agentString.get("nwaku") @@ -663,7 +667,7 @@ proc build*( agentString: agentString, colocationLimit: colocationLimit, maxRelayPeers: builder.maxRelayPeers, - relayServiceRatio: builder.relayServiceRatio.get("60:40"), + relayServiceRatio: builder.relayServiceRatio.get("50:50"), rateLimit: rateLimit, circuitRelayClient: builder.circuitRelayClient.get(false), staticNodes: builder.staticNodes, diff --git a/waku/node/peer_manager/peer_manager.nim b/waku/node/peer_manager/peer_manager.nim index 487d3894d..bdb68905e 100644 --- a/waku/node/peer_manager/peer_manager.nim +++ b/waku/node/peer_manager/peer_manager.nim @@ -1041,7 +1041,7 @@ proc new*( wakuMetadata: WakuMetadata = nil, maxRelayPeers: Option[int] = none(int), maxServicePeers: Option[int] = none(int), - relayServiceRatio: string = "60:40", + relayServiceRatio: string = "50:50", storage: PeerStorage = nil, initialBackoffInSec = InitialBackoffInSec, backoffFactor = BackoffFactor, diff --git a/waku/waku_archive/archive.nim b/waku/waku_archive/archive.nim index 707c757a3..8eb1fc051 100644 --- a/waku/waku_archive/archive.nim +++ b/waku/waku_archive/archive.nim @@ -61,9 +61,19 @@ proc validate*(msg: WakuMessage): Result[void, string] = upperBound = now + MaxMessageTimestampVariance if msg.timestamp < lowerBound: + warn "rejecting message with old timestamp", + msgTimestamp = msg.timestamp, + lowerBound = lowerBound, + now = now, + drift = (now - msg.timestamp) div 1_000_000_000 return err(invalidMessageOld) if upperBound < msg.timestamp: + warn "rejecting message with future timestamp", + msgTimestamp = msg.timestamp, + upperBound = upperBound, + now = now, + drift = (msg.timestamp - now) div 1_000_000_000 return err(invalidMessageFuture) return ok() diff --git a/waku/waku_store/client.nim b/waku/waku_store/client.nim index 308d7f98e..5b261af47 100644 --- a/waku/waku_store/client.nim +++ b/waku/waku_store/client.nim @@ -1,6 +1,12 @@ {.push raises: [].} -import std/[options, tables], results, chronicles, chronos, metrics, bearssl/rand +import + std/[options, tables, sequtils, algorithm], + results, + chronicles, + chronos, + metrics, + bearssl/rand import ../node/peer_manager, ../utils/requests, ./protocol_metrics, ./common, ./rpc_codec @@ -10,6 +16,8 @@ logScope: const DefaultPageSize*: uint = 20 # A recommended default number of waku messages per page +const MaxQueryRetries = 5 # Maximum number of store peers to try before giving up + type WakuStoreClient* = ref object peerManager: PeerManager rng: ref rand.HmacDrbgContext @@ -79,18 +87,33 @@ proc query*( proc queryToAny*( self: WakuStoreClient, request: StoreQueryRequest, peerId = none(PeerId) ): Future[StoreQueryResult] {.async.} = - ## This proc is similar to the query one but in this case - ## we don't specify a particular peer and instead we get it from peer manager + ## we don't specify a particular peer and instead we get it from peer manager. + ## It will retry with different store peers if the dial fails. if request.paginationCursor.isSome() and request.paginationCursor.get() == EmptyCursor: return err(StoreError(kind: ErrorCode.BAD_REQUEST, cause: "invalid cursor")) - let peer = self.peerManager.selectPeer(WakuStoreCodec).valueOr: + # Get all available store peers + var peers = self.peerManager.switch.peerStore.getPeersByProtocol(WakuStoreCodec) + if peers.len == 0: return err(StoreError(kind: BAD_RESPONSE, cause: "no service store peer connected")) - let connection = (await self.peerManager.dialPeer(peer, WakuStoreCodec)).valueOr: - waku_store_errors.inc(labelValues = [DialFailure]) + # Shuffle to distribute load and limit retries + let peersToTry = peers[0 ..< min(peers.len, MaxQueryRetries)] - return err(StoreError(kind: ErrorCode.PEER_DIAL_FAILURE, address: $peer)) + var lastError: StoreError + for peer in peersToTry: + let connection = (await self.peerManager.dialPeer(peer, WakuStoreCodec)).valueOr: + waku_store_errors.inc(labelValues = [DialFailure]) + warn "failed to dial store peer, trying next" + lastError = StoreError(kind: ErrorCode.PEER_DIAL_FAILURE, address: $peer) + continue - return await self.sendStoreRequest(request, connection) + let response = (await self.sendStoreRequest(request, connection)).valueOr: + warn "store query failed, trying next peer", peerId = peer.peerId, error = $error + lastError = error + continue + + return ok(response) + + return err(lastError) From a561ec3a3843defcb1dce01bd7878a1f6a7c4743 Mon Sep 17 00:00:00 2001 From: markoburcul Date: Tue, 20 Jan 2026 09:29:07 +0100 Subject: [PATCH 046/155] nix: add libwaku target, fix compiling Nim using NBS Use Nim built by NBS otherwise it doesn't work for both libwaku and wakucanary. Referenced issue: * https://github.com/status-im/status-go/issues/7152 --- flake.nix | 13 ++++++++++++- nix/checksums.nix | 2 +- nix/default.nix | 35 +++++++++++++++++++++++++---------- nix/nimble.nix | 2 +- nix/shell.nix | 1 + nix/zippy.nix | 9 +++++++++ 6 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 nix/zippy.nix diff --git a/flake.nix b/flake.nix index 72eaebef1..93d7d23f0 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "NWaku build flake"; + description = "Logos Messaging Nim build flake"; nixConfig = { extra-substituters = [ "https://nix-cache.status.im/" ]; @@ -54,10 +54,21 @@ zerokitRln = zerokit.packages.${system}.rln-android-arm64; }; + libwaku = pkgs.callPackage ./nix/default.nix { + inherit stableSystems; + src = self; + targets = ["libwaku"]; + # We are not able to compile the code with nim-unwrapped-2_0 + useSystemNim = false; + zerokitRln = zerokit.packages.${system}.rln; + }; + wakucanary = pkgs.callPackage ./nix/default.nix { inherit stableSystems; src = self; targets = ["wakucanary"]; + # We are not able to compile the code with nim-unwrapped-2_0 + useSystemNim = false; zerokitRln = zerokit.packages.${system}.rln; }; diff --git a/nix/checksums.nix b/nix/checksums.nix index 510f2b41a..c9c9f3d45 100644 --- a/nix/checksums.nix +++ b/nix/checksums.nix @@ -8,5 +8,5 @@ in pkgs.fetchFromGitHub { repo = "checksums"; rev = tools.findKeyValue "^ +ChecksumsStableCommit = \"([a-f0-9]+)\".*$" sourceFile; # WARNING: Requires manual updates when Nim compiler version changes. - hash = "sha256-Bm5iJoT2kAvcTexiLMFBa9oU5gf7d4rWjo3OiN7obWQ="; + hash = "sha256-JZhWqn4SrAgNw/HLzBK0rrj3WzvJ3Tv1nuDMn83KoYY="; } diff --git a/nix/default.nix b/nix/default.nix index d78f9935f..73838a4a1 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -23,7 +23,7 @@ let in stdenv.mkDerivation rec { - pname = "nwaku"; + pname = "logos-messaging-nim"; version = "1.0.0-${revision}"; @@ -70,6 +70,7 @@ in stdenv.mkDerivation rec { "QUICK_AND_DIRTY_COMPILER=${if quickAndDirty then "1" else "0"}" "QUICK_AND_DIRTY_NIMBLE=${if quickAndDirty then "1" else "0"}" "USE_SYSTEM_NIM=${if useSystemNim then "1" else "0"}" + "LIBRLN_FILE=${zerokitRln}/target/release/librln.a" ]; configurePhase = '' @@ -78,19 +79,28 @@ in stdenv.mkDerivation rec { make nimbus-build-system-nimble-dir ''; + # For the Nim v2.2.4 built with NBS we added sat and zippy preBuild = '' ln -s waku.nimble waku.nims + + ${lib.optionalString (!useSystemNim) '' pushd vendor/nimbus-build-system/vendor/Nim + mkdir dist - cp -r ${callPackage ./nimble.nix {}} dist/nimble - cp -r ${callPackage ./checksums.nix {}} dist/checksums - cp -r ${callPackage ./csources.nix {}} csources_v2 + mkdir -p dist/nimble/vendor/sat + mkdir -p dist/nimble/vendor/checksums + mkdir -p dist/nimble/vendor/zippy + + cp -r ${callPackage ./nimble.nix {}}/. dist/nimble + cp -r ${callPackage ./checksums.nix {}}/. dist/checksums + cp -r ${callPackage ./csources.nix {}}/. csources_v2 + cp -r ${callPackage ./sat.nix {}}/. dist/nimble/vendor/sat + cp -r ${callPackage ./checksums.nix {}}/. dist/nimble/vendor/checksums + cp -r ${callPackage ./zippy.nix {}}/. dist/nimble/vendor/zippy chmod 777 -R dist/nimble csources_v2 + popd - cp -r ${zerokitRln}/target vendor/zerokit/ - find vendor/zerokit/target - # FIXME - cp vendor/zerokit/target/*/release/librln.a librln_v${zerokitRln.version}.a + ''} ''; installPhase = if abidir != null then '' @@ -99,8 +109,13 @@ in stdenv.mkDerivation rec { echo '${androidManifest}' > $out/jni/AndroidManifest.xml cd $out && zip -r libwaku.aar * '' else '' - mkdir -p $out/bin - cp -r build/* $out/bin + mkdir -p $out/bin $out/include + + # Copy library files + cp build/* $out/bin/ 2>/dev/null || true + + # Copy the header file + cp library/libwaku.h $out/include/ ''; meta = with pkgs.lib; { diff --git a/nix/nimble.nix b/nix/nimble.nix index f9d87da6d..337ecd672 100644 --- a/nix/nimble.nix +++ b/nix/nimble.nix @@ -8,5 +8,5 @@ in pkgs.fetchFromGitHub { repo = "nimble"; rev = tools.findKeyValue "^ +NimbleStableCommit = \"([a-f0-9]+)\".*$" sourceFile; # WARNING: Requires manual updates when Nim compiler version changes. - hash = "sha256-MVHf19UbOWk8Zba2scj06PxdYYOJA6OXrVyDQ9Ku6Us="; + hash = "sha256-8iutVgNzDtttZ7V+7S11KfLEuwhKA9TsgS51mlUI08k="; } diff --git a/nix/shell.nix b/nix/shell.nix index 0db73dc25..fe0b065b4 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -16,6 +16,7 @@ pkgs.mkShell { git cargo rustup + rustc cmake nim-unwrapped-2_0 ]; diff --git a/nix/zippy.nix b/nix/zippy.nix new file mode 100644 index 000000000..ec59dfc07 --- /dev/null +++ b/nix/zippy.nix @@ -0,0 +1,9 @@ +{ pkgs }: + +pkgs.fetchFromGitHub { + owner = "guzba"; + repo = "zippy"; + rev = "a99f6a7d8a8e3e0213b3cad0daf0ea974bf58e3f"; + # WARNING: Requires manual updates when Nim compiler version changes. + hash = "sha256-e2ma2Oyp0dlNx8pJsdZl5o5KnaoAX87tqfY0RLG3DZs="; +} \ No newline at end of file From a02aaab53c90087f2477b8f77b1a1da9b4bf98c6 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:32:07 +0100 Subject: [PATCH 047/155] bump nim-ffi to v0.1.3 (#3696) --- vendor/nim-ffi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-ffi b/vendor/nim-ffi index d7a549212..06111de15 160000 --- a/vendor/nim-ffi +++ b/vendor/nim-ffi @@ -1 +1 @@ -Subproject commit d7a5492121aad190cf549436836e2fa42e34ff9b +Subproject commit 06111de155253b34e47ed2aaed1d61d08d62cc1b From 74b19c05d1fead6cc3b523b2aa1337d70b142bfe Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:48:34 +0100 Subject: [PATCH 048/155] simple refactor to reduce PRs CI load (#3701) * add discord notification in ci-daily --- .github/workflows/ci-daily.yml | 79 +++++++++++++++++++ .github/workflows/ci.yml | 2 +- Makefile | 6 +- library/kernel_api/debug_node_api.nim | 3 +- .../test_waku_filter_dos_protection.nim | 3 +- tests/waku_lightpush/test_ratelimit.nim | 5 +- 6 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci-daily.yml diff --git a/.github/workflows/ci-daily.yml b/.github/workflows/ci-daily.yml new file mode 100644 index 000000000..236bd5216 --- /dev/null +++ b/.github/workflows/ci-daily.yml @@ -0,0 +1,79 @@ +name: Daily logos-messaging-nim CI + +on: + schedule: + - cron: '30 6 * * *' + +env: + NPROC: 2 + MAKEFLAGS: "-j${NPROC}" + NIMFLAGS: "--parallelBuild:${NPROC} --colors:off -d:chronicles_colors:none" + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, macos-15] + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + + name: build-${{ matrix.os }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get submodules hash + id: submodules + run: | + echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT + + - name: Cache submodules + uses: actions/cache@v3 + with: + path: | + vendor/ + .git/modules + key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + + - name: Make update + run: make update + + - name: Build binaries + run: make V=1 QUICK_AND_DIRTY_COMPILER=1 examples tools + + - name: Notify Discord + if: always() + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + run: | + STATUS="${{ job.status }}" + OS="${{ matrix.os }}" + REPO="${{ github.repository }}" + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + if [ "$STATUS" = "success" ]; then + COLOR=3066993 + TITLE="✅ CI Success" + else + COLOR=15158332 + TITLE="❌ CI Failed" + fi + + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{ + \"embeds\": [{ + \"title\": \"$TITLE\", + \"color\": $COLOR, + \"fields\": [ + {\"name\": \"Repository\", \"value\": \"$REPO\", \"inline\": true}, + {\"name\": \"OS\", \"value\": \"$OS\", \"inline\": true}, + {\"name\": \"Status\", \"value\": \"$STATUS\", \"inline\": true} + ], + \"url\": \"$RUN_URL\", + \"footer\": {\"text\": \"Daily logos-messaging-nim CI\"} + }] + }" \ + "$DISCORD_WEBHOOK_URL" + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da8383e43..3de6eb4f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: run: make update - name: Build binaries - run: make V=1 QUICK_AND_DIRTY_COMPILER=1 all tools + run: make V=1 QUICK_AND_DIRTY_COMPILER=1 all build-windows: needs: changes diff --git a/Makefile b/Makefile index 87bd7bc74..9e241bbfa 100644 --- a/Makefile +++ b/Makefile @@ -51,10 +51,12 @@ endif ########## ## Main ## ########## -.PHONY: all test update clean +.PHONY: all test update clean examples # default target, because it's the first one that doesn't start with '.' -all: | wakunode2 example2 chat2 chat2bridge libwaku +all: | wakunode2 libwaku + +examples: | example2 chat2 chat2bridge test_file := $(word 2,$(MAKECMDGOALS)) define test_name diff --git a/library/kernel_api/debug_node_api.nim b/library/kernel_api/debug_node_api.nim index 98f5332b4..9d5a7f134 100644 --- a/library/kernel_api/debug_node_api.nim +++ b/library/kernel_api/debug_node_api.nim @@ -8,7 +8,8 @@ import libp2p/peerid, metrics, ffi -import waku/factory/waku, waku/node/waku_node, waku/node/health_monitor, library/declare_lib +import + waku/factory/waku, waku/node/waku_node, waku/node/health_monitor, library/declare_lib proc getMultiaddresses(node: WakuNode): seq[string] = return node.info().listenAddresses diff --git a/tests/waku_filter_v2/test_waku_filter_dos_protection.nim b/tests/waku_filter_v2/test_waku_filter_dos_protection.nim index fd3d8c837..be92fc409 100644 --- a/tests/waku_filter_v2/test_waku_filter_dos_protection.nim +++ b/tests/waku_filter_v2/test_waku_filter_dos_protection.nim @@ -217,7 +217,8 @@ suite "Waku Filter - DOS protection": for fut in finished: check not fut.failed() let pingRes = fut.read() - if pingRes.isErr() and pingRes.error().kind == FilterSubscribeErrorKind.TOO_MANY_REQUESTS: + if pingRes.isErr() and + pingRes.error().kind == FilterSubscribeErrorKind.TOO_MANY_REQUESTS: gotTooMany = true break diff --git a/tests/waku_lightpush/test_ratelimit.nim b/tests/waku_lightpush/test_ratelimit.nim index bdab3f074..ffbd1a06d 100644 --- a/tests/waku_lightpush/test_ratelimit.nim +++ b/tests/waku_lightpush/test_ratelimit.nim @@ -122,9 +122,8 @@ suite "Rate limited push service": # ensure period of time has passed and the client can again use the service await sleepAsync(tokenPeriod + 100.millis) - let recoveryRes = await client.publish( - some(DefaultPubsubTopic), fakeWakuMessage(), serverPeerId - ) + let recoveryRes = + await client.publish(some(DefaultPubsubTopic), fakeWakuMessage(), serverPeerId) check recoveryRes.isOk() ## Cleanup From 361d914f8794948d094867e51884910467643838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Soko=C5=82owski?= Date: Fri, 23 Jan 2026 14:05:58 +0100 Subject: [PATCH 049/155] nix: pin nixpkgs commit to same as status-go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoids fetching different nixpkgs versions. Signed-off-by: Jakub Sokołowski --- flake.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 93d7d23f0..3427ff6ac 100644 --- a/flake.nix +++ b/flake.nix @@ -7,7 +7,9 @@ }; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs?rev=f44bd8ca21e026135061a0a57dcf3d0775b67a49"; + # We are pinning the commit because ultimately we want to use same commit across different projects. + # A commit from nixpkgs 24.11 release : https://github.com/NixOS/nixpkgs/tree/release-24.11 + nixpkgs.url = "github:NixOS/nixpkgs/0ef228213045d2cdb5a169a95d63ded38670b293"; zerokit = { url = "github:vacp2p/zerokit?rev=dc0b31752c91e7b4fefc441cfa6a8210ad7dba7b"; inputs.nixpkgs.follows = "nixpkgs"; From 538b279b947e997606ab43dd556c0f40e40166d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Soko=C5=82owski?= Date: Fri, 23 Jan 2026 14:06:30 +0100 Subject: [PATCH 050/155] nix: drop unnecessay asert for Android SDK on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Newer nixpkgs should have Android SDK for aarch64. Signed-off-by: Jakub Sokołowski --- .github/workflows/ci-nix.yml | 48 +++++++++++++++++++++++++++ Makefile | 17 ++++++---- flake.lock | 28 ++++++++-------- flake.nix | 9 ++--- nix/default.nix | 56 ++++++++++---------------------- nix/pkgs/android-sdk/compose.nix | 7 ++-- nix/shell.nix | 19 ++++------- scripts/build_rln_android.sh | 1 - vendor/zerokit | 2 +- 9 files changed, 104 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/ci-nix.yml diff --git a/.github/workflows/ci-nix.yml b/.github/workflows/ci-nix.yml new file mode 100644 index 000000000..8fc7ac985 --- /dev/null +++ b/.github/workflows/ci-nix.yml @@ -0,0 +1,48 @@ +name: ci / nix +permissions: + contents: read + pull-requests: read + checks: write +on: + pull_request: + branches: [master] + +jobs: + build: + strategy: + fail-fast: false + matrix: + system: + - aarch64-darwin + - x86_64-linux + nixpkg: + - libwaku + - libwaku-android-arm64 + - wakucanary + + exclude: + # Android SDK limitation + - system: aarch64-darwin + nixpkg: libwaku-android-arm64 + + include: + - system: aarch64-darwin + runs_on: [self-hosted, macOS, ARM64] + + - system: x86_64-linux + runs_on: [self-hosted, Linux, X64] + + name: '${{ matrix.system }} / ${{ matrix.nixpkg }}' + runs-on: ${{ matrix.runs_on }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: 'Run Nix build for {{ matrix.nixpkg }}' + shell: bash + run: nix build -L '.?submodules=1#${{ matrix.nixpkg }}' + + - name: 'Show result contents' + shell: bash + run: find result -type f diff --git a/Makefile b/Makefile index 9e241bbfa..b19d5eaf8 100644 --- a/Makefile +++ b/Makefile @@ -153,7 +153,7 @@ NIM_PARAMS := $(NIM_PARAMS) -d:disable_libbacktrace endif # enable experimental exit is dest feature in libp2p mix -NIM_PARAMS := $(NIM_PARAMS) -d:libp2p_mix_experimental_exit_is_dest +NIM_PARAMS := $(NIM_PARAMS) -d:libp2p_mix_experimental_exit_is_dest libbacktrace: + $(MAKE) -C vendor/nim-libbacktrace --no-print-directory BUILD_CXX_LIB=0 @@ -192,9 +192,9 @@ LIBRLN_BUILDDIR := $(CURDIR)/vendor/zerokit LIBRLN_VERSION := v0.9.0 ifeq ($(detected_OS),Windows) -LIBRLN_FILE := rln.lib +LIBRLN_FILE ?= rln.lib else -LIBRLN_FILE := librln_$(LIBRLN_VERSION).a +LIBRLN_FILE ?= librln_$(LIBRLN_VERSION).a endif $(LIBRLN_FILE): @@ -481,8 +481,13 @@ ifndef ANDROID_NDK_HOME endif build-libwaku-for-android-arch: - $(MAKE) rebuild-nat-libs CC=$(ANDROID_TOOLCHAIN_DIR)/bin/$(ANDROID_COMPILER) && \ - ./scripts/build_rln_android.sh $(CURDIR)/build $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(CROSS_TARGET) $(ABIDIR) && \ +ifneq ($(findstring /nix/store,$(LIBRLN_FILE)),) + mkdir -p $(CURDIR)/build/android/$(ABIDIR)/ + cp $(LIBRLN_FILE) $(CURDIR)/build/android/$(ABIDIR)/ +else + ./scripts/build_rln_android.sh $(CURDIR)/build $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(CROSS_TARGET) $(ABIDIR) +endif + $(MAKE) rebuild-nat-libs CC=$(ANDROID_TOOLCHAIN_DIR)/bin/$(ANDROID_COMPILER) CPU=$(CPU) ABIDIR=$(ABIDIR) ANDROID_ARCH=$(ANDROID_ARCH) ANDROID_COMPILER=$(ANDROID_COMPILER) ANDROID_TOOLCHAIN_DIR=$(ANDROID_TOOLCHAIN_DIR) $(ENV_SCRIPT) nim libWakuAndroid $(NIM_PARAMS) waku.nims libwaku-android-arm64: ANDROID_ARCH=aarch64-linux-android @@ -541,7 +546,7 @@ else $(error iOS builds are only supported on macOS) endif -# Build for iOS architecture +# Build for iOS architecture build-libwaku-for-ios-arch: IOS_SDK=$(IOS_SDK) IOS_ARCH=$(IOS_ARCH) IOS_SDK_PATH=$(IOS_SDK_PATH) $(ENV_SCRIPT) nim libWakuIOS $(NIM_PARAMS) waku.nims diff --git a/flake.lock b/flake.lock index 0700e6a43..b927e8807 100644 --- a/flake.lock +++ b/flake.lock @@ -2,17 +2,17 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1740603184, - "narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=", + "lastModified": 1757590060, + "narHash": "sha256-EWwwdKLMZALkgHFyKW7rmyhxECO74+N+ZO5xTDnY/5c=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49", + "rev": "0ef228213045d2cdb5a169a95d63ded38670b293", "type": "github" }, "original": { "owner": "NixOS", "repo": "nixpkgs", - "rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49", + "rev": "0ef228213045d2cdb5a169a95d63ded38670b293", "type": "github" } }, @@ -51,18 +51,18 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1749115386, - "narHash": "sha256-UexIE2D7zr6aRajwnKongXwCZCeRZDXOL0kfjhqUFSU=", - "owner": "vacp2p", - "repo": "zerokit", - "rev": "dc0b31752c91e7b4fefc441cfa6a8210ad7dba7b", - "type": "github" + "lastModified": 1762211504, + "narHash": "sha256-SbDoBElFYJ4cYebltxlO2lYnz6qOaDAVY6aNJ5bqHDE=", + "ref": "refs/heads/master", + "rev": "3160d9504d07791f2fc9b610948a6cf9a58ed488", + "revCount": 342, + "type": "git", + "url": "https://github.com/vacp2p/zerokit" }, "original": { - "owner": "vacp2p", - "repo": "zerokit", - "rev": "dc0b31752c91e7b4fefc441cfa6a8210ad7dba7b", - "type": "github" + "rev": "3160d9504d07791f2fc9b610948a6cf9a58ed488", + "type": "git", + "url": "https://github.com/vacp2p/zerokit" } } }, diff --git a/flake.nix b/flake.nix index 3427ff6ac..88229a826 100644 --- a/flake.nix +++ b/flake.nix @@ -10,8 +10,9 @@ # We are pinning the commit because ultimately we want to use same commit across different projects. # A commit from nixpkgs 24.11 release : https://github.com/NixOS/nixpkgs/tree/release-24.11 nixpkgs.url = "github:NixOS/nixpkgs/0ef228213045d2cdb5a169a95d63ded38670b293"; + # WARNING: Remember to update commit and use 'nix flake update' to update flake.lock. zerokit = { - url = "github:vacp2p/zerokit?rev=dc0b31752c91e7b4fefc441cfa6a8210ad7dba7b"; + url = "git+https://github.com/vacp2p/zerokit?rev=3160d9504d07791f2fc9b610948a6cf9a58ed488"; inputs.nixpkgs.follows = "nixpkgs"; }; }; @@ -60,8 +61,6 @@ inherit stableSystems; src = self; targets = ["libwaku"]; - # We are not able to compile the code with nim-unwrapped-2_0 - useSystemNim = false; zerokitRln = zerokit.packages.${system}.rln; }; @@ -69,12 +68,10 @@ inherit stableSystems; src = self; targets = ["wakucanary"]; - # We are not able to compile the code with nim-unwrapped-2_0 - useSystemNim = false; zerokitRln = zerokit.packages.${system}.rln; }; - default = libwaku-android-arm64; + default = libwaku; }); devShells = forAllSystems (system: { diff --git a/nix/default.nix b/nix/default.nix index 73838a4a1..d77862e8f 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,9 +1,8 @@ { - config ? {}, - pkgs ? import { }, + pkgs, src ? ../., targets ? ["libwaku-android-arm64"], - verbosity ? 2, + verbosity ? 1, useSystemNim ? true, quickAndDirty ? true, stableSystems ? [ @@ -19,43 +18,31 @@ assert pkgs.lib.assertMsg ((src.submodules or true) == true) let inherit (pkgs) stdenv lib writeScriptBin callPackage; - revision = lib.substring 0 8 (src.rev or "dirty"); + androidManifest = ""; -in stdenv.mkDerivation rec { + tools = pkgs.callPackage ./tools.nix {}; + version = tools.findKeyValue "^version = \"([a-f0-9.-]+)\"$" ../waku.nimble; + revision = lib.substring 0 8 (src.rev or src.dirtyRev or "00000000"); +in stdenv.mkDerivation { pname = "logos-messaging-nim"; - - version = "1.0.0-${revision}"; + version = "${version}-${revision}"; inherit src; + # Runtime dependencies buildInputs = with pkgs; [ - openssl - gmp - zip + openssl gmp zip ]; # Dependencies that should only exist in the build environment. nativeBuildInputs = let # Fix for Nim compiler calling 'git rev-parse' and 'lsb_release'. fakeGit = writeScriptBin "git" "echo ${version}"; - # Fix for the zerokit package that is built with cargo/rustup/cross. - fakeCargo = writeScriptBin "cargo" "echo ${version}"; - # Fix for the zerokit package that is built with cargo/rustup/cross. - fakeRustup = writeScriptBin "rustup" "echo ${version}"; - # Fix for the zerokit package that is built with cargo/rustup/cross. - fakeCross = writeScriptBin "cross" "echo ${version}"; - in - with pkgs; [ - cmake - which - lsb-release - zerokitRln - nim-unwrapped-2_0 - fakeGit - fakeCargo - fakeRustup - fakeCross + in with pkgs; [ + cmake which zerokitRln nim-unwrapped-2_2 fakeGit + ] ++ lib.optionals stdenv.isDarwin [ + pkgs.darwin.cctools gcc # Necessary for libbacktrace ]; # Environment variables required for Android builds @@ -63,14 +50,13 @@ in stdenv.mkDerivation rec { ANDROID_NDK_HOME="${pkgs.androidPkgs.ndk}"; NIMFLAGS = "-d:disableMarchNative -d:git_revision_override=${revision}"; XDG_CACHE_HOME = "/tmp"; - androidManifest = ""; makeFlags = targets ++ [ "V=${toString verbosity}" "QUICK_AND_DIRTY_COMPILER=${if quickAndDirty then "1" else "0"}" "QUICK_AND_DIRTY_NIMBLE=${if quickAndDirty then "1" else "0"}" "USE_SYSTEM_NIM=${if useSystemNim then "1" else "0"}" - "LIBRLN_FILE=${zerokitRln}/target/release/librln.a" + "LIBRLN_FILE=${zerokitRln}/lib/librln.${if abidir != null then "so" else "a"}" ]; configurePhase = '' @@ -80,12 +66,8 @@ in stdenv.mkDerivation rec { ''; # For the Nim v2.2.4 built with NBS we added sat and zippy - preBuild = '' - ln -s waku.nimble waku.nims - - ${lib.optionalString (!useSystemNim) '' + preBuild = lib.optionalString (!useSystemNim) '' pushd vendor/nimbus-build-system/vendor/Nim - mkdir dist mkdir -p dist/nimble/vendor/sat mkdir -p dist/nimble/vendor/checksums @@ -98,9 +80,7 @@ in stdenv.mkDerivation rec { cp -r ${callPackage ./checksums.nix {}}/. dist/nimble/vendor/checksums cp -r ${callPackage ./zippy.nix {}}/. dist/nimble/vendor/zippy chmod 777 -R dist/nimble csources_v2 - popd - ''} ''; installPhase = if abidir != null then '' @@ -110,10 +90,10 @@ in stdenv.mkDerivation rec { cd $out && zip -r libwaku.aar * '' else '' mkdir -p $out/bin $out/include - + # Copy library files cp build/* $out/bin/ 2>/dev/null || true - + # Copy the header file cp library/libwaku.h $out/include/ ''; diff --git a/nix/pkgs/android-sdk/compose.nix b/nix/pkgs/android-sdk/compose.nix index c73aaee43..9a8536ddb 100644 --- a/nix/pkgs/android-sdk/compose.nix +++ b/nix/pkgs/android-sdk/compose.nix @@ -5,19 +5,16 @@ { androidenv, lib, stdenv }: -assert lib.assertMsg (stdenv.system != "aarch64-darwin") - "aarch64-darwin not supported for Android SDK. Use: NIXPKGS_SYSTEM_OVERRIDE=x86_64-darwin"; - # The "android-sdk-license" license is accepted # by setting android_sdk.accept_license = true. androidenv.composeAndroidPackages { cmdLineToolsVersion = "9.0"; toolsVersion = "26.1.1"; - platformToolsVersion = "33.0.3"; + platformToolsVersion = "34.0.5"; buildToolsVersions = [ "34.0.0" ]; platformVersions = [ "34" ]; cmakeVersions = [ "3.22.1" ]; - ndkVersion = "25.2.9519653"; + ndkVersion = "27.2.12479018"; includeNDK = true; includeExtras = [ "extras;android;m2repository" diff --git a/nix/shell.nix b/nix/shell.nix index fe0b065b4..3b83ac93d 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,16 +1,12 @@ -{ - pkgs ? import { }, -}: -let - optionalDarwinDeps = pkgs.lib.optionals pkgs.stdenv.isDarwin [ - pkgs.libiconv - pkgs.darwin.apple_sdk.frameworks.Security - ]; -in +{ pkgs }: + pkgs.mkShell { inputsFrom = [ pkgs.androidShell - ] ++ optionalDarwinDeps; + ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ + pkgs.libiconv + pkgs.darwin.apple_sdk.frameworks.Security + ]; buildInputs = with pkgs; [ git @@ -18,7 +14,6 @@ pkgs.mkShell { rustup rustc cmake - nim-unwrapped-2_0 + nim-unwrapped-2_2 ]; - } diff --git a/scripts/build_rln_android.sh b/scripts/build_rln_android.sh index 93a8c47ff..15b81ce9c 100755 --- a/scripts/build_rln_android.sh +++ b/scripts/build_rln_android.sh @@ -25,4 +25,3 @@ cargo clean cross rustc --release --lib --target=${android_arch} --crate-type=cdylib cp ../target/${android_arch}/release/librln.so ${output_dir}/. popd - diff --git a/vendor/zerokit b/vendor/zerokit index a4bb3feb5..70c79fbc9 160000 --- a/vendor/zerokit +++ b/vendor/zerokit @@ -1 +1 @@ -Subproject commit a4bb3feb5054e6fd24827adf204493e6e173437b +Subproject commit 70c79fbc989d4f87d9352b2f4bddcb60ebe55b19 From 1fd25355e0ea23f7e456b5fe95a702059fbaedb5 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Fri, 30 Jan 2026 01:06:00 +0100 Subject: [PATCH 051/155] feat: waku api send (#3669) * Introduce api/send Added events and requests for support. Reworked delivery_monitor into a featured devlivery_service, that - supports relay publish and lightpush depending on configuration but with fallback options - if available and configured it utilizes store api to confirm message delivery - emits message delivery events accordingly prepare for use in api_example * Fix edge mode config and test added * Fix some import issues, start and stop waku shall not throw exception but return with result properly * Utlize sync RequestBroker, adapt to non-async broker usage and gcsafe where appropriate, removed leftover * add api_example app to examples2 * Adapt after merge from master * Adapt code for using broker context * Fix brokerCtx settings for all usedbrokers, cover locked node init * Various fixes upon test failures. Added initial of subscribe API and auto-subscribe for send api * More test added * Fix multi propagate event emit, fix fail send test case * Fix rebase * Fix PushMessageHandlers in tests * adapt libwaku to api changes * Fix relay test by adapting publish return error in case NoPeersToPublish * Addressing all remaining review findings. Removed leftovers. Fixed loggings and typos * Fix rln relay broker, missed brokerCtx * Fix rest relay test failed, due to publish will fail if no peer avail * ignore anvil test state file * Make terst_wakunode_rln_relay broker context aware to fix * Fix waku rln tests by having them broker context aware * fix typo in test_app.nim --- .gitignore | 2 + Makefile | 4 + .../liteprotocoltester/liteprotocoltester.nim | 6 +- apps/wakunode2/wakunode2.nim | 6 +- examples/api_example/api_example.nim | 89 +++ examples/waku_example.nim | 40 -- library/kernel_api/node_lifecycle_api.nim | 8 +- tests/api/test_api_send.nim | 431 +++++++++++++++ tests/api/test_node_conf.nim | 21 + tests/node/test_wakunode_legacy_lightpush.nim | 20 - tests/node/test_wakunode_lightpush.nim | 17 - tests/waku_lightpush/test_client.nim | 4 +- tests/waku_lightpush/test_ratelimit.nim | 4 +- tests/waku_lightpush_legacy/test_client.nim | 4 +- .../waku_lightpush_legacy/test_ratelimit.nim | 4 +- tests/waku_relay/test_wakunode_relay.nim | 5 +- tests/waku_rln_relay/test_waku_rln_relay.nim | 32 +- .../test_wakunode_rln_relay.nim | 510 ++++++++++-------- tests/wakunode2/test_app.nim | 6 +- tests/wakunode_rest/test_rest_relay.nim | 41 +- waku.nim | 6 +- waku.nimble | 6 +- waku/api.nim | 3 +- waku/api/api.nim | 57 +- waku/api/api_conf.nim | 15 +- waku/api/send_api.md | 46 ++ waku/api/types.nim | 65 +++ waku/events/delivery_events.nim | 27 + waku/events/events.nim | 3 + waku/events/message_events.nim | 30 ++ waku/factory/waku.nim | 173 ++++-- .../delivery_monitor/delivery_callback.nim | 17 - .../delivery_monitor/delivery_monitor.nim | 43 -- .../delivery_monitor/publish_observer.nim | 9 - waku/node/delivery_monitor/send_monitor.nim | 212 -------- .../subscriptions_observer.nim | 13 - .../delivery_service/delivery_service.nim | 46 ++ .../not_delivered_storage/migrations.nim | 2 +- .../not_delivered_storage.nim | 8 +- waku/node/delivery_service/recv_service.nim | 3 + .../recv_service/recv_service.nim} | 117 ++-- waku/node/delivery_service/send_service.nim | 6 + .../send_service/delivery_task.nim | 74 +++ .../send_service/lightpush_processor.nim | 81 +++ .../send_service/relay_processor.nim | 78 +++ .../send_service/send_processor.nim | 36 ++ .../send_service/send_service.nim | 269 +++++++++ .../delivery_service/subscription_service.nim | 64 +++ .../health_monitor}/topic_health.nim | 4 +- waku/node/kernel_api/lightpush.nim | 33 +- waku/node/kernel_api/relay.nim | 85 +-- waku/node/waku_node.nim | 108 ++-- waku/requests/health_request.nim | 21 + waku/requests/node_requests.nim | 11 + waku/requests/requests.nim | 3 + waku/requests/rln_requests.nim | 9 + waku/waku_core/message/digest.nim | 5 + waku/waku_filter_v2/client.nim | 21 +- waku/waku_lightpush/callbacks.nim | 4 +- waku/waku_lightpush/client.nim | 84 +-- waku/waku_lightpush/common.nim | 4 +- waku/waku_lightpush/protocol.nim | 2 +- waku/waku_lightpush_legacy/callbacks.nim | 4 +- waku/waku_lightpush_legacy/client.nim | 11 - waku/waku_lightpush_legacy/common.nim | 2 +- waku/waku_lightpush_legacy/protocol.nim | 2 +- waku/waku_relay.nim | 3 +- waku/waku_relay/protocol.nim | 33 +- waku/waku_rln_relay/rln_relay.nim | 47 +- 69 files changed, 2331 insertions(+), 928 deletions(-) create mode 100644 examples/api_example/api_example.nim delete mode 100644 examples/waku_example.nim create mode 100644 tests/api/test_api_send.nim create mode 100644 waku/api/send_api.md create mode 100644 waku/api/types.nim create mode 100644 waku/events/delivery_events.nim create mode 100644 waku/events/events.nim create mode 100644 waku/events/message_events.nim delete mode 100644 waku/node/delivery_monitor/delivery_callback.nim delete mode 100644 waku/node/delivery_monitor/delivery_monitor.nim delete mode 100644 waku/node/delivery_monitor/publish_observer.nim delete mode 100644 waku/node/delivery_monitor/send_monitor.nim delete mode 100644 waku/node/delivery_monitor/subscriptions_observer.nim create mode 100644 waku/node/delivery_service/delivery_service.nim rename waku/node/{delivery_monitor => delivery_service}/not_delivered_storage/migrations.nim (95%) rename waku/node/{delivery_monitor => delivery_service}/not_delivered_storage/not_delivered_storage.nim (93%) create mode 100644 waku/node/delivery_service/recv_service.nim rename waku/node/{delivery_monitor/recv_monitor.nim => delivery_service/recv_service/recv_service.nim} (67%) create mode 100644 waku/node/delivery_service/send_service.nim create mode 100644 waku/node/delivery_service/send_service/delivery_task.nim create mode 100644 waku/node/delivery_service/send_service/lightpush_processor.nim create mode 100644 waku/node/delivery_service/send_service/relay_processor.nim create mode 100644 waku/node/delivery_service/send_service/send_processor.nim create mode 100644 waku/node/delivery_service/send_service/send_service.nim create mode 100644 waku/node/delivery_service/subscription_service.nim rename waku/{waku_relay => node/health_monitor}/topic_health.nim (84%) create mode 100644 waku/requests/health_request.nim create mode 100644 waku/requests/node_requests.nim create mode 100644 waku/requests/requests.nim create mode 100644 waku/requests/rln_requests.nim diff --git a/.gitignore b/.gitignore index f03c4ebaf..5222a0d5e 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,5 @@ AGENTS.md nimble.develop nimble.paths nimbledeps + +**/anvil_state/state-deployed-contracts-mint-and-approved.json diff --git a/Makefile b/Makefile index b19d5eaf8..13882253e 100644 --- a/Makefile +++ b/Makefile @@ -272,6 +272,10 @@ lightpushwithmix: | build deps librln echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim lightpushwithmix $(NIM_PARAMS) waku.nims +api_example: | build deps librln + echo -e $(BUILD_MSG) "build/$@" && \ + $(ENV_SCRIPT) nim api_example $(NIM_PARAMS) waku.nims + build/%: | build deps librln echo -e $(BUILD_MSG) "build/$*" && \ $(ENV_SCRIPT) nim buildone $(NIM_PARAMS) waku.nims $* diff --git a/apps/liteprotocoltester/liteprotocoltester.nim b/apps/liteprotocoltester/liteprotocoltester.nim index adb1b0f8a..46c85e910 100644 --- a/apps/liteprotocoltester/liteprotocoltester.nim +++ b/apps/liteprotocoltester/liteprotocoltester.nim @@ -130,7 +130,8 @@ when isMainModule: info "Setting up shutdown hooks" proc asyncStopper(waku: Waku) {.async: (raises: [Exception]).} = - await waku.stop() + (await waku.stop()).isOkOr: + error "Waku shutdown failed", error = error quit(QuitSuccess) # Handle Ctrl-C SIGINT @@ -160,7 +161,8 @@ when isMainModule: # Not available in -d:release mode writeStackTrace() - waitFor waku.stop() + (waitFor waku.stop()).isOkOr: + error "Waku shutdown failed", error = error quit(QuitFailure) c_signal(ansi_c.SIGSEGV, handleSigsegv) diff --git a/apps/wakunode2/wakunode2.nim b/apps/wakunode2/wakunode2.nim index b50c7113b..c8132ff4e 100644 --- a/apps/wakunode2/wakunode2.nim +++ b/apps/wakunode2/wakunode2.nim @@ -62,7 +62,8 @@ when isMainModule: info "Setting up shutdown hooks" proc asyncStopper(waku: Waku) {.async: (raises: [Exception]).} = - await waku.stop() + (await waku.stop()).isOkOr: + error "Waku shutdown failed", error = error quit(QuitSuccess) # Handle Ctrl-C SIGINT @@ -92,7 +93,8 @@ when isMainModule: # Not available in -d:release mode writeStackTrace() - waitFor waku.stop() + (waitFor waku.stop()).isOkOr: + error "Waku shutdown failed", error = error quit(QuitFailure) c_signal(ansi_c.SIGSEGV, handleSigsegv) diff --git a/examples/api_example/api_example.nim b/examples/api_example/api_example.nim new file mode 100644 index 000000000..37dd5d34b --- /dev/null +++ b/examples/api_example/api_example.nim @@ -0,0 +1,89 @@ +import std/options +import chronos, results, confutils, confutils/defs +import waku + +type CliArgs = object + ethRpcEndpoint* {. + defaultValue: "", desc: "ETH RPC Endpoint, if passed, RLN is enabled" + .}: string + +proc periodicSender(w: Waku): Future[void] {.async.} = + let sentListener = MessageSentEvent.listen( + proc(event: MessageSentEvent) {.async: (raises: []).} = + echo "Message sent with request ID: ", + event.requestId, " hash: ", event.messageHash + ).valueOr: + echo "Failed to listen to message sent event: ", error + return + + let errorListener = MessageErrorEvent.listen( + proc(event: MessageErrorEvent) {.async: (raises: []).} = + echo "Message failed to send with request ID: ", + event.requestId, " error: ", event.error + ).valueOr: + echo "Failed to listen to message error event: ", error + return + + let propagatedListener = MessagePropagatedEvent.listen( + proc(event: MessagePropagatedEvent) {.async: (raises: []).} = + echo "Message propagated with request ID: ", + event.requestId, " hash: ", event.messageHash + ).valueOr: + echo "Failed to listen to message propagated event: ", error + return + + defer: + MessageSentEvent.dropListener(sentListener) + MessageErrorEvent.dropListener(errorListener) + MessagePropagatedEvent.dropListener(propagatedListener) + + ## Periodically sends a Waku message every 30 seconds + var counter = 0 + while true: + let envelope = MessageEnvelope.init( + contentTopic = "example/content/topic", + payload = "Hello Waku! Message number: " & $counter, + ) + + let sendRequestId = (await w.send(envelope)).valueOr: + echo "Failed to send message: ", error + quit(QuitFailure) + + echo "Sending message with request ID: ", sendRequestId, " counter: ", counter + + counter += 1 + await sleepAsync(30.seconds) + +when isMainModule: + let args = CliArgs.load() + + echo "Starting Waku node..." + + let config = + if (args.ethRpcEndpoint == ""): + # Create a basic configuration for the Waku node + # No RLN as we don't have an ETH RPC Endpoint + NodeConfig.init( + protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 42) + ) + else: + # Connect to TWN, use ETH RPC Endpoint for RLN + NodeConfig.init(mode = WakuMode.Core, ethRpcEndpoints = @[args.ethRpcEndpoint]) + + # Create the node using the library API's createNode function + let node = (waitFor createNode(config)).valueOr: + echo "Failed to create node: ", error + quit(QuitFailure) + + echo("Waku node created successfully!") + + # Start the node + (waitFor startWaku(addr node)).isOkOr: + echo "Failed to start node: ", error + quit(QuitFailure) + + echo "Node started successfully!" + + asyncSpawn periodicSender(node) + + runForever() diff --git a/examples/waku_example.nim b/examples/waku_example.nim deleted file mode 100644 index ebac0b466..000000000 --- a/examples/waku_example.nim +++ /dev/null @@ -1,40 +0,0 @@ -import std/options -import chronos, results, confutils, confutils/defs -import waku - -type CliArgs = object - ethRpcEndpoint* {. - defaultValue: "", desc: "ETH RPC Endpoint, if passed, RLN is enabled" - .}: string - -when isMainModule: - let args = CliArgs.load() - - echo "Starting Waku node..." - - let config = - if (args.ethRpcEndpoint == ""): - # Create a basic configuration for the Waku node - # No RLN as we don't have an ETH RPC Endpoint - NodeConfig.init( - protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 42) - ) - else: - # Connect to TWN, use ETH RPC Endpoint for RLN - NodeConfig.init(ethRpcEndpoints = @[args.ethRpcEndpoint]) - - # Create the node using the library API's createNode function - let node = (waitFor createNode(config)).valueOr: - echo "Failed to create node: ", error - quit(QuitFailure) - - echo("Waku node created successfully!") - - # Start the node - (waitFor startWaku(addr node)).isOkOr: - echo "Failed to start node: ", error - quit(QuitFailure) - - echo "Node started successfully!" - - runForever() diff --git a/library/kernel_api/node_lifecycle_api.nim b/library/kernel_api/node_lifecycle_api.nim index a2bb25609..8f3e99b24 100644 --- a/library/kernel_api/node_lifecycle_api.nim +++ b/library/kernel_api/node_lifecycle_api.nim @@ -79,9 +79,7 @@ proc waku_start( proc waku_stop( ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer ) {.ffi.} = - try: - await ctx.myLib[].stop() - except Exception as exc: - error "STOP_NODE failed", error = exc.msg - return err("failed to stop: " & exc.msg) + (await ctx.myLib[].stop()).isOkOr: + error "STOP_NODE failed", error = error + return err("failed to stop: " & $error) return ok("") diff --git a/tests/api/test_api_send.nim b/tests/api/test_api_send.nim new file mode 100644 index 000000000..e247c65ce --- /dev/null +++ b/tests/api/test_api_send.nim @@ -0,0 +1,431 @@ +{.used.} + +import std/strutils +import chronos, testutils/unittests, stew/byteutils, libp2p/[switch, peerinfo] +import ../testlib/[common, wakucore, wakunode, testasync] +import ../waku_archive/archive_utils +import + waku, waku/[waku_node, waku_core, waku_relay/protocol, common/broker/broker_context] +import waku/api/api_conf, waku/factory/waku_conf + +type SendEventOutcome {.pure.} = enum + Sent + Propagated + Error + +type SendEventListenerManager = ref object + brokerCtx: BrokerContext + sentListener: MessageSentEventListener + errorListener: MessageErrorEventListener + propagatedListener: MessagePropagatedEventListener + sentFuture: Future[void] + errorFuture: Future[void] + propagatedFuture: Future[void] + sentCount: int + errorCount: int + propagatedCount: int + sentRequestIds: seq[RequestId] + errorRequestIds: seq[RequestId] + propagatedRequestIds: seq[RequestId] + +proc newSendEventListenerManager(brokerCtx: BrokerContext): SendEventListenerManager = + let manager = SendEventListenerManager(brokerCtx: brokerCtx) + manager.sentFuture = newFuture[void]("sentEvent") + manager.errorFuture = newFuture[void]("errorEvent") + manager.propagatedFuture = newFuture[void]("propagatedEvent") + + manager.sentListener = MessageSentEvent.listen( + brokerCtx, + proc(event: MessageSentEvent) {.async: (raises: []).} = + inc manager.sentCount + manager.sentRequestIds.add(event.requestId) + echo "SENT EVENT TRIGGERED (#", + manager.sentCount, "): requestId=", event.requestId + if not manager.sentFuture.finished(): + manager.sentFuture.complete() + , + ).valueOr: + raiseAssert error + + manager.errorListener = MessageErrorEvent.listen( + brokerCtx, + proc(event: MessageErrorEvent) {.async: (raises: []).} = + inc manager.errorCount + manager.errorRequestIds.add(event.requestId) + echo "ERROR EVENT TRIGGERED (#", manager.errorCount, "): ", event.error + if not manager.errorFuture.finished(): + manager.errorFuture.fail( + newException(CatchableError, "Error event triggered: " & event.error) + ) + , + ).valueOr: + raiseAssert error + + manager.propagatedListener = MessagePropagatedEvent.listen( + brokerCtx, + proc(event: MessagePropagatedEvent) {.async: (raises: []).} = + inc manager.propagatedCount + manager.propagatedRequestIds.add(event.requestId) + echo "PROPAGATED EVENT TRIGGERED (#", + manager.propagatedCount, "): requestId=", event.requestId + if not manager.propagatedFuture.finished(): + manager.propagatedFuture.complete() + , + ).valueOr: + raiseAssert error + + return manager + +proc teardown(manager: SendEventListenerManager) = + MessageSentEvent.dropListener(manager.brokerCtx, manager.sentListener) + MessageErrorEvent.dropListener(manager.brokerCtx, manager.errorListener) + MessagePropagatedEvent.dropListener(manager.brokerCtx, manager.propagatedListener) + +proc waitForEvents( + manager: SendEventListenerManager, timeout: Duration +): Future[bool] {.async.} = + return await allFutures( + manager.sentFuture, manager.propagatedFuture, manager.errorFuture + ) + .withTimeout(timeout) + +proc outcomes(manager: SendEventListenerManager): set[SendEventOutcome] = + if manager.sentFuture.completed(): + result.incl(SendEventOutcome.Sent) + if manager.propagatedFuture.completed(): + result.incl(SendEventOutcome.Propagated) + if manager.errorFuture.failed(): + result.incl(SendEventOutcome.Error) + +proc validate(manager: SendEventListenerManager, expected: set[SendEventOutcome]) = + echo "EVENT COUNTS: sent=", + manager.sentCount, ", propagated=", manager.propagatedCount, ", error=", + manager.errorCount + check manager.outcomes() == expected + +proc validate( + manager: SendEventListenerManager, + expected: set[SendEventOutcome], + expectedRequestId: RequestId, +) = + manager.validate(expected) + for requestId in manager.sentRequestIds: + check requestId == expectedRequestId + for requestId in manager.propagatedRequestIds: + check requestId == expectedRequestId + for requestId in manager.errorRequestIds: + check requestId == expectedRequestId + +proc createApiNodeConf(mode: WakuMode = WakuMode.Core): NodeConfig = + result = NodeConfig.init( + mode = mode, + protocolsConfig = ProtocolsConfig.init( + entryNodes = @[], + clusterId = 1, + autoShardingConfig = AutoShardingConfig(numShardsInCluster: 1), + ), + p2pReliability = true, + ) + +suite "Waku API - Send": + var + relayNode1 {.threadvar.}: WakuNode + relayNode1PeerInfo {.threadvar.}: RemotePeerInfo + relayNode1PeerId {.threadvar.}: PeerId + + relayNode2 {.threadvar.}: WakuNode + relayNode2PeerInfo {.threadvar.}: RemotePeerInfo + relayNode2PeerId {.threadvar.}: PeerId + + lightpushNode {.threadvar.}: WakuNode + lightpushNodePeerInfo {.threadvar.}: RemotePeerInfo + lightpushNodePeerId {.threadvar.}: PeerId + + storeNode {.threadvar.}: WakuNode + storeNodePeerInfo {.threadvar.}: RemotePeerInfo + storeNodePeerId {.threadvar.}: PeerId + + asyncSetup: + lockNewGlobalBrokerContext: + relayNode1 = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + relayNode1.mountMetadata(1, @[0'u16]).isOkOr: + raiseAssert "Failed to mount metadata: " & error + (await relayNode1.mountRelay()).isOkOr: + raiseAssert "Failed to mount relay" + await relayNode1.mountLibp2pPing() + await relayNode1.start() + + lockNewGlobalBrokerContext: + relayNode2 = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + relayNode2.mountMetadata(1, @[0'u16]).isOkOr: + raiseAssert "Failed to mount metadata: " & error + (await relayNode2.mountRelay()).isOkOr: + raiseAssert "Failed to mount relay" + await relayNode2.mountLibp2pPing() + await relayNode2.start() + + lockNewGlobalBrokerContext: + lightpushNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + lightpushNode.mountMetadata(1, @[0'u16]).isOkOr: + raiseAssert "Failed to mount metadata: " & error + (await lightpushNode.mountRelay()).isOkOr: + raiseAssert "Failed to mount relay" + (await lightpushNode.mountLightPush()).isOkOr: + raiseAssert "Failed to mount lightpush" + await lightpushNode.mountLibp2pPing() + await lightpushNode.start() + + lockNewGlobalBrokerContext: + storeNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + storeNode.mountMetadata(1, @[0'u16]).isOkOr: + raiseAssert "Failed to mount metadata: " & error + (await storeNode.mountRelay()).isOkOr: + raiseAssert "Failed to mount relay" + # Mount archive so store can persist messages + let archiveDriver = newSqliteArchiveDriver() + storeNode.mountArchive(archiveDriver).isOkOr: + raiseAssert "Failed to mount archive: " & error + await storeNode.mountStore() + await storeNode.mountLibp2pPing() + await storeNode.start() + + relayNode1PeerInfo = relayNode1.peerInfo.toRemotePeerInfo() + relayNode1PeerId = relayNode1.peerInfo.peerId + + relayNode2PeerInfo = relayNode2.peerInfo.toRemotePeerInfo() + relayNode2PeerId = relayNode2.peerInfo.peerId + + lightpushNodePeerInfo = lightpushNode.peerInfo.toRemotePeerInfo() + lightpushNodePeerId = lightpushNode.peerInfo.peerId + + storeNodePeerInfo = storeNode.peerInfo.toRemotePeerInfo() + storeNodePeerId = storeNode.peerInfo.peerId + + # Subscribe all relay nodes to the default shard topic + const testPubsubTopic = PubsubTopic("/waku/2/rs/1/0") + proc dummyHandler( + topic: PubsubTopic, msg: WakuMessage + ): Future[void] {.async, gcsafe.} = + discard + + relayNode1.subscribe((kind: PubsubSub, topic: testPubsubTopic), dummyHandler).isOkOr: + raiseAssert "Failed to subscribe relayNode1: " & error + relayNode2.subscribe((kind: PubsubSub, topic: testPubsubTopic), dummyHandler).isOkOr: + raiseAssert "Failed to subscribe relayNode2: " & error + + lightpushNode.subscribe((kind: PubsubSub, topic: testPubsubTopic), dummyHandler).isOkOr: + raiseAssert "Failed to subscribe lightpushNode: " & error + storeNode.subscribe((kind: PubsubSub, topic: testPubsubTopic), dummyHandler).isOkOr: + raiseAssert "Failed to subscribe storeNode: " & error + + # Subscribe all relay nodes to the default shard topic + await relayNode1.connectToNodes(@[relayNode2PeerInfo, storeNodePeerInfo]) + await lightpushNode.connectToNodes(@[relayNode2PeerInfo]) + + asyncTeardown: + await allFutures( + relayNode1.stop(), relayNode2.stop(), lightpushNode.stop(), storeNode.stop() + ) + + asyncTest "Check API availability (unhealthy node)": + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf())).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + # node is not connected ! + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let sendResult = await node.send(envelope) + + check sendResult.isErr() # Depending on implementation, it might say "not healthy" + check sendResult.error().contains("not healthy") + + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error + + asyncTest "Send fully validated": + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf())).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + + await node.node.connectToNodes( + @[relayNode1PeerInfo, lightpushNodePeerInfo, storeNodePeerInfo] + ) + + let eventManager = newSendEventListenerManager(node.brokerCtx) + defer: + eventManager.teardown() + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let requestId = (await node.send(envelope)).valueOr: + raiseAssert error + + # Wait for events with timeout + const eventTimeout = 10.seconds + discard await eventManager.waitForEvents(eventTimeout) + + eventManager.validate( + {SendEventOutcome.Sent, SendEventOutcome.Propagated}, requestId + ) + + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error + + asyncTest "Send only propagates": + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf())).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + + await node.node.connectToNodes(@[relayNode1PeerInfo]) + + let eventManager = newSendEventListenerManager(node.brokerCtx) + defer: + eventManager.teardown() + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let requestId = (await node.send(envelope)).valueOr: + raiseAssert error + + # Wait for events with timeout + const eventTimeout = 10.seconds + discard await eventManager.waitForEvents(eventTimeout) + + eventManager.validate({SendEventOutcome.Propagated}, requestId) + + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error + + asyncTest "Send only propagates fallback to lightpush": + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf())).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + + await node.node.connectToNodes(@[lightpushNodePeerInfo]) + + let eventManager = newSendEventListenerManager(node.brokerCtx) + defer: + eventManager.teardown() + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let requestId = (await node.send(envelope)).valueOr: + raiseAssert error + + # Wait for events with timeout + const eventTimeout = 10.seconds + discard await eventManager.waitForEvents(eventTimeout) + + eventManager.validate({SendEventOutcome.Propagated}, requestId) + + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error + + asyncTest "Send fully validates fallback to lightpush": + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf())).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + + await node.node.connectToNodes(@[lightpushNodePeerInfo, storeNodePeerInfo]) + + let eventManager = newSendEventListenerManager(node.brokerCtx) + defer: + eventManager.teardown() + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let requestId = (await node.send(envelope)).valueOr: + raiseAssert error + + # Wait for events with timeout + const eventTimeout = 10.seconds + discard await eventManager.waitForEvents(eventTimeout) + + eventManager.validate( + {SendEventOutcome.Propagated, SendEventOutcome.Sent}, requestId + ) + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error + + asyncTest "Send fails with event": + var fakeLightpushNode: WakuNode + lockNewGlobalBrokerContext: + fakeLightpushNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + fakeLightpushNode.mountMetadata(1, @[0'u16]).isOkOr: + raiseAssert "Failed to mount metadata: " & error + (await fakeLightpushNode.mountRelay()).isOkOr: + raiseAssert "Failed to mount relay" + (await fakeLightpushNode.mountLightPush()).isOkOr: + raiseAssert "Failed to mount lightpush" + await fakeLightpushNode.mountLibp2pPing() + await fakeLightpushNode.start() + let fakeLightpushNodePeerInfo = fakeLightpushNode.peerInfo.toRemotePeerInfo() + proc dummyHandler( + topic: PubsubTopic, msg: WakuMessage + ): Future[void] {.async, gcsafe.} = + discard + + fakeLightpushNode.subscribe( + (kind: PubsubSub, topic: PubsubTopic("/waku/2/rs/1/0")), dummyHandler + ).isOkOr: + raiseAssert "Failed to subscribe fakeLightpushNode: " & error + + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf(WakuMode.Edge))).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + + await node.node.connectToNodes(@[fakeLightpushNodePeerInfo]) + + let eventManager = newSendEventListenerManager(node.brokerCtx) + defer: + eventManager.teardown() + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let requestId = (await node.send(envelope)).valueOr: + raiseAssert error + + echo "Sent message with requestId=", requestId + # Wait for events with timeout + const eventTimeout = 62.seconds + discard await eventManager.waitForEvents(eventTimeout) + + eventManager.validate({SendEventOutcome.Error}, requestId) + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index 232ffc7d2..4dfbd4b51 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -21,6 +21,27 @@ suite "LibWaku Conf - toWakuConf": wakuConf.shardingConf.numShardsInCluster == 8 wakuConf.staticNodes.len == 0 + test "Edge mode configuration": + ## Given + let protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 1) + + let nodeConfig = NodeConfig.init(mode = Edge, protocolsConfig = protocolsConfig) + + ## When + let wakuConfRes = toWakuConf(nodeConfig) + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == false + wakuConf.lightPush == false + wakuConf.filterServiceConf.isSome() == false + wakuConf.storeServiceConf.isSome() == false + wakuConf.peerExchangeService == true + wakuConf.clusterId == 1 + test "Core mode configuration": ## Given let protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 1) diff --git a/tests/node/test_wakunode_legacy_lightpush.nim b/tests/node/test_wakunode_legacy_lightpush.nim index 4aedd7d4b..902464bcd 100644 --- a/tests/node/test_wakunode_legacy_lightpush.nim +++ b/tests/node/test_wakunode_legacy_lightpush.nim @@ -25,9 +25,6 @@ import suite "Waku Legacy Lightpush - End To End": var - handlerFuture {.threadvar.}: Future[(PubsubTopic, WakuMessage)] - handler {.threadvar.}: PushMessageHandler - server {.threadvar.}: WakuNode client {.threadvar.}: WakuNode @@ -37,13 +34,6 @@ suite "Waku Legacy Lightpush - End To End": message {.threadvar.}: WakuMessage asyncSetup: - handlerFuture = newPushHandlerFuture() - handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage - ): Future[WakuLightPushResult[void]] {.async.} = - handlerFuture.complete((pubsubTopic, message)) - return ok() - let serverKey = generateSecp256k1Key() clientKey = generateSecp256k1Key() @@ -108,9 +98,6 @@ suite "Waku Legacy Lightpush - End To End": suite "RLN Proofs as a Lightpush Service": var - handlerFuture {.threadvar.}: Future[(PubsubTopic, WakuMessage)] - handler {.threadvar.}: PushMessageHandler - server {.threadvar.}: WakuNode client {.threadvar.}: WakuNode anvilProc {.threadvar.}: Process @@ -122,13 +109,6 @@ suite "RLN Proofs as a Lightpush Service": message {.threadvar.}: WakuMessage asyncSetup: - handlerFuture = newPushHandlerFuture() - handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage - ): Future[WakuLightPushResult[void]] {.async.} = - handlerFuture.complete((pubsubTopic, message)) - return ok() - let serverKey = generateSecp256k1Key() clientKey = generateSecp256k1Key() diff --git a/tests/node/test_wakunode_lightpush.nim b/tests/node/test_wakunode_lightpush.nim index 7b4da6d4c..66b87b85e 100644 --- a/tests/node/test_wakunode_lightpush.nim +++ b/tests/node/test_wakunode_lightpush.nim @@ -37,13 +37,6 @@ suite "Waku Lightpush - End To End": message {.threadvar.}: WakuMessage asyncSetup: - handlerFuture = newPushHandlerFuture() - handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage - ): Future[WakuLightPushResult] {.async.} = - handlerFuture.complete((pubsubTopic, message)) - return ok(PublishedToOnePeer) - let serverKey = generateSecp256k1Key() clientKey = generateSecp256k1Key() @@ -108,9 +101,6 @@ suite "Waku Lightpush - End To End": suite "RLN Proofs as a Lightpush Service": var - handlerFuture {.threadvar.}: Future[(PubsubTopic, WakuMessage)] - handler {.threadvar.}: PushMessageHandler - server {.threadvar.}: WakuNode client {.threadvar.}: WakuNode anvilProc {.threadvar.}: Process @@ -122,13 +112,6 @@ suite "RLN Proofs as a Lightpush Service": message {.threadvar.}: WakuMessage asyncSetup: - handlerFuture = newPushHandlerFuture() - handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage - ): Future[WakuLightPushResult] {.async.} = - handlerFuture.complete((pubsubTopic, message)) - return ok(PublishedToOnePeer) - let serverKey = generateSecp256k1Key() clientKey = generateSecp256k1Key() diff --git a/tests/waku_lightpush/test_client.nim b/tests/waku_lightpush/test_client.nim index af22ffa5d..0bc9afdd4 100644 --- a/tests/waku_lightpush/test_client.nim +++ b/tests/waku_lightpush/test_client.nim @@ -38,7 +38,7 @@ suite "Waku Lightpush Client": asyncSetup: handlerFuture = newPushHandlerFuture() handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = let msgLen = message.encode().buffer.len if msgLen > int(DefaultMaxWakuMessageSize) + 64 * 1024: @@ -287,7 +287,7 @@ suite "Waku Lightpush Client": handlerError = "handler-error" handlerFuture2 = newFuture[void]() handler2 = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = handlerFuture2.complete() return lighpushErrorResult(LightPushErrorCode.PAYLOAD_TOO_LARGE, handlerError) diff --git a/tests/waku_lightpush/test_ratelimit.nim b/tests/waku_lightpush/test_ratelimit.nim index ffbd1a06d..e023bf3f5 100644 --- a/tests/waku_lightpush/test_ratelimit.nim +++ b/tests/waku_lightpush/test_ratelimit.nim @@ -19,7 +19,7 @@ suite "Rate limited push service": ## Given var handlerFuture = newFuture[(string, WakuMessage)]() let handler: PushMessageHandler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = handlerFuture.complete((pubsubTopic, message)) return lightpushSuccessResult(1) # succeed to publish to 1 peer. @@ -84,7 +84,7 @@ suite "Rate limited push service": # CI can be slow enough that sequential requests accidentally refill tokens. # Instead we issue a small burst and assert we observe at least one rejection. let handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = return lightpushSuccessResult(1) diff --git a/tests/waku_lightpush_legacy/test_client.nim b/tests/waku_lightpush_legacy/test_client.nim index 1dcb466c9..3d3027e9c 100644 --- a/tests/waku_lightpush_legacy/test_client.nim +++ b/tests/waku_lightpush_legacy/test_client.nim @@ -35,7 +35,7 @@ suite "Waku Legacy Lightpush Client": asyncSetup: handlerFuture = newPushHandlerFuture() handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = let msgLen = message.encode().buffer.len if msgLen > int(DefaultMaxWakuMessageSize) + 64 * 1024: @@ -282,7 +282,7 @@ suite "Waku Legacy Lightpush Client": handlerError = "handler-error" handlerFuture2 = newFuture[void]() handler2 = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = handlerFuture2.complete() return err(handlerError) diff --git a/tests/waku_lightpush_legacy/test_ratelimit.nim b/tests/waku_lightpush_legacy/test_ratelimit.nim index 37c43a066..ae5f5ed28 100644 --- a/tests/waku_lightpush_legacy/test_ratelimit.nim +++ b/tests/waku_lightpush_legacy/test_ratelimit.nim @@ -25,7 +25,7 @@ suite "Rate limited push service": ## Given var handlerFuture = newFuture[(string, WakuMessage)]() let handler: PushMessageHandler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = handlerFuture.complete((pubsubTopic, message)) return ok() @@ -87,7 +87,7 @@ suite "Rate limited push service": ## Given let handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = return ok() diff --git a/tests/waku_relay/test_wakunode_relay.nim b/tests/waku_relay/test_wakunode_relay.nim index 2b4f32617..a687119bd 100644 --- a/tests/waku_relay/test_wakunode_relay.nim +++ b/tests/waku_relay/test_wakunode_relay.nim @@ -1,7 +1,7 @@ {.used.} import - std/[os, sequtils, sysrand, math], + std/[os, strutils, sequtils, sysrand, math], stew/byteutils, testutils/unittests, chronos, @@ -450,7 +450,8 @@ suite "WakuNode - Relay": await sleepAsync(500.millis) let res = await node2.publish(some($shard), message) - assert res.isOk(), $res.error + check res.isErr() + check contains($res.error, "NoPeersToPublish") await sleepAsync(500.millis) diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index 3430657ad..d9fe0d890 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -15,6 +15,7 @@ import waku_rln_relay/rln, waku_rln_relay/protocol_metrics, waku_keystore, + common/broker/broker_context, ], ./rln/waku_rln_relay_utils, ./utils_onchain, @@ -233,8 +234,10 @@ suite "Waku rln relay": let index = MembershipIndex(5) let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = index) - let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: - raiseAssert $error + var wakuRlnRelay: WakuRlnRelay + lockNewGlobalBrokerContext: + wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: + raiseAssert $error let manager = cast[OnchainGroupManager](wakuRlnRelay.groupManager) let idCredentials = generateCredentials() @@ -290,8 +293,10 @@ suite "Waku rln relay": let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = index) - let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: - raiseAssert $error + var wakuRlnRelay: WakuRlnRelay + lockNewGlobalBrokerContext: + wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: + raiseAssert $error let manager = cast[OnchainGroupManager](wakuRlnRelay.groupManager) let idCredentials = generateCredentials() @@ -340,8 +345,10 @@ suite "Waku rln relay": asyncTest "multiple senders with same external nullifier": let index1 = MembershipIndex(5) let rlnConf1 = getWakuRlnConfig(manager = manager, index = index1) - let wakuRlnRelay1 = (await WakuRlnRelay.new(rlnConf1)).valueOr: - raiseAssert "failed to create waku rln relay: " & $error + var wakuRlnRelay1: WakuRlnRelay + lockNewGlobalBrokerContext: + wakuRlnRelay1 = (await WakuRlnRelay.new(rlnConf1)).valueOr: + raiseAssert "failed to create waku rln relay: " & $error let manager1 = cast[OnchainGroupManager](wakuRlnRelay1.groupManager) let idCredentials1 = generateCredentials() @@ -354,8 +361,10 @@ suite "Waku rln relay": let index2 = MembershipIndex(6) let rlnConf2 = getWakuRlnConfig(manager = manager, index = index2) - let wakuRlnRelay2 = (await WakuRlnRelay.new(rlnConf2)).valueOr: - raiseAssert "failed to create waku rln relay: " & $error + var wakuRlnRelay2: WakuRlnRelay + lockNewGlobalBrokerContext: + wakuRlnRelay2 = (await WakuRlnRelay.new(rlnConf2)).valueOr: + raiseAssert "failed to create waku rln relay: " & $error let manager2 = cast[OnchainGroupManager](wakuRlnRelay2.groupManager) let idCredentials2 = generateCredentials() @@ -486,9 +495,10 @@ suite "Waku rln relay": let wakuRlnConfig = getWakuRlnConfig( manager = manager, index = index, epochSizeSec = rlnEpochSizeSec.uint64 ) - - let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: - raiseAssert $error + var wakuRlnRelay: WakuRlnRelay + lockNewGlobalBrokerContext: + wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: + raiseAssert $error let rlnMaxEpochGap = wakuRlnRelay.rlnMaxEpochGap let testProofMetadata = default(ProofMetadata) diff --git a/tests/waku_rln_relay/test_wakunode_rln_relay.nim b/tests/waku_rln_relay/test_wakunode_rln_relay.nim index 1850b5277..fcf97a671 100644 --- a/tests/waku_rln_relay/test_wakunode_rln_relay.nim +++ b/tests/waku_rln_relay/test_wakunode_rln_relay.nim @@ -12,7 +12,8 @@ import waku/[waku_core, waku_node, waku_rln_relay], ../testlib/[wakucore, futures, wakunode, testutils], ./utils_onchain, - ./rln/waku_rln_relay_utils + ./rln/waku_rln_relay_utils, + waku/common/broker/broker_context from std/times import epochTime @@ -37,68 +38,70 @@ procSuite "WakuNode - RLN relay": stopAnvil(anvilProc) asyncTest "testing rln-relay with valid proof": - let - # publisher node - nodeKey1 = generateSecp256k1Key() + var node1, node2, node3: WakuNode # publisher node + let contentTopic = ContentTopic("/waku/2/default-content/proto") + # set up three nodes + lockNewGlobalBrokerContext: + let nodeKey1 = generateSecp256k1Key() node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + (await node1.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + + # mount rlnrelay in off-chain mode + let wakuRlnConfig1 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + + await node1.mountRlnRelay(wakuRlnConfig1) + await node1.start() + + # Registration is mandatory before sending messages with rln-relay + let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) + let idCredentials1 = generateCredentials() + + try: + waitFor manager1.register(idCredentials1, UserMessageLimit(20)) + except Exception, CatchableError: + assert false, + "exception raised when calling register: " & getCurrentExceptionMsg() + + let rootUpdated1 = waitFor manager1.updateRoots() + info "Updated root for node1", rootUpdated1 + + lockNewGlobalBrokerContext: # Relay node - nodeKey2 = generateSecp256k1Key() + let nodeKey2 = generateSecp256k1Key() node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + + (await node2.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + # mount rlnrelay in off-chain mode + let wakuRlnConfig2 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) + + await node2.mountRlnRelay(wakuRlnConfig2) + await node2.start() + + let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) + let rootUpdated2 = waitFor manager2.updateRoots() + info "Updated root for node2", rootUpdated2 + + lockNewGlobalBrokerContext: # Subscriber - nodeKey3 = generateSecp256k1Key() + let nodeKey3 = generateSecp256k1Key() node3 = newTestWakuNode(nodeKey3, parseIpAddress("0.0.0.0"), Port(0)) - contentTopic = ContentTopic("/waku/2/default-content/proto") + (await node3.mountRelay()).isOkOr: + assert false, "Failed to mount relay" - # set up three nodes - # node1 - (await node1.mountRelay()).isOkOr: - assert false, "Failed to mount relay" + let wakuRlnConfig3 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - # mount rlnrelay in off-chain mode - let wakuRlnConfig1 = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + await node3.mountRlnRelay(wakuRlnConfig3) + await node3.start() - await node1.mountRlnRelay(wakuRlnConfig1) - await node1.start() - - # Registration is mandatory before sending messages with rln-relay - let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) - let idCredentials1 = generateCredentials() - - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() - - let rootUpdated1 = waitFor manager1.updateRoots() - info "Updated root for node1", rootUpdated1 - - # node 2 - (await node2.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - # mount rlnrelay in off-chain mode - let wakuRlnConfig2 = getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) - - await node2.mountRlnRelay(wakuRlnConfig2) - await node2.start() - - let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) - let rootUpdated2 = waitFor manager2.updateRoots() - info "Updated root for node2", rootUpdated2 - - # node 3 - (await node3.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - - let wakuRlnConfig3 = getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - - await node3.mountRlnRelay(wakuRlnConfig3) - await node3.start() - - let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) - let rootUpdated3 = waitFor manager3.updateRoots() - info "Updated root for node3", rootUpdated3 + let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) + let rootUpdated3 = waitFor manager3.updateRoots() + info "Updated root for node3", rootUpdated3 # connect them together await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) @@ -156,10 +159,67 @@ procSuite "WakuNode - RLN relay": asyncTest "testing rln-relay is applied in all rln shards/content topics": # create 3 nodes - let nodes = toSeq(0 ..< 3).mapIt( - newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) - ) - await allFutures(nodes.mapIt(it.start())) + var node1, node2, node3: WakuNode + lockNewGlobalBrokerContext: + let nodeKey1 = generateSecp256k1Key() + node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + (await node1.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + let wakuRlnConfig1 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + await node1.mountRlnRelay(wakuRlnConfig1) + await node1.start() + let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) + let idCredentials1 = generateCredentials() + + try: + waitFor manager1.register(idCredentials1, UserMessageLimit(20)) + except Exception, CatchableError: + assert false, + "exception raised when calling register: " & getCurrentExceptionMsg() + + let rootUpdated1 = waitFor manager1.updateRoots() + info "Updated root for node", node = 1, rootUpdated = rootUpdated1 + lockNewGlobalBrokerContext: + let nodeKey2 = generateSecp256k1Key() + node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + (await node2.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + let wakuRlnConfig2 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) + await node2.mountRlnRelay(wakuRlnConfig2) + await node2.start() + let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) + let idCredentials2 = generateCredentials() + + try: + waitFor manager2.register(idCredentials2, UserMessageLimit(20)) + except Exception, CatchableError: + assert false, + "exception raised when calling register: " & getCurrentExceptionMsg() + + let rootUpdated2 = waitFor manager2.updateRoots() + info "Updated root for node", node = 2, rootUpdated = rootUpdated2 + lockNewGlobalBrokerContext: + let nodeKey3 = generateSecp256k1Key() + node3 = newTestWakuNode(nodeKey3, parseIpAddress("0.0.0.0"), Port(0)) + (await node3.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + let wakuRlnConfig3 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) + await node3.mountRlnRelay(wakuRlnConfig3) + await node3.start() + let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) + let idCredentials3 = generateCredentials() + + try: + waitFor manager3.register(idCredentials3, UserMessageLimit(20)) + except Exception, CatchableError: + assert false, + "exception raised when calling register: " & getCurrentExceptionMsg() + + let rootUpdated3 = waitFor manager3.updateRoots() + info "Updated root for node", node = 3, rootUpdated = rootUpdated3 let shards = @[RelayShard(clusterId: 0, shardId: 0), RelayShard(clusterId: 0, shardId: 1)] @@ -169,31 +229,9 @@ procSuite "WakuNode - RLN relay": ContentTopic("/waku/2/content-topic-b/proto"), ] - # set up three nodes - await allFutures(nodes.mapIt(it.mountRelay())) - - # mount rlnrelay in off-chain mode - for index, node in nodes: - let wakuRlnConfig = - getWakuRlnConfig(manager = manager, index = MembershipIndex(index + 1)) - - await node.mountRlnRelay(wakuRlnConfig) - await node.start() - let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) - let idCredentials = generateCredentials() - - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() - - let rootUpdated = waitFor manager.updateRoots() - info "Updated root for node", node = index + 1, rootUpdated = rootUpdated - # connect them together - await nodes[0].connectToNodes(@[nodes[1].switch.peerInfo.toRemotePeerInfo()]) - await nodes[2].connectToNodes(@[nodes[1].switch.peerInfo.toRemotePeerInfo()]) + await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) + await node3.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) var rxMessagesTopic1 = 0 var rxMessagesTopic2 = 0 @@ -211,15 +249,15 @@ procSuite "WakuNode - RLN relay": ): Future[void] {.async, gcsafe.} = await sleepAsync(0.milliseconds) - nodes[0].subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), simpleHandler).isOkOr: - assert false, "Failed to subscribe to pubsub topic in nodes[0]: " & $error - nodes[1].subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), simpleHandler).isOkOr: - assert false, "Failed to subscribe to pubsub topic in nodes[1]: " & $error + node1.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), simpleHandler).isOkOr: + assert false, "Failed to subscribe to pubsub topic in node1: " & $error + node2.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), simpleHandler).isOkOr: + assert false, "Failed to subscribe to pubsub topic in node2: " & $error # mount the relay handlers - nodes[2].subscribe((kind: PubsubSub, topic: $shards[0]), relayHandler).isOkOr: + node3.subscribe((kind: PubsubSub, topic: $shards[0]), relayHandler).isOkOr: assert false, "Failed to subscribe to pubsub topic: " & $error - nodes[2].subscribe((kind: PubsubSub, topic: $shards[1]), relayHandler).isOkOr: + node3.subscribe((kind: PubsubSub, topic: $shards[1]), relayHandler).isOkOr: assert false, "Failed to subscribe to pubsub topic: " & $error await sleepAsync(1000.millis) @@ -236,8 +274,8 @@ procSuite "WakuNode - RLN relay": contentTopic: contentTopics[0], ) - nodes[0].wakuRlnRelay.unsafeAppendRLNProof( - message, nodes[0].wakuRlnRelay.getCurrentEpoch(), MessageId(i.uint8) + node1.wakuRlnRelay.unsafeAppendRLNProof( + message, node1.wakuRlnRelay.getCurrentEpoch(), MessageId(i.uint8) ).isOkOr: raiseAssert $error messages1.add(message) @@ -249,8 +287,8 @@ procSuite "WakuNode - RLN relay": contentTopic: contentTopics[1], ) - nodes[1].wakuRlnRelay.unsafeAppendRLNProof( - message, nodes[1].wakuRlnRelay.getCurrentEpoch(), MessageId(i.uint8) + node2.wakuRlnRelay.unsafeAppendRLNProof( + message, node2.wakuRlnRelay.getCurrentEpoch(), MessageId(i.uint8) ).isOkOr: raiseAssert $error messages2.add(message) @@ -258,9 +296,9 @@ procSuite "WakuNode - RLN relay": # publish 3 messages from node[0] (last 2 are spam, window is 10 secs) # publish 3 messages from node[1] (last 2 are spam, window is 10 secs) for msg in messages1: - discard await nodes[0].publish(some($shards[0]), msg) + discard await node1.publish(some($shards[0]), msg) for msg in messages2: - discard await nodes[1].publish(some($shards[1]), msg) + discard await node2.publish(some($shards[1]), msg) # wait for gossip to propagate await sleepAsync(5000.millis) @@ -271,70 +309,70 @@ procSuite "WakuNode - RLN relay": rxMessagesTopic1 == 3 rxMessagesTopic2 == 3 - await allFutures(nodes.mapIt(it.stop())) + await node1.stop() + await node2.stop() + await node3.stop() asyncTest "testing rln-relay with invalid proof": - let + var node1, node2, node3: WakuNode + let contentTopic = ContentTopic("/waku/2/default-content/proto") + lockNewGlobalBrokerContext: # publisher node - nodeKey1 = generateSecp256k1Key() + let nodeKey1 = generateSecp256k1Key() node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + (await node1.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + + # mount rlnrelay in off-chain mode + let wakuRlnConfig1 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + + await node1.mountRlnRelay(wakuRlnConfig1) + await node1.start() + + let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) + let idCredentials1 = generateCredentials() + + try: + waitFor manager1.register(idCredentials1, UserMessageLimit(20)) + except Exception, CatchableError: + assert false, + "exception raised when calling register: " & getCurrentExceptionMsg() + + let rootUpdated1 = waitFor manager1.updateRoots() + info "Updated root for node1", rootUpdated1 + lockNewGlobalBrokerContext: # Relay node - nodeKey2 = generateSecp256k1Key() + let nodeKey2 = generateSecp256k1Key() node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + (await node2.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + # mount rlnrelay in off-chain mode + let wakuRlnConfig2 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) + + await node2.mountRlnRelay(wakuRlnConfig2) + await node2.start() + + let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) + let rootUpdated2 = waitFor manager2.updateRoots() + info "Updated root for node2", rootUpdated2 + lockNewGlobalBrokerContext: # Subscriber - nodeKey3 = generateSecp256k1Key() + let nodeKey3 = generateSecp256k1Key() node3 = newTestWakuNode(nodeKey3, parseIpAddress("0.0.0.0"), Port(0)) + (await node3.mountRelay()).isOkOr: + assert false, "Failed to mount relay" - contentTopic = ContentTopic("/waku/2/default-content/proto") + let wakuRlnConfig3 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - # set up three nodes - # node1 - (await node1.mountRelay()).isOkOr: - assert false, "Failed to mount relay" + await node3.mountRlnRelay(wakuRlnConfig3) + await node3.start() - # mount rlnrelay in off-chain mode - let wakuRlnConfig1 = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) - - await node1.mountRlnRelay(wakuRlnConfig1) - await node1.start() - - let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) - let idCredentials1 = generateCredentials() - - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() - - let rootUpdated1 = waitFor manager1.updateRoots() - info "Updated root for node1", rootUpdated1 - - # node 2 - (await node2.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - # mount rlnrelay in off-chain mode - let wakuRlnConfig2 = getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) - - await node2.mountRlnRelay(wakuRlnConfig2) - await node2.start() - - let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) - let rootUpdated2 = waitFor manager2.updateRoots() - info "Updated root for node2", rootUpdated2 - - # node 3 - (await node3.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - - let wakuRlnConfig3 = getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - - await node3.mountRlnRelay(wakuRlnConfig3) - await node3.start() - - let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) - let rootUpdated3 = waitFor manager3.updateRoots() - info "Updated root for node3", rootUpdated3 + let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) + let rootUpdated3 = waitFor manager3.updateRoots() + info "Updated root for node3", rootUpdated3 # connect them together await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) @@ -390,72 +428,70 @@ procSuite "WakuNode - RLN relay": await node3.stop() asyncTest "testing rln-relay double-signaling detection": - let + var node1, node2, node3: WakuNode + let contentTopic = ContentTopic("/waku/2/default-content/proto") + lockNewGlobalBrokerContext: # publisher node - nodeKey1 = generateSecp256k1Key() + let nodeKey1 = generateSecp256k1Key() node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + (await node1.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + + # mount rlnrelay in off-chain mode + let wakuRlnConfig1 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + + await node1.mountRlnRelay(wakuRlnConfig1) + await node1.start() + + # Registration is mandatory before sending messages with rln-relay + let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) + let idCredentials1 = generateCredentials() + + try: + waitFor manager1.register(idCredentials1, UserMessageLimit(20)) + except Exception, CatchableError: + assert false, + "exception raised when calling register: " & getCurrentExceptionMsg() + + let rootUpdated1 = waitFor manager1.updateRoots() + info "Updated root for node1", rootUpdated1 + lockNewGlobalBrokerContext: # Relay node - nodeKey2 = generateSecp256k1Key() + let nodeKey2 = generateSecp256k1Key() node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + (await node2.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + + # mount rlnrelay in off-chain mode + let wakuRlnConfig2 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) + + await node2.mountRlnRelay(wakuRlnConfig2) + await node2.start() + + # Registration is mandatory before sending messages with rln-relay + let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) + let rootUpdated2 = waitFor manager2.updateRoots() + info "Updated root for node2", rootUpdated2 + lockNewGlobalBrokerContext: # Subscriber - nodeKey3 = generateSecp256k1Key() + let nodeKey3 = generateSecp256k1Key() node3 = newTestWakuNode(nodeKey3, parseIpAddress("0.0.0.0"), Port(0)) + (await node3.mountRelay()).isOkOr: + assert false, "Failed to mount relay" - contentTopic = ContentTopic("/waku/2/default-content/proto") + # mount rlnrelay in off-chain mode + let wakuRlnConfig3 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - # set up three nodes - # node1 - (await node1.mountRelay()).isOkOr: - assert false, "Failed to mount relay" + await node3.mountRlnRelay(wakuRlnConfig3) + await node3.start() - # mount rlnrelay in off-chain mode - let wakuRlnConfig1 = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) - - await node1.mountRlnRelay(wakuRlnConfig1) - await node1.start() - - # Registration is mandatory before sending messages with rln-relay - let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) - let idCredentials1 = generateCredentials() - - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() - - let rootUpdated1 = waitFor manager1.updateRoots() - info "Updated root for node1", rootUpdated1 - - # node 2 - (await node2.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - - # mount rlnrelay in off-chain mode - let wakuRlnConfig2 = getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) - - await node2.mountRlnRelay(wakuRlnConfig2) - await node2.start() - - # Registration is mandatory before sending messages with rln-relay - let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) - let rootUpdated2 = waitFor manager2.updateRoots() - info "Updated root for node2", rootUpdated2 - - # node 3 - (await node3.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - - # mount rlnrelay in off-chain mode - let wakuRlnConfig3 = getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - - await node3.mountRlnRelay(wakuRlnConfig3) - await node3.start() - - # Registration is mandatory before sending messages with rln-relay - let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) - let rootUpdated3 = waitFor manager3.updateRoots() - info "Updated root for node3", rootUpdated3 + # Registration is mandatory before sending messages with rln-relay + let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) + let rootUpdated3 = waitFor manager3.updateRoots() + info "Updated root for node3", rootUpdated3 # connect the nodes together node1 <-> node2 <-> node3 await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) @@ -565,49 +601,49 @@ procSuite "WakuNode - RLN relay": xasyncTest "clearNullifierLog: should clear epochs > MaxEpochGap": ## This is skipped because is flaky and made CI randomly fail but is useful to run manually # Given two nodes + var node1, node2: WakuNode let contentTopic = ContentTopic("/waku/2/default-content/proto") shardSeq = @[DefaultRelayShard] - nodeKey1 = generateSecp256k1Key() - node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) - nodeKey2 = generateSecp256k1Key() - node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) epochSizeSec: uint64 = 5 # This means rlnMaxEpochGap = 4 + lockNewGlobalBrokerContext: + let nodeKey1 = generateSecp256k1Key() + node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + (await node1.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + let wakuRlnConfig1 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + await node1.mountRlnRelay(wakuRlnConfig1) + await node1.start() - # Given both nodes mount relay and rlnrelay - (await node1.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - let wakuRlnConfig1 = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) - await node1.mountRlnRelay(wakuRlnConfig1) - await node1.start() + # Registration is mandatory before sending messages with rln-relay + let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) + let idCredentials1 = generateCredentials() - # Registration is mandatory before sending messages with rln-relay - let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) - let idCredentials1 = generateCredentials() + try: + waitFor manager1.register(idCredentials1, UserMessageLimit(20)) + except Exception, CatchableError: + assert false, + "exception raised when calling register: " & getCurrentExceptionMsg() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + let rootUpdated1 = waitFor manager1.updateRoots() + info "Updated root for node1", rootUpdated1 + lockNewGlobalBrokerContext: + let nodeKey2 = generateSecp256k1Key() + node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + (await node2.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + let wakuRlnConfig2 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) + await node2.mountRlnRelay(wakuRlnConfig2) + await node2.start() - let rootUpdated1 = waitFor manager1.updateRoots() - info "Updated root for node1", rootUpdated1 - - # Mount rlnrelay in node2 in off-chain mode - (await node2.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - let wakuRlnConfig2 = getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) - await node2.mountRlnRelay(wakuRlnConfig2) - await node2.start() - - # Registration is mandatory before sending messages with rln-relay - let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) - let rootUpdated2 = waitFor manager2.updateRoots() - info "Updated root for node2", rootUpdated2 + # Registration is mandatory before sending messages with rln-relay + let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) + let rootUpdated2 = waitFor manager2.updateRoots() + info "Updated root for node2", rootUpdated2 # Given the two nodes are started and connected - waitFor allFutures(node1.start(), node2.start()) await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) # Given some messages diff --git a/tests/wakunode2/test_app.nim b/tests/wakunode2/test_app.nim index b16880787..e94a3b21d 100644 --- a/tests/wakunode2/test_app.nim +++ b/tests/wakunode2/test_app.nim @@ -60,7 +60,8 @@ suite "Wakunode2 - Waku initialization": not node.wakuRendezvous.isNil() ## Cleanup - waitFor waku.stop() + (waitFor waku.stop()).isOkOr: + raiseAssert error test "app properly handles dynamic port configuration": ## Given @@ -96,4 +97,5 @@ suite "Wakunode2 - Waku initialization": typedNodeEnr.get().tcp.get() != 0 ## Cleanup - waitFor waku.stop() + (waitFor waku.stop()).isOkOr: + raiseAssert error diff --git a/tests/wakunode_rest/test_rest_relay.nim b/tests/wakunode_rest/test_rest_relay.nim index ca9f7cb17..f16e5c4f4 100644 --- a/tests/wakunode_rest/test_rest_relay.nim +++ b/tests/wakunode_rest/test_rest_relay.nim @@ -21,6 +21,7 @@ import rest_api/endpoint/relay/client as relay_rest_client, waku_relay, waku_rln_relay, + common/broker/broker_context, ], ../testlib/wakucore, ../testlib/wakunode, @@ -505,15 +506,41 @@ suite "Waku v2 Rest API - Relay": asyncTest "Post a message to a content topic - POST /relay/v1/auto/messages/{topic}": ## "Relay API: publish and subscribe/unsubscribe": # Given - let node = testWakuNode() - (await node.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - require node.mountAutoSharding(1, 8).isOk + var meshNode: WakuNode + lockNewGlobalBrokerContext: + meshNode = testWakuNode() + (await meshNode.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + require meshNode.mountAutoSharding(1, 8).isOk - let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + let wakuRlnConfig = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + + await meshNode.mountRlnRelay(wakuRlnConfig) + await meshNode.start() + const testPubsubTopic = PubsubTopic("/waku/2/rs/1/0") + proc dummyHandler( + topic: PubsubTopic, msg: WakuMessage + ): Future[void] {.async, gcsafe.} = + discard + + meshNode.subscribe((kind: ContentSub, topic: DefaultContentTopic), dummyHandler).isOkOr: + raiseAssert "Failed to subscribe meshNode: " & error + + var node: WakuNode + lockNewGlobalBrokerContext: + node = testWakuNode() + (await node.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + require node.mountAutoSharding(1, 8).isOk + + let wakuRlnConfig = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + + await node.mountRlnRelay(wakuRlnConfig) + await node.start() + await node.connectToNodes(@[meshNode.peerInfo.toRemotePeerInfo()]) - await node.mountRlnRelay(wakuRlnConfig) - await node.start() # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() diff --git a/waku.nim b/waku.nim index 18d52741e..65a017c5a 100644 --- a/waku.nim +++ b/waku.nim @@ -1,10 +1,10 @@ ## Main module for using nwaku as a Nimble library -## +## ## This module re-exports the public API for creating and managing Waku nodes ## when using nwaku as a library dependency. -import waku/api/[api, api_conf] -export api, api_conf +import waku/api +export api import waku/factory/waku export waku diff --git a/waku.nimble b/waku.nimble index afc0ad634..3ff41c5bd 100644 --- a/waku.nimble +++ b/waku.nimble @@ -136,7 +136,7 @@ task testwakunode2, "Build & run wakunode2 app tests": test "all_tests_wakunode2" task example2, "Build Waku examples": - buildBinary "waku_example", "examples/" + buildBinary "api_example", "examples/api_example/" buildBinary "publisher", "examples/" buildBinary "subscriber", "examples/" buildBinary "filter_subscriber", "examples/" @@ -176,6 +176,10 @@ task lightpushwithmix, "Build lightpushwithmix": let name = "lightpush_publisher_mix" buildBinary name, "examples/lightpush_mix/" +task api_example, "Build api_example": + let name = "api_example" + buildBinary name, "examples/api_example/" + task buildone, "Build custom target": let filepath = paramStr(paramCount()) discard buildModule filepath diff --git a/waku/api.nim b/waku/api.nim index c3211867d..110a8f431 100644 --- a/waku/api.nim +++ b/waku/api.nim @@ -1,3 +1,4 @@ import ./api/[api, api_conf, entry_nodes] +import ./events/message_events -export api, api_conf, entry_nodes +export api, api_conf, entry_nodes, message_events diff --git a/waku/api/api.nim b/waku/api/api.nim index 5bab06188..41f4fd240 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -1,8 +1,13 @@ import chronicles, chronos, results import waku/factory/waku +import waku/[requests/health_request, waku_core, waku_node] +import waku/node/delivery_service/send_service +import waku/node/delivery_service/subscription_service +import ./[api_conf, types] -import ./api_conf +logScope: + topics = "api" # TODO: Specs says it should return a `WakuNode`. As `send` and other APIs are defined, we can align. proc createNode*(config: NodeConfig): Future[Result[Waku, string]] {.async.} = @@ -15,3 +20,53 @@ proc createNode*(config: NodeConfig): Future[Result[Waku, string]] {.async.} = return err("Failed setting up Waku: " & $error) return ok(wakuRes) + +proc checkApiAvailability(w: Waku): Result[void, string] = + if w.isNil(): + return err("Waku node is not initialized") + + # check if health is satisfactory + # If Node is not healthy, return err("Waku node is not healthy") + let healthStatus = RequestNodeHealth.request(w.brokerCtx) + + if healthStatus.isErr(): + warn "Failed to get Waku node health status: ", error = healthStatus.error + # Let's suppose the node is hesalthy enough, go ahead + else: + if healthStatus.get().healthStatus == NodeHealth.Unhealthy: + return err("Waku node is not healthy, has got no connections.") + + return ok() + +proc subscribe*( + w: Waku, contentTopic: ContentTopic +): Future[Result[void, string]] {.async.} = + ?checkApiAvailability(w) + + return w.deliveryService.subscriptionService.subscribe(contentTopic) + +proc unsubscribe*(w: Waku, contentTopic: ContentTopic): Result[void, string] = + ?checkApiAvailability(w) + + return w.deliveryService.subscriptionService.unsubscribe(contentTopic) + +proc send*( + w: Waku, envelope: MessageEnvelope +): Future[Result[RequestId, string]] {.async.} = + ?checkApiAvailability(w) + + let requestId = RequestId.new(w.rng) + + let deliveryTask = DeliveryTask.new(requestId, envelope, w.brokerCtx).valueOr: + return err("API send: Failed to create delivery task: " & error) + + info "API send: scheduling delivery task", + requestId = $requestId, + pubsubTopic = deliveryTask.pubsubTopic, + contentTopic = deliveryTask.msg.contentTopic, + msgHash = deliveryTask.msgHash.to0xHex(), + myPeerId = w.node.peerId() + + asyncSpawn w.deliveryService.sendService.send(deliveryTask) + + return ok(requestId) diff --git a/waku/api/api_conf.nim b/waku/api/api_conf.nim index 155554dfd..47aa9e7d8 100644 --- a/waku/api/api_conf.nim +++ b/waku/api/api_conf.nim @@ -86,6 +86,7 @@ type NodeConfig* {.requiresInit.} = object protocolsConfig: ProtocolsConfig networkingConfig: NetworkingConfig ethRpcEndpoints: seq[string] + p2pReliability: bool proc init*( T: typedesc[NodeConfig], @@ -93,12 +94,14 @@ proc init*( protocolsConfig: ProtocolsConfig = TheWakuNetworkPreset, networkingConfig: NetworkingConfig = DefaultNetworkingConfig, ethRpcEndpoints: seq[string] = @[], + p2pReliability: bool = false, ): T = return T( mode: mode, protocolsConfig: protocolsConfig, networkingConfig: networkingConfig, ethRpcEndpoints: ethRpcEndpoints, + p2pReliability: p2pReliability, ) proc toWakuConf*(nodeConfig: NodeConfig): Result[WakuConf, string] = @@ -131,7 +134,16 @@ proc toWakuConf*(nodeConfig: NodeConfig): Result[WakuConf, string] = b.rateLimitConf.withRateLimits(@["filter:100/1s", "lightpush:5/1s", "px:5/1s"]) of Edge: - return err("Edge mode is not implemented") + # All client side protocols are mounted by default + # Peer exchange client is always enabled and start_node will start the px loop + # Metadata is always mounted + b.withPeerExchange(true) + # switch off all service side protocols and relay + b.withRelay(false) + b.filterServiceConf.withEnabled(false) + b.withLightPush(false) + b.storeServiceConf.withEnabled(false) + # Leave discv5 and rendezvous for user choice ## Network Conf let protocolsConfig = nodeConfig.protocolsConfig @@ -193,6 +205,7 @@ proc toWakuConf*(nodeConfig: NodeConfig): Result[WakuConf, string] = ## Various configurations b.withNatStrategy("any") + b.withP2PReliability(nodeConfig.p2pReliability) let wakuConf = b.build().valueOr: return err("Failed to build configuration: " & error) diff --git a/waku/api/send_api.md b/waku/api/send_api.md new file mode 100644 index 000000000..2a5a2f8a4 --- /dev/null +++ b/waku/api/send_api.md @@ -0,0 +1,46 @@ +# SEND API + +**THIS IS TO BE REMOVED BEFORE PR MERGE** + +This document collects logic and todo's around the Send API. + +## Overview + +Send api hides the complex logic of using raw protocols for reliable message delivery. +The delivery method is chosen based on the node configuration and actual availabilities of peers. + +## Delivery task + +Each message send request is bundled into a task that not just holds the composed message but also the state of the delivery. + +## Delivery methods + +Depending on the configuration and the availability of store client protocol + actual configured and/or discovered store nodes: +- P2PReliability validation - checking network store node whether the message is reached at least a store node. +- Simple retry until message is propagated to the network + - Relay says >0 peers as publish result + - LightpushClient returns with success + +Depending on node config: +- Relay +- Lightpush + +These methods are used in combination to achieve the best reliability. +Fallback mechanism is used to switch between methods if the current one fails. + +Relay+StoreCheck -> Relay+simple retry -> Lightpush+StoreCheck -> Lightpush simple retry -> Error + +Combination is dynamically chosen on node configuration. Levels can be skipped depending on actual connectivity. +Actual connectivity is checked: +- Relay's topic health check - at least dLow peers in the mesh for the topic +- Store nodes availability - at least one store service node is available in peer manager +- Lightpush client availability - at least one lightpush service node is available in peer manager + +## Delivery processing + +At every send request, each task is tried to be delivered right away. +Any further retries and store check is done as a background task in a loop with predefined intervals. +Each task is set for a maximum number of retries and/or maximum time to live. + +In each round of store check and retry send tasks are selected based on their state. +The state is updated based on the result of the delivery method. diff --git a/waku/api/types.nim b/waku/api/types.nim new file mode 100644 index 000000000..a0626e98c --- /dev/null +++ b/waku/api/types.nim @@ -0,0 +1,65 @@ +{.push raises: [].} + +import bearssl/rand, std/times, chronos +import stew/byteutils +import waku/utils/requests as request_utils +import waku/waku_core/[topics/content_topic, message/message, time] +import waku/requests/requests + +type + MessageEnvelope* = object + contentTopic*: ContentTopic + payload*: seq[byte] + ephemeral*: bool + + RequestId* = distinct string + + NodeHealth* {.pure.} = enum + Healthy + MinimallyHealthy + Unhealthy + +proc new*(T: typedesc[RequestId], rng: ref HmacDrbgContext): T = + ## Generate a new RequestId using the provided RNG. + RequestId(request_utils.generateRequestId(rng)) + +proc `$`*(r: RequestId): string {.inline.} = + string(r) + +proc `==`*(a, b: RequestId): bool {.inline.} = + string(a) == string(b) + +proc init*( + T: type MessageEnvelope, + contentTopic: ContentTopic, + payload: seq[byte] | string, + ephemeral: bool = false, +): MessageEnvelope = + when payload is seq[byte]: + MessageEnvelope(contentTopic: contentTopic, payload: payload, ephemeral: ephemeral) + else: + MessageEnvelope( + contentTopic: contentTopic, payload: payload.toBytes(), ephemeral: ephemeral + ) + +proc toWakuMessage*(envelope: MessageEnvelope): WakuMessage = + ## Convert a MessageEnvelope to a WakuMessage. + var wm = WakuMessage( + contentTopic: envelope.contentTopic, + payload: envelope.payload, + ephemeral: envelope.ephemeral, + timestamp: getNowInNanosecondTime(), + ) + + ## TODO: First find out if proof is needed at all + ## Follow up: left it to the send logic to add RLN proof if needed and possible + # let requestedProof = ( + # waitFor RequestGenerateRlnProof.request(wm, getTime().toUnixFloat()) + # ).valueOr: + # warn "Failed to add RLN proof to WakuMessage: ", error = error + # return wm + + # wm.proof = requestedProof.proof + return wm + +{.pop.} diff --git a/waku/events/delivery_events.nim b/waku/events/delivery_events.nim new file mode 100644 index 000000000..f8eb0f48d --- /dev/null +++ b/waku/events/delivery_events.nim @@ -0,0 +1,27 @@ +import waku/waku_core/[message/message, message/digest], waku/common/broker/event_broker + +type DeliveryDirection* {.pure.} = enum + PUBLISHING + RECEIVING + +type DeliverySuccess* {.pure.} = enum + SUCCESSFUL + UNSUCCESSFUL + +EventBroker: + type DeliveryFeedbackEvent* = ref object + success*: DeliverySuccess + dir*: DeliveryDirection + comment*: string + msgHash*: WakuMessageHash + msg*: WakuMessage + +EventBroker: + type OnFilterSubscribeEvent* = object + pubsubTopic*: string + contentTopics*: seq[string] + +EventBroker: + type OnFilterUnSubscribeEvent* = object + pubsubTopic*: string + contentTopics*: seq[string] diff --git a/waku/events/events.nim b/waku/events/events.nim new file mode 100644 index 000000000..2a0af8828 --- /dev/null +++ b/waku/events/events.nim @@ -0,0 +1,3 @@ +import ./[message_events, delivery_events] + +export message_events, delivery_events diff --git a/waku/events/message_events.nim b/waku/events/message_events.nim new file mode 100644 index 000000000..cf3dac9b7 --- /dev/null +++ b/waku/events/message_events.nim @@ -0,0 +1,30 @@ +import waku/common/broker/event_broker +import waku/api/types +import waku/waku_core/message + +export types + +EventBroker: + # Event emitted when a message is sent to the network + type MessageSentEvent* = object + requestId*: RequestId + messageHash*: string + +EventBroker: + # Event emitted when a message send operation fails + type MessageErrorEvent* = object + requestId*: RequestId + messageHash*: string + error*: string + +EventBroker: + # Confirmation that a message has been correctly delivered to some neighbouring nodes. + type MessagePropagatedEvent* = object + requestId*: RequestId + messageHash*: string + +EventBroker: + # Event emitted when a message is received via Waku + type MessageReceivedEvent* = object + messageHash*: string + message*: WakuMessage diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index d55206f97..c452d44c5 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -25,7 +25,7 @@ import ../node/peer_manager, ../node/health_monitor, ../node/waku_metrics, - ../node/delivery_monitor/delivery_monitor, + ../node/delivery_service/delivery_service, ../rest_api/message_cache, ../rest_api/endpoint/server, ../rest_api/endpoint/builder as rest_server_builder, @@ -42,7 +42,10 @@ import ../factory/internal_config, ../factory/app_callbacks, ../waku_enr/multiaddr, - ./waku_conf + ./waku_conf, + ../common/broker/broker_context, + ../requests/health_request, + ../api/types logScope: topics = "wakunode waku" @@ -66,12 +69,14 @@ type Waku* = ref object healthMonitor*: NodeHealthMonitor - deliveryMonitor: DeliveryMonitor + deliveryService*: DeliveryService restServer*: WakuRestServerRef metricsServer*: MetricsHttpServerRef appCallbacks*: AppCallbacks + brokerCtx*: BrokerContext + func version*(waku: Waku): string = waku.version @@ -160,6 +165,7 @@ proc new*( T: type Waku, wakuConf: WakuConf, appCallbacks: AppCallbacks = nil ): Future[Result[Waku, string]] {.async.} = let rng = crypto.newRng() + let brokerCtx = globalBrokerContext() logging.setupLog(wakuConf.logLevel, wakuConf.logFormat) @@ -197,16 +203,8 @@ proc new*( return err("Failed setting up app callbacks: " & $error) ## Delivery Monitor - var deliveryMonitor: DeliveryMonitor - if wakuConf.p2pReliability: - if wakuConf.remoteStoreNode.isNone(): - return err("A storenode should be set when reliability mode is on") - - let deliveryMonitor = DeliveryMonitor.new( - node.wakuStoreClient, node.wakuRelay, node.wakuLightpushClient, - node.wakuFilterClient, - ).valueOr: - return err("could not create delivery monitor: " & $error) + let deliveryService = DeliveryService.new(wakuConf.p2pReliability, node).valueOr: + return err("could not create delivery service: " & $error) var waku = Waku( version: git_version, @@ -215,9 +213,10 @@ proc new*( key: wakuConf.nodeKey, node: node, healthMonitor: healthMonitor, - deliveryMonitor: deliveryMonitor, + deliveryService: deliveryService, appCallbacks: appCallbacks, restServer: restServer, + brokerCtx: brokerCtx, ) waku.setupSwitchServices(wakuConf, relay, rng) @@ -353,7 +352,7 @@ proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} = error "failed to connect to dynamic bootstrap nodes: " & getCurrentExceptionMsg() return -proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async.} = +proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: []).} = if waku[].node.started: warn "startWaku: waku node already started" return ok() @@ -363,9 +362,15 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async.} = if conf.dnsDiscoveryConf.isSome(): let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get() - let dynamicBootstrapNodesRes = await waku_dnsdisc.retrieveDynamicBootstrapNodes( - dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers - ) + let dynamicBootstrapNodesRes = + try: + await waku_dnsdisc.retrieveDynamicBootstrapNodes( + dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers + ) + except CatchableError as exc: + Result[seq[RemotePeerInfo], string].err( + "Retrieving dynamic bootstrap nodes failed: " & exc.msg + ) if dynamicBootstrapNodesRes.isErr(): error "Retrieving dynamic bootstrap nodes failed", @@ -379,8 +384,11 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async.} = return err("error while calling startNode: " & $error) ## Update waku data that is set dynamically on node start - (await updateWaku(waku)).isOkOr: - return err("Error in updateApp: " & $error) + try: + (await updateWaku(waku)).isOkOr: + return err("Error in updateApp: " & $error) + except CatchableError: + return err("Caught exception in updateApp: " & getCurrentExceptionMsg()) ## Discv5 if conf.discv5Conf.isSome(): @@ -400,13 +408,68 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async.} = return err("failed to start waku discovery v5: " & $error) ## Reliability - if not waku[].deliveryMonitor.isNil(): - waku[].deliveryMonitor.startDeliveryMonitor() + if not waku[].deliveryService.isNil(): + waku[].deliveryService.startDeliveryService() ## Health Monitor waku[].healthMonitor.startHealthMonitor().isOkOr: return err("failed to start health monitor: " & $error) + ## Setup RequestNodeHealth provider + + RequestNodeHealth.setProvider( + globalBrokerContext(), + proc(): Result[RequestNodeHealth, string] = + let healthReportFut = waku[].healthMonitor.getNodeHealthReport() + if not healthReportFut.completed(): + return err("Health report not available") + try: + let healthReport = healthReportFut.read() + + # Check if Relay or Lightpush Client is ready (MinimallyHealthy condition) + var relayReady = false + var lightpushClientReady = false + var storeClientReady = false + var filterClientReady = false + + for protocolHealth in healthReport.protocolsHealth: + if protocolHealth.protocol == "Relay" and + protocolHealth.health == HealthStatus.READY: + relayReady = true + elif protocolHealth.protocol == "Lightpush Client" and + protocolHealth.health == HealthStatus.READY: + lightpushClientReady = true + elif protocolHealth.protocol == "Store Client" and + protocolHealth.health == HealthStatus.READY: + storeClientReady = true + elif protocolHealth.protocol == "Filter Client" and + protocolHealth.health == HealthStatus.READY: + filterClientReady = true + + # Determine node health based on protocol states + let isMinimallyHealthy = relayReady or lightpushClientReady + let nodeHealth = + if isMinimallyHealthy and storeClientReady and filterClientReady: + NodeHealth.Healthy + elif isMinimallyHealthy: + NodeHealth.MinimallyHealthy + else: + NodeHealth.Unhealthy + + debug "Providing health report", + nodeHealth = $nodeHealth, + relayReady = relayReady, + lightpushClientReady = lightpushClientReady, + storeClientReady = storeClientReady, + filterClientReady = filterClientReady, + details = $(healthReport) + + return ok(RequestNodeHealth(healthStatus: nodeHealth)) + except CatchableError as exc: + err("Failed to read health report: " & exc.msg), + ).isOkOr: + error "Failed to set RequestNodeHealth provider", error = error + if conf.restServerConf.isSome(): rest_server_builder.startRestServerProtocolSupport( waku[].restServer, @@ -422,41 +485,65 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async.} = return err ("Starting protocols support REST server failed: " & $error) if conf.metricsServerConf.isSome(): - waku[].metricsServer = ( - await ( - waku_metrics.startMetricsServerAndLogging( - conf.metricsServerConf.get(), conf.portsShift + try: + waku[].metricsServer = ( + await ( + waku_metrics.startMetricsServerAndLogging( + conf.metricsServerConf.get(), conf.portsShift + ) ) + ).valueOr: + return err("Starting monitoring and external interfaces failed: " & error) + except CatchableError: + return err( + "Caught exception starting monitoring and external interfaces failed: " & + getCurrentExceptionMsg() ) - ).valueOr: - return err("Starting monitoring and external interfaces failed: " & error) - waku[].healthMonitor.setOverallHealth(HealthStatus.READY) return ok() -proc stop*(waku: Waku): Future[void] {.async: (raises: [Exception]).} = +proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} = ## Waku shutdown if not waku.node.started: warn "stop: attempting to stop node that isn't running" - waku.healthMonitor.setOverallHealth(HealthStatus.SHUTTING_DOWN) + try: + waku.healthMonitor.setOverallHealth(HealthStatus.SHUTTING_DOWN) - if not waku.metricsServer.isNil(): - await waku.metricsServer.stop() + if not waku.metricsServer.isNil(): + await waku.metricsServer.stop() - if not waku.wakuDiscv5.isNil(): - await waku.wakuDiscv5.stop() + if not waku.wakuDiscv5.isNil(): + await waku.wakuDiscv5.stop() - if not waku.node.isNil(): - await waku.node.stop() + if not waku.node.isNil(): + await waku.node.stop() - if not waku.dnsRetryLoopHandle.isNil(): - await waku.dnsRetryLoopHandle.cancelAndWait() + if not waku.dnsRetryLoopHandle.isNil(): + await waku.dnsRetryLoopHandle.cancelAndWait() - if not waku.healthMonitor.isNil(): - await waku.healthMonitor.stopHealthMonitor() + if not waku.healthMonitor.isNil(): + await waku.healthMonitor.stopHealthMonitor() - if not waku.restServer.isNil(): - await waku.restServer.stop() + ## Clear RequestNodeHealth provider + RequestNodeHealth.clearProvider(waku.brokerCtx) + + if not waku.restServer.isNil(): + await waku.restServer.stop() + except Exception: + error "waku stop failed: " & getCurrentExceptionMsg() + return err("waku stop failed: " & getCurrentExceptionMsg()) + + return ok() + +proc isModeCoreAvailable*(waku: Waku): bool = + return not waku.node.wakuRelay.isNil() + +proc isModeEdgeAvailable*(waku: Waku): bool = + return + waku.node.wakuRelay.isNil() and not waku.node.wakuStoreClient.isNil() and + not waku.node.wakuFilterClient.isNil() and not waku.node.wakuLightPushClient.isNil() + +{.pop.} diff --git a/waku/node/delivery_monitor/delivery_callback.nim b/waku/node/delivery_monitor/delivery_callback.nim deleted file mode 100644 index c996bc7b0..000000000 --- a/waku/node/delivery_monitor/delivery_callback.nim +++ /dev/null @@ -1,17 +0,0 @@ -import ../../waku_core - -type DeliveryDirection* {.pure.} = enum - PUBLISHING - RECEIVING - -type DeliverySuccess* {.pure.} = enum - SUCCESSFUL - UNSUCCESSFUL - -type DeliveryFeedbackCallback* = proc( - success: DeliverySuccess, - dir: DeliveryDirection, - comment: string, - msgHash: WakuMessageHash, - msg: WakuMessage, -) {.gcsafe, raises: [].} diff --git a/waku/node/delivery_monitor/delivery_monitor.nim b/waku/node/delivery_monitor/delivery_monitor.nim deleted file mode 100644 index 4dda542cc..000000000 --- a/waku/node/delivery_monitor/delivery_monitor.nim +++ /dev/null @@ -1,43 +0,0 @@ -## This module helps to ensure the correct transmission and reception of messages - -import results -import chronos -import - ./recv_monitor, - ./send_monitor, - ./delivery_callback, - ../../waku_core, - ../../waku_store/client, - ../../waku_relay/protocol, - ../../waku_lightpush/client, - ../../waku_filter_v2/client - -type DeliveryMonitor* = ref object - sendMonitor: SendMonitor - recvMonitor: RecvMonitor - -proc new*( - T: type DeliveryMonitor, - storeClient: WakuStoreClient, - wakuRelay: protocol.WakuRelay, - wakuLightpushClient: WakuLightpushClient, - wakuFilterClient: WakuFilterClient, -): Result[T, string] = - ## storeClient is needed to give store visitility to DeliveryMonitor - ## wakuRelay and wakuLightpushClient are needed to give a mechanism to SendMonitor to re-publish - let sendMonitor = ?SendMonitor.new(storeClient, wakuRelay, wakuLightpushClient) - let recvMonitor = RecvMonitor.new(storeClient, wakuFilterClient) - return ok(DeliveryMonitor(sendMonitor: sendMonitor, recvMonitor: recvMonitor)) - -proc startDeliveryMonitor*(self: DeliveryMonitor) = - self.sendMonitor.startSendMonitor() - self.recvMonitor.startRecvMonitor() - -proc stopDeliveryMonitor*(self: DeliveryMonitor) {.async.} = - self.sendMonitor.stopSendMonitor() - await self.recvMonitor.stopRecvMonitor() - -proc setDeliveryCallback*(self: DeliveryMonitor, deliveryCb: DeliveryFeedbackCallback) = - ## The deliveryCb is a proc defined by the api client so that it can get delivery feedback - self.sendMonitor.setDeliveryCallback(deliveryCb) - self.recvMonitor.setDeliveryCallback(deliveryCb) diff --git a/waku/node/delivery_monitor/publish_observer.nim b/waku/node/delivery_monitor/publish_observer.nim deleted file mode 100644 index 1f517f8bd..000000000 --- a/waku/node/delivery_monitor/publish_observer.nim +++ /dev/null @@ -1,9 +0,0 @@ -import chronicles -import ../../waku_core/message/message - -type PublishObserver* = ref object of RootObj - -method onMessagePublished*( - self: PublishObserver, pubsubTopic: string, message: WakuMessage -) {.base, gcsafe, raises: [].} = - error "onMessagePublished not implemented" diff --git a/waku/node/delivery_monitor/send_monitor.nim b/waku/node/delivery_monitor/send_monitor.nim deleted file mode 100644 index 15b16065f..000000000 --- a/waku/node/delivery_monitor/send_monitor.nim +++ /dev/null @@ -1,212 +0,0 @@ -## This module reinforces the publish operation with regular store-v3 requests. -## - -import std/[sequtils, tables] -import chronos, chronicles, libp2p/utility -import - ./delivery_callback, - ./publish_observer, - ../../waku_core, - ./not_delivered_storage/not_delivered_storage, - ../../waku_store/[client, common], - ../../waku_archive/archive, - ../../waku_relay/protocol, - ../../waku_lightpush/client - -const MaxTimeInCache* = chronos.minutes(1) - ## Messages older than this time will get completely forgotten on publication and a - ## feedback will be given when that happens - -const SendCheckInterval* = chronos.seconds(3) - ## Interval at which we check that messages have been properly received by a store node - -const MaxMessagesToCheckAtOnce = 100 - ## Max number of messages to check if they were properly archived by a store node - -const ArchiveTime = chronos.seconds(3) - ## Estimation of the time we wait until we start confirming that a message has been properly - ## received and archived by a store node - -type DeliveryInfo = object - pubsubTopic: string - msg: WakuMessage - -type SendMonitor* = ref object of PublishObserver - publishedMessages: Table[WakuMessageHash, DeliveryInfo] - ## Cache that contains the delivery info per message hash. - ## This is needed to make sure the published messages are properly published - - msgStoredCheckerHandle: Future[void] ## handle that allows to stop the async task - - notDeliveredStorage: NotDeliveredStorage - ## NOTE: this is not fully used because that might be tackled by higher abstraction layers - - storeClient: WakuStoreClient - deliveryCb: DeliveryFeedbackCallback - - wakuRelay: protocol.WakuRelay - wakuLightpushClient: WakuLightPushClient - -proc new*( - T: type SendMonitor, - storeClient: WakuStoreClient, - wakuRelay: protocol.WakuRelay, - wakuLightpushClient: WakuLightPushClient, -): Result[T, string] = - if wakuRelay.isNil() and wakuLightpushClient.isNil(): - return err( - "Could not create SendMonitor. wakuRelay or wakuLightpushClient should be set" - ) - - let notDeliveredStorage = ?NotDeliveredStorage.new() - - let sendMonitor = SendMonitor( - notDeliveredStorage: notDeliveredStorage, - storeClient: storeClient, - wakuRelay: wakuRelay, - wakuLightpushClient: wakuLightPushClient, - ) - - if not wakuRelay.isNil(): - wakuRelay.addPublishObserver(sendMonitor) - - if not wakuLightpushClient.isNil(): - wakuLightpushClient.addPublishObserver(sendMonitor) - - return ok(sendMonitor) - -proc performFeedbackAndCleanup( - self: SendMonitor, - msgsToDiscard: Table[WakuMessageHash, DeliveryInfo], - success: DeliverySuccess, - dir: DeliveryDirection, - comment: string, -) = - ## This procs allows to bring delivery feedback to the API client - ## It requires a 'deliveryCb' to be registered beforehand. - if self.deliveryCb.isNil(): - error "deliveryCb is nil in performFeedbackAndCleanup", - success, dir, comment, hashes = toSeq(msgsToDiscard.keys).mapIt(shortLog(it)) - return - - for hash, deliveryInfo in msgsToDiscard: - info "send monitor performFeedbackAndCleanup", - success, dir, comment, msg_hash = shortLog(hash) - - self.deliveryCb(success, dir, comment, hash, deliveryInfo.msg) - self.publishedMessages.del(hash) - -proc checkMsgsInStore( - self: SendMonitor, msgsToValidate: Table[WakuMessageHash, DeliveryInfo] -): Future[ - Result[ - tuple[ - publishedCorrectly: Table[WakuMessageHash, DeliveryInfo], - notYetPublished: Table[WakuMessageHash, DeliveryInfo], - ], - void, - ] -] {.async.} = - let hashesToValidate = toSeq(msgsToValidate.keys) - - let storeResp: StoreQueryResponse = ( - await self.storeClient.queryToAny( - StoreQueryRequest(includeData: false, messageHashes: hashesToValidate) - ) - ).valueOr: - error "checkMsgsInStore failed to get remote msgHashes", - hashes = hashesToValidate.mapIt(shortLog(it)), error = $error - return err() - - let publishedHashes = storeResp.messages.mapIt(it.messageHash) - - var notYetPublished: Table[WakuMessageHash, DeliveryInfo] - var publishedCorrectly: Table[WakuMessageHash, DeliveryInfo] - - for msgHash, deliveryInfo in msgsToValidate.pairs: - if publishedHashes.contains(msgHash): - publishedCorrectly[msgHash] = deliveryInfo - self.publishedMessages.del(msgHash) ## we will no longer track that message - else: - notYetPublished[msgHash] = deliveryInfo - - return ok((publishedCorrectly: publishedCorrectly, notYetPublished: notYetPublished)) - -proc processMessages(self: SendMonitor) {.async.} = - var msgsToValidate: Table[WakuMessageHash, DeliveryInfo] - var msgsToDiscard: Table[WakuMessageHash, DeliveryInfo] - - let now = getNowInNanosecondTime() - let timeToCheckThreshold = now - ArchiveTime.nanos - let maxLifeTime = now - MaxTimeInCache.nanos - - for hash, deliveryInfo in self.publishedMessages.pairs: - if deliveryInfo.msg.timestamp < maxLifeTime: - ## message is too old - msgsToDiscard[hash] = deliveryInfo - - if deliveryInfo.msg.timestamp < timeToCheckThreshold: - msgsToValidate[hash] = deliveryInfo - - ## Discard the messages that are too old - self.performFeedbackAndCleanup( - msgsToDiscard, DeliverySuccess.UNSUCCESSFUL, DeliveryDirection.PUBLISHING, - "Could not publish messages. Please try again.", - ) - - let (publishedCorrectly, notYetPublished) = ( - await self.checkMsgsInStore(msgsToValidate) - ).valueOr: - return ## the error log is printed in checkMsgsInStore - - ## Give positive feedback for the correctly published messages - self.performFeedbackAndCleanup( - publishedCorrectly, DeliverySuccess.SUCCESSFUL, DeliveryDirection.PUBLISHING, - "messages published correctly", - ) - - ## Try to publish again - for msgHash, deliveryInfo in notYetPublished.pairs: - let pubsubTopic = deliveryInfo.pubsubTopic - let msg = deliveryInfo.msg - if not self.wakuRelay.isNil(): - info "trying to publish again with wakuRelay", msgHash, pubsubTopic - (await self.wakuRelay.publish(pubsubTopic, msg)).isOkOr: - error "could not publish with wakuRelay.publish", - msgHash, pubsubTopic, error = $error - continue - - if not self.wakuLightpushClient.isNil(): - info "trying to publish again with wakuLightpushClient", msgHash, pubsubTopic - (await self.wakuLightpushClient.publishToAny(pubsubTopic, msg)).isOkOr: - error "could not publish with publishToAny", error = $error - continue - -proc checkIfMessagesStored(self: SendMonitor) {.async.} = - ## Continuously monitors that the sent messages have been received by a store node - while true: - await self.processMessages() - await sleepAsync(SendCheckInterval) - -method onMessagePublished( - self: SendMonitor, pubsubTopic: string, msg: WakuMessage -) {.gcsafe, raises: [].} = - ## Implementation of the PublishObserver interface. - ## - ## When publishing a message either through relay or lightpush, we want to add some extra effort - ## to make sure it is received to one store node. Hence, keep track of those published messages. - - info "onMessagePublished" - let msgHash = computeMessageHash(pubSubTopic, msg) - - if not self.publishedMessages.hasKey(msgHash): - self.publishedMessages[msgHash] = DeliveryInfo(pubsubTopic: pubsubTopic, msg: msg) - -proc startSendMonitor*(self: SendMonitor) = - self.msgStoredCheckerHandle = self.checkIfMessagesStored() - -proc stopSendMonitor*(self: SendMonitor) = - discard self.msgStoredCheckerHandle.cancelAndWait() - -proc setDeliveryCallback*(self: SendMonitor, deliveryCb: DeliveryFeedbackCallback) = - self.deliveryCb = deliveryCb diff --git a/waku/node/delivery_monitor/subscriptions_observer.nim b/waku/node/delivery_monitor/subscriptions_observer.nim deleted file mode 100644 index 800117ae9..000000000 --- a/waku/node/delivery_monitor/subscriptions_observer.nim +++ /dev/null @@ -1,13 +0,0 @@ -import chronicles - -type SubscriptionObserver* = ref object of RootObj - -method onSubscribe*( - self: SubscriptionObserver, pubsubTopic: string, contentTopics: seq[string] -) {.base, gcsafe, raises: [].} = - error "onSubscribe not implemented" - -method onUnsubscribe*( - self: SubscriptionObserver, pubsubTopic: string, contentTopics: seq[string] -) {.base, gcsafe, raises: [].} = - error "onUnsubscribe not implemented" diff --git a/waku/node/delivery_service/delivery_service.nim b/waku/node/delivery_service/delivery_service.nim new file mode 100644 index 000000000..8106cba9f --- /dev/null +++ b/waku/node/delivery_service/delivery_service.nim @@ -0,0 +1,46 @@ +## This module helps to ensure the correct transmission and reception of messages + +import results +import chronos +import + ./recv_service, + ./send_service, + ./subscription_service, + waku/[ + waku_core, + waku_node, + waku_store/client, + waku_relay/protocol, + waku_lightpush/client, + waku_filter_v2/client, + ] + +type DeliveryService* = ref object + sendService*: SendService + recvService: RecvService + subscriptionService*: SubscriptionService + +proc new*( + T: type DeliveryService, useP2PReliability: bool, w: WakuNode +): Result[T, string] = + ## storeClient is needed to give store visitility to DeliveryService + ## wakuRelay and wakuLightpushClient are needed to give a mechanism to SendService to re-publish + let subscriptionService = SubscriptionService.new(w) + let sendService = ?SendService.new(useP2PReliability, w, subscriptionService) + let recvService = RecvService.new(w, subscriptionService) + + return ok( + DeliveryService( + sendService: sendService, + recvService: recvService, + subscriptionService: subscriptionService, + ) + ) + +proc startDeliveryService*(self: DeliveryService) = + self.sendService.startSendService() + self.recvService.startRecvService() + +proc stopDeliveryService*(self: DeliveryService) {.async.} = + self.sendService.stopSendService() + await self.recvService.stopRecvService() diff --git a/waku/node/delivery_monitor/not_delivered_storage/migrations.nim b/waku/node/delivery_service/not_delivered_storage/migrations.nim similarity index 95% rename from waku/node/delivery_monitor/not_delivered_storage/migrations.nim rename to waku/node/delivery_service/not_delivered_storage/migrations.nim index 8175aea62..807074d64 100644 --- a/waku/node/delivery_monitor/not_delivered_storage/migrations.nim +++ b/waku/node/delivery_service/not_delivered_storage/migrations.nim @@ -4,7 +4,7 @@ import std/[tables, strutils, os], results, chronicles import ../../../common/databases/db_sqlite, ../../../common/databases/common logScope: - topics = "waku node delivery_monitor" + topics = "waku node delivery_service" const TargetSchemaVersion* = 1 # increase this when there is an update in the database schema diff --git a/waku/node/delivery_monitor/not_delivered_storage/not_delivered_storage.nim b/waku/node/delivery_service/not_delivered_storage/not_delivered_storage.nim similarity index 93% rename from waku/node/delivery_monitor/not_delivered_storage/not_delivered_storage.nim rename to waku/node/delivery_service/not_delivered_storage/not_delivered_storage.nim index 85611310b..b0f5f5828 100644 --- a/waku/node/delivery_monitor/not_delivered_storage/not_delivered_storage.nim +++ b/waku/node/delivery_service/not_delivered_storage/not_delivered_storage.nim @@ -1,17 +1,17 @@ ## This module is aimed to keep track of the sent/published messages that are considered ## not being properly delivered. -## +## ## The archiving of such messages will happen in a local sqlite database. -## +## ## In the very first approach, we consider that a message is sent properly is it has been ## received by any store node. -## +## import results import ../../../common/databases/db_sqlite, ../../../waku_core/message/message, - ../../../node/delivery_monitor/not_delivered_storage/migrations + ../../../node/delivery_service/not_delivered_storage/migrations const NotDeliveredMessagesDbUrl = "not-delivered-messages.db" diff --git a/waku/node/delivery_service/recv_service.nim b/waku/node/delivery_service/recv_service.nim new file mode 100644 index 000000000..c4dcf4fef --- /dev/null +++ b/waku/node/delivery_service/recv_service.nim @@ -0,0 +1,3 @@ +import ./recv_service/recv_service + +export recv_service diff --git a/waku/node/delivery_monitor/recv_monitor.nim b/waku/node/delivery_service/recv_service/recv_service.nim similarity index 67% rename from waku/node/delivery_monitor/recv_monitor.nim rename to waku/node/delivery_service/recv_service/recv_service.nim index 6ea35d301..12780033a 100644 --- a/waku/node/delivery_monitor/recv_monitor.nim +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -4,13 +4,18 @@ import std/[tables, sequtils, options] import chronos, chronicles, libp2p/utility +import ../[subscription_service] import - ../../waku_core, - ./delivery_callback, - ./subscriptions_observer, - ../../waku_store/[client, common], - ../../waku_filter_v2/client, - ../../waku_core/topics + waku/[ + waku_core, + waku_store/client, + waku_store/common, + waku_filter_v2/client, + waku_core/topics, + events/delivery_events, + waku_node, + common/broker/broker_context, + ] const StoreCheckPeriod = chronos.minutes(5) ## How often to perform store queries @@ -28,14 +33,16 @@ type RecvMessage = object rxTime: Timestamp ## timestamp of the rx message. We will not keep the rx messages forever -type RecvMonitor* = ref object of SubscriptionObserver +type RecvService* = ref object of RootObj + brokerCtx: BrokerContext topicsInterest: Table[PubsubTopic, seq[ContentTopic]] ## Tracks message verification requests and when was the last time a ## pubsub topic was verified for missing messages ## The key contains pubsub-topics - - storeClient: WakuStoreClient - deliveryCb: DeliveryFeedbackCallback + node: WakuNode + onSubscribeListener: OnFilterSubscribeEventListener + onUnsubscribeListener: OnFilterUnsubscribeEventListener + subscriptionService: SubscriptionService recentReceivedMsgs: seq[RecvMessage] @@ -46,10 +53,10 @@ type RecvMonitor* = ref object of SubscriptionObserver endTimeToCheck: Timestamp proc getMissingMsgsFromStore( - self: RecvMonitor, msgHashes: seq[WakuMessageHash] + self: RecvService, msgHashes: seq[WakuMessageHash] ): Future[Result[seq[TupleHashAndMsg], string]] {.async.} = let storeResp: StoreQueryResponse = ( - await self.storeClient.queryToAny( + await self.node.wakuStoreClient.queryToAny( StoreQueryRequest(includeData: true, messageHashes: msgHashes) ) ).valueOr: @@ -62,35 +69,35 @@ proc getMissingMsgsFromStore( ) proc performDeliveryFeedback( - self: RecvMonitor, + self: RecvService, success: DeliverySuccess, dir: DeliveryDirection, comment: string, msgHash: WakuMessageHash, msg: WakuMessage, ) {.gcsafe, raises: [].} = - ## This procs allows to bring delivery feedback to the API client - ## It requires a 'deliveryCb' to be registered beforehand. - if self.deliveryCb.isNil(): - error "deliveryCb is nil in performDeliveryFeedback", - success, dir, comment, msg_hash - return - info "recv monitor performDeliveryFeedback", success, dir, comment, msg_hash = shortLog(msgHash) - self.deliveryCb(success, dir, comment, msgHash, msg) -proc msgChecker(self: RecvMonitor) {.async.} = + DeliveryFeedbackEvent.emit( + brokerCtx = self.brokerCtx, + success = success, + dir = dir, + comment = comment, + msgHash = msgHash, + msg = msg, + ) + +proc msgChecker(self: RecvService) {.async.} = ## Continuously checks if a message has been received while true: await sleepAsync(StoreCheckPeriod) - self.endTimeToCheck = getNowInNanosecondTime() var msgHashesInStore = newSeq[WakuMessageHash](0) for pubsubTopic, cTopics in self.topicsInterest.pairs: let storeResp: StoreQueryResponse = ( - await self.storeClient.queryToAny( + await self.node.wakuStoreClient.queryToAny( StoreQueryRequest( includeData: false, pubsubTopic: some(PubsubTopic(pubsubTopic)), @@ -126,8 +133,8 @@ proc msgChecker(self: RecvMonitor) {.async.} = ## update next check times self.startTimeToCheck = self.endTimeToCheck -method onSubscribe( - self: RecvMonitor, pubsubTopic: string, contentTopics: seq[string] +proc onSubscribe( + self: RecvService, pubsubTopic: string, contentTopics: seq[string] ) {.gcsafe, raises: [].} = info "onSubscribe", pubsubTopic, contentTopics self.topicsInterest.withValue(pubsubTopic, contentTopicsOfInterest): @@ -135,8 +142,8 @@ method onSubscribe( do: self.topicsInterest[pubsubTopic] = contentTopics -method onUnsubscribe( - self: RecvMonitor, pubsubTopic: string, contentTopics: seq[string] +proc onUnsubscribe( + self: RecvService, pubsubTopic: string, contentTopics: seq[string] ) {.gcsafe, raises: [].} = info "onUnsubscribe", pubsubTopic, contentTopics @@ -150,47 +157,63 @@ method onUnsubscribe( do: error "onUnsubscribe unsubscribing from wrong topic", pubsubTopic, contentTopics -proc new*( - T: type RecvMonitor, - storeClient: WakuStoreClient, - wakuFilterClient: WakuFilterClient, -): T = +proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionService): T = ## The storeClient will help to acquire any possible missed messages let now = getNowInNanosecondTime() - var recvMonitor = RecvMonitor(storeClient: storeClient, startTimeToCheck: now) - - if not wakuFilterClient.isNil(): - wakuFilterClient.addSubscrObserver(recvMonitor) + var recvService = RecvService( + node: node, + startTimeToCheck: now, + brokerCtx: node.brokerCtx, + subscriptionService: s, + topicsInterest: initTable[PubsubTopic, seq[ContentTopic]](), + recentReceivedMsgs: @[], + ) + if not node.wakuFilterClient.isNil(): let filterPushHandler = proc( pubsubTopic: PubsubTopic, message: WakuMessage ) {.async, closure.} = - ## Captures all the messages recived through filter + ## Captures all the messages received through filter let msgHash = computeMessageHash(pubSubTopic, message) let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp) - recvMonitor.recentReceivedMsgs.add(rxMsg) + recvService.recentReceivedMsgs.add(rxMsg) - wakuFilterClient.registerPushHandler(filterPushHandler) + node.wakuFilterClient.registerPushHandler(filterPushHandler) - return recvMonitor + return recvService -proc loopPruneOldMessages(self: RecvMonitor) {.async.} = +proc loopPruneOldMessages(self: RecvService) {.async.} = while true: let oldestAllowedTime = getNowInNanosecondTime() - MaxMessageLife.nanos self.recentReceivedMsgs.keepItIf(it.rxTime > oldestAllowedTime) await sleepAsync(PruneOldMsgsPeriod) -proc startRecvMonitor*(self: RecvMonitor) = +proc startRecvService*(self: RecvService) = self.msgCheckerHandler = self.msgChecker() self.msgPrunerHandler = self.loopPruneOldMessages() -proc stopRecvMonitor*(self: RecvMonitor) {.async.} = + self.onSubscribeListener = OnFilterSubscribeEvent.listen( + self.brokerCtx, + proc(subsEv: OnFilterSubscribeEvent) {.async: (raises: []).} = + self.onSubscribe(subsEv.pubsubTopic, subsEv.contentTopics), + ).valueOr: + error "Failed to set OnFilterSubscribeEvent listener", error = error + quit(QuitFailure) + + self.onUnsubscribeListener = OnFilterUnsubscribeEvent.listen( + self.brokerCtx, + proc(subsEv: OnFilterUnsubscribeEvent) {.async: (raises: []).} = + self.onUnsubscribe(subsEv.pubsubTopic, subsEv.contentTopics), + ).valueOr: + error "Failed to set OnFilterUnsubscribeEvent listener", error = error + quit(QuitFailure) + +proc stopRecvService*(self: RecvService) {.async.} = + OnFilterSubscribeEvent.dropListener(self.brokerCtx, self.onSubscribeListener) + OnFilterUnsubscribeEvent.dropListener(self.brokerCtx, self.onUnsubscribeListener) if not self.msgCheckerHandler.isNil(): await self.msgCheckerHandler.cancelAndWait() if not self.msgPrunerHandler.isNil(): await self.msgPrunerHandler.cancelAndWait() - -proc setDeliveryCallback*(self: RecvMonitor, deliveryCb: DeliveryFeedbackCallback) = - self.deliveryCb = deliveryCb diff --git a/waku/node/delivery_service/send_service.nim b/waku/node/delivery_service/send_service.nim new file mode 100644 index 000000000..de0dbf6a3 --- /dev/null +++ b/waku/node/delivery_service/send_service.nim @@ -0,0 +1,6 @@ +## This module reinforces the publish operation with regular store-v3 requests. +## + +import ./send_service/[send_service, delivery_task] + +export send_service, delivery_task diff --git a/waku/node/delivery_service/send_service/delivery_task.nim b/waku/node/delivery_service/send_service/delivery_task.nim new file mode 100644 index 000000000..0ff151f6e --- /dev/null +++ b/waku/node/delivery_service/send_service/delivery_task.nim @@ -0,0 +1,74 @@ +import std/[options, times], chronos +import waku/waku_core, waku/api/types, waku/requests/node_requests +import waku/common/broker/broker_context + +type DeliveryState* {.pure.} = enum + Entry + SuccessfullyPropagated + # message is known to be sent to the network but not yet validated + SuccessfullyValidated + # message is known to be stored at least on one store node, thus validated + FallbackRetry # retry sending with fallback processor if available + NextRoundRetry # try sending in next loop + FailedToDeliver # final state of failed delivery + +type DeliveryTask* = ref object + requestId*: RequestId + pubsubTopic*: PubsubTopic + msg*: WakuMessage + msgHash*: WakuMessageHash + tryCount*: int + state*: DeliveryState + deliveryTime*: Moment + propagateEventEmitted*: bool + errorDesc*: string + +proc new*( + T: typedesc[DeliveryTask], + requestId: RequestId, + envelop: MessageEnvelope, + brokerCtx: BrokerContext, +): Result[T, string] = + let msg = envelop.toWakuMessage() + # TODO: use sync request for such as soon as available + let relayShardRes = ( + RequestRelayShard.request(brokerCtx, none[PubsubTopic](), envelop.contentTopic) + ).valueOr: + error "RequestRelayShard.request failed", error = error + return err("Failed create DeliveryTask: " & $error) + + let pubsubTopic = relayShardRes.relayShard.toPubsubTopic() + let msgHash = computeMessageHash(pubsubTopic, msg) + + return ok( + T( + requestId: requestId, + pubsubTopic: pubsubTopic, + msg: msg, + msgHash: msgHash, + tryCount: 0, + state: DeliveryState.Entry, + ) + ) + +func `==`*(r, l: DeliveryTask): bool = + if r.isNil() == l.isNil(): + r.isNil() or r.msgHash == l.msgHash + else: + false + +proc messageAge*(self: DeliveryTask): timer.Duration = + let actual = getNanosecondTime(getTime().toUnixFloat()) + if self.msg.timestamp >= 0 and self.msg.timestamp < actual: + nanoseconds(actual - self.msg.timestamp) + else: + ZeroDuration + +proc deliveryAge*(self: DeliveryTask): timer.Duration = + if self.state == DeliveryState.SuccessfullyPropagated: + timer.Moment.now() - self.deliveryTime + else: + ZeroDuration + +proc isEphemeral*(self: DeliveryTask): bool = + return self.msg.ephemeral diff --git a/waku/node/delivery_service/send_service/lightpush_processor.nim b/waku/node/delivery_service/send_service/lightpush_processor.nim new file mode 100644 index 000000000..40a754757 --- /dev/null +++ b/waku/node/delivery_service/send_service/lightpush_processor.nim @@ -0,0 +1,81 @@ +import chronicles, chronos, results +import std/options + +import + waku/node/peer_manager, + waku/waku_core, + waku/waku_lightpush/[common, client, rpc], + waku/common/broker/broker_context + +import ./[delivery_task, send_processor] + +logScope: + topics = "send service lightpush processor" + +type LightpushSendProcessor* = ref object of BaseSendProcessor + peerManager: PeerManager + lightpushClient: WakuLightPushClient + +proc new*( + T: typedesc[LightpushSendProcessor], + peerManager: PeerManager, + lightpushClient: WakuLightPushClient, + brokerCtx: BrokerContext, +): T = + return + T(peerManager: peerManager, lightpushClient: lightpushClient, brokerCtx: brokerCtx) + +proc isLightpushPeerAvailable( + self: LightpushSendProcessor, pubsubTopic: PubsubTopic +): bool = + return self.peerManager.selectPeer(WakuLightPushCodec, some(pubsubTopic)).isSome() + +method isValidProcessor*( + self: LightpushSendProcessor, task: DeliveryTask +): bool {.gcsafe.} = + return self.isLightpushPeerAvailable(task.pubsubTopic) + +method sendImpl*( + self: LightpushSendProcessor, task: DeliveryTask +): Future[void] {.async.} = + task.tryCount.inc() + info "Trying message delivery via Lightpush", + requestId = task.requestId, + msgHash = task.msgHash.to0xHex(), + tryCount = task.tryCount + + let peer = self.peerManager.selectPeer(WakuLightPushCodec, some(task.pubsubTopic)).valueOr: + debug "No peer available for Lightpush, request pushed back for next round", + requestId = task.requestId + task.state = DeliveryState.NextRoundRetry + return + + let numLightpushServers = ( + await self.lightpushClient.publish(some(task.pubsubTopic), task.msg, peer) + ).valueOr: + error "LightpushSendProcessor.sendImpl failed", error = error.desc.get($error.code) + case error.code + of LightPushErrorCode.NO_PEERS_TO_RELAY, LightPushErrorCode.TOO_MANY_REQUESTS, + LightPushErrorCode.OUT_OF_RLN_PROOF, LightPushErrorCode.SERVICE_NOT_AVAILABLE, + LightPushErrorCode.INTERNAL_SERVER_ERROR: + task.state = DeliveryState.NextRoundRetry + else: + # the message is malformed, send error + task.state = DeliveryState.FailedToDeliver + task.errorDesc = error.desc.get($error.code) + task.deliveryTime = Moment.now() + return + + if numLightpushServers > 0: + info "Message propagated via Lightpush", + requestId = task.requestId, msgHash = task.msgHash.to0xHex() + task.state = DeliveryState.SuccessfullyPropagated + task.deliveryTime = Moment.now() + # TODO: with a simple retry processor it might be more accurate to say `Sent` + else: + # Controversial state, publish says ok but no peer. It should not happen. + debug "Lightpush publish returned zero peers, request pushed back for next round", + requestId = task.requestId + task.state = DeliveryState.NextRoundRetry + + return diff --git a/waku/node/delivery_service/send_service/relay_processor.nim b/waku/node/delivery_service/send_service/relay_processor.nim new file mode 100644 index 000000000..94cb63776 --- /dev/null +++ b/waku/node/delivery_service/send_service/relay_processor.nim @@ -0,0 +1,78 @@ +import std/options +import chronos, chronicles +import waku/[waku_core], waku/waku_lightpush/[common, rpc] +import waku/requests/health_request +import waku/common/broker/broker_context +import waku/api/types +import ./[delivery_task, send_processor] + +logScope: + topics = "send service relay processor" + +type RelaySendProcessor* = ref object of BaseSendProcessor + publishProc: PushMessageHandler + fallbackStateToSet: DeliveryState + +proc new*( + T: typedesc[RelaySendProcessor], + lightpushAvailable: bool, + publishProc: PushMessageHandler, + brokerCtx: BrokerContext, +): RelaySendProcessor = + let fallbackStateToSet = + if lightpushAvailable: + DeliveryState.FallbackRetry + else: + DeliveryState.FailedToDeliver + + return RelaySendProcessor( + publishProc: publishProc, + fallbackStateToSet: fallbackStateToSet, + brokerCtx: brokerCtx, + ) + +proc isTopicHealthy(self: RelaySendProcessor, topic: PubsubTopic): bool {.gcsafe.} = + let healthReport = RequestRelayTopicsHealth.request(self.brokerCtx, @[topic]).valueOr: + error "isTopicHealthy: failed to get health report", topic = topic, error = error + return false + + if healthReport.topicHealth.len() < 1: + warn "isTopicHealthy: no topic health entries", topic = topic + return false + let health = healthReport.topicHealth[0].health + debug "isTopicHealthy: topic health is ", topic = topic, health = health + return health == MINIMALLY_HEALTHY or health == SUFFICIENTLY_HEALTHY + +method isValidProcessor*( + self: RelaySendProcessor, task: DeliveryTask +): bool {.gcsafe.} = + # Topic health query is not reliable enough after a fresh subscribe... + # return self.isTopicHealthy(task.pubsubTopic) + return true + +method sendImpl*(self: RelaySendProcessor, task: DeliveryTask) {.async.} = + task.tryCount.inc() + info "Trying message delivery via Relay", + requestId = task.requestId, + msgHash = task.msgHash.to0xHex(), + tryCount = task.tryCount + + let noOfPublishedPeers = (await self.publishProc(task.pubsubTopic, task.msg)).valueOr: + let errorMessage = error.desc.get($error.code) + error "Failed to publish message with relay", + request = task.requestId, msgHash = task.msgHash.to0xHex(), error = errorMessage + if error.code != LightPushErrorCode.NO_PEERS_TO_RELAY: + task.state = DeliveryState.FailedToDeliver + task.errorDesc = errorMessage + else: + task.state = self.fallbackStateToSet + return + + if noOfPublishedPeers > 0: + info "Message propagated via Relay", + requestId = task.requestId, msgHash = task.msgHash.to0xHex(), noOfPeers = noOfPublishedPeers + task.state = DeliveryState.SuccessfullyPropagated + task.deliveryTime = Moment.now() + else: + # It shall not happen, but still covering it + task.state = self.fallbackStateToSet diff --git a/waku/node/delivery_service/send_service/send_processor.nim b/waku/node/delivery_service/send_service/send_processor.nim new file mode 100644 index 000000000..0108eacd0 --- /dev/null +++ b/waku/node/delivery_service/send_service/send_processor.nim @@ -0,0 +1,36 @@ +import chronos +import ./delivery_task +import waku/common/broker/broker_context + +{.push raises: [].} + +type BaseSendProcessor* = ref object of RootObj + fallbackProcessor*: BaseSendProcessor + brokerCtx*: BrokerContext + +proc chain*(self: BaseSendProcessor, next: BaseSendProcessor) = + self.fallbackProcessor = next + +method isValidProcessor*( + self: BaseSendProcessor, task: DeliveryTask +): bool {.base, gcsafe.} = + return false + +method sendImpl*( + self: BaseSendProcessor, task: DeliveryTask +): Future[void] {.async, base.} = + assert false, "Not implemented" + +method process*( + self: BaseSendProcessor, task: DeliveryTask +): Future[void] {.async, base.} = + var currentProcessor: BaseSendProcessor = self + var keepTrying = true + while not currentProcessor.isNil() and keepTrying: + if currentProcessor.isValidProcessor(task): + await currentProcessor.sendImpl(task) + currentProcessor = currentProcessor.fallbackProcessor + keepTrying = task.state == DeliveryState.FallbackRetry + + if task.state == DeliveryState.FallbackRetry: + task.state = DeliveryState.NextRoundRetry diff --git a/waku/node/delivery_service/send_service/send_service.nim b/waku/node/delivery_service/send_service/send_service.nim new file mode 100644 index 000000000..f6a6ac94c --- /dev/null +++ b/waku/node/delivery_service/send_service/send_service.nim @@ -0,0 +1,269 @@ +## This module reinforces the publish operation with regular store-v3 requests. +## + +import std/[sequtils, tables, options] +import chronos, chronicles, libp2p/utility +import + ./[send_processor, relay_processor, lightpush_processor, delivery_task], + ../[subscription_service], + waku/[ + waku_core, + node/waku_node, + node/peer_manager, + waku_store/client, + waku_store/common, + waku_relay/protocol, + waku_rln_relay/rln_relay, + waku_lightpush/client, + waku_lightpush/callbacks, + events/message_events, + common/broker/broker_context, + ] + +logScope: + topics = "send service" + +# This useful util is missing from sequtils, this extends applyIt with predicate... +template applyItIf*(varSeq, pred, op: untyped) = + for i in low(varSeq) .. high(varSeq): + let it {.inject.} = varSeq[i] + if pred: + op + varSeq[i] = it + +template forEach*(varSeq, op: untyped) = + for i in low(varSeq) .. high(varSeq): + let it {.inject.} = varSeq[i] + op + +const MaxTimeInCache* = chronos.minutes(1) + ## Messages older than this time will get completely forgotten on publication and a + ## feedback will be given when that happens + +const ServiceLoopInterval* = chronos.seconds(1) + ## Interval at which we check that messages have been properly received by a store node + +const ArchiveTime = chronos.seconds(3) + ## Estimation of the time we wait until we start confirming that a message has been properly + ## received and archived by a store node + +type SendService* = ref object of RootObj + brokerCtx: BrokerContext + taskCache: seq[DeliveryTask] + ## Cache that contains the delivery task per message hash. + ## This is needed to make sure the published messages are properly published + + serviceLoopHandle: Future[void] ## handle that allows to stop the async task + sendProcessor: BaseSendProcessor + + node: WakuNode + checkStoreForMessages: bool + subscriptionService: SubscriptionService + +proc setupSendProcessorChain( + peerManager: PeerManager, + lightpushClient: WakuLightPushClient, + relay: WakuRelay, + rlnRelay: WakuRLNRelay, + brokerCtx: BrokerContext, +): Result[BaseSendProcessor, string] = + let isRelayAvail = not relay.isNil() + let isLightPushAvail = not lightpushClient.isNil() + + if not isRelayAvail and not isLightPushAvail: + return err("No valid send processor found for the delivery task") + + var processors = newSeq[BaseSendProcessor]() + + if isRelayAvail: + let rln: Option[WakuRLNRelay] = + if rlnRelay.isNil(): + none[WakuRLNRelay]() + else: + some(rlnRelay) + let publishProc = getRelayPushHandler(relay, rln) + + processors.add(RelaySendProcessor.new(isLightPushAvail, publishProc, brokerCtx)) + if isLightPushAvail: + processors.add(LightpushSendProcessor.new(peerManager, lightpushClient, brokerCtx)) + + var currentProcessor: BaseSendProcessor = processors[0] + for i in 1 ..< processors.len: + currentProcessor.chain(processors[i]) + currentProcessor = processors[i] + + return ok(processors[0]) + +proc new*( + T: typedesc[SendService], + preferP2PReliability: bool, + w: WakuNode, + s: SubscriptionService, +): Result[T, string] = + if w.wakuRelay.isNil() and w.wakuLightpushClient.isNil(): + return err( + "Could not create SendService. wakuRelay or wakuLightpushClient should be set" + ) + + let checkStoreForMessages = preferP2PReliability and not w.wakuStoreClient.isNil() + + let sendProcessorChain = setupSendProcessorChain( + w.peerManager, w.wakuLightPushClient, w.wakuRelay, w.wakuRlnRelay, w.brokerCtx + ).valueOr: + return err("failed to setup SendProcessorChain: " & $error) + + let sendService = SendService( + brokerCtx: w.brokerCtx, + taskCache: newSeq[DeliveryTask](), + serviceLoopHandle: nil, + sendProcessor: sendProcessorChain, + node: w, + checkStoreForMessages: checkStoreForMessages, + subscriptionService: s, + ) + + return ok(sendService) + +proc addTask(self: SendService, task: DeliveryTask) = + self.taskCache.addUnique(task) + +proc isStorePeerAvailable*(sendService: SendService): bool = + return sendService.node.peerManager.selectPeer(WakuStoreCodec).isSome() + +proc checkMsgsInStore(self: SendService, tasksToValidate: seq[DeliveryTask]) {.async.} = + if tasksToValidate.len() == 0: + return + + if not isStorePeerAvailable(self): + warn "Skipping store validation for ", + messageCount = tasksToValidate.len(), error = "no store peer available" + return + + var hashesToValidate = tasksToValidate.mapIt(it.msgHash) + # TODO: confirm hash format for store query!!! + + let storeResp: StoreQueryResponse = ( + await self.node.wakuStoreClient.queryToAny( + StoreQueryRequest(includeData: false, messageHashes: hashesToValidate) + ) + ).valueOr: + error "Failed to get store validation for messages", + hashes = hashesToValidate.mapIt(shortLog(it)), error = $error + return + + let storedItems = storeResp.messages.mapIt(it.messageHash) + + # Set success state for messages found in store + self.taskCache.applyItIf(storedItems.contains(it.msgHash)): + it.state = DeliveryState.SuccessfullyValidated + + # set retry state for messages not found in store + hashesToValidate.keepItIf(not storedItems.contains(it)) + self.taskCache.applyItIf(hashesToValidate.contains(it.msgHash)): + it.state = DeliveryState.NextRoundRetry + +proc checkStoredMessages(self: SendService) {.async.} = + if not self.checkStoreForMessages: + return + + let tasksToValidate = self.taskCache.filterIt( + it.state == DeliveryState.SuccessfullyPropagated and it.deliveryAge() > ArchiveTime and + not it.isEphemeral() + ) + + await self.checkMsgsInStore(tasksToValidate) + +proc reportTaskResult(self: SendService, task: DeliveryTask) = + case task.state + of DeliveryState.SuccessfullyPropagated: + # TODO: in case of unable to strore check messages shall we report success instead? + if not task.propagateEventEmitted: + info "Message successfully propagated", + requestId = task.requestId, msgHash = task.msgHash.to0xHex() + MessagePropagatedEvent.emit( + self.brokerCtx, task.requestId, task.msgHash.to0xHex() + ) + task.propagateEventEmitted = true + return + of DeliveryState.SuccessfullyValidated: + info "Message successfully sent", + requestId = task.requestId, msgHash = task.msgHash.to0xHex() + MessageSentEvent.emit(self.brokerCtx, task.requestId, task.msgHash.to0xHex()) + return + of DeliveryState.FailedToDeliver: + error "Failed to send message", + requestId = task.requestId, + msgHash = task.msgHash.to0xHex(), + error = task.errorDesc + MessageErrorEvent.emit( + self.brokerCtx, task.requestId, task.msgHash.to0xHex(), task.errorDesc + ) + return + else: + # rest of the states are intermediate and does not translate to event + discard + + if task.messageAge() > MaxTimeInCache: + error "Failed to send message", + requestId = task.requestId, + msgHash = task.msgHash.to0xHex(), + error = "Message too old", + age = task.messageAge() + task.state = DeliveryState.FailedToDeliver + MessageErrorEvent.emit( + self.brokerCtx, + task.requestId, + task.msgHash.to0xHex(), + "Unable to send within retry time window", + ) + +proc evaluateAndCleanUp(self: SendService) = + self.taskCache.forEach(self.reportTaskResult(it)) + self.taskCache.keepItIf( + it.state != DeliveryState.SuccessfullyValidated and + it.state != DeliveryState.FailedToDeliver + ) + + # remove propagated ephemeral messages as no store check is possible + self.taskCache.keepItIf( + not (it.isEphemeral() and it.state == DeliveryState.SuccessfullyPropagated) + ) + +proc trySendMessages(self: SendService) {.async.} = + let tasksToSend = self.taskCache.filterIt(it.state == DeliveryState.NextRoundRetry) + + for task in tasksToSend: + # Todo, check if it has any perf gain to run them concurrent... + await self.sendProcessor.process(task) + +proc serviceLoop(self: SendService) {.async.} = + ## Continuously monitors that the sent messages have been received by a store node + while true: + await self.trySendMessages() + await self.checkStoredMessages() + self.evaluateAndCleanUp() + ## TODO: add circuit breaker to avoid infinite looping in case of persistent failures + ## Use OnlineStateChange observers to pause/resume the loop + await sleepAsync(ServiceLoopInterval) + +proc startSendService*(self: SendService) = + self.serviceLoopHandle = self.serviceLoop() + +proc stopSendService*(self: SendService) = + if not self.serviceLoopHandle.isNil(): + discard self.serviceLoopHandle.cancelAndWait() + +proc send*(self: SendService, task: DeliveryTask) {.async.} = + assert(not task.isNil(), "task for send must not be nil") + + info "SendService.send: processing delivery task", + requestId = task.requestId, msgHash = task.msgHash.to0xHex() + + self.subscriptionService.subscribe(task.msg.contentTopic).isOkOr: + error "SendService.send: failed to subscribe to content topic", + contentTopic = task.msg.contentTopic, error = error + + await self.sendProcessor.process(task) + reportTaskResult(self, task) + if task.state != DeliveryState.FailedToDeliver: + self.addTask(task) diff --git a/waku/node/delivery_service/subscription_service.nim b/waku/node/delivery_service/subscription_service.nim new file mode 100644 index 000000000..78763161b --- /dev/null +++ b/waku/node/delivery_service/subscription_service.nim @@ -0,0 +1,64 @@ +import chronos, chronicles +import + waku/[ + waku_core, + waku_core/topics, + events/message_events, + waku_node, + common/broker/broker_context, + ] + +type SubscriptionService* = ref object of RootObj + brokerCtx: BrokerContext + node: WakuNode + +proc new*(T: typedesc[SubscriptionService], node: WakuNode): T = + ## The storeClient will help to acquire any possible missed messages + + return SubscriptionService(brokerCtx: node.brokerCtx, node: node) + +proc isSubscribed*( + self: SubscriptionService, topic: ContentTopic +): Result[bool, string] = + var isSubscribed = false + if self.node.wakuRelay.isNil() == false: + return self.node.isSubscribed((kind: ContentSub, topic: topic)) + + # TODO: Add support for edge mode with Filter subscription management + return ok(isSubscribed) + +#TODO: later PR may consider to refactor or place this function elsewhere +# The only important part is that it emits MessageReceivedEvent +proc getReceiveHandler(self: SubscriptionService): WakuRelayHandler = + return proc(topic: PubsubTopic, msg: WakuMessage): Future[void] {.async, gcsafe.} = + let msgHash = computeMessageHash(topic, msg).to0xHex() + info "API received message", + pubsubTopic = topic, contentTopic = msg.contentTopic, msgHash = msgHash + + MessageReceivedEvent.emit(self.brokerCtx, msgHash, msg) + +proc subscribe*(self: SubscriptionService, topic: ContentTopic): Result[void, string] = + let isSubscribed = self.isSubscribed(topic).valueOr: + error "Failed to check subscription status: ", error = error + return err("Failed to check subscription status: " & error) + + if isSubscribed == false: + if self.node.wakuRelay.isNil() == false: + self.node.subscribe((kind: ContentSub, topic: topic), self.getReceiveHandler()).isOkOr: + error "Failed to subscribe: ", error = error + return err("Failed to subscribe: " & error) + + # TODO: Add support for edge mode with Filter subscription management + + return ok() + +proc unsubscribe*( + self: SubscriptionService, topic: ContentTopic +): Result[void, string] = + if self.node.wakuRelay.isNil() == false: + self.node.unsubscribe((kind: ContentSub, topic: topic)).isOkOr: + error "Failed to unsubscribe: ", error = error + return err("Failed to unsubscribe: " & error) + + # TODO: Add support for edge mode with Filter subscription management + return ok() diff --git a/waku/waku_relay/topic_health.nim b/waku/node/health_monitor/topic_health.nim similarity index 84% rename from waku/waku_relay/topic_health.nim rename to waku/node/health_monitor/topic_health.nim index 774abc584..5a1ea0a16 100644 --- a/waku/waku_relay/topic_health.nim +++ b/waku/node/health_monitor/topic_health.nim @@ -1,11 +1,12 @@ import chronos -import ../waku_core +import waku/waku_core type TopicHealth* = enum UNHEALTHY MINIMALLY_HEALTHY SUFFICIENTLY_HEALTHY + NOT_SUBSCRIBED proc `$`*(t: TopicHealth): string = result = @@ -13,6 +14,7 @@ proc `$`*(t: TopicHealth): string = of UNHEALTHY: "UnHealthy" of MINIMALLY_HEALTHY: "MinimallyHealthy" of SUFFICIENTLY_HEALTHY: "SufficientlyHealthy" + of NOT_SUBSCRIBED: "NotSubscribed" type TopicHealthChangeHandler* = proc( pubsubTopic: PubsubTopic, topicHealth: TopicHealth diff --git a/waku/node/kernel_api/lightpush.nim b/waku/node/kernel_api/lightpush.nim index 2a5f6acbb..ffe2afdac 100644 --- a/waku/node/kernel_api/lightpush.nim +++ b/waku/node/kernel_api/lightpush.nim @@ -193,7 +193,6 @@ proc lightpushPublishHandler( mixify: bool = false, ): Future[lightpush_protocol.WakuLightPushResult] {.async.} = let msgHash = pubsubTopic.computeMessageHash(message).to0xHex() - if not node.wakuLightpushClient.isNil(): notice "publishing message with lightpush", pubsubTopic = pubsubTopic, @@ -201,21 +200,23 @@ proc lightpushPublishHandler( target_peer_id = peer.peerId, msg_hash = msgHash, mixify = mixify - if mixify: #indicates we want to use mix to send the message - #TODO: How to handle multiple addresses? - let conn = node.wakuMix.toConnection( - MixDestination.exitNode(peer.peerId), - WakuLightPushCodec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - # indicating we only want a single path to be used for reply hence numSurbs = 1 - ).valueOr: - error "could not create mix connection" - return lighpushErrorResult( - LightPushErrorCode.SERVICE_NOT_AVAILABLE, - "Waku lightpush with mix not available", - ) + if defined(libp2p_mix_experimental_exit_is_dest) and mixify: + #indicates we want to use mix to send the message + when defined(libp2p_mix_experimental_exit_is_dest): + #TODO: How to handle multiple addresses? + let conn = node.wakuMix.toConnection( + MixDestination.exitNode(peer.peerId), + WakuLightPushCodec, + MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), + # indicating we only want a single path to be used for reply hence numSurbs = 1 + ).valueOr: + error "could not create mix connection" + return lighpushErrorResult( + LightPushErrorCode.SERVICE_NOT_AVAILABLE, + "Waku lightpush with mix not available", + ) - return await node.wakuLightpushClient.publish(some(pubsubTopic), message, conn) + return await node.wakuLightpushClient.publish(some(pubsubTopic), message, conn) else: return await node.wakuLightpushClient.publish(some(pubsubTopic), message, peer) @@ -264,7 +265,7 @@ proc lightpushPublish*( LightPushErrorCode.NO_PEERS_TO_RELAY, "no suitable remote peers" ) - let pubsubForPublish = pubSubTopic.valueOr: + let pubsubForPublish = pubsubTopic.valueOr: if node.wakuAutoSharding.isNone(): let msg = "Pubsub topic must be specified when static sharding is enabled" error "lightpush publish error", error = msg diff --git a/waku/node/kernel_api/relay.nim b/waku/node/kernel_api/relay.nim index 827cc1e5f..a0a128449 100644 --- a/waku/node/kernel_api/relay.nim +++ b/waku/node/kernel_api/relay.nim @@ -30,6 +30,8 @@ import ../peer_manager, ../../waku_rln_relay +export waku_relay.WakuRelayHandler + declarePublicHistogram waku_histogram_message_size, "message size histogram in kB", buckets = [ @@ -91,6 +93,23 @@ proc registerRelayHandler( node.wakuRelay.subscribe(topic, uniqueTopicHandler) +proc getTopicOfSubscriptionEvent( + node: WakuNode, subscription: SubscriptionEvent +): Result[(PubsubTopic, Option[ContentTopic]), string] = + case subscription.kind + of ContentSub, ContentUnsub: + if node.wakuAutoSharding.isSome(): + let shard = node.wakuAutoSharding.get().getShard((subscription.topic)).valueOr: + return err("Autosharding error: " & error) + return ok(($shard, some(subscription.topic))) + else: + return + err("Static sharding is used, relay subscriptions must specify a pubsub topic") + of PubsubSub, PubsubUnsub: + return ok((subscription.topic, none[ContentTopic]())) + else: + return err("Unsupported subscription type in relay getTopicOfSubscriptionEvent") + proc subscribe*( node: WakuNode, subscription: SubscriptionEvent, handler: WakuRelayHandler ): Result[void, string] = @@ -101,27 +120,15 @@ proc subscribe*( error "Invalid API call to `subscribe`. WakuRelay not mounted." return err("Invalid API call to `subscribe`. WakuRelay not mounted.") - let (pubsubTopic, contentTopicOp) = - case subscription.kind - of ContentSub: - if node.wakuAutoSharding.isSome(): - let shard = node.wakuAutoSharding.get().getShard((subscription.topic)).valueOr: - error "Autosharding error", error = error - return err("Autosharding error: " & error) - ($shard, some(subscription.topic)) - else: - return err( - "Static sharding is used, relay subscriptions must specify a pubsub topic" - ) - of PubsubSub: - (subscription.topic, none(ContentTopic)) - else: - return err("Unsupported subscription type in relay subscribe") + let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr: + error "Failed to decode subscription event", error = error + return err("Failed to decode subscription event: " & error) if node.wakuRelay.isSubscribed(pubsubTopic): warn "No-effect API call to subscribe. Already subscribed to topic", pubsubTopic return ok() + info "subscribe", pubsubTopic, contentTopicOp node.registerRelayHandler(pubsubTopic, handler) node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: pubsubTopic)) @@ -136,22 +143,9 @@ proc unsubscribe*( error "Invalid API call to `unsubscribe`. WakuRelay not mounted." return err("Invalid API call to `unsubscribe`. WakuRelay not mounted.") - let (pubsubTopic, contentTopicOp) = - case subscription.kind - of ContentUnsub: - if node.wakuAutoSharding.isSome(): - let shard = node.wakuAutoSharding.get().getShard((subscription.topic)).valueOr: - error "Autosharding error", error = error - return err("Autosharding error: " & error) - ($shard, some(subscription.topic)) - else: - return err( - "Static sharding is used, relay subscriptions must specify a pubsub topic" - ) - of PubsubUnsub: - (subscription.topic, none(ContentTopic)) - else: - return err("Unsupported subscription type in relay unsubscribe") + let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr: + error "Failed to decode unsubscribe event", error = error + return err("Failed to decode unsubscribe event: " & error) if not node.wakuRelay.isSubscribed(pubsubTopic): warn "No-effect API call to `unsubscribe`. Was not subscribed", pubsubTopic @@ -163,9 +157,22 @@ proc unsubscribe*( return ok() +proc isSubscribed*( + node: WakuNode, subscription: SubscriptionEvent +): Result[bool, string] = + if node.wakuRelay.isNil(): + error "Invalid API call to `isSubscribed`. WakuRelay not mounted." + return err("Invalid API call to `isSubscribed`. WakuRelay not mounted.") + + let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr: + error "Failed to decode subscription event", error = error + return err("Failed to decode subscription event: " & error) + + return ok(node.wakuRelay.isSubscribed(pubsubTopic)) + proc publish*( node: WakuNode, pubsubTopicOp: Option[PubsubTopic], message: WakuMessage -): Future[Result[void, string]] {.async, gcsafe.} = +): Future[Result[int, string]] {.async, gcsafe.} = ## Publish a `WakuMessage`. Pubsub topic contains; none, a named or static shard. ## `WakuMessage` should contain a `contentTopic` field for light node functionality. ## It is also used to determine the shard. @@ -184,16 +191,20 @@ proc publish*( let msg = "Autosharding error: " & error return err(msg) - #TODO instead of discard return error when 0 peers received the message - discard await node.wakuRelay.publish(pubsubTopic, message) + let numPeers = (await node.wakuRelay.publish(pubsubTopic, message)).valueOr: + warn "waku.relay did not publish", error = error + # Todo: If NoPeersToPublish, we might want to return ok(0) instead!!! + return err("publish failed in relay: " & $error) notice "waku.relay published", peerId = node.peerId, pubsubTopic = pubsubTopic, msg_hash = pubsubTopic.computeMessageHash(message).to0xHex(), - publishTime = getNowInNanosecondTime() + publishTime = getNowInNanosecondTime(), + numPeers = numPeers - return ok() + # TODO: investigate if we can return error in case numPeers is 0 + ok(numPeers) proc mountRelay*( node: WakuNode, diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 07e36dd13..d556811ac 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -27,38 +27,42 @@ import libp2p/protocols/mix/mix_protocol import - ../waku_core, - ../waku_core/topics/sharding, - ../waku_relay, - ../waku_archive, - ../waku_archive_legacy, - ../waku_store_legacy/protocol as legacy_store, - ../waku_store_legacy/client as legacy_store_client, - ../waku_store_legacy/common as legacy_store_common, - ../waku_store/protocol as store, - ../waku_store/client as store_client, - ../waku_store/common as store_common, - ../waku_store/resume, - ../waku_store_sync, - ../waku_filter_v2, - ../waku_filter_v2/client as filter_client, - ../waku_metadata, - ../waku_rendezvous/protocol, - ../waku_rendezvous/client as rendezvous_client, - ../waku_rendezvous/waku_peer_record, - ../waku_lightpush_legacy/client as legacy_ligntpuhs_client, - ../waku_lightpush_legacy as legacy_lightpush_protocol, - ../waku_lightpush/client as ligntpuhs_client, - ../waku_lightpush as lightpush_protocol, - ../waku_enr, - ../waku_peer_exchange, - ../waku_rln_relay, + waku/[ + waku_core, + waku_core/topics/sharding, + waku_relay, + waku_archive, + waku_archive_legacy, + waku_store_legacy/protocol as legacy_store, + waku_store_legacy/client as legacy_store_client, + waku_store_legacy/common as legacy_store_common, + waku_store/protocol as store, + waku_store/client as store_client, + waku_store/common as store_common, + waku_store/resume, + waku_store_sync, + waku_filter_v2, + waku_filter_v2/client as filter_client, + waku_metadata, + waku_rendezvous/protocol, + waku_rendezvous/client as rendezvous_client, + waku_rendezvous/waku_peer_record, + waku_lightpush_legacy/client as legacy_ligntpuhs_client, + waku_lightpush_legacy as legacy_lightpush_protocol, + waku_lightpush/client as ligntpuhs_client, + waku_lightpush as lightpush_protocol, + waku_enr, + waku_peer_exchange, + waku_rln_relay, + common/rate_limit/setting, + common/callbacks, + common/nimchronos, + waku_mix, + requests/node_requests, + common/broker/broker_context, + ], ./net_config, - ./peer_manager, - ../common/rate_limit/setting, - ../common/callbacks, - ../common/nimchronos, - ../waku_mix + ./peer_manager declarePublicCounter waku_node_messages, "number of messages received", ["type"] @@ -123,6 +127,7 @@ type enr*: enr.Record libp2pPing*: Ping rng*: ref rand.HmacDrbgContext + brokerCtx*: BrokerContext wakuRendezvous*: WakuRendezVous wakuRendezvousClient*: rendezvous_client.WakuRendezVousClient announcedAddresses*: seq[MultiAddress] @@ -131,6 +136,23 @@ type rateLimitSettings*: ProtocolRateLimitSettings wakuMix*: WakuMix +proc deduceRelayShard( + node: WakuNode, + contentTopic: ContentTopic, + pubsubTopicOp: Option[PubsubTopic] = none[PubsubTopic](), +): Result[RelayShard, string] = + let pubsubTopic = pubsubTopicOp.valueOr: + if node.wakuAutoSharding.isNone(): + return err("Pubsub topic must be specified when static sharding is enabled.") + let shard = node.wakuAutoSharding.get().getShard(contentTopic).valueOr: + let msg = "Deducing shard failed: " & error + return err(msg) + return ok(shard) + + let shard = RelayShard.parse(pubsubTopic).valueOr: + return err("Invalid topic:" & pubsubTopic & " " & $error) + return ok(shard) + proc getShardsGetter(node: WakuNode): GetShards = return proc(): seq[uint16] {.closure, gcsafe, raises: [].} = # fetch pubsubTopics subscribed to relay and convert them to shards @@ -177,11 +199,14 @@ proc new*( info "Initializing networking", addrs = $netConfig.announcedAddresses + let brokerCtx = globalBrokerContext() + let queue = newAsyncEventQueue[SubscriptionEvent](0) let node = WakuNode( peerManager: peerManager, switch: switch, rng: rng, + brokerCtx: brokerCtx, enr: enr, announcedAddresses: netConfig.announcedAddresses, topicSubscriptionQueue: queue, @@ -252,6 +277,7 @@ proc mountAutoSharding*( info "mounting auto sharding", clusterId = clusterId, shardCount = shardCount node.wakuAutoSharding = some(Sharding(clusterId: clusterId, shardCountGenZero: shardCount)) + return ok() proc getMixNodePoolSize*(node: WakuNode): int = @@ -443,6 +469,21 @@ proc updateAnnouncedAddrWithPrimaryIpAddr*(node: WakuNode): Result[void, string] return ok() +proc startProvidersAndListeners(node: WakuNode) = + RequestRelayShard.setProvider( + node.brokerCtx, + proc( + pubsubTopic: Option[PubsubTopic], contentTopic: ContentTopic + ): Result[RequestRelayShard, string] = + let shard = node.deduceRelayShard(contentTopic, pubsubTopic).valueOr: + return err($error) + return ok(RequestRelayShard(relayShard: shard)), + ).isOkOr: + error "Can't set provider for RequestRelayShard", error = error + +proc stopProvidersAndListeners(node: WakuNode) = + RequestRelayShard.clearProvider(node.brokerCtx) + proc start*(node: WakuNode) {.async.} = ## Starts a created Waku Node and ## all its mounted protocols. @@ -491,6 +532,8 @@ proc start*(node: WakuNode) {.async.} = ## The switch will update addresses after start using the addressMapper await node.switch.start() + node.startProvidersAndListeners() + node.started = true if not zeroPortPresent: @@ -503,6 +546,9 @@ proc start*(node: WakuNode) {.async.} = proc stop*(node: WakuNode) {.async.} = ## By stopping the switch we are stopping all the underlying mounted protocols + + node.stopProvidersAndListeners() + await node.switch.stop() node.peerManager.stop() diff --git a/waku/requests/health_request.nim b/waku/requests/health_request.nim new file mode 100644 index 000000000..9f98eba67 --- /dev/null +++ b/waku/requests/health_request.nim @@ -0,0 +1,21 @@ +import waku/common/broker/[request_broker, multi_request_broker] + +import waku/api/types +import waku/node/health_monitor/[protocol_health, topic_health] +import waku/waku_core/topics + +export protocol_health, topic_health + +RequestBroker(sync): + type RequestNodeHealth* = object + healthStatus*: NodeHealth + +RequestBroker(sync): + type RequestRelayTopicsHealth* = object + topicHealth*: seq[tuple[topic: PubsubTopic, health: TopicHealth]] + + proc signature(topics: seq[PubsubTopic]): Result[RequestRelayTopicsHealth, string] + +MultiRequestBroker: + type RequestProtocolHealth* = object + healthStatus*: ProtocolHealth diff --git a/waku/requests/node_requests.nim b/waku/requests/node_requests.nim new file mode 100644 index 000000000..a4ccc6de4 --- /dev/null +++ b/waku/requests/node_requests.nim @@ -0,0 +1,11 @@ +import std/options +import waku/common/broker/[request_broker, multi_request_broker] +import waku/waku_core/[topics] + +RequestBroker(sync): + type RequestRelayShard* = object + relayShard*: RelayShard + + proc signature( + pubsubTopic: Option[PubsubTopic], contentTopic: ContentTopic + ): Result[RequestRelayShard, string] diff --git a/waku/requests/requests.nim b/waku/requests/requests.nim new file mode 100644 index 000000000..03e10f882 --- /dev/null +++ b/waku/requests/requests.nim @@ -0,0 +1,3 @@ +import ./[health_request, rln_requests, node_requests] + +export health_request, rln_requests, node_requests diff --git a/waku/requests/rln_requests.nim b/waku/requests/rln_requests.nim new file mode 100644 index 000000000..8b61f9fcd --- /dev/null +++ b/waku/requests/rln_requests.nim @@ -0,0 +1,9 @@ +import waku/common/broker/request_broker, waku/waku_core/message/message + +RequestBroker: + type RequestGenerateRlnProof* = object + proof*: seq[byte] + + proc signature( + message: WakuMessage, senderEpoch: float64 + ): Future[Result[RequestGenerateRlnProof, string]] {.async.} diff --git a/waku/waku_core/message/digest.nim b/waku/waku_core/message/digest.nim index 8b99abd7e..3f82ce8f6 100644 --- a/waku/waku_core/message/digest.nim +++ b/waku/waku_core/message/digest.nim @@ -19,6 +19,11 @@ func shortLog*(hash: WakuMessageHash): string = func `$`*(hash: WakuMessageHash): string = shortLog(hash) +func to0xHex*(hash: WakuMessageHash): string = + var hexhash = newStringOfCap(64) + hexhash &= hash.toOpenArray(hash.low, hash.high).to0xHex() + hexhash + const EmptyWakuMessageHash*: WakuMessageHash = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, diff --git a/waku/waku_filter_v2/client.nim b/waku/waku_filter_v2/client.nim index c42bca3db..323cc6da8 100644 --- a/waku/waku_filter_v2/client.nim +++ b/waku/waku_filter_v2/client.nim @@ -10,9 +10,8 @@ import bearssl/rand, stew/byteutils import - ../node/peer_manager, - ../node/delivery_monitor/subscriptions_observer, - ../waku_core, + waku/ + [node/peer_manager, waku_core, events/delivery_events, common/broker/broker_context], ./common, ./protocol_metrics, ./rpc_codec, @@ -22,19 +21,16 @@ logScope: topics = "waku filter client" type WakuFilterClient* = ref object of LPProtocol + brokerCtx: BrokerContext rng: ref HmacDrbgContext peerManager: PeerManager pushHandlers: seq[FilterPushHandler] - subscrObservers: seq[SubscriptionObserver] func generateRequestId(rng: ref HmacDrbgContext): string = var bytes: array[10, byte] hmacDrbgGenerate(rng[], bytes) return toHex(bytes) -proc addSubscrObserver*(wfc: WakuFilterClient, obs: SubscriptionObserver) = - wfc.subscrObservers.add(obs) - proc sendSubscribeRequest( wfc: WakuFilterClient, servicePeer: RemotePeerInfo, @@ -132,8 +128,7 @@ proc subscribe*( ?await wfc.sendSubscribeRequest(servicePeer, filterSubscribeRequest) - for obs in wfc.subscrObservers: - obs.onSubscribe(pubSubTopic, contentTopicSeq) + OnFilterSubscribeEvent.emit(wfc.brokerCtx, pubsubTopic, contentTopicSeq) return ok() @@ -156,8 +151,7 @@ proc unsubscribe*( ?await wfc.sendSubscribeRequest(servicePeer, filterSubscribeRequest) - for obs in wfc.subscrObservers: - obs.onUnsubscribe(pubSubTopic, contentTopicSeq) + OnFilterUnSubscribeEvent.emit(wfc.brokerCtx, pubsubTopic, contentTopicSeq) return ok() @@ -210,6 +204,9 @@ proc initProtocolHandler(wfc: WakuFilterClient) = proc new*( T: type WakuFilterClient, peerManager: PeerManager, rng: ref HmacDrbgContext ): T = - let wfc = WakuFilterClient(rng: rng, peerManager: peerManager, pushHandlers: @[]) + let brokerCtx = globalBrokerContext() + let wfc = WakuFilterClient( + brokerCtx: brokerCtx, rng: rng, peerManager: peerManager, pushHandlers: @[] + ) wfc.initProtocolHandler() wfc diff --git a/waku/waku_lightpush/callbacks.nim b/waku/waku_lightpush/callbacks.nim index bde4e3e26..ac2e562b6 100644 --- a/waku/waku_lightpush/callbacks.nim +++ b/waku/waku_lightpush/callbacks.nim @@ -31,7 +31,7 @@ proc checkAndGenerateRLNProof*( proc getNilPushHandler*(): PushMessageHandler = return proc( - peer: PeerId, pubsubTopic: string, message: WakuMessage + pubsubTopic: string, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = return lightpushResultInternalError("no waku relay found") @@ -39,7 +39,7 @@ proc getRelayPushHandler*( wakuRelay: WakuRelay, rlnPeer: Option[WakuRLNRelay] = none[WakuRLNRelay]() ): PushMessageHandler = return proc( - peer: PeerId, pubsubTopic: string, message: WakuMessage + pubsubTopic: string, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = # append RLN proof let msgWithProof = checkAndGenerateRLNProof(rlnPeer, message).valueOr: diff --git a/waku/waku_lightpush/client.nim b/waku/waku_lightpush/client.nim index b528b4c76..fd12c49d2 100644 --- a/waku/waku_lightpush/client.nim +++ b/waku/waku_lightpush/client.nim @@ -5,7 +5,6 @@ import libp2p/peerid, libp2p/stream/connection import ../waku_core/peers, ../node/peer_manager, - ../node/delivery_monitor/publish_observer, ../utils/requests, ../waku_core, ./common, @@ -19,16 +18,12 @@ logScope: type WakuLightPushClient* = ref object rng*: ref rand.HmacDrbgContext peerManager*: PeerManager - publishObservers: seq[PublishObserver] proc new*( T: type WakuLightPushClient, peerManager: PeerManager, rng: ref rand.HmacDrbgContext ): T = WakuLightPushClient(peerManager: peerManager, rng: rng) -proc addPublishObserver*(wl: WakuLightPushClient, obs: PublishObserver) = - wl.publishObservers.add(obs) - proc ensureTimestampSet(message: var WakuMessage) = if message.timestamp == 0: message.timestamp = getNowInNanosecondTime() @@ -40,36 +35,43 @@ func shortPeerId(peer: PeerId): string = func shortPeerId(peer: RemotePeerInfo): string = shortLog(peer.peerId) -proc sendPushRequestToConn( - wl: WakuLightPushClient, request: LightPushRequest, conn: Connection +proc sendPushRequest( + wl: WakuLightPushClient, + req: LightPushRequest, + peer: PeerId | RemotePeerInfo, + conn: Option[Connection] = none(Connection), ): Future[WakuLightPushResult] {.async.} = - try: - await conn.writeLp(request.encode().buffer) - except LPStreamRemoteClosedError: - error "Failed to write request to peer", error = getCurrentExceptionMsg() - return lightpushResultInternalError( - "Failed to write request to peer: " & getCurrentExceptionMsg() - ) + let connection = conn.valueOr: + (await wl.peerManager.dialPeer(peer, WakuLightPushCodec)).valueOr: + waku_lightpush_v3_errors.inc(labelValues = [dialFailure]) + return lighpushErrorResult( + LightPushErrorCode.NO_PEERS_TO_RELAY, + dialFailure & ": " & $peer & " is not accessible", + ) + + defer: + await connection.closeWithEOF() + + await connection.writeLP(req.encode().buffer) var buffer: seq[byte] try: - buffer = await conn.readLp(DefaultMaxRpcSize.int) + buffer = await connection.readLp(DefaultMaxRpcSize.int) except LPStreamRemoteClosedError: error "Failed to read response from peer", error = getCurrentExceptionMsg() return lightpushResultInternalError( "Failed to read response from peer: " & getCurrentExceptionMsg() ) + let response = LightpushResponse.decode(buffer).valueOr: - error "failed to decode response", error = $error + error "failed to decode response" waku_lightpush_v3_errors.inc(labelValues = [decodeRpcFailure]) return lightpushResultInternalError(decodeRpcFailure) - let requestIdMismatch = response.requestId != request.requestId - let tooManyRequests = response.statusCode == LightPushErrorCode.TOO_MANY_REQUESTS - if requestIdMismatch and (not tooManyRequests): - # response with TOO_MANY_REQUESTS error code has no requestId by design + if response.requestId != req.requestId and + response.statusCode != LightPushErrorCode.TOO_MANY_REQUESTS: error "response failure, requestId mismatch", - requestId = request.requestId, responseRequestId = response.requestId + requestId = req.requestId, responseRequestId = response.requestId return lightpushResultInternalError("response failure, requestId mismatch") return toPushResult(response) @@ -80,37 +82,32 @@ proc publish*( wakuMessage: WakuMessage, dest: Connection | PeerId | RemotePeerInfo, ): Future[WakuLightPushResult] {.async, gcsafe.} = - let conn = - when dest is Connection: - dest - else: - (await wl.peerManager.dialPeer(dest, WakuLightPushCodec)).valueOr: - waku_lightpush_v3_errors.inc(labelValues = [dialFailure]) - return lighpushErrorResult( - LightPushErrorCode.NO_PEERS_TO_RELAY, - "Peer is not accessible: " & dialFailure & " - " & $dest, - ) - - defer: - await conn.closeWithEOF() - var message = wakuMessage ensureTimestampSet(message) let msgHash = computeMessageHash(pubSubTopic.get(""), message).to0xHex() + + let peerIdStr = + when dest is Connection: + shortPeerId(dest.peerId) + else: + shortPeerId(dest) + info "publish", myPeerId = wl.peerManager.switch.peerInfo.peerId, - peerId = shortPeerId(conn.peerId), + peerId = peerIdStr, msgHash = msgHash, sentTime = getNowInNanosecondTime() let request = LightpushRequest( requestId: generateRequestId(wl.rng), pubsubTopic: pubSubTopic, message: message ) - let relayPeerCount = ?await wl.sendPushRequestToConn(request, conn) - for obs in wl.publishObservers: - obs.onMessagePublished(pubSubTopic.get(""), message) + let relayPeerCount = + when dest is Connection: + ?await wl.sendPushRequest(request, dest.peerId, some(dest)) + else: + ?await wl.sendPushRequest(request, dest) return lightpushSuccessResult(relayPeerCount) @@ -124,3 +121,12 @@ proc publishToAny*( LightPushErrorCode.NO_PEERS_TO_RELAY, "no suitable remote peers" ) return await wl.publish(some(pubsubTopic), wakuMessage, peer) + +proc publishWithConn*( + wl: WakuLightPushClient, + pubSubTopic: PubsubTopic, + message: WakuMessage, + conn: Connection, + destPeer: PeerId, +): Future[WakuLightPushResult] {.async, gcsafe.} = + return await wl.publish(some(pubSubTopic), message, conn) diff --git a/waku/waku_lightpush/common.nim b/waku/waku_lightpush/common.nim index 9c2ea7ced..f0762e2d2 100644 --- a/waku/waku_lightpush/common.nim +++ b/waku/waku_lightpush/common.nim @@ -25,7 +25,7 @@ type ErrorStatus* = tuple[code: LightpushStatusCode, desc: Option[string]] type WakuLightPushResult* = Result[uint32, ErrorStatus] type PushMessageHandler* = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} const TooManyRequestsMessage* = "Request rejected due to too many requests" @@ -39,7 +39,7 @@ func toPushResult*(response: LightPushResponse): WakuLightPushResult = return ( if (relayPeerCount == 0): # Consider publishing to zero peers an error even if the service node - # sent us a "successful" response with zero peers + # sent us a "successful" response with zero peers err((LightPushErrorCode.NO_PEERS_TO_RELAY, response.statusDesc)) else: ok(relayPeerCount) diff --git a/waku/waku_lightpush/protocol.nim b/waku/waku_lightpush/protocol.nim index 95bfc003e..ecbff8461 100644 --- a/waku/waku_lightpush/protocol.nim +++ b/waku/waku_lightpush/protocol.nim @@ -71,7 +71,7 @@ proc handleRequest( msg_hash = msg_hash, receivedTime = getNowInNanosecondTime() - let res = (await wl.pushHandler(peerId, pubsubTopic, pushRequest.message)).valueOr: + let res = (await wl.pushHandler(pubsubTopic, pushRequest.message)).valueOr: return err((code: error.code, desc: error.desc)) return ok(res) diff --git a/waku/waku_lightpush_legacy/callbacks.nim b/waku/waku_lightpush_legacy/callbacks.nim index 1fe4cf302..a5b88b5b8 100644 --- a/waku/waku_lightpush_legacy/callbacks.nim +++ b/waku/waku_lightpush_legacy/callbacks.nim @@ -30,7 +30,7 @@ proc checkAndGenerateRLNProof*( proc getNilPushHandler*(): PushMessageHandler = return proc( - peer: PeerId, pubsubTopic: string, message: WakuMessage + pubsubTopic: string, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = return err("no waku relay found") @@ -38,7 +38,7 @@ proc getRelayPushHandler*( wakuRelay: WakuRelay, rlnPeer: Option[WakuRLNRelay] = none[WakuRLNRelay]() ): PushMessageHandler = return proc( - peer: PeerId, pubsubTopic: string, message: WakuMessage + pubsubTopic: string, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = # append RLN proof let msgWithProof = ?checkAndGenerateRLNProof(rlnPeer, message) diff --git a/waku/waku_lightpush_legacy/client.nim b/waku/waku_lightpush_legacy/client.nim index 0e3c9bd6f..ab489bec9 100644 --- a/waku/waku_lightpush_legacy/client.nim +++ b/waku/waku_lightpush_legacy/client.nim @@ -5,7 +5,6 @@ import libp2p/peerid import ../waku_core/peers, ../node/peer_manager, - ../node/delivery_monitor/publish_observer, ../utils/requests, ../waku_core, ./common, @@ -19,7 +18,6 @@ logScope: type WakuLegacyLightPushClient* = ref object peerManager*: PeerManager rng*: ref rand.HmacDrbgContext - publishObservers: seq[PublishObserver] proc new*( T: type WakuLegacyLightPushClient, @@ -28,9 +26,6 @@ proc new*( ): T = WakuLegacyLightPushClient(peerManager: peerManager, rng: rng) -proc addPublishObserver*(wl: WakuLegacyLightPushClient, obs: PublishObserver) = - wl.publishObservers.add(obs) - proc sendPushRequest( wl: WakuLegacyLightPushClient, req: PushRequest, peer: PeerId | RemotePeerInfo ): Future[WakuLightPushResult[void]] {.async, gcsafe.} = @@ -86,9 +81,6 @@ proc publish*( let pushRequest = PushRequest(pubSubTopic: pubSubTopic, message: message) ?await wl.sendPushRequest(pushRequest, peer) - for obs in wl.publishObservers: - obs.onMessagePublished(pubSubTopic, message) - notice "publishing message with lightpush", pubsubTopic = pubsubTopic, contentTopic = message.contentTopic, @@ -111,7 +103,4 @@ proc publishToAny*( let pushRequest = PushRequest(pubSubTopic: pubSubTopic, message: message) ?await wl.sendPushRequest(pushRequest, peer) - for obs in wl.publishObservers: - obs.onMessagePublished(pubSubTopic, message) - return ok() diff --git a/waku/waku_lightpush_legacy/common.nim b/waku/waku_lightpush_legacy/common.nim index fcdf1814c..1b40ba72b 100644 --- a/waku/waku_lightpush_legacy/common.nim +++ b/waku/waku_lightpush_legacy/common.nim @@ -9,7 +9,7 @@ export WakuLegacyLightPushCodec type WakuLightPushResult*[T] = Result[T, string] type PushMessageHandler* = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} const TooManyRequestsMessage* = "TOO_MANY_REQUESTS" diff --git a/waku/waku_lightpush_legacy/protocol.nim b/waku/waku_lightpush_legacy/protocol.nim index d51943cff..72fc963ee 100644 --- a/waku/waku_lightpush_legacy/protocol.nim +++ b/waku/waku_lightpush_legacy/protocol.nim @@ -53,7 +53,7 @@ proc handleRequest*( msg_hash = msg_hash, receivedTime = getNowInNanosecondTime() - let handleRes = await wl.pushHandler(peerId, pubsubTopic, message) + let handleRes = await wl.pushHandler(pubsubTopic, message) isSuccess = handleRes.isOk() pushResponseInfo = (if isSuccess: "OK" else: handleRes.error) diff --git a/waku/waku_relay.nim b/waku/waku_relay.nim index 96328d984..a91033cf1 100644 --- a/waku/waku_relay.nim +++ b/waku/waku_relay.nim @@ -1,3 +1,4 @@ -import ./waku_relay/[protocol, topic_health] +import ./waku_relay/protocol +import waku/node/health_monitor/topic_health export protocol, topic_health diff --git a/waku/waku_relay/protocol.nim b/waku/waku_relay/protocol.nim index cbf9123dd..3f343269a 100644 --- a/waku/waku_relay/protocol.nim +++ b/waku/waku_relay/protocol.nim @@ -17,8 +17,13 @@ import libp2p/protocols/pubsub/rpc/messages, libp2p/stream/connection, libp2p/switch + import - ../waku_core, ./message_id, ./topic_health, ../node/delivery_monitor/publish_observer + waku/waku_core, + waku/node/health_monitor/topic_health, + waku/requests/health_request, + ./message_id, + waku/common/broker/broker_context from ../waku_core/codecs import WakuRelayCodec export WakuRelayCodec @@ -157,7 +162,6 @@ type # map topic with its assigned validator within pubsub topicHandlers: Table[PubsubTopic, TopicHandler] # map topic with the TopicHandler proc in charge of attending topic's incoming message events - publishObservers: seq[PublishObserver] topicsHealth*: Table[string, TopicHealth] onTopicHealthChange*: TopicHealthChangeHandler topicHealthLoopHandle*: Future[void] @@ -321,6 +325,18 @@ proc initRelayObservers(w: WakuRelay) = w.addObserver(administrativeObserver) +proc initRequestProviders(w: WakuRelay) = + RequestRelayTopicsHealth.setProvider( + globalBrokerContext(), + proc(topics: seq[PubsubTopic]): Result[RequestRelayTopicsHealth, string] = + var collectedRes: RequestRelayTopicsHealth + for topic in topics: + let health = w.topicsHealth.getOrDefault(topic, TopicHealth.NOT_SUBSCRIBED) + collectedRes.topicHealth.add((topic, health)) + return ok(collectedRes), + ).isOkOr: + error "Cannot set Relay Topics Health request provider", error = error + proc new*( T: type WakuRelay, switch: Switch, maxMessageSize = int(DefaultMaxWakuMessageSize) ): WakuRelayResult[T] = @@ -340,9 +356,10 @@ proc new*( ) procCall GossipSub(w).initPubSub() + w.topicsHealth = initTable[string, TopicHealth]() w.initProtocolHandler() w.initRelayObservers() - w.topicsHealth = initTable[string, TopicHealth]() + w.initRequestProviders() except InitializationError: return err("initialization error: " & getCurrentExceptionMsg()) @@ -353,12 +370,6 @@ proc addValidator*( ) {.gcsafe.} = w.wakuValidators.add((handler, errorMessage)) -proc addPublishObserver*(w: WakuRelay, obs: PublishObserver) = - ## Observer when the api client performed a publish operation. This - ## is initially aimed for bringing an additional layer of delivery reliability thanks - ## to store - w.publishObservers.add(obs) - proc addObserver*(w: WakuRelay, observer: PubSubObserver) {.gcsafe.} = ## Observes when a message is sent/received from the GossipSub PoV procCall GossipSub(w).addObserver(observer) @@ -573,6 +584,7 @@ proc subscribe*(w: WakuRelay, pubsubTopic: PubsubTopic, handler: WakuRelayHandle procCall GossipSub(w).subscribe(pubsubTopic, topicHandler) w.topicHandlers[pubsubTopic] = topicHandler + asyncSpawn w.updateTopicsHealth() proc unsubscribeAll*(w: WakuRelay, pubsubTopic: PubsubTopic) = ## Unsubscribe all handlers on this pubsub topic @@ -628,9 +640,6 @@ proc publish*( if relayedPeerCount <= 0: return err(NoPeersToPublish) - for obs in w.publishObservers: - obs.onMessagePublished(pubSubTopic, message) - return ok(relayedPeerCount) proc getConnectedPubSubPeers*( diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index 6a8fea2b5..8758a7bcd 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -24,10 +24,14 @@ import ./nonce_manager import - ../common/error_handling, - ../waku_relay, # for WakuRelayHandler - ../waku_core, - ../waku_keystore + waku/[ + common/error_handling, + waku_relay, # for WakuRelayHandler + waku_core, + requests/rln_requests, + waku_keystore, + common/broker/broker_context, + ] logScope: topics = "waku rln_relay" @@ -65,6 +69,7 @@ type WakuRLNRelay* = ref object of RootObj nonceManager*: NonceManager epochMonitorFuture*: Future[void] rootChangesFuture*: Future[void] + brokerCtx*: BrokerContext proc calcEpoch*(rlnPeer: WakuRLNRelay, t: float64): Epoch = ## gets time `t` as `flaot64` with subseconds resolution in the fractional part @@ -91,6 +96,7 @@ proc stop*(rlnPeer: WakuRLNRelay) {.async: (raises: [Exception]).} = # stop the group sync, and flush data to tree db info "stopping rln-relay" + RequestGenerateRlnProof.clearProvider(rlnPeer.brokerCtx) await rlnPeer.groupManager.stop() proc hasDuplicate*( @@ -275,11 +281,11 @@ proc validateMessageAndUpdateLog*( return isValidMessage -proc appendRLNProof*( - rlnPeer: WakuRLNRelay, msg: var WakuMessage, senderEpochTime: float64 -): RlnRelayResult[void] = - ## returns true if it can create and append a `RateLimitProof` to the supplied `msg` - ## returns false otherwise +proc createRlnProof( + rlnPeer: WakuRLNRelay, msg: WakuMessage, senderEpochTime: float64 +): RlnRelayResult[seq[byte]] = + ## returns a new `RateLimitProof` for the supplied `msg` + ## returns an error if it cannot create the proof ## `senderEpochTime` indicates the number of seconds passed since Unix epoch. The fractional part holds sub-seconds. ## The `epoch` field of `RateLimitProof` is derived from the provided `senderEpochTime` (using `calcEpoch()`) @@ -291,7 +297,14 @@ proc appendRLNProof*( let proof = rlnPeer.groupManager.generateProof(input, epoch, nonce).valueOr: return err("could not generate rln-v2 proof: " & $error) - msg.proof = proof.encode().buffer + return ok(proof.encode().buffer) + +proc appendRLNProof*( + rlnPeer: WakuRLNRelay, msg: var WakuMessage, senderEpochTime: float64 +): RlnRelayResult[void] = + msg.proof = rlnPeer.createRlnProof(msg, senderEpochTime).valueOr: + return err($error) + return ok() proc clearNullifierLog*(rlnPeer: WakuRlnRelay) = @@ -429,6 +442,7 @@ proc mount( rlnMaxEpochGap: max(uint64(MaxClockGapSeconds / float64(conf.epochSizeSec)), 1), rlnMaxTimestampGap: uint64(MaxClockGapSeconds), onFatalErrorAction: conf.onFatalErrorAction, + brokerCtx: globalBrokerContext(), ) # track root changes on smart contract merkle tree @@ -438,6 +452,19 @@ proc mount( # Start epoch monitoring in the background wakuRlnRelay.epochMonitorFuture = monitorEpochs(wakuRlnRelay) + + RequestGenerateRlnProof.setProvider( + wakuRlnRelay.brokerCtx, + proc( + msg: WakuMessage, senderEpochTime: float64 + ): Future[Result[RequestGenerateRlnProof, string]] {.async.} = + let proof = createRlnProof(wakuRlnRelay, msg, senderEpochTime).valueOr: + return err("Could not create RLN proof: " & $error) + + return ok(RequestGenerateRlnProof(proof: proof)), + ).isOkOr: + return err("Proof generator provider cannot be set: " & $error) + return ok(wakuRlnRelay) proc isReady*(rlnPeer: WakuRLNRelay): Future[bool] {.async: (raises: [Exception]).} = From beb1dde1b5bd27018e6bd224c4bd15d6a2c7e65f Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Fri, 30 Jan 2026 09:06:51 +0100 Subject: [PATCH 052/155] force epoll is used in chronos for Android (#3705) --- waku.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku.nimble b/waku.nimble index 3ff41c5bd..7368ba74b 100644 --- a/waku.nimble +++ b/waku.nimble @@ -93,7 +93,7 @@ proc buildMobileAndroid(srcDir = ".", params = "") = extra_params &= " " & paramStr(i) exec "nim c" & " --out:" & outDir & - "/libwaku.so --threads:on --app:lib --opt:size --noMain --mm:refc -d:chronicles_sinks=textlines[dynamic] --header --passL:-L" & + "/libwaku.so --threads:on --app:lib --opt:size --noMain --mm:refc -d:chronicles_sinks=textlines[dynamic] --header -d:chronosEventEngine=epoll --passL:-L" & outdir & " --passL:-lrln --passL:-llog --cpu:" & cpu & " --os:android -d:androidNDK " & extra_params & " " & srcDir & "/libwaku.nim" From 77f6bc6d727eb28e3eec074d2f2a47f62482d6c7 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Fri, 30 Jan 2026 11:52:25 +0000 Subject: [PATCH 053/155] docs: update licenses --- LICENSE-APACHEv2 => LICENSE-APACHE-2.0 | 5 +---- LICENSE-MIT | 18 +++++++----------- 2 files changed, 8 insertions(+), 15 deletions(-) rename LICENSE-APACHEv2 => LICENSE-APACHE-2.0 (98%) diff --git a/LICENSE-APACHEv2 b/LICENSE-APACHE-2.0 similarity index 98% rename from LICENSE-APACHEv2 rename to LICENSE-APACHE-2.0 index 7b6a3cb27..d64569567 100644 --- a/LICENSE-APACHEv2 +++ b/LICENSE-APACHE-2.0 @@ -1,6 +1,3 @@ -nim-waku is licensed under the Apache License version 2 -Copyright (c) 2018 Status Research & Development GmbH ------------------------------------------------------ Apache License Version 2.0, January 2004 @@ -190,7 +187,7 @@ Copyright (c) 2018 Status Research & Development GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2018 Status Research & Development GmbH + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index aab8020f0..d4c697062 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,25 +1,21 @@ -nim-waku is licensed under the MIT License -Copyright (c) 2018 Status Research & Development GmbH ------------------------------------------------------ - The MIT License (MIT) -Copyright (c) 2018 Status Research & Development GmbH +Copyright © 2025-2026 Logos Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal +of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file From 09034837e64146f51f05266774099c0ef8283c60 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:35:37 +0100 Subject: [PATCH 054/155] fix build_rln.sh script (#3704) --- scripts/build_rln.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_rln.sh b/scripts/build_rln.sh index 5e1b0caa5..1a8b63177 100755 --- a/scripts/build_rln.sh +++ b/scripts/build_rln.sh @@ -18,7 +18,7 @@ output_filename=$3 host_triplet=$(rustc --version --verbose | awk '/host:/{print $2}') tarball="${host_triplet}" - +tarball+="-stateless" tarball+="-rln.tar.gz" # Download the prebuilt rln library if it is available From 2c2d8e1c1512921353cde7582a1eb7faf2417423 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Thu, 5 Feb 2026 00:30:16 +0000 Subject: [PATCH 055/155] chore: update license files to comply with Logos licensing requirements --- LICENSE-APACHE-2.0 => LICENSE-APACHE | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE-APACHE-2.0 => LICENSE-APACHE (100%) diff --git a/LICENSE-APACHE-2.0 b/LICENSE-APACHE similarity index 100% rename from LICENSE-APACHE-2.0 rename to LICENSE-APACHE From 6421685ecad9039c4252c9b362531d54801715cc Mon Sep 17 00:00:00 2001 From: Darshan <35736874+darshankabariya@users.noreply.github.com> Date: Wed, 11 Feb 2026 03:00:57 +0530 Subject: [PATCH 056/155] chore: bump v0.38.0 (#3712) --- .github/workflows/windows-build.yml | 9 +- Makefile | 1 + scripts/build_rln.sh | 58 ++++------ scripts/install_nasm_in_windows.sh | 37 +++++++ scripts/regenerate_anvil_state.sh | 104 ++++++++++++++++++ tests/testlib/wakunode.nim | 4 +- tests/waku_core/test_message_digest.nim | 8 +- ...ployed-contracts-mint-and-approved.json.gz | Bin 118346 -> 118972 bytes tests/waku_store/test_wakunode_store.nim | 6 +- vendor/nim-dnsdisc | 2 +- vendor/nim-faststreams | 2 +- vendor/nim-http-utils | 2 +- vendor/nim-json-serialization | 2 +- vendor/nim-libp2p | 2 +- vendor/nim-lsquic | 2 +- vendor/nim-metrics | 2 +- vendor/nim-presto | 2 +- vendor/nim-serialization | 2 +- vendor/nim-sqlite3-abi | 2 +- vendor/nim-stew | 2 +- vendor/nim-testutils | 2 +- vendor/nim-toml-serialization | 2 +- vendor/nim-unittest2 | 2 +- vendor/nim-websock | 2 +- vendor/waku-rlnv2-contract | 2 +- vendor/zerokit | 2 +- .../send_service/relay_processor.nim | 4 +- waku/utils/requests.nim | 2 +- .../postgres_driver/postgres_driver.nim | 14 +-- .../postgres_driver/postgres_driver.nim | 14 +-- waku/waku_filter_v2/client.nim | 2 +- waku/waku_rln_relay/rln_relay.nim | 2 +- waku/waku_store_sync/reconciliation.nim | 11 +- 33 files changed, 225 insertions(+), 85 deletions(-) create mode 100644 scripts/install_nasm_in_windows.sh create mode 100755 scripts/regenerate_anvil_state.sh diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index ed6d2cb17..9c1b1eab0 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -33,6 +33,7 @@ jobs: make cmake upx + unzip mingw-w64-x86_64-rust mingw-w64-x86_64-postgresql mingw-w64-x86_64-gcc @@ -44,6 +45,12 @@ jobs: mingw-w64-x86_64-cmake mingw-w64-x86_64-llvm mingw-w64-x86_64-clang + mingw-w64-x86_64-nasm + + - name: Manually install nasm + run: | + bash scripts/install_nasm_in_windows.sh + source $HOME/.bashrc - name: Add UPX to PATH run: | @@ -54,7 +61,7 @@ jobs: - name: Verify dependencies run: | - which upx gcc g++ make cmake cargo rustc python + which upx gcc g++ make cmake cargo rustc python nasm - name: Updating submodules run: git submodule update --init --recursive diff --git a/Makefile b/Makefile index 13882253e..6457b3c0f 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ ifeq ($(detected_OS),Windows) LIBS = -lws2_32 -lbcrypt -liphlpapi -luserenv -lntdll -lminiupnpc -lnatpmp -lpq NIM_PARAMS += $(foreach lib,$(LIBS),--passL:"$(lib)") + NIM_PARAMS += --passL:"-Wl,--allow-multiple-definition" export PATH := /c/msys64/usr/bin:/c/msys64/mingw64/bin:/c/msys64/usr/lib:/c/msys64/mingw64/lib:$(PATH) diff --git a/scripts/build_rln.sh b/scripts/build_rln.sh index 1a8b63177..b36ebe807 100755 --- a/scripts/build_rln.sh +++ b/scripts/build_rln.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# This script is used to build the rln library for the current platform, or download it from the -# release page if it is available. +# This script is used to build the rln library for the current platform. +# Previously downloaded prebuilt binaries, but due to compatibility issues +# we now always build from source. set -e @@ -14,41 +15,26 @@ output_filename=$3 [[ -z "${rln_version}" ]] && { echo "No rln version specified"; exit 1; } [[ -z "${output_filename}" ]] && { echo "No output filename specified"; exit 1; } -# Get the host triplet -host_triplet=$(rustc --version --verbose | awk '/host:/{print $2}') +echo "Building RLN library from source (version ${rln_version})..." -tarball="${host_triplet}" -tarball+="-stateless" -tarball+="-rln.tar.gz" +# Check if submodule version = version in Makefile +cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" -# Download the prebuilt rln library if it is available -if curl --silent --fail-with-body -L \ - "https://github.com/vacp2p/zerokit/releases/download/$rln_version/$tarball" \ - -o "${tarball}"; -then - echo "Downloaded ${tarball}" - tar -xzf "${tarball}" - mv "release/librln.a" "${output_filename}" - rm -rf "${tarball}" release +detected_OS=$(uname -s) +if [[ "$detected_OS" == MINGW* || "$detected_OS" == MSYS* ]]; then + submodule_version=$(cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" | sed -n 's/.*"name":"rln","version":"\([^"]*\)".*/\1/p') else - echo "Failed to download ${tarball}" - # Build rln instead - # first, check if submodule version = version in Makefile - cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" - - detected_OS=$(uname -s) - if [[ "$detected_OS" == MINGW* || "$detected_OS" == MSYS* ]]; then - submodule_version=$(cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" | sed -n 's/.*"name":"rln","version":"\([^"]*\)".*/\1/p') - else - submodule_version=$(cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" | jq -r '.packages[] | select(.name == "rln") | .version') - fi - - if [[ "v${submodule_version}" != "${rln_version}" ]]; then - echo "Submodule version (v${submodule_version}) does not match version in Makefile (${rln_version})" - echo "Please update the submodule to ${rln_version}" - exit 1 - fi - # if submodule version = version in Makefile, build rln - cargo build --release -p rln --manifest-path "${build_dir}/rln/Cargo.toml" - cp "${build_dir}/target/release/librln.a" "${output_filename}" + submodule_version=$(cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" | jq -r '.packages[] | select(.name == "rln") | .version') fi + +if [[ "v${submodule_version}" != "${rln_version}" ]]; then + echo "Submodule version (v${submodule_version}) does not match version in Makefile (${rln_version})" + echo "Please update the submodule to ${rln_version}" + exit 1 +fi + +# Build rln from source +cargo build --release -p rln --manifest-path "${build_dir}/rln/Cargo.toml" +cp "${build_dir}/target/release/librln.a" "${output_filename}" + +echo "Successfully built ${output_filename}" diff --git a/scripts/install_nasm_in_windows.sh b/scripts/install_nasm_in_windows.sh new file mode 100644 index 000000000..2bba5ecd4 --- /dev/null +++ b/scripts/install_nasm_in_windows.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env sh +set -e + +NASM_VERSION="2.16.01" +NASM_ZIP="nasm-${NASM_VERSION}-win64.zip" +NASM_URL="https://www.nasm.us/pub/nasm/releasebuilds/${NASM_VERSION}/win64/${NASM_ZIP}" + +INSTALL_DIR="$HOME/.local/nasm" +BIN_DIR="$INSTALL_DIR/bin" + +echo "Installing NASM ${NASM_VERSION}..." + +# Create directories +mkdir -p "$BIN_DIR" +cd "$INSTALL_DIR" + +# Download +if [ ! -f "$NASM_ZIP" ]; then + echo "Downloading NASM..." + curl -LO "$NASM_URL" +fi + +# Extract +echo "Extracting..." +unzip -o "$NASM_ZIP" + +# Move binaries +cp nasm-*/nasm.exe "$BIN_DIR/" +cp nasm-*/ndisasm.exe "$BIN_DIR/" + +# Add to PATH in bashrc (idempotent) +if ! grep -q 'nasm/bin' "$HOME/.bashrc"; then + echo '' >> "$HOME/.bashrc" + echo '# NASM' >> "$HOME/.bashrc" + echo 'export PATH="$HOME/.local/nasm/bin:$PATH"' >> "$HOME/.bashrc" +fi + diff --git a/scripts/regenerate_anvil_state.sh b/scripts/regenerate_anvil_state.sh new file mode 100755 index 000000000..9474591d9 --- /dev/null +++ b/scripts/regenerate_anvil_state.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash + +# Simple script to regenerate the Anvil state file +# This creates a state file compatible with the current Foundry version + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +STATE_DIR="$PROJECT_ROOT/tests/waku_rln_relay/anvil_state" +STATE_FILE="$STATE_DIR/state-deployed-contracts-mint-and-approved.json" +STATE_FILE_GZ="${STATE_FILE}.gz" + +echo "===================================" +echo "Anvil State File Regeneration Tool" +echo "===================================" +echo "" + +# Check if Foundry is installed +if ! command -v anvil &> /dev/null; then + echo "ERROR: anvil is not installed!" + echo "Please run: make rln-deps" + exit 1 +fi + +ANVIL_VERSION=$(anvil --version 2>/dev/null | head -n1) +echo "Using Foundry: $ANVIL_VERSION" +echo "" + +# Backup existing state file +if [ -f "$STATE_FILE_GZ" ]; then + BACKUP_FILE="${STATE_FILE_GZ}.backup-$(date +%Y%m%d-%H%M%S)" + echo "Backing up existing state file to: $(basename $BACKUP_FILE)" + cp "$STATE_FILE_GZ" "$BACKUP_FILE" +fi + +# Remove old state files +rm -f "$STATE_FILE" "$STATE_FILE_GZ" + +echo "" +echo "Running test to generate fresh state file..." +echo "This will:" +echo " 1. Build RLN library" +echo " 2. Start Anvil with state dump enabled" +echo " 3. Deploy contracts" +echo " 4. Save state and compress it" +echo "" + +cd "$PROJECT_ROOT" + +# Run a single test that deploys contracts +# The test framework will handle state dump +make test tests/waku_rln_relay/test_rln_group_manager_onchain.nim "RLN instances" || { + echo "" + echo "Test execution completed (exit status: $?)" + echo "Checking if state file was generated..." +} + +# Check if state file was created +if [ -f "$STATE_FILE" ]; then + echo "" + echo "✓ State file generated: $STATE_FILE" + + # Compress it + gzip -c "$STATE_FILE" > "$STATE_FILE_GZ" + echo "✓ Compressed: $STATE_FILE_GZ" + + # File sizes + STATE_SIZE=$(du -h "$STATE_FILE" | cut -f1) + GZ_SIZE=$(du -h "$STATE_FILE_GZ" | cut -f1) + echo "" + echo "File sizes:" + echo " Uncompressed: $STATE_SIZE" + echo " Compressed: $GZ_SIZE" + + # Optionally remove uncompressed + echo "" + read -p "Remove uncompressed state file? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + rm "$STATE_FILE" + echo "✓ Removed uncompressed file" + fi + + echo "" + echo "============================================" + echo "✓ SUCCESS! State file regenerated" + echo "============================================" + echo "" + echo "Next steps:" + echo " 1. Test locally: make test tests/node/test_wakunode_lightpush.nim" + echo " 2. If tests pass, commit: git add $STATE_FILE_GZ" + echo " 3. Push and verify CI passes" + echo "" +else + echo "" + echo "============================================" + echo "✗ ERROR: State file was not generated" + echo "============================================" + echo "" + echo "The state file should have been created at: $STATE_FILE" + echo "Please check the test output above for errors." + exit 1 +fi diff --git a/tests/testlib/wakunode.nim b/tests/testlib/wakunode.nim index 36aacce03..f59546ec8 100644 --- a/tests/testlib/wakunode.nim +++ b/tests/testlib/wakunode.nim @@ -27,7 +27,7 @@ import # TODO: migrate to usage of a test cluster conf proc defaultTestWakuConfBuilder*(): WakuConfBuilder = var builder = WakuConfBuilder.init() - builder.withP2pTcpPort(Port(60000)) + builder.withP2pTcpPort(Port(0)) builder.withP2pListenAddress(parseIpAddress("0.0.0.0")) builder.restServerConf.withListenAddress(parseIpAddress("127.0.0.1")) builder.withDnsAddrsNameServers( @@ -80,7 +80,7 @@ proc newTestWakuNode*( # Update extPort to default value if it's missing and there's an extIp or a DNS domain let extPort = if (extIp.isSome() or dns4DomainName.isSome()) and extPort.isNone(): - some(Port(60000)) + some(Port(0)) else: extPort diff --git a/tests/waku_core/test_message_digest.nim b/tests/waku_core/test_message_digest.nim index 1d1f71225..22a10d84d 100644 --- a/tests/waku_core/test_message_digest.nim +++ b/tests/waku_core/test_message_digest.nim @@ -35,7 +35,7 @@ suite "Waku Message - Deterministic hashing": byteutils.toHex(message.payload) == "010203045445535405060708" byteutils.toHex(message.meta) == "" byteutils.toHex(toBytesBE(uint64(message.timestamp))) == "175789bfa23f8400" - messageHash.toHex() == + byteutils.toHex(messageHash) == "cccab07fed94181c83937c8ca8340c9108492b7ede354a6d95421ad34141fd37" test "digest computation - meta field (12 bytes)": @@ -69,7 +69,7 @@ suite "Waku Message - Deterministic hashing": byteutils.toHex(message.payload) == "010203045445535405060708" byteutils.toHex(message.meta) == "73757065722d736563726574" byteutils.toHex(toBytesBE(uint64(message.timestamp))) == "175789bfa23f8400" - messageHash.toHex() == + byteutils.toHex(messageHash) == "b9b4852f9d8c489846e8bfc6c5ca6a1a8d460a40d28832a966e029eb39619199" test "digest computation - meta field (64 bytes)": @@ -104,7 +104,7 @@ suite "Waku Message - Deterministic hashing": byteutils.toHex(message.meta) == "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" byteutils.toHex(toBytesBE(uint64(message.timestamp))) == "175789bfa23f8400" - messageHash.toHex() == + byteutils.toHex(messageHash) == "653460d04f66c5b11814d235152f4f246e6f03ef80a305a825913636fbafd0ba" test "digest computation - zero length payload": @@ -132,7 +132,7 @@ suite "Waku Message - Deterministic hashing": ## Then check: - messageHash.toHex() == + byteutils.toHex(messageHash) == "0f6448cc23b2db6c696aa6ab4b693eff4cf3549ff346fe1dbeb281697396a09f" test "waku message - check meta size is enforced": diff --git a/tests/waku_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 index ceb081c77788d7b5a3a933b2d0510303247694a1..b5fdebb741f2836e72a37eddc2e4cce6efe79b53 100644 GIT binary patch literal 118972 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%0DH@LYO;kGJ&VFipb80ePab@gq} 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

Y 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[vhcB}$QAdK7h~uvNm7smeS3``zD_;*p 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 VmB|RDAOku$xApL9e~9Hv{{jM|=LrA+ 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 diff --git a/tests/waku_store/test_wakunode_store.nim b/tests/waku_store/test_wakunode_store.nim index 7d1a44ecc..e30854906 100644 --- a/tests/waku_store/test_wakunode_store.nim +++ b/tests/waku_store/test_wakunode_store.nim @@ -386,7 +386,7 @@ procSuite "WakuNode - Store": let mountArchiveRes = server.mountArchive(archiveA) assert mountArchiveRes.isOk(), mountArchiveRes.error - waitFor server.mountStore((3, 500.millis)) + waitFor server.mountStore((3, 200.millis)) client.mountStoreClient() @@ -413,11 +413,11 @@ procSuite "WakuNode - Store": for count in 0 ..< 3: waitFor successProc() - waitFor sleepAsync(5.millis) + waitFor sleepAsync(1.millis) waitFor failsProc() - waitFor sleepAsync(500.millis) + waitFor sleepAsync(200.millis) for count in 0 ..< 3: waitFor successProc() diff --git a/vendor/nim-dnsdisc b/vendor/nim-dnsdisc index b71d029f4..203abd2b3 160000 --- a/vendor/nim-dnsdisc +++ b/vendor/nim-dnsdisc @@ -1 +1 @@ -Subproject commit b71d029f4da4ec56974d54c04518bada00e1b623 +Subproject commit 203abd2b3e758e0ea3ae325769b20a7e1bcd1010 diff --git a/vendor/nim-faststreams b/vendor/nim-faststreams index c3ac3f639..ce27581a3 160000 --- a/vendor/nim-faststreams +++ b/vendor/nim-faststreams @@ -1 +1 @@ -Subproject commit c3ac3f639ed1d62f59d3077d376a29c63ac9750c +Subproject commit ce27581a3e881f782f482cb66dc5b07a02bd615e diff --git a/vendor/nim-http-utils b/vendor/nim-http-utils index 79cbab146..c53852d9e 160000 --- a/vendor/nim-http-utils +++ b/vendor/nim-http-utils @@ -1 +1 @@ -Subproject commit 79cbab1460f4c0cdde2084589d017c43a3d7b4f1 +Subproject commit c53852d9e24205b6363bba517fa8ee7bde823691 diff --git a/vendor/nim-json-serialization b/vendor/nim-json-serialization index b65fd6a7e..c343b0e24 160000 --- a/vendor/nim-json-serialization +++ b/vendor/nim-json-serialization @@ -1 +1 @@ -Subproject commit b65fd6a7e64c864dabe40e7dfd6c7d07db0014ac +Subproject commit c343b0e243d9e17e2c40f3a8a24340f7c4a71d44 diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index eb7e6ff89..ca48c3718 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit eb7e6ff89889e41b57515f891ba82986c54809fb +Subproject commit ca48c3718246bb411ff0e354a70cb82d9a28de0d diff --git a/vendor/nim-lsquic b/vendor/nim-lsquic index f3fe33462..4fb03ee7b 160000 --- a/vendor/nim-lsquic +++ b/vendor/nim-lsquic @@ -1 +1 @@ -Subproject commit f3fe33462601ea34eb2e8e9c357c92e61f8d121b +Subproject commit 4fb03ee7bfb39aecb3316889fdcb60bec3d0936f diff --git a/vendor/nim-metrics b/vendor/nim-metrics index ecf64c607..11d0cddfb 160000 --- a/vendor/nim-metrics +++ b/vendor/nim-metrics @@ -1 +1 @@ -Subproject commit ecf64c6078d1276d3b7d9b3d931fbdb70004db11 +Subproject commit 11d0cddfb0e711aa2a8c75d1892ae24a64c299fc diff --git a/vendor/nim-presto b/vendor/nim-presto index 92b1c7ff1..d66043dd7 160000 --- a/vendor/nim-presto +++ b/vendor/nim-presto @@ -1 +1 @@ -Subproject commit 92b1c7ff141e6920e1f8a98a14c35c1fa098e3be +Subproject commit d66043dd7ede146442e6c39720c76a20bde5225f diff --git a/vendor/nim-serialization b/vendor/nim-serialization index 6f525d544..b0f2fa329 160000 --- a/vendor/nim-serialization +++ b/vendor/nim-serialization @@ -1 +1 @@ -Subproject commit 6f525d5447d97256750ca7856faead03e562ed20 +Subproject commit b0f2fa32960ea532a184394b0f27be37bd80248b diff --git a/vendor/nim-sqlite3-abi b/vendor/nim-sqlite3-abi index bdf01cf42..89ba51f55 160000 --- a/vendor/nim-sqlite3-abi +++ b/vendor/nim-sqlite3-abi @@ -1 +1 @@ -Subproject commit bdf01cf4236fb40788f0733466cdf6708783cbac +Subproject commit 89ba51f557414d3a3e17ab3df8270e1bdaa3ca2a diff --git a/vendor/nim-stew b/vendor/nim-stew index e57400149..b66168735 160000 --- a/vendor/nim-stew +++ b/vendor/nim-stew @@ -1 +1 @@ -Subproject commit e5740014961438610d336cd81706582dbf2c96f0 +Subproject commit b66168735d6f3841c5239c3169d3fe5fe98b1257 diff --git a/vendor/nim-testutils b/vendor/nim-testutils index 94d68e796..e4d37dc16 160000 --- a/vendor/nim-testutils +++ b/vendor/nim-testutils @@ -1 +1 @@ -Subproject commit 94d68e796c045d5b37cabc6be32d7bfa168f8857 +Subproject commit e4d37dc1652d5c63afb89907efb5a5e812261797 diff --git a/vendor/nim-toml-serialization b/vendor/nim-toml-serialization index fea85b27f..b5b387e6f 160000 --- a/vendor/nim-toml-serialization +++ b/vendor/nim-toml-serialization @@ -1 +1 @@ -Subproject commit fea85b27f0badcf617033ca1bc05444b5fd8aa7a +Subproject commit b5b387e6fb2a7cc75d54a269b07cc6218361bd46 diff --git a/vendor/nim-unittest2 b/vendor/nim-unittest2 index 8b51e99b4..26f2ef3ae 160000 --- a/vendor/nim-unittest2 +++ b/vendor/nim-unittest2 @@ -1 +1 @@ -Subproject commit 8b51e99b4a57fcfb31689230e75595f024543024 +Subproject commit 26f2ef3ae0ec72a2a75bfe557e02e88f6a31c189 diff --git a/vendor/nim-websock b/vendor/nim-websock index ebe308a79..35ae76f15 160000 --- a/vendor/nim-websock +++ b/vendor/nim-websock @@ -1 +1 @@ -Subproject commit ebe308a79a7b440a11dfbe74f352be86a3883508 +Subproject commit 35ae76f1559e835c80f9c1a3943bf995d3dd9eb5 diff --git a/vendor/waku-rlnv2-contract b/vendor/waku-rlnv2-contract index 8a338f354..d9906ef40 160000 --- a/vendor/waku-rlnv2-contract +++ b/vendor/waku-rlnv2-contract @@ -1 +1 @@ -Subproject commit 8a338f354481e8a3f3d64a72e38fad4c62e32dcd +Subproject commit d9906ef40f1e113fcf51de4ad27c61aa45375c2d diff --git a/vendor/zerokit b/vendor/zerokit index 70c79fbc9..a4bb3feb5 160000 --- a/vendor/zerokit +++ b/vendor/zerokit @@ -1 +1 @@ -Subproject commit 70c79fbc989d4f87d9352b2f4bddcb60ebe55b19 +Subproject commit a4bb3feb5054e6fd24827adf204493e6e173437b diff --git a/waku/node/delivery_service/send_service/relay_processor.nim b/waku/node/delivery_service/send_service/relay_processor.nim index 94cb63776..974c22f6c 100644 --- a/waku/node/delivery_service/send_service/relay_processor.nim +++ b/waku/node/delivery_service/send_service/relay_processor.nim @@ -70,7 +70,9 @@ method sendImpl*(self: RelaySendProcessor, task: DeliveryTask) {.async.} = if noOfPublishedPeers > 0: info "Message propagated via Relay", - requestId = task.requestId, msgHash = task.msgHash.to0xHex(), noOfPeers = noOfPublishedPeers + requestId = task.requestId, + msgHash = task.msgHash.to0xHex(), + noOfPeers = noOfPublishedPeers task.state = DeliveryState.SuccessfullyPropagated task.deliveryTime = Moment.now() else: diff --git a/waku/utils/requests.nim b/waku/utils/requests.nim index 5e5b9d960..d9afd2887 100644 --- a/waku/utils/requests.nim +++ b/waku/utils/requests.nim @@ -7,4 +7,4 @@ import bearssl/rand, stew/byteutils proc generateRequestId*(rng: ref HmacDrbgContext): string = var bytes: array[10, byte] hmacDrbgGenerate(rng[], bytes) - return toHex(bytes) + return byteutils.toHex(bytes) diff --git a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim index 9b0e14c84..4877cb126 100644 --- a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim @@ -297,13 +297,13 @@ method put*( pubsubTopic: PubsubTopic, message: WakuMessage, ): Future[ArchiveDriverResult[void]] {.async.} = - let messageHash = toHex(messageHash) + let messageHash = byteutils.toHex(messageHash) let contentTopic = message.contentTopic - let payload = toHex(message.payload) + let payload = byteutils.toHex(message.payload) let version = $message.version let timestamp = $message.timestamp - let meta = toHex(message.meta) + let meta = byteutils.toHex(message.meta) trace "put PostgresDriver", messageHash, contentTopic, payload, version, timestamp, meta @@ -439,7 +439,7 @@ proc getMessagesArbitraryQuery( var args: seq[string] if cursor.isSome(): - let hashHex = toHex(cursor.get()) + let hashHex = byteutils.toHex(cursor.get()) let timeCursor = ?await s.getTimeCursor(hashHex) @@ -520,7 +520,7 @@ proc getMessageHashesArbitraryQuery( var args: seq[string] if cursor.isSome(): - let hashHex = toHex(cursor.get()) + let hashHex = byteutils.toHex(cursor.get()) let timeCursor = ?await s.getTimeCursor(hashHex) @@ -630,7 +630,7 @@ proc getMessagesPreparedStmt( return ok(rows) - let hashHex = toHex(cursor.get()) + let hashHex = byteutils.toHex(cursor.get()) let timeCursor = ?await s.getTimeCursor(hashHex) @@ -723,7 +723,7 @@ proc getMessageHashesPreparedStmt( return ok(rows) - let hashHex = toHex(cursor.get()) + let hashHex = byteutils.toHex(cursor.get()) let timeCursor = ?await s.getTimeCursor(hashHex) diff --git a/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim index 1a39c1267..a6784e4f8 100644 --- a/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim @@ -213,13 +213,13 @@ method put*( messageHash: WakuMessageHash, receivedTime: Timestamp, ): Future[ArchiveDriverResult[void]] {.async.} = - let digest = toHex(digest.data) - let messageHash = toHex(messageHash) + let digest = byteutils.toHex(digest.data) + let messageHash = byteutils.toHex(messageHash) let contentTopic = message.contentTopic - let payload = toHex(message.payload) + let payload = byteutils.toHex(message.payload) let version = $message.version let timestamp = $message.timestamp - let meta = toHex(message.meta) + let meta = byteutils.toHex(message.meta) trace "put PostgresDriver", timestamp = timestamp @@ -312,7 +312,7 @@ proc getMessagesArbitraryQuery( args.add(pubsubTopic.get()) if cursor.isSome(): - let hashHex = toHex(cursor.get().hash) + let hashHex = byteutils.toHex(cursor.get().hash) var entree: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] proc entreeCallback(pqResult: ptr PGresult) = @@ -463,7 +463,7 @@ proc getMessagesPreparedStmt( let limit = $maxPageSize if cursor.isSome(): - let hash = toHex(cursor.get().hash) + let hash = byteutils.toHex(cursor.get().hash) var entree: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] @@ -576,7 +576,7 @@ proc getMessagesV2PreparedStmt( var stmtDef = if ascOrder: SelectWithCursorV2AscStmtDef else: SelectWithCursorV2DescStmtDef - let digest = toHex(cursor.get().digest.data) + let digest = byteutils.toHex(cursor.get().digest.data) let timestamp = $cursor.get().storeTime ( diff --git a/waku/waku_filter_v2/client.nim b/waku/waku_filter_v2/client.nim index 323cc6da8..ba8cd3d0c 100644 --- a/waku/waku_filter_v2/client.nim +++ b/waku/waku_filter_v2/client.nim @@ -29,7 +29,7 @@ type WakuFilterClient* = ref object of LPProtocol func generateRequestId(rng: ref HmacDrbgContext): string = var bytes: array[10, byte] hmacDrbgGenerate(rng[], bytes) - return toHex(bytes) + return byteutils.toHex(bytes) proc sendSubscribeRequest( wfc: WakuFilterClient, diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index 8758a7bcd..5c893e2a2 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -346,7 +346,7 @@ proc generateRlnValidator*( let validationRes = wakuRlnRelay.validateMessageAndUpdateLog(message) let - proof = toHex(msgProof.proof) + proof = byteutils.toHex(msgProof.proof) epoch = fromEpoch(msgProof.epoch) root = inHex(msgProof.merkleRoot) shareX = inHex(msgProof.shareX) diff --git a/waku/waku_store_sync/reconciliation.nim b/waku/waku_store_sync/reconciliation.nim index 0cc15d0df..23f513322 100644 --- a/waku/waku_store_sync/reconciliation.nim +++ b/waku/waku_store_sync/reconciliation.nim @@ -79,7 +79,8 @@ proc messageIngress*( let id = SyncID(time: msg.timestamp, hash: msgHash) self.storage.insert(id, pubsubTopic, msg.contentTopic).isOkOr: - error "failed to insert new message", msg_hash = $id.hash.toHex(), error = $error + error "failed to insert new message", + msg_hash = byteutils.toHex(id.hash), error = $error proc messageIngress*( self: SyncReconciliation, @@ -87,7 +88,7 @@ proc messageIngress*( pubsubTopic: PubsubTopic, msg: WakuMessage, ) = - trace "message ingress", msg_hash = msgHash.toHex(), msg = msg + trace "message ingress", msg_hash = byteutils.toHex(msgHash), msg = msg if msg.ephemeral: return @@ -95,7 +96,8 @@ proc messageIngress*( let id = SyncID(time: msg.timestamp, hash: msgHash) self.storage.insert(id, pubsubTopic, msg.contentTopic).isOkOr: - error "failed to insert new message", msg_hash = $id.hash.toHex(), error = $error + error "failed to insert new message", + msg_hash = byteutils.toHex(id.hash), error = $error proc messageIngress*( self: SyncReconciliation, @@ -104,7 +106,8 @@ proc messageIngress*( contentTopic: ContentTopic, ) = self.storage.insert(id, pubsubTopic, contentTopic).isOkOr: - error "failed to insert new message", msg_hash = $id.hash.toHex(), error = $error + error "failed to insert new message", + msg_hash = byteutils.toHex(id.hash), error = $error proc preProcessPayload( self: SyncReconciliation, payload: RangesData From a8bdbca98a3d2eeee8aaff05ce037cd1dc32bde9 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Wed, 11 Feb 2026 10:36:37 -0300 Subject: [PATCH 057/155] Simplify NodeHealthMonitor creation (#3716) Simplify NodeHealthMonitor creation * Force NodeHealthMonitor.new() to set up a WakuNode * Remove all checks for isNil(node) in NodeHealthMonitor * Fix tests to use the new NodeHealthMonitor.new() Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> --- tests/test_waku_keepalive.nim | 3 +- tests/wakunode_rest/test_rest_health.nim | 22 ++---- waku/factory/waku.nim | 20 ++---- .../health_monitor/node_health_monitor.nim | 69 ++++++------------- 4 files changed, 35 insertions(+), 79 deletions(-) diff --git a/tests/test_waku_keepalive.nim b/tests/test_waku_keepalive.nim index c12f20a05..5d8402268 100644 --- a/tests/test_waku_keepalive.nim +++ b/tests/test_waku_keepalive.nim @@ -44,8 +44,7 @@ suite "Waku Keepalive": await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) - let healthMonitor = NodeHealthMonitor() - healthMonitor.setNodeToHealthMonitor(node1) + let healthMonitor = NodeHealthMonitor.new(node1) healthMonitor.startKeepalive(2.seconds).isOkOr: assert false, "Failed to start keepalive" diff --git a/tests/wakunode_rest/test_rest_health.nim b/tests/wakunode_rest/test_rest_health.nim index ed8269f55..2a70fee5f 100644 --- a/tests/wakunode_rest/test_rest_health.nim +++ b/tests/wakunode_rest/test_rest_health.nim @@ -50,33 +50,22 @@ suite "Waku v2 REST API - health": asyncTest "Get node health info - GET /health": # Given let node = testWakuNode() - let healthMonitor = NodeHealthMonitor() await node.start() (await node.mountRelay()).isOkOr: assert false, "Failed to mount relay" - healthMonitor.setOverallHealth(HealthStatus.INITIALIZING) - var restPort = Port(0) let restAddress = parseIpAddress("0.0.0.0") let restServer = WakuRestServerRef.init(restAddress, restPort).tryGet() restPort = restServer.httpServer.address.port # update with bound port for client use + let healthMonitor = NodeHealthMonitor.new(node) + installHealthApiHandler(restServer.router, healthMonitor) restServer.start() let client = newRestHttpClient(initTAddress(restAddress, restPort)) - # When - var response = await client.healthCheck() - - # Then - check: - response.status == 200 - $response.contentType == $MIMETYPE_JSON - response.data == - HealthReport(nodeHealth: HealthStatus.INITIALIZING, protocolsHealth: @[]) - - # now kick in rln (currently the only check for health) + # kick in rln (currently the only check for health) await node.mountRlnRelay( getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) ) @@ -84,10 +73,11 @@ suite "Waku v2 REST API - health": node.mountLightPushClient() await node.mountFilterClient() - healthMonitor.setNodeToHealthMonitor(node) + # We don't have a Waku, so we need to set the overall health to READY here in its behalf healthMonitor.setOverallHealth(HealthStatus.READY) + # When - response = await client.healthCheck() + var response = await client.healthCheck() # Then check: diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index c452d44c5..3748847f1 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -172,7 +172,13 @@ proc new*( ?wakuConf.validate() wakuConf.logConf() - let healthMonitor = NodeHealthMonitor.new(wakuConf.dnsAddrsNameServers) + let relay = newCircuitRelay(wakuConf.circuitRelayClient) + + let node = (await setupNode(wakuConf, rng, relay)).valueOr: + error "Failed setting up node", error = $error + return err("Failed setting up node: " & $error) + + let healthMonitor = NodeHealthMonitor.new(node, wakuConf.dnsAddrsNameServers) let restServer: WakuRestServerRef = if wakuConf.restServerConf.isSome(): @@ -186,18 +192,6 @@ proc new*( else: nil - var relay = newCircuitRelay(wakuConf.circuitRelayClient) - - let node = (await setupNode(wakuConf, rng, relay)).valueOr: - error "Failed setting up node", error = $error - return err("Failed setting up node: " & $error) - - healthMonitor.setNodeToHealthMonitor(node) - healthMonitor.onlineMonitor.setPeerStoreToOnlineMonitor(node.switch.peerStore) - healthMonitor.onlineMonitor.addOnlineStateObserver( - node.peerManager.getOnlineStateObserver() - ) - node.setupAppCallbacks(wakuConf, appCallbacks).isOkOr: error "Failed setting up app callbacks", error = error return err("Failed setting up app callbacks: " & $error) diff --git a/waku/node/health_monitor/node_health_monitor.nim b/waku/node/health_monitor/node_health_monitor.nim index eb5d0ed8c..4b13dfd3d 100644 --- a/waku/node/health_monitor/node_health_monitor.nim +++ b/waku/node/health_monitor/node_health_monitor.nim @@ -33,14 +33,8 @@ type onlineMonitor*: OnlineMonitor keepAliveFut: Future[void] -template checkWakuNodeNotNil(node: WakuNode, p: ProtocolHealth): untyped = - if node.isNil(): - warn "WakuNode is not set, cannot check health", protocol_health_instance = $p - return p.notMounted() - proc getRelayHealth(hm: NodeHealthMonitor): ProtocolHealth = var p = ProtocolHealth.init("Relay") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuRelay == nil: return p.notMounted() @@ -55,10 +49,6 @@ proc getRelayHealth(hm: NodeHealthMonitor): ProtocolHealth = proc getRlnRelayHealth(hm: NodeHealthMonitor): Future[ProtocolHealth] {.async.} = var p = ProtocolHealth.init("Rln Relay") - if hm.node.isNil(): - warn "WakuNode is not set, cannot check health", protocol_health_instance = $p - return p.notMounted() - if hm.node.wakuRlnRelay.isNil(): return p.notMounted() @@ -83,7 +73,6 @@ proc getLightpushHealth( hm: NodeHealthMonitor, relayHealth: HealthStatus ): ProtocolHealth = var p = ProtocolHealth.init("Lightpush") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuLightPush == nil: return p.notMounted() @@ -97,7 +86,6 @@ proc getLightpushClientHealth( hm: NodeHealthMonitor, relayHealth: HealthStatus ): ProtocolHealth = var p = ProtocolHealth.init("Lightpush Client") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuLightpushClient == nil: return p.notMounted() @@ -115,7 +103,6 @@ proc getLegacyLightpushHealth( hm: NodeHealthMonitor, relayHealth: HealthStatus ): ProtocolHealth = var p = ProtocolHealth.init("Legacy Lightpush") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuLegacyLightPush == nil: return p.notMounted() @@ -129,7 +116,6 @@ proc getLegacyLightpushClientHealth( hm: NodeHealthMonitor, relayHealth: HealthStatus ): ProtocolHealth = var p = ProtocolHealth.init("Legacy Lightpush Client") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuLegacyLightpushClient == nil: return p.notMounted() @@ -142,7 +128,6 @@ proc getLegacyLightpushClientHealth( proc getFilterHealth(hm: NodeHealthMonitor, relayHealth: HealthStatus): ProtocolHealth = var p = ProtocolHealth.init("Filter") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuFilter == nil: return p.notMounted() @@ -156,7 +141,6 @@ proc getFilterClientHealth( hm: NodeHealthMonitor, relayHealth: HealthStatus ): ProtocolHealth = var p = ProtocolHealth.init("Filter Client") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuFilterClient == nil: return p.notMounted() @@ -168,7 +152,6 @@ proc getFilterClientHealth( proc getStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = var p = ProtocolHealth.init("Store") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuStore == nil: return p.notMounted() @@ -177,7 +160,6 @@ proc getStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = proc getStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = var p = ProtocolHealth.init("Store Client") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuStoreClient == nil: return p.notMounted() @@ -191,7 +173,6 @@ proc getStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = proc getLegacyStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = var p = ProtocolHealth.init("Legacy Store") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuLegacyStore == nil: return p.notMounted() @@ -200,7 +181,6 @@ proc getLegacyStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = proc getLegacyStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = var p = ProtocolHealth.init("Legacy Store Client") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuLegacyStoreClient == nil: return p.notMounted() @@ -215,7 +195,6 @@ proc getLegacyStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = proc getPeerExchangeHealth(hm: NodeHealthMonitor): ProtocolHealth = var p = ProtocolHealth.init("Peer Exchange") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuPeerExchange == nil: return p.notMounted() @@ -224,7 +203,6 @@ proc getPeerExchangeHealth(hm: NodeHealthMonitor): ProtocolHealth = proc getRendezvousHealth(hm: NodeHealthMonitor): ProtocolHealth = var p = ProtocolHealth.init("Rendezvous") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuRendezvous == nil: return p.notMounted() @@ -236,7 +214,6 @@ proc getRendezvousHealth(hm: NodeHealthMonitor): ProtocolHealth = proc getMixHealth(hm: NodeHealthMonitor): ProtocolHealth = var p = ProtocolHealth.init("Mix") - checkWakuNodeNotNil(hm.node, p) if hm.node.wakuMix.isNil(): return p.notMounted() @@ -386,29 +363,25 @@ proc getNodeHealthReport*(hm: NodeHealthMonitor): Future[HealthReport] {.async.} var report: HealthReport report.nodeHealth = hm.nodeHealth - if not hm.node.isNil(): - let relayHealth = hm.getRelayHealth() - report.protocolsHealth.add(relayHealth) - report.protocolsHealth.add(await hm.getRlnRelayHealth()) - report.protocolsHealth.add(hm.getLightpushHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getLegacyLightpushHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getFilterHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getStoreHealth()) - report.protocolsHealth.add(hm.getLegacyStoreHealth()) - report.protocolsHealth.add(hm.getPeerExchangeHealth()) - report.protocolsHealth.add(hm.getRendezvousHealth()) - report.protocolsHealth.add(hm.getMixHealth()) + let relayHealth = hm.getRelayHealth() + report.protocolsHealth.add(relayHealth) + report.protocolsHealth.add(await hm.getRlnRelayHealth()) + report.protocolsHealth.add(hm.getLightpushHealth(relayHealth.health)) + report.protocolsHealth.add(hm.getLegacyLightpushHealth(relayHealth.health)) + report.protocolsHealth.add(hm.getFilterHealth(relayHealth.health)) + report.protocolsHealth.add(hm.getStoreHealth()) + report.protocolsHealth.add(hm.getLegacyStoreHealth()) + report.protocolsHealth.add(hm.getPeerExchangeHealth()) + report.protocolsHealth.add(hm.getRendezvousHealth()) + report.protocolsHealth.add(hm.getMixHealth()) - report.protocolsHealth.add(hm.getLightpushClientHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getLegacyLightpushClientHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getStoreClientHealth()) - report.protocolsHealth.add(hm.getLegacyStoreClientHealth()) - report.protocolsHealth.add(hm.getFilterClientHealth(relayHealth.health)) + report.protocolsHealth.add(hm.getLightpushClientHealth(relayHealth.health)) + report.protocolsHealth.add(hm.getLegacyLightpushClientHealth(relayHealth.health)) + report.protocolsHealth.add(hm.getStoreClientHealth()) + report.protocolsHealth.add(hm.getLegacyStoreClientHealth()) + report.protocolsHealth.add(hm.getFilterClientHealth(relayHealth.health)) return report -proc setNodeToHealthMonitor*(hm: NodeHealthMonitor, node: WakuNode) = - hm.node = node - proc setOverallHealth*(hm: NodeHealthMonitor, health: HealthStatus) = hm.nodeHealth = health @@ -427,10 +400,10 @@ proc stopHealthMonitor*(hm: NodeHealthMonitor) {.async.} = proc new*( T: type NodeHealthMonitor, + node: WakuNode, dnsNameServers = @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")], ): T = - T( - nodeHealth: INITIALIZING, - node: nil, - onlineMonitor: OnlineMonitor.init(dnsNameServers), - ) + let om = OnlineMonitor.init(dnsNameServers) + om.setPeerStoreToOnlineMonitor(node.switch.peerStore) + om.addOnlineStateObserver(node.peerManager.getOnlineStateObserver()) + T(nodeHealth: INITIALIZING, node: node, onlineMonitor: om) From dd8dc7429d724dff6b7254bd5206a2811223461a Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:19:58 +0100 Subject: [PATCH 058/155] canary exits with error if ping fails (#3711) --- apps/wakucanary/wakucanary.nim | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/wakucanary/wakucanary.nim b/apps/wakucanary/wakucanary.nim index 6e02c2a8f..40bf4db45 100644 --- a/apps/wakucanary/wakucanary.nim +++ b/apps/wakucanary/wakucanary.nim @@ -278,6 +278,10 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = pingSuccess = false error "Ping operation failed or timed out", error = exc.msg + if not pingSuccess: + error "Ping to the node failed", peerId = peer.peerId, conStatus = $conStatus + quit(QuitFailure) + if conStatus in [Connected, CanConnect]: let nodeProtocols = lp2pPeerStore[ProtoBook][peer.peerId] @@ -285,11 +289,6 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = error "Not all protocols are supported", expected = conf.protocols, supported = nodeProtocols quit(QuitFailure) - - # Check ping result if ping was enabled - if conf.ping and not pingSuccess: - error "Node is reachable and supports protocols but ping failed - connection may be unstable" - quit(QuitFailure) elif conStatus == CannotConnect: error "Could not connect", peerId = peer.peerId quit(QuitFailure) From 1fb4d1eab0460b3e033e0ab87bd4ac3b6966f3c9 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Thu, 12 Feb 2026 14:52:39 -0300 Subject: [PATCH 059/155] feat: implement Waku API Health spec (#3689) * Fix protocol strength metric to consider connected peers only * Remove polling loop; event-driven node connection health updates * Remove 10s WakuRelay topic health polling loop; now event-driven * Change NodeHealthStatus to ConnectionStatus * Change new nodeState (rest API /health) field to connectionStatus * Add getSyncProtocolHealthInfo and getSyncNodeHealthReport * Add ConnectionStatusChangeEvent * Add RequestHealthReport * Refactor sync/async protocol health queries in the health monitor * Add EventRelayTopicHealthChange * Add EventWakuPeer emitted by PeerManager * Add Edge support for topics health requests and events * Rename "RelayTopic" -> "Topic" * Add RequestContentTopicsHealth sync request * Add EventContentTopicHealthChange * Rename RequestTopicsHealth -> RequestShardTopicsHealth * Remove health check gating from checkApiAvailability * Add basic health smoke tests * Other misc improvements, refactors, fixes Co-authored-by: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> --- .../json_connection_status_change_event.nim | 19 + library/libwaku.nim | 8 + tests/api/test_all.nim | 2 +- tests/api/test_api_health.nim | 296 +++++++++ tests/api/test_api_send.nim | 9 +- tests/node/test_all.nim | 3 +- tests/node/test_wakunode_health_monitor.nim | 301 +++++++++ tests/waku_relay/utils.nim | 18 +- tests/wakunode_rest/test_rest_health.nim | 63 +- waku/api/api.nim | 16 +- waku/api/types.nim | 8 +- waku/common/waku_protocol.nim | 24 + waku/events/events.nim | 4 +- waku/events/health_events.nim | 27 + waku/events/peer_events.nim | 13 + waku/factory/app_callbacks.nim | 3 +- waku/factory/builder.nim | 3 +- waku/factory/waku.nim | 165 +++-- .../send_service/relay_processor.nim | 4 +- waku/node/health_monitor.nim | 9 +- .../node/health_monitor/connection_status.nim | 15 + waku/node/health_monitor/health_report.nim | 10 + .../health_monitor/node_health_monitor.nim | 598 ++++++++++++++---- waku/node/health_monitor/protocol_health.nim | 10 +- waku/node/peer_manager/peer_manager.nim | 128 +++- waku/node/peer_manager/waku_peer_store.nim | 7 +- waku/node/waku_node.nim | 126 +++- waku/requests/health_request.nim | 21 - waku/requests/health_requests.nim | 39 ++ waku/requests/requests.nim | 4 +- waku/rest_api/endpoint/health/types.nim | 22 +- waku/waku_relay/protocol.nim | 146 +++-- 32 files changed, 1727 insertions(+), 394 deletions(-) create mode 100644 library/events/json_connection_status_change_event.nim create mode 100644 tests/api/test_api_health.nim create mode 100644 tests/node/test_wakunode_health_monitor.nim create mode 100644 waku/common/waku_protocol.nim create mode 100644 waku/events/health_events.nim create mode 100644 waku/events/peer_events.nim create mode 100644 waku/node/health_monitor/connection_status.nim create mode 100644 waku/node/health_monitor/health_report.nim delete mode 100644 waku/requests/health_request.nim create mode 100644 waku/requests/health_requests.nim diff --git a/library/events/json_connection_status_change_event.nim b/library/events/json_connection_status_change_event.nim new file mode 100644 index 000000000..347a84c48 --- /dev/null +++ b/library/events/json_connection_status_change_event.nim @@ -0,0 +1,19 @@ +{.push raises: [].} + +import system, std/json +import ./json_base_event +import ../../waku/api/types + +type JsonConnectionStatusChangeEvent* = ref object of JsonEvent + status*: ConnectionStatus + +proc new*( + T: type JsonConnectionStatusChangeEvent, status: ConnectionStatus +): T = + return JsonConnectionStatusChangeEvent( + eventType: "node_health_change", + status: status + ) + +method `$`*(event: JsonConnectionStatusChangeEvent): string = + $(%*event) diff --git a/library/libwaku.nim b/library/libwaku.nim index c71e823d6..eb3cdff5e 100644 --- a/library/libwaku.nim +++ b/library/libwaku.nim @@ -7,9 +7,11 @@ import ./events/json_message_event, ./events/json_topic_health_change_event, ./events/json_connection_change_event, + ./events/json_connection_status_change_event, ../waku/factory/app_callbacks, waku/factory/waku, waku/node/waku_node, + waku/node/health_monitor/health_status, ./declare_lib ################################################################################ @@ -61,10 +63,16 @@ proc waku_new( callEventCallback(ctx, "onConnectionChange"): $JsonConnectionChangeEvent.new($peerId, peerEvent) + proc onConnectionStatusChange(ctx: ptr FFIContext): ConnectionStatusChangeHandler = + return proc(status: ConnectionStatus) {.async.} = + callEventCallback(ctx, "onConnectionStatusChange"): + $JsonConnectionStatusChangeEvent.new(status) + let appCallbacks = AppCallbacks( relayHandler: onReceivedMessage(ctx), topicHealthChangeHandler: onTopicHealthChange(ctx), connectionChangeHandler: onConnectionChange(ctx), + connectionStatusChangeHandler: onConnectionStatusChange(ctx) ) ffi.sendRequestToFFIThread( diff --git a/tests/api/test_all.nim b/tests/api/test_all.nim index 99c1b3b4c..57f7f37f2 100644 --- a/tests/api/test_all.nim +++ b/tests/api/test_all.nim @@ -1,3 +1,3 @@ {.used.} -import ./test_entry_nodes, ./test_node_conf +import ./test_entry_nodes, ./test_node_conf, ./test_api_send, ./test_api_health diff --git a/tests/api/test_api_health.nim b/tests/api/test_api_health.nim new file mode 100644 index 000000000..b7aab43f9 --- /dev/null +++ b/tests/api/test_api_health.nim @@ -0,0 +1,296 @@ +{.used.} + +import std/[options, sequtils, times] +import chronos, testutils/unittests, stew/byteutils, libp2p/[switch, peerinfo] +import ../testlib/[common, wakucore, wakunode, testasync] + +import + waku, + waku/[waku_node, waku_core, waku_relay/protocol, common/broker/broker_context], + waku/node/health_monitor/[topic_health, health_status, protocol_health, health_report], + waku/requests/health_requests, + waku/requests/node_requests, + waku/events/health_events, + waku/common/waku_protocol, + waku/factory/waku_conf + +const TestTimeout = chronos.seconds(10) +const DefaultShard = PubsubTopic("/waku/2/rs/1/0") +const TestContentTopic = ContentTopic("/waku/2/default-content/proto") + +proc dummyHandler( + topic: PubsubTopic, msg: WakuMessage +): Future[void] {.async, gcsafe.} = + discard + +proc waitForConnectionStatus( + brokerCtx: BrokerContext, expected: ConnectionStatus +) {.async.} = + var future = newFuture[void]("waitForConnectionStatus") + + let handler: EventConnectionStatusChangeListenerProc = proc( + e: EventConnectionStatusChange + ) {.async: (raises: []), gcsafe.} = + if not future.finished: + if e.connectionStatus == expected: + future.complete() + + let handle = EventConnectionStatusChange.listen(brokerCtx, handler).valueOr: + raiseAssert error + + try: + if not await future.withTimeout(TestTimeout): + raiseAssert "Timeout waiting for status: " & $expected + finally: + EventConnectionStatusChange.dropListener(brokerCtx, handle) + +proc waitForShardHealthy( + brokerCtx: BrokerContext +): Future[EventShardTopicHealthChange] {.async.} = + var future = newFuture[EventShardTopicHealthChange]("waitForShardHealthy") + + let handler: EventShardTopicHealthChangeListenerProc = proc( + e: EventShardTopicHealthChange + ) {.async: (raises: []), gcsafe.} = + if not future.finished: + if e.health == TopicHealth.MINIMALLY_HEALTHY or + e.health == TopicHealth.SUFFICIENTLY_HEALTHY: + future.complete(e) + + let handle = EventShardTopicHealthChange.listen(brokerCtx, handler).valueOr: + raiseAssert error + + try: + if await future.withTimeout(TestTimeout): + return future.read() + else: + raiseAssert "Timeout waiting for shard health event" + finally: + EventShardTopicHealthChange.dropListener(brokerCtx, handle) + +suite "LM API health checking": + var + serviceNode {.threadvar.}: WakuNode + client {.threadvar.}: Waku + servicePeerInfo {.threadvar.}: RemotePeerInfo + + asyncSetup: + lockNewGlobalBrokerContext: + serviceNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + (await serviceNode.mountRelay()).isOkOr: + raiseAssert error + serviceNode.mountMetadata(1, @[0'u16]).isOkOr: + raiseAssert error + await serviceNode.mountLibp2pPing() + await serviceNode.start() + + servicePeerInfo = serviceNode.peerInfo.toRemotePeerInfo() + serviceNode.wakuRelay.subscribe(DefaultShard, dummyHandler) + + lockNewGlobalBrokerContext: + let conf = NodeConfig.init( + mode = WakuMode.Core, + networkingConfig = + NetworkingConfig(listenIpv4: "0.0.0.0", p2pTcpPort: 0, discv5UdpPort: 0), + protocolsConfig = ProtocolsConfig.init( + entryNodes = @[], + clusterId = 1'u16, + autoShardingConfig = AutoShardingConfig(numShardsInCluster: 1), + ), + ) + + client = (await createNode(conf)).valueOr: + raiseAssert error + (await startWaku(addr client)).isOkOr: + raiseAssert error + + asyncTeardown: + discard await client.stop() + await serviceNode.stop() + + asyncTest "RequestShardTopicsHealth, check PubsubTopic health": + client.node.wakuRelay.subscribe(DefaultShard, dummyHandler) + await client.node.connectToNodes(@[servicePeerInfo]) + + var isHealthy = false + let start = Moment.now() + while Moment.now() - start < TestTimeout: + let req = RequestShardTopicsHealth.request(client.brokerCtx, @[DefaultShard]).valueOr: + raiseAssert "RequestShardTopicsHealth failed" + + if req.topicHealth.len > 0: + let h = req.topicHealth[0].health + if h == TopicHealth.MINIMALLY_HEALTHY or h == TopicHealth.SUFFICIENTLY_HEALTHY: + isHealthy = true + break + await sleepAsync(chronos.milliseconds(100)) + + check isHealthy == true + + asyncTest "RequestShardTopicsHealth, check disconnected PubsubTopic": + const GhostShard = PubsubTopic("/waku/2/rs/1/666") + client.node.wakuRelay.subscribe(GhostShard, dummyHandler) + + let req = RequestShardTopicsHealth.request(client.brokerCtx, @[GhostShard]).valueOr: + raiseAssert "Request failed" + + check req.topicHealth.len > 0 + check req.topicHealth[0].health == TopicHealth.UNHEALTHY + + asyncTest "RequestProtocolHealth, check relay status": + await client.node.connectToNodes(@[servicePeerInfo]) + + var isReady = false + let start = Moment.now() + while Moment.now() - start < TestTimeout: + let relayReq = await RequestProtocolHealth.request( + client.brokerCtx, WakuProtocol.RelayProtocol + ) + if relayReq.isOk() and relayReq.get().healthStatus.health == HealthStatus.READY: + isReady = true + break + await sleepAsync(chronos.milliseconds(100)) + + check isReady == true + + let storeReq = + await RequestProtocolHealth.request(client.brokerCtx, WakuProtocol.StoreProtocol) + if storeReq.isOk(): + check storeReq.get().healthStatus.health != HealthStatus.READY + + asyncTest "RequestProtocolHealth, check unmounted protocol": + let req = + await RequestProtocolHealth.request(client.brokerCtx, WakuProtocol.StoreProtocol) + check req.isOk() + + let status = req.get().healthStatus + check status.health == HealthStatus.NOT_MOUNTED + check status.desc.isNone() + + asyncTest "RequestConnectionStatus, check connectivity state": + let initialReq = RequestConnectionStatus.request(client.brokerCtx).valueOr: + raiseAssert "RequestConnectionStatus failed" + check initialReq.connectionStatus == ConnectionStatus.Disconnected + + await client.node.connectToNodes(@[servicePeerInfo]) + + var isConnected = false + let start = Moment.now() + while Moment.now() - start < TestTimeout: + let req = RequestConnectionStatus.request(client.brokerCtx).valueOr: + raiseAssert "RequestConnectionStatus failed" + + if req.connectionStatus == ConnectionStatus.PartiallyConnected or + req.connectionStatus == ConnectionStatus.Connected: + isConnected = true + break + await sleepAsync(chronos.milliseconds(100)) + + check isConnected == true + + asyncTest "EventConnectionStatusChange, detect connect and disconnect": + let connectFuture = + waitForConnectionStatus(client.brokerCtx, ConnectionStatus.PartiallyConnected) + + await client.node.connectToNodes(@[servicePeerInfo]) + await connectFuture + + let disconnectFuture = + waitForConnectionStatus(client.brokerCtx, ConnectionStatus.Disconnected) + await client.node.disconnectNode(servicePeerInfo) + await disconnectFuture + + asyncTest "EventShardTopicHealthChange, detect health improvement": + client.node.wakuRelay.subscribe(DefaultShard, dummyHandler) + + let healthEventFuture = waitForShardHealthy(client.brokerCtx) + + await client.node.connectToNodes(@[servicePeerInfo]) + + let event = await healthEventFuture + check event.topic == DefaultShard + + asyncTest "RequestHealthReport, check aggregate report": + let req = await RequestHealthReport.request(client.brokerCtx) + + check req.isOk() + + let report = req.get().healthReport + check report.nodeHealth == HealthStatus.READY + check report.protocolsHealth.len > 0 + check report.protocolsHealth.anyIt(it.protocol == $WakuProtocol.RelayProtocol) + + asyncTest "RequestContentTopicsHealth, smoke test": + let fictionalTopic = ContentTopic("/waku/2/this-does-not-exist/proto") + + let req = RequestContentTopicsHealth.request(client.brokerCtx, @[fictionalTopic]) + + check req.isOk() + + let res = req.get() + check res.contentTopicHealth.len == 1 + check res.contentTopicHealth[0].topic == fictionalTopic + check res.contentTopicHealth[0].health == TopicHealth.NOT_SUBSCRIBED + + asyncTest "RequestContentTopicsHealth, core mode trivial 1-shard autosharding": + let cTopic = ContentTopic("/waku/2/my-content-topic/proto") + + let shardReq = + RequestRelayShard.request(client.brokerCtx, none(PubsubTopic), cTopic) + + check shardReq.isOk() + let targetShard = $shardReq.get().relayShard + + client.node.wakuRelay.subscribe(targetShard, dummyHandler) + serviceNode.wakuRelay.subscribe(targetShard, dummyHandler) + + await client.node.connectToNodes(@[servicePeerInfo]) + + var isHealthy = false + let start = Moment.now() + while Moment.now() - start < TestTimeout: + let req = RequestContentTopicsHealth.request(client.brokerCtx, @[cTopic]).valueOr: + raiseAssert "Request failed" + + if req.contentTopicHealth.len > 0: + let h = req.contentTopicHealth[0].health + if h == TopicHealth.MINIMALLY_HEALTHY or h == TopicHealth.SUFFICIENTLY_HEALTHY: + isHealthy = true + break + + await sleepAsync(chronos.milliseconds(100)) + + check isHealthy == true + + asyncTest "RequestProtocolHealth, edge mode smoke test": + var edgeWaku: Waku + + lockNewGlobalBrokerContext: + let edgeConf = NodeConfig.init( + mode = WakuMode.Edge, + networkingConfig = + NetworkingConfig(listenIpv4: "0.0.0.0", p2pTcpPort: 0, discv5UdpPort: 0), + protocolsConfig = ProtocolsConfig.init( + entryNodes = @[], + clusterId = 1'u16, + messageValidation = + MessageValidation(maxMessageSize: "150 KiB", rlnConfig: none(RlnConfig)), + ), + ) + + edgeWaku = (await createNode(edgeConf)).valueOr: + raiseAssert "Failed to create edge node: " & error + + (await startWaku(addr edgeWaku)).isOkOr: + raiseAssert "Failed to start edge waku: " & error + + let relayReq = await RequestProtocolHealth.request( + edgeWaku.brokerCtx, WakuProtocol.RelayProtocol + ) + check relayReq.isOk() + check relayReq.get().healthStatus.health == HealthStatus.NOT_MOUNTED + + check not edgeWaku.node.wakuFilterClient.isNil() + + discard await edgeWaku.stop() diff --git a/tests/api/test_api_send.nim b/tests/api/test_api_send.nim index e247c65ce..7343fc655 100644 --- a/tests/api/test_api_send.nim +++ b/tests/api/test_api_send.nim @@ -117,6 +117,9 @@ proc validate( check requestId == expectedRequestId proc createApiNodeConf(mode: WakuMode = WakuMode.Core): NodeConfig = + # allocate random ports to avoid port-already-in-use errors + let netConf = NetworkingConfig(listenIpv4: "0.0.0.0", p2pTcpPort: 0, discv5UdpPort: 0) + result = NodeConfig.init( mode = mode, protocolsConfig = ProtocolsConfig.init( @@ -124,6 +127,7 @@ proc createApiNodeConf(mode: WakuMode = WakuMode.Core): NodeConfig = clusterId = 1, autoShardingConfig = AutoShardingConfig(numShardsInCluster: 1), ), + networkingConfig = netConf, p2pReliability = true, ) @@ -246,8 +250,9 @@ suite "Waku API - Send": let sendResult = await node.send(envelope) - check sendResult.isErr() # Depending on implementation, it might say "not healthy" - check sendResult.error().contains("not healthy") + # TODO: The API is not enforcing a health check before the send, + # so currently this test cannot successfully fail to send. + check sendResult.isOk() (await node.stop()).isOkOr: raiseAssert "Failed to stop node: " & error diff --git a/tests/node/test_all.nim b/tests/node/test_all.nim index f6e7507b7..fe785dee2 100644 --- a/tests/node/test_all.nim +++ b/tests/node/test_all.nim @@ -7,4 +7,5 @@ import ./test_wakunode_peer_exchange, ./test_wakunode_store, ./test_wakunode_legacy_store, - ./test_wakunode_peer_manager + ./test_wakunode_peer_manager, + ./test_wakunode_health_monitor diff --git a/tests/node/test_wakunode_health_monitor.nim b/tests/node/test_wakunode_health_monitor.nim new file mode 100644 index 000000000..8be9c444d --- /dev/null +++ b/tests/node/test_wakunode_health_monitor.nim @@ -0,0 +1,301 @@ +{.used.} + +import + std/[json, options, sequtils, strutils, tables], testutils/unittests, chronos, results + +import + waku/[ + waku_core, + common/waku_protocol, + node/waku_node, + node/peer_manager, + node/health_monitor/health_status, + node/health_monitor/connection_status, + node/health_monitor/protocol_health, + node/health_monitor/node_health_monitor, + node/kernel_api/relay, + node/kernel_api/store, + node/kernel_api/lightpush, + node/kernel_api/filter, + waku_archive, + ] + +import ../testlib/[wakunode, wakucore], ../waku_archive/archive_utils + +const MockDLow = 4 # Mocked GossipSub DLow value + +const TestConnectivityTimeLimit = 3.seconds + +proc protoHealthMock(kind: WakuProtocol, health: HealthStatus): ProtocolHealth = + var ph = ProtocolHealth.init(kind) + if health == HealthStatus.READY: + return ph.ready() + else: + return ph.notReady("mock") + +suite "Health Monitor - health state calculation": + test "Disconnected, zero peers": + let protocols = + @[ + protoHealthMock(RelayProtocol, HealthStatus.NOT_READY), + protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), + protoHealthMock(FilterClientProtocol, HealthStatus.NOT_READY), + protoHealthMock(LightpushClientProtocol, HealthStatus.NOT_READY), + ] + let strength = initTable[WakuProtocol, int]() + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.Disconnected + + test "PartiallyConnected, weak relay": + let weakCount = MockDLow - 1 + let protocols = @[protoHealthMock(RelayProtocol, HealthStatus.READY)] + var strength = initTable[WakuProtocol, int]() + strength[RelayProtocol] = weakCount + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + # Partially connected since relay connectivity is weak (> 0, but < dLow) + check state == ConnectionStatus.PartiallyConnected + + test "Connected, robust relay": + let protocols = @[protoHealthMock(RelayProtocol, HealthStatus.READY)] + var strength = initTable[WakuProtocol, int]() + strength[RelayProtocol] = MockDLow + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + # Fully connected since relay connectivity is ideal (>= dLow) + check state == ConnectionStatus.Connected + + test "Connected, robust edge": + let protocols = + @[ + protoHealthMock(RelayProtocol, HealthStatus.NOT_MOUNTED), + protoHealthMock(LightpushClientProtocol, HealthStatus.READY), + protoHealthMock(FilterClientProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.READY), + ] + var strength = initTable[WakuProtocol, int]() + strength[LightpushClientProtocol] = HealthyThreshold + strength[FilterClientProtocol] = HealthyThreshold + strength[StoreClientProtocol] = HealthyThreshold + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.Connected + + test "Disconnected, edge missing store": + let protocols = + @[ + protoHealthMock(LightpushClientProtocol, HealthStatus.READY), + protoHealthMock(FilterClientProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), + ] + var strength = initTable[WakuProtocol, int]() + strength[LightpushClientProtocol] = HealthyThreshold + strength[FilterClientProtocol] = HealthyThreshold + strength[StoreClientProtocol] = 0 + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.Disconnected + + test "PartiallyConnected, edge meets minimum failover requirement": + let weakCount = max(1, HealthyThreshold - 1) + let protocols = + @[ + protoHealthMock(LightpushClientProtocol, HealthStatus.READY), + protoHealthMock(FilterClientProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.READY), + ] + var strength = initTable[WakuProtocol, int]() + strength[LightpushClientProtocol] = weakCount + strength[FilterClientProtocol] = weakCount + strength[StoreClientProtocol] = weakCount + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.PartiallyConnected + + test "Connected, robust relay ignores store server": + let protocols = + @[ + protoHealthMock(RelayProtocol, HealthStatus.READY), + protoHealthMock(StoreProtocol, HealthStatus.READY), + ] + var strength = initTable[WakuProtocol, int]() + strength[RelayProtocol] = MockDLow + strength[StoreProtocol] = 0 + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.Connected + + test "Connected, robust relay ignores store client": + let protocols = + @[ + protoHealthMock(RelayProtocol, HealthStatus.READY), + protoHealthMock(StoreProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), + ] + var strength = initTable[WakuProtocol, int]() + strength[RelayProtocol] = MockDLow + strength[StoreProtocol] = 0 + strength[StoreClientProtocol] = 0 + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.Connected + +suite "Health Monitor - events": + asyncTest "Core (relay) health update": + let + nodeAKey = generateSecp256k1Key() + nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0)) + + (await nodeA.mountRelay()).expect("Node A failed to mount Relay") + + await nodeA.start() + + let monitorA = NodeHealthMonitor.new(nodeA) + + var + lastStatus = ConnectionStatus.Disconnected + callbackCount = 0 + healthChangeSignal = newAsyncEvent() + + monitorA.onConnectionStatusChange = proc(status: ConnectionStatus) {.async.} = + lastStatus = status + callbackCount.inc() + healthChangeSignal.fire() + + monitorA.startHealthMonitor().expect("Health monitor failed to start") + + let + nodeBKey = generateSecp256k1Key() + nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0)) + + let driver = newSqliteArchiveDriver() + nodeB.mountArchive(driver).expect("Node B failed to mount archive") + + (await nodeB.mountRelay()).expect("Node B failed to mount relay") + await nodeB.mountStore() + + await nodeB.start() + + await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()]) + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage): Future[void] {.async.} = + discard + + nodeA.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), dummyHandler).expect( + "Node A failed to subscribe" + ) + nodeB.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), dummyHandler).expect( + "Node B failed to subscribe" + ) + + let connectTimeLimit = Moment.now() + TestConnectivityTimeLimit + var gotConnected = false + + while Moment.now() < connectTimeLimit: + if lastStatus == ConnectionStatus.PartiallyConnected: + gotConnected = true + break + + if await healthChangeSignal.wait().withTimeout(connectTimeLimit - Moment.now()): + healthChangeSignal.clear() + + check: + gotConnected == true + callbackCount >= 1 + lastStatus == ConnectionStatus.PartiallyConnected + + healthChangeSignal.clear() + + await nodeB.stop() + await nodeA.disconnectNode(nodeB.switch.peerInfo.toRemotePeerInfo()) + + let disconnectTimeLimit = Moment.now() + TestConnectivityTimeLimit + var gotDisconnected = false + + while Moment.now() < disconnectTimeLimit: + if lastStatus == ConnectionStatus.Disconnected: + gotDisconnected = true + break + + if await healthChangeSignal.wait().withTimeout(disconnectTimeLimit - Moment.now()): + healthChangeSignal.clear() + + check: + gotDisconnected == true + + await monitorA.stopHealthMonitor() + await nodeA.stop() + + asyncTest "Edge (light client) health update": + let + nodeAKey = generateSecp256k1Key() + nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0)) + + nodeA.mountLightpushClient() + await nodeA.mountFilterClient() + nodeA.mountStoreClient() + + await nodeA.start() + + let monitorA = NodeHealthMonitor.new(nodeA) + + var + lastStatus = ConnectionStatus.Disconnected + callbackCount = 0 + healthChangeSignal = newAsyncEvent() + + monitorA.onConnectionStatusChange = proc(status: ConnectionStatus) {.async.} = + lastStatus = status + callbackCount.inc() + healthChangeSignal.fire() + + monitorA.startHealthMonitor().expect("Health monitor failed to start") + + let + nodeBKey = generateSecp256k1Key() + nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0)) + + let driver = newSqliteArchiveDriver() + nodeB.mountArchive(driver).expect("Node B failed to mount archive") + + (await nodeB.mountRelay()).expect("Node B failed to mount relay") + + (await nodeB.mountLightpush()).expect("Node B failed to mount lightpush") + await nodeB.mountFilter() + await nodeB.mountStore() + + await nodeB.start() + + await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()]) + + let connectTimeLimit = Moment.now() + TestConnectivityTimeLimit + var gotConnected = false + + while Moment.now() < connectTimeLimit: + if lastStatus == ConnectionStatus.PartiallyConnected: + gotConnected = true + break + + if await healthChangeSignal.wait().withTimeout(connectTimeLimit - Moment.now()): + healthChangeSignal.clear() + + check: + gotConnected == true + callbackCount >= 1 + lastStatus == ConnectionStatus.PartiallyConnected + + healthChangeSignal.clear() + + await nodeB.stop() + await nodeA.disconnectNode(nodeB.switch.peerInfo.toRemotePeerInfo()) + + let disconnectTimeLimit = Moment.now() + TestConnectivityTimeLimit + var gotDisconnected = false + + while Moment.now() < disconnectTimeLimit: + if lastStatus == ConnectionStatus.Disconnected: + gotDisconnected = true + break + + if await healthChangeSignal.wait().withTimeout(disconnectTimeLimit - Moment.now()): + healthChangeSignal.clear() + + check: + gotDisconnected == true + lastStatus == ConnectionStatus.Disconnected + + await monitorA.stopHealthMonitor() + await nodeA.stop() diff --git a/tests/waku_relay/utils.nim b/tests/waku_relay/utils.nim index d5703d415..4e958a4ea 100644 --- a/tests/waku_relay/utils.nim +++ b/tests/waku_relay/utils.nim @@ -11,15 +11,15 @@ import from std/times import epochTime import - waku/ - [ - waku_relay, - node/waku_node, - node/peer_manager, - waku_core, - waku_node, - waku_rln_relay, - ], + waku/[ + waku_relay, + node/waku_node, + node/peer_manager, + waku_core, + waku_node, + waku_rln_relay, + common/broker/broker_context, + ], ../waku_store/store_utils, ../waku_archive/archive_utils, ../testlib/[wakucore, futures] diff --git a/tests/wakunode_rest/test_rest_health.nim b/tests/wakunode_rest/test_rest_health.nim index 2a70fee5f..37abaf4f5 100644 --- a/tests/wakunode_rest/test_rest_health.nim +++ b/tests/wakunode_rest/test_rest_health.nim @@ -10,6 +10,7 @@ import libp2p/crypto/crypto import waku/[ + common/waku_protocol, waku_node, node/waku_node as waku_node2, # TODO: Remove after moving `git_version` to the app code. @@ -78,47 +79,39 @@ suite "Waku v2 REST API - health": # When var response = await client.healthCheck() + let report = response.data # Then check: response.status == 200 $response.contentType == $MIMETYPE_JSON - response.data.nodeHealth == HealthStatus.READY - response.data.protocolsHealth.len() == 15 - response.data.protocolsHealth[0].protocol == "Relay" - response.data.protocolsHealth[0].health == HealthStatus.NOT_READY - response.data.protocolsHealth[0].desc == some("No connected peers") - response.data.protocolsHealth[1].protocol == "Rln Relay" - response.data.protocolsHealth[1].health == HealthStatus.READY - response.data.protocolsHealth[2].protocol == "Lightpush" - response.data.protocolsHealth[2].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[3].protocol == "Legacy Lightpush" - response.data.protocolsHealth[3].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[4].protocol == "Filter" - response.data.protocolsHealth[4].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[5].protocol == "Store" - response.data.protocolsHealth[5].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[6].protocol == "Legacy Store" - response.data.protocolsHealth[6].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[7].protocol == "Peer Exchange" - response.data.protocolsHealth[7].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[8].protocol == "Rendezvous" - response.data.protocolsHealth[8].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[9].protocol == "Mix" - response.data.protocolsHealth[9].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[10].protocol == "Lightpush Client" - response.data.protocolsHealth[10].health == HealthStatus.NOT_READY - response.data.protocolsHealth[10].desc == + report.nodeHealth == HealthStatus.READY + report.protocolsHealth.len() == 15 + + report.getHealth(RelayProtocol).health == HealthStatus.NOT_READY + report.getHealth(RelayProtocol).desc == some("No connected peers") + + report.getHealth(RlnRelayProtocol).health == HealthStatus.READY + + report.getHealth(LightpushProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(LegacyLightpushProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(FilterProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(StoreProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(LegacyStoreProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(PeerExchangeProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(RendezvousProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(MixProtocol).health == HealthStatus.NOT_MOUNTED + + report.getHealth(LightpushClientProtocol).health == HealthStatus.NOT_READY + report.getHealth(LightpushClientProtocol).desc == some("No Lightpush service peer available yet") - response.data.protocolsHealth[11].protocol == "Legacy Lightpush Client" - response.data.protocolsHealth[11].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[12].protocol == "Store Client" - response.data.protocolsHealth[12].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[13].protocol == "Legacy Store Client" - response.data.protocolsHealth[13].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[14].protocol == "Filter Client" - response.data.protocolsHealth[14].health == HealthStatus.NOT_READY - response.data.protocolsHealth[14].desc == + + report.getHealth(LegacyLightpushClientProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(StoreClientProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(LegacyStoreClientProtocol).health == HealthStatus.NOT_MOUNTED + + report.getHealth(FilterClientProtocol).health == HealthStatus.NOT_READY + report.getHealth(FilterClientProtocol).desc == some("No Filter service peer available yet") await restServer.stop() diff --git a/waku/api/api.nim b/waku/api/api.nim index 41f4fd240..7f13919b3 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -1,7 +1,7 @@ -import chronicles, chronos, results +import chronicles, chronos, results, std/strutils import waku/factory/waku -import waku/[requests/health_request, waku_core, waku_node] +import waku/[requests/health_requests, waku_core, waku_node] import waku/node/delivery_service/send_service import waku/node/delivery_service/subscription_service import ./[api_conf, types] @@ -25,16 +25,8 @@ proc checkApiAvailability(w: Waku): Result[void, string] = if w.isNil(): return err("Waku node is not initialized") - # check if health is satisfactory - # If Node is not healthy, return err("Waku node is not healthy") - let healthStatus = RequestNodeHealth.request(w.brokerCtx) - - if healthStatus.isErr(): - warn "Failed to get Waku node health status: ", error = healthStatus.error - # Let's suppose the node is hesalthy enough, go ahead - else: - if healthStatus.get().healthStatus == NodeHealth.Unhealthy: - return err("Waku node is not healthy, has got no connections.") + # TODO: Conciliate request-bouncing health checks here with unit testing. + # (For now, better to just allow all sends and rely on retries.) return ok() diff --git a/waku/api/types.nim b/waku/api/types.nim index a0626e98c..9eae503c8 100644 --- a/waku/api/types.nim +++ b/waku/api/types.nim @@ -14,10 +14,10 @@ type RequestId* = distinct string - NodeHealth* {.pure.} = enum - Healthy - MinimallyHealthy - Unhealthy + ConnectionStatus* {.pure.} = enum + Disconnected + PartiallyConnected + Connected proc new*(T: typedesc[RequestId], rng: ref HmacDrbgContext): T = ## Generate a new RequestId using the provided RNG. diff --git a/waku/common/waku_protocol.nim b/waku/common/waku_protocol.nim new file mode 100644 index 000000000..5063f4c98 --- /dev/null +++ b/waku/common/waku_protocol.nim @@ -0,0 +1,24 @@ +{.push raises: [].} + +type WakuProtocol* {.pure.} = enum + RelayProtocol = "Relay" + RlnRelayProtocol = "Rln Relay" + StoreProtocol = "Store" + LegacyStoreProtocol = "Legacy Store" + FilterProtocol = "Filter" + LightpushProtocol = "Lightpush" + LegacyLightpushProtocol = "Legacy Lightpush" + PeerExchangeProtocol = "Peer Exchange" + RendezvousProtocol = "Rendezvous" + MixProtocol = "Mix" + StoreClientProtocol = "Store Client" + LegacyStoreClientProtocol = "Legacy Store Client" + FilterClientProtocol = "Filter Client" + LightpushClientProtocol = "Lightpush Client" + LegacyLightpushClientProtocol = "Legacy Lightpush Client" + +const + RelayProtocols* = {RelayProtocol} + StoreClientProtocols* = {StoreClientProtocol, LegacyStoreClientProtocol} + LightpushClientProtocols* = {LightpushClientProtocol, LegacyLightpushClientProtocol} + FilterClientProtocols* = {FilterClientProtocol} diff --git a/waku/events/events.nim b/waku/events/events.nim index 2a0af8828..46dd4fdd3 100644 --- a/waku/events/events.nim +++ b/waku/events/events.nim @@ -1,3 +1,3 @@ -import ./[message_events, delivery_events] +import ./[message_events, delivery_events, health_events, peer_events] -export message_events, delivery_events +export message_events, delivery_events, health_events, peer_events diff --git a/waku/events/health_events.nim b/waku/events/health_events.nim new file mode 100644 index 000000000..1e6decedb --- /dev/null +++ b/waku/events/health_events.nim @@ -0,0 +1,27 @@ +import waku/common/broker/event_broker + +import waku/api/types +import waku/node/health_monitor/[protocol_health, topic_health] +import waku/waku_core/topics + +export protocol_health, topic_health + +# Notify health changes to node connectivity +EventBroker: + type EventConnectionStatusChange* = object + connectionStatus*: ConnectionStatus + +# Notify health changes to a subscribed topic +# TODO: emit content topic health change events when subscribe/unsubscribe +# from/to content topic is provided in the new API (so we know which +# content topics are of interest to the application) +EventBroker: + type EventContentTopicHealthChange* = object + contentTopic*: ContentTopic + health*: TopicHealth + +# Notify health changes to a shard (pubsub topic) +EventBroker: + type EventShardTopicHealthChange* = object + topic*: PubsubTopic + health*: TopicHealth diff --git a/waku/events/peer_events.nim b/waku/events/peer_events.nim new file mode 100644 index 000000000..49dfa9f9a --- /dev/null +++ b/waku/events/peer_events.nim @@ -0,0 +1,13 @@ +import waku/common/broker/event_broker +import libp2p/switch + +type WakuPeerEventKind* {.pure.} = enum + EventConnected + EventDisconnected + EventIdentified + EventMetadataUpdated + +EventBroker: + type EventWakuPeer* = object + peerId*: PeerId + kind*: WakuPeerEventKind diff --git a/waku/factory/app_callbacks.nim b/waku/factory/app_callbacks.nim index d28b9f2d1..f1d3369be 100644 --- a/waku/factory/app_callbacks.nim +++ b/waku/factory/app_callbacks.nim @@ -1,6 +1,7 @@ -import ../waku_relay, ../node/peer_manager +import ../waku_relay, ../node/peer_manager, ../node/health_monitor/connection_status type AppCallbacks* = ref object relayHandler*: WakuRelayHandler topicHealthChangeHandler*: TopicHealthChangeHandler connectionChangeHandler*: ConnectionChangeHandler + connectionStatusChangeHandler*: ConnectionStatusChangeHandler diff --git a/waku/factory/builder.nim b/waku/factory/builder.nim index f379f92bb..e0b643fc0 100644 --- a/waku/factory/builder.nim +++ b/waku/factory/builder.nim @@ -15,7 +15,8 @@ import ../waku_node, ../node/peer_manager, ../common/rate_limit/setting, - ../common/utils/parse_size_units + ../common/utils/parse_size_units, + ../common/broker/broker_context type WakuNodeBuilder* = object # General diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 3748847f1..dd253129c 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -17,35 +17,36 @@ import eth/p2p/discoveryv5/enr, presto, metrics, - metrics/chronos_httpserver -import - ../common/logging, - ../waku_core, - ../waku_node, - ../node/peer_manager, - ../node/health_monitor, - ../node/waku_metrics, - ../node/delivery_service/delivery_service, - ../rest_api/message_cache, - ../rest_api/endpoint/server, - ../rest_api/endpoint/builder as rest_server_builder, - ../waku_archive, - ../waku_relay/protocol, - ../discovery/waku_dnsdisc, - ../discovery/waku_discv5, - ../discovery/autonat_service, - ../waku_enr/sharding, - ../waku_rln_relay, - ../waku_store, - ../waku_filter_v2, - ../factory/node_factory, - ../factory/internal_config, - ../factory/app_callbacks, - ../waku_enr/multiaddr, - ./waku_conf, - ../common/broker/broker_context, - ../requests/health_request, - ../api/types + metrics/chronos_httpserver, + waku/[ + waku_core, + waku_node, + waku_archive, + waku_rln_relay, + waku_store, + waku_filter_v2, + waku_relay/protocol, + waku_enr/sharding, + waku_enr/multiaddr, + api/types, + common/logging, + common/broker/broker_context, + node/peer_manager, + node/health_monitor, + node/waku_metrics, + node/delivery_service/delivery_service, + rest_api/message_cache, + rest_api/endpoint/server, + rest_api/endpoint/builder as rest_server_builder, + discovery/waku_dnsdisc, + discovery/waku_discv5, + discovery/autonat_service, + requests/health_requests, + factory/node_factory, + factory/internal_config, + factory/app_callbacks, + ], + ./waku_conf logScope: topics = "wakunode waku" @@ -118,7 +119,10 @@ proc newCircuitRelay(isRelayClient: bool): Relay = return Relay.new() proc setupAppCallbacks( - node: WakuNode, conf: WakuConf, appCallbacks: AppCallbacks + node: WakuNode, + conf: WakuConf, + appCallbacks: AppCallbacks, + healthMonitor: NodeHealthMonitor, ): Result[void, string] = if appCallbacks.isNil(): info "No external callbacks to be set" @@ -159,6 +163,13 @@ proc setupAppCallbacks( err("Cannot configure connectionChangeHandler callback with empty peer manager") node.peerManager.onConnectionChange = appCallbacks.connectionChangeHandler + if not appCallbacks.connectionStatusChangeHandler.isNil(): + if healthMonitor.isNil(): + return + err("Cannot configure connectionStatusChangeHandler with empty health monitor") + + healthMonitor.onConnectionStatusChange = appCallbacks.connectionStatusChangeHandler + return ok() proc new*( @@ -192,7 +203,7 @@ proc new*( else: nil - node.setupAppCallbacks(wakuConf, appCallbacks).isOkOr: + node.setupAppCallbacks(wakuConf, appCallbacks, healthMonitor).isOkOr: error "Failed setting up app callbacks", error = error return err("Failed setting up app callbacks: " & $error) @@ -409,60 +420,48 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: waku[].healthMonitor.startHealthMonitor().isOkOr: return err("failed to start health monitor: " & $error) - ## Setup RequestNodeHealth provider + ## Setup RequestConnectionStatus provider - RequestNodeHealth.setProvider( + RequestConnectionStatus.setProvider( globalBrokerContext(), - proc(): Result[RequestNodeHealth, string] = - let healthReportFut = waku[].healthMonitor.getNodeHealthReport() - if not healthReportFut.completed(): - return err("Health report not available") + proc(): Result[RequestConnectionStatus, string] = try: - let healthReport = healthReportFut.read() - - # Check if Relay or Lightpush Client is ready (MinimallyHealthy condition) - var relayReady = false - var lightpushClientReady = false - var storeClientReady = false - var filterClientReady = false - - for protocolHealth in healthReport.protocolsHealth: - if protocolHealth.protocol == "Relay" and - protocolHealth.health == HealthStatus.READY: - relayReady = true - elif protocolHealth.protocol == "Lightpush Client" and - protocolHealth.health == HealthStatus.READY: - lightpushClientReady = true - elif protocolHealth.protocol == "Store Client" and - protocolHealth.health == HealthStatus.READY: - storeClientReady = true - elif protocolHealth.protocol == "Filter Client" and - protocolHealth.health == HealthStatus.READY: - filterClientReady = true - - # Determine node health based on protocol states - let isMinimallyHealthy = relayReady or lightpushClientReady - let nodeHealth = - if isMinimallyHealthy and storeClientReady and filterClientReady: - NodeHealth.Healthy - elif isMinimallyHealthy: - NodeHealth.MinimallyHealthy - else: - NodeHealth.Unhealthy - - debug "Providing health report", - nodeHealth = $nodeHealth, - relayReady = relayReady, - lightpushClientReady = lightpushClientReady, - storeClientReady = storeClientReady, - filterClientReady = filterClientReady, - details = $(healthReport) - - return ok(RequestNodeHealth(healthStatus: nodeHealth)) - except CatchableError as exc: - err("Failed to read health report: " & exc.msg), + let healthReport = waku[].healthMonitor.getSyncNodeHealthReport() + return + ok(RequestConnectionStatus(connectionStatus: healthReport.connectionStatus)) + except CatchableError: + err("Failed to read health report: " & getCurrentExceptionMsg()), ).isOkOr: - error "Failed to set RequestNodeHealth provider", error = error + error "Failed to set RequestConnectionStatus provider", error = error + + ## Setup RequestProtocolHealth provider + + RequestProtocolHealth.setProvider( + globalBrokerContext(), + proc( + protocol: WakuProtocol + ): Future[Result[RequestProtocolHealth, string]] {.async.} = + try: + let protocolHealthStatus = + await waku[].healthMonitor.getProtocolHealthInfo(protocol) + return ok(RequestProtocolHealth(healthStatus: protocolHealthStatus)) + except CatchableError: + return err("Failed to get protocol health: " & getCurrentExceptionMsg()), + ).isOkOr: + error "Failed to set RequestProtocolHealth provider", error = error + + ## Setup RequestHealthReport provider (The lost child) + + RequestHealthReport.setProvider( + globalBrokerContext(), + proc(): Future[Result[RequestHealthReport, string]] {.async.} = + try: + let report = await waku[].healthMonitor.getNodeHealthReport() + return ok(RequestHealthReport(healthReport: report)) + except CatchableError: + return err("Failed to get health report: " & getCurrentExceptionMsg()), + ).isOkOr: + error "Failed to set RequestHealthReport provider", error = error if conf.restServerConf.isSome(): rest_server_builder.startRestServerProtocolSupport( @@ -521,8 +520,8 @@ proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} = if not waku.healthMonitor.isNil(): await waku.healthMonitor.stopHealthMonitor() - ## Clear RequestNodeHealth provider - RequestNodeHealth.clearProvider(waku.brokerCtx) + ## Clear RequestConnectionStatus provider + RequestConnectionStatus.clearProvider(waku.brokerCtx) if not waku.restServer.isNil(): await waku.restServer.stop() diff --git a/waku/node/delivery_service/send_service/relay_processor.nim b/waku/node/delivery_service/send_service/relay_processor.nim index 974c22f6c..833d15845 100644 --- a/waku/node/delivery_service/send_service/relay_processor.nim +++ b/waku/node/delivery_service/send_service/relay_processor.nim @@ -1,7 +1,7 @@ import std/options import chronos, chronicles import waku/[waku_core], waku/waku_lightpush/[common, rpc] -import waku/requests/health_request +import waku/requests/health_requests import waku/common/broker/broker_context import waku/api/types import ./[delivery_task, send_processor] @@ -32,7 +32,7 @@ proc new*( ) proc isTopicHealthy(self: RelaySendProcessor, topic: PubsubTopic): bool {.gcsafe.} = - let healthReport = RequestRelayTopicsHealth.request(self.brokerCtx, @[topic]).valueOr: + let healthReport = RequestShardTopicsHealth.request(self.brokerCtx, @[topic]).valueOr: error "isTopicHealthy: failed to get health report", topic = topic, error = error return false diff --git a/waku/node/health_monitor.nim b/waku/node/health_monitor.nim index 854a8bbc0..6e42352d4 100644 --- a/waku/node/health_monitor.nim +++ b/waku/node/health_monitor.nim @@ -1,4 +1,9 @@ import - health_monitor/[node_health_monitor, protocol_health, online_monitor, health_status] + health_monitor/[ + node_health_monitor, protocol_health, online_monitor, health_status, + connection_status, health_report, + ] -export node_health_monitor, protocol_health, online_monitor, health_status +export + node_health_monitor, protocol_health, online_monitor, health_status, + connection_status, health_report diff --git a/waku/node/health_monitor/connection_status.nim b/waku/node/health_monitor/connection_status.nim new file mode 100644 index 000000000..77696130a --- /dev/null +++ b/waku/node/health_monitor/connection_status.nim @@ -0,0 +1,15 @@ +import chronos, results, std/strutils, ../../api/types + +export ConnectionStatus + +proc init*( + t: typedesc[ConnectionStatus], strRep: string +): Result[ConnectionStatus, string] = + try: + let status = parseEnum[ConnectionStatus](strRep) + return ok(status) + except ValueError: + return err("Invalid ConnectionStatus string representation: " & strRep) + +type ConnectionStatusChangeHandler* = + proc(status: ConnectionStatus): Future[void] {.gcsafe, raises: [Defect].} diff --git a/waku/node/health_monitor/health_report.nim b/waku/node/health_monitor/health_report.nim new file mode 100644 index 000000000..d6c23cd28 --- /dev/null +++ b/waku/node/health_monitor/health_report.nim @@ -0,0 +1,10 @@ +{.push raises: [].} + +import ./health_status, ./connection_status, ./protocol_health + +type HealthReport* = object + ## Rest API type returned for /health endpoint + ## + nodeHealth*: HealthStatus # legacy "READY" health indicator + connectionStatus*: ConnectionStatus # new "Connected" health indicator + protocolsHealth*: seq[ProtocolHealth] diff --git a/waku/node/health_monitor/node_health_monitor.nim b/waku/node/health_monitor/node_health_monitor.nim index 4b13dfd3d..ba0518e61 100644 --- a/waku/node/health_monitor/node_health_monitor.nim +++ b/waku/node/health_monitor/node_health_monitor.nim @@ -1,55 +1,89 @@ {.push raises: [].} import - std/[options, sets, random, sequtils], + std/[options, sets, random, sequtils, json, strutils, tables], chronos, chronicles, - libp2p/protocols/rendezvous - -import - ../waku_node, - ../kernel_api, - ../../waku_rln_relay, - ../../waku_relay, - ../peer_manager, - ./online_monitor, - ./health_status, - ./protocol_health + libp2p/protocols/rendezvous, + libp2p/protocols/pubsub, + libp2p/protocols/pubsub/rpc/messages, + waku/[ + waku_relay, + waku_rln_relay, + api/types, + events/health_events, + events/peer_events, + node/waku_node, + node/peer_manager, + node/kernel_api, + node/health_monitor/online_monitor, + node/health_monitor/health_status, + node/health_monitor/health_report, + node/health_monitor/connection_status, + node/health_monitor/protocol_health, + ] ## This module is aimed to check the state of the "self" Waku Node # randomize initializes sdt/random's random number generator # if not called, the outcome of randomization procedures will be the same in every run -randomize() +random.randomize() -type - HealthReport* = object - nodeHealth*: HealthStatus - protocolsHealth*: seq[ProtocolHealth] +const HealthyThreshold* = 2 + ## minimum peers required for all services for a Connected status, excluding Relay - NodeHealthMonitor* = ref object - nodeHealth: HealthStatus - node: WakuNode - onlineMonitor*: OnlineMonitor - keepAliveFut: Future[void] +type NodeHealthMonitor* = ref object + nodeHealth: HealthStatus + node: WakuNode + onlineMonitor*: OnlineMonitor + keepAliveFut: Future[void] + healthLoopFut: Future[void] + healthUpdateEvent: AsyncEvent + connectionStatus: ConnectionStatus + onConnectionStatusChange*: ConnectionStatusChangeHandler + cachedProtocols: seq[ProtocolHealth] + ## state of each protocol to report. + ## calculated on last event that can change any protocol's state so fetching a report is fast. + strength: Table[WakuProtocol, int] + ## latest known connectivity strength (e.g. connected peer count) metric for each protocol. + ## if it doesn't make sense for the protocol in question, this is set to zero. + relayObserver: PubSubObserver + peerEventListener: EventWakuPeerListener + +func getHealth*(report: HealthReport, kind: WakuProtocol): ProtocolHealth = + for h in report.protocolsHealth: + if h.protocol == $kind: + return h + # Shouldn't happen, but if it does, then assume protocol is not mounted + return ProtocolHealth.init(kind) + +proc countCapablePeers(hm: NodeHealthMonitor, codec: string): int = + if isNil(hm.node.peerManager): + return 0 + + return hm.node.peerManager.getCapablePeersCount(codec) proc getRelayHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Relay") + var p = ProtocolHealth.init(WakuProtocol.RelayProtocol) - if hm.node.wakuRelay == nil: + if isNil(hm.node.wakuRelay): + hm.strength[WakuProtocol.RelayProtocol] = 0 return p.notMounted() let relayPeers = hm.node.wakuRelay.getConnectedPubSubPeers(pubsubTopic = "").valueOr: + hm.strength[WakuProtocol.RelayProtocol] = 0 return p.notMounted() - if relayPeers.len() == 0: + let count = relayPeers.len + hm.strength[WakuProtocol.RelayProtocol] = count + if count == 0: return p.notReady("No connected peers") return p.ready() proc getRlnRelayHealth(hm: NodeHealthMonitor): Future[ProtocolHealth] {.async.} = - var p = ProtocolHealth.init("Rln Relay") - if hm.node.wakuRlnRelay.isNil(): + var p = ProtocolHealth.init(WakuProtocol.RlnRelayProtocol) + if isNil(hm.node.wakuRlnRelay): return p.notMounted() const FutIsReadyTimout = 5.seconds @@ -72,121 +106,144 @@ proc getRlnRelayHealth(hm: NodeHealthMonitor): Future[ProtocolHealth] {.async.} proc getLightpushHealth( hm: NodeHealthMonitor, relayHealth: HealthStatus ): ProtocolHealth = - var p = ProtocolHealth.init("Lightpush") + var p = ProtocolHealth.init(WakuProtocol.LightpushProtocol) - if hm.node.wakuLightPush == nil: + if isNil(hm.node.wakuLightPush): + hm.strength[WakuProtocol.LightpushProtocol] = 0 return p.notMounted() + let peerCount = countCapablePeers(hm, WakuLightPushCodec) + hm.strength[WakuProtocol.LightpushProtocol] = peerCount + if relayHealth == HealthStatus.READY: return p.ready() return p.notReady("Node has no relay peers to fullfill push requests") -proc getLightpushClientHealth( - hm: NodeHealthMonitor, relayHealth: HealthStatus -): ProtocolHealth = - var p = ProtocolHealth.init("Lightpush Client") - - if hm.node.wakuLightpushClient == nil: - return p.notMounted() - - let selfServiceAvailable = - hm.node.wakuLightPush != nil and relayHealth == HealthStatus.READY - let servicePeerAvailable = hm.node.peerManager.selectPeer(WakuLightPushCodec).isSome() - - if selfServiceAvailable or servicePeerAvailable: - return p.ready() - - return p.notReady("No Lightpush service peer available yet") - proc getLegacyLightpushHealth( hm: NodeHealthMonitor, relayHealth: HealthStatus ): ProtocolHealth = - var p = ProtocolHealth.init("Legacy Lightpush") + var p = ProtocolHealth.init(WakuProtocol.LegacyLightpushProtocol) - if hm.node.wakuLegacyLightPush == nil: + if isNil(hm.node.wakuLegacyLightPush): + hm.strength[WakuProtocol.LegacyLightpushProtocol] = 0 return p.notMounted() + let peerCount = countCapablePeers(hm, WakuLegacyLightPushCodec) + hm.strength[WakuProtocol.LegacyLightpushProtocol] = peerCount + if relayHealth == HealthStatus.READY: return p.ready() return p.notReady("Node has no relay peers to fullfill push requests") -proc getLegacyLightpushClientHealth( - hm: NodeHealthMonitor, relayHealth: HealthStatus -): ProtocolHealth = - var p = ProtocolHealth.init("Legacy Lightpush Client") - - if hm.node.wakuLegacyLightpushClient == nil: - return p.notMounted() - - if (hm.node.wakuLegacyLightPush != nil and relayHealth == HealthStatus.READY) or - hm.node.peerManager.selectPeer(WakuLegacyLightPushCodec).isSome(): - return p.ready() - - return p.notReady("No Lightpush service peer available yet") - proc getFilterHealth(hm: NodeHealthMonitor, relayHealth: HealthStatus): ProtocolHealth = - var p = ProtocolHealth.init("Filter") + var p = ProtocolHealth.init(WakuProtocol.FilterProtocol) - if hm.node.wakuFilter == nil: + if isNil(hm.node.wakuFilter): + hm.strength[WakuProtocol.FilterProtocol] = 0 return p.notMounted() + let peerCount = countCapablePeers(hm, WakuFilterSubscribeCodec) + hm.strength[WakuProtocol.FilterProtocol] = peerCount + if relayHealth == HealthStatus.READY: return p.ready() return p.notReady("Relay is not ready, filter will not be able to sort out messages") -proc getFilterClientHealth( - hm: NodeHealthMonitor, relayHealth: HealthStatus -): ProtocolHealth = - var p = ProtocolHealth.init("Filter Client") - - if hm.node.wakuFilterClient == nil: - return p.notMounted() - - if hm.node.peerManager.selectPeer(WakuFilterSubscribeCodec).isSome(): - return p.ready() - - return p.notReady("No Filter service peer available yet") - proc getStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Store") + var p = ProtocolHealth.init(WakuProtocol.StoreProtocol) - if hm.node.wakuStore == nil: + if isNil(hm.node.wakuStore): + hm.strength[WakuProtocol.StoreProtocol] = 0 return p.notMounted() + let peerCount = countCapablePeers(hm, WakuStoreCodec) + hm.strength[WakuProtocol.StoreProtocol] = peerCount return p.ready() -proc getStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Store Client") +proc getLegacyStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = + var p = ProtocolHealth.init(WakuProtocol.LegacyStoreProtocol) - if hm.node.wakuStoreClient == nil: + if isNil(hm.node.wakuLegacyStore): + hm.strength[WakuProtocol.LegacyStoreProtocol] = 0 return p.notMounted() - if hm.node.peerManager.selectPeer(WakuStoreCodec).isSome() or hm.node.wakuStore != nil: + let peerCount = hm.countCapablePeers(WakuLegacyStoreCodec) + hm.strength[WakuProtocol.LegacyStoreProtocol] = peerCount + return p.ready() + +proc getLightpushClientHealth(hm: NodeHealthMonitor): ProtocolHealth = + var p = ProtocolHealth.init(WakuProtocol.LightpushClientProtocol) + + if isNil(hm.node.wakuLightpushClient): + hm.strength[WakuProtocol.LightpushClientProtocol] = 0 + return p.notMounted() + + let peerCount = countCapablePeers(hm, WakuLightPushCodec) + hm.strength[WakuProtocol.LightpushClientProtocol] = peerCount + + if peerCount > 0: + return p.ready() + return p.notReady("No Lightpush service peer available yet") + +proc getLegacyLightpushClientHealth(hm: NodeHealthMonitor): ProtocolHealth = + var p = ProtocolHealth.init(WakuProtocol.LegacyLightpushClientProtocol) + + if isNil(hm.node.wakuLegacyLightpushClient): + hm.strength[WakuProtocol.LegacyLightpushClientProtocol] = 0 + return p.notMounted() + + let peerCount = countCapablePeers(hm, WakuLegacyLightPushCodec) + hm.strength[WakuProtocol.LegacyLightpushClientProtocol] = peerCount + + if peerCount > 0: + return p.ready() + return p.notReady("No Lightpush service peer available yet") + +proc getFilterClientHealth(hm: NodeHealthMonitor): ProtocolHealth = + var p = ProtocolHealth.init(WakuProtocol.FilterClientProtocol) + + if isNil(hm.node.wakuFilterClient): + hm.strength[WakuProtocol.FilterClientProtocol] = 0 + return p.notMounted() + + let peerCount = countCapablePeers(hm, WakuFilterSubscribeCodec) + hm.strength[WakuProtocol.FilterClientProtocol] = peerCount + + if peerCount > 0: + return p.ready() + return p.notReady("No Filter service peer available yet") + +proc getStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = + var p = ProtocolHealth.init(WakuProtocol.StoreClientProtocol) + + if isNil(hm.node.wakuStoreClient): + hm.strength[WakuProtocol.StoreClientProtocol] = 0 + return p.notMounted() + + let peerCount = countCapablePeers(hm, WakuStoreCodec) + hm.strength[WakuProtocol.StoreClientProtocol] = peerCount + + if peerCount > 0 or not isNil(hm.node.wakuStore): return p.ready() return p.notReady( "No Store service peer available yet, neither Store service set up for the node" ) -proc getLegacyStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Legacy Store") - - if hm.node.wakuLegacyStore == nil: - return p.notMounted() - - return p.ready() - proc getLegacyStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Legacy Store Client") + var p = ProtocolHealth.init(WakuProtocol.LegacyStoreClientProtocol) - if hm.node.wakuLegacyStoreClient == nil: + if isNil(hm.node.wakuLegacyStoreClient): + hm.strength[WakuProtocol.LegacyStoreClientProtocol] = 0 return p.notMounted() - if hm.node.peerManager.selectPeer(WakuLegacyStoreCodec).isSome() or - hm.node.wakuLegacyStore != nil: + let peerCount = countCapablePeers(hm, WakuLegacyStoreCodec) + hm.strength[WakuProtocol.LegacyStoreClientProtocol] = peerCount + + if peerCount > 0 or not isNil(hm.node.wakuLegacyStore): return p.ready() return p.notReady( @@ -194,38 +251,305 @@ proc getLegacyStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = ) proc getPeerExchangeHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Peer Exchange") + var p = ProtocolHealth.init(WakuProtocol.PeerExchangeProtocol) - if hm.node.wakuPeerExchange == nil: + if isNil(hm.node.wakuPeerExchange): + hm.strength[WakuProtocol.PeerExchangeProtocol] = 0 return p.notMounted() + let peerCount = countCapablePeers(hm, WakuPeerExchangeCodec) + hm.strength[WakuProtocol.PeerExchangeProtocol] = peerCount + return p.ready() proc getRendezvousHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Rendezvous") + var p = ProtocolHealth.init(WakuProtocol.RendezvousProtocol) - if hm.node.wakuRendezvous == nil: + if isNil(hm.node.wakuRendezvous): + hm.strength[WakuProtocol.RendezvousProtocol] = 0 return p.notMounted() - if hm.node.peerManager.switch.peerStore.peers(RendezVousCodec).len() == 0: + let peerCount = countCapablePeers(hm, RendezVousCodec) + hm.strength[WakuProtocol.RendezvousProtocol] = peerCount + if peerCount == 0: return p.notReady("No Rendezvous peers are available yet") return p.ready() proc getMixHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Mix") + var p = ProtocolHealth.init(WakuProtocol.MixProtocol) - if hm.node.wakuMix.isNil(): + if isNil(hm.node.wakuMix): return p.notMounted() return p.ready() +proc getSyncProtocolHealthInfo*( + hm: NodeHealthMonitor, protocol: WakuProtocol +): ProtocolHealth = + ## Get ProtocolHealth for a given protocol that can provide it synchronously + ## + case protocol + of WakuProtocol.RelayProtocol: + return hm.getRelayHealth() + of WakuProtocol.StoreProtocol: + return hm.getStoreHealth() + of WakuProtocol.LegacyStoreProtocol: + return hm.getLegacyStoreHealth() + of WakuProtocol.FilterProtocol: + return hm.getFilterHealth(hm.getRelayHealth().health) + of WakuProtocol.LightpushProtocol: + return hm.getLightpushHealth(hm.getRelayHealth().health) + of WakuProtocol.LegacyLightpushProtocol: + return hm.getLegacyLightpushHealth(hm.getRelayHealth().health) + of WakuProtocol.PeerExchangeProtocol: + return hm.getPeerExchangeHealth() + of WakuProtocol.RendezvousProtocol: + return hm.getRendezvousHealth() + of WakuProtocol.MixProtocol: + return hm.getMixHealth() + of WakuProtocol.StoreClientProtocol: + return hm.getStoreClientHealth() + of WakuProtocol.LegacyStoreClientProtocol: + return hm.getLegacyStoreClientHealth() + of WakuProtocol.FilterClientProtocol: + return hm.getFilterClientHealth() + of WakuProtocol.LightpushClientProtocol: + return hm.getLightpushClientHealth() + of WakuProtocol.LegacyLightpushClientProtocol: + return hm.getLegacyLightpushClientHealth() + of WakuProtocol.RlnRelayProtocol: + # Could waitFor here but we don't want to block the main thread. + # Could also return a cached value from a previous check. + var p = ProtocolHealth.init(protocol) + return p.notReady("RLN Relay health check is async") + else: + var p = ProtocolHealth.init(protocol) + return p.notMounted() + +proc getProtocolHealthInfo*( + hm: NodeHealthMonitor, protocol: WakuProtocol +): Future[ProtocolHealth] {.async.} = + ## Get ProtocolHealth for a given protocol + ## + case protocol + of WakuProtocol.RlnRelayProtocol: + return await hm.getRlnRelayHealth() + else: + return hm.getSyncProtocolHealthInfo(protocol) + +proc getSyncAllProtocolHealthInfo(hm: NodeHealthMonitor): seq[ProtocolHealth] = + ## Get ProtocolHealth for the subset of protocols that can provide it synchronously + ## + var protocols: seq[ProtocolHealth] = @[] + let relayHealth = hm.getRelayHealth() + protocols.add(relayHealth) + + protocols.add(hm.getLightpushHealth(relayHealth.health)) + protocols.add(hm.getLegacyLightpushHealth(relayHealth.health)) + protocols.add(hm.getFilterHealth(relayHealth.health)) + protocols.add(hm.getStoreHealth()) + protocols.add(hm.getLegacyStoreHealth()) + protocols.add(hm.getPeerExchangeHealth()) + protocols.add(hm.getRendezvousHealth()) + protocols.add(hm.getMixHealth()) + + protocols.add(hm.getLightpushClientHealth()) + protocols.add(hm.getLegacyLightpushClientHealth()) + protocols.add(hm.getStoreClientHealth()) + protocols.add(hm.getLegacyStoreClientHealth()) + protocols.add(hm.getFilterClientHealth()) + return protocols + +proc getAllProtocolHealthInfo( + hm: NodeHealthMonitor +): Future[seq[ProtocolHealth]] {.async.} = + ## Get ProtocolHealth for all protocols + ## + var protocols = hm.getSyncAllProtocolHealthInfo() + + let rlnHealth = await hm.getRlnRelayHealth() + protocols.add(rlnHealth) + + return protocols + +proc calculateConnectionState*( + protocols: seq[ProtocolHealth], + strength: Table[WakuProtocol, int], ## latest connectivity strength (e.g. peer count) for a protocol + dLowOpt: Option[int], ## minimum relay peers for Connected status if in Core (Relay) mode +): ConnectionStatus = + var + relayCount = 0 + lightpushCount = 0 + filterCount = 0 + storeClientCount = 0 + + for p in protocols: + let kind = + try: + parseEnum[WakuProtocol](p.protocol) + except ValueError: + continue + + if p.health != HealthStatus.READY: + continue + + let strength = strength.getOrDefault(kind, 0) + + if kind in RelayProtocols: + relayCount = max(relayCount, strength) + elif kind in StoreClientProtocols: + storeClientCount = max(storeClientCount, strength) + elif kind in LightpushClientProtocols: + lightpushCount = max(lightpushCount, strength) + elif kind in FilterClientProtocols: + filterCount = max(filterCount, strength) + + debug "calculateConnectionState", + protocol = kind, + strength = strength, + relayCount = relayCount, + storeClientCount = storeClientCount, + lightpushCount = lightpushCount, + filterCount = filterCount + + # Relay connectivity should be a sufficient check in Core mode. + # "Store peers" are relay peers because incoming messages in + # the relay are input to the store server. + # But if Store server (or client, even) is not mounted as well, this logic assumes + # the user knows what they're doing. + + if dLowOpt.isSome(): + if relayCount >= dLowOpt.get(): + return ConnectionStatus.Connected + + if relayCount > 0: + return ConnectionStatus.PartiallyConnected + + # No relay connectivity. Relay might not be mounted, or may just have zero peers. + # Fall back to Edge check in any case to be sure. + + let canSend = lightpushCount > 0 + let canReceive = filterCount > 0 + let canStore = storeClientCount > 0 + + let meetsMinimum = canSend and canReceive and canStore + + if not meetsMinimum: + return ConnectionStatus.Disconnected + + let isEdgeRobust = + (lightpushCount >= HealthyThreshold) and (filterCount >= HealthyThreshold) and + (storeClientCount >= HealthyThreshold) + + if isEdgeRobust: + return ConnectionStatus.Connected + + return ConnectionStatus.PartiallyConnected + +proc calculateConnectionState*(hm: NodeHealthMonitor): ConnectionStatus = + let dLow = + if isNil(hm.node.wakuRelay): + none(int) + else: + some(hm.node.wakuRelay.parameters.dLow) + return calculateConnectionState(hm.cachedProtocols, hm.strength, dLow) + +proc getNodeHealthReport*(hm: NodeHealthMonitor): Future[HealthReport] {.async.} = + ## Get a HealthReport that includes all protocols + ## + var report: HealthReport + + if hm.nodeHealth == HealthStatus.INITIALIZING or + hm.nodeHealth == HealthStatus.SHUTTING_DOWN: + report.nodeHealth = hm.nodeHealth + report.connectionStatus = ConnectionStatus.Disconnected + return report + + if hm.cachedProtocols.len == 0: + hm.cachedProtocols = await hm.getAllProtocolHealthInfo() + hm.connectionStatus = hm.calculateConnectionState() + + report.nodeHealth = HealthStatus.READY + report.connectionStatus = hm.connectionStatus + report.protocolsHealth = hm.cachedProtocols + return report + +proc getSyncNodeHealthReport*(hm: NodeHealthMonitor): HealthReport = + ## Get a HealthReport that includes the subset of protocols that inform health synchronously + ## + var report: HealthReport + + if hm.nodeHealth == HealthStatus.INITIALIZING or + hm.nodeHealth == HealthStatus.SHUTTING_DOWN: + report.nodeHealth = hm.nodeHealth + report.connectionStatus = ConnectionStatus.Disconnected + return report + + if hm.cachedProtocols.len == 0: + hm.cachedProtocols = hm.getSyncAllProtocolHealthInfo() + hm.connectionStatus = hm.calculateConnectionState() + + report.nodeHealth = HealthStatus.READY + report.connectionStatus = hm.connectionStatus + report.protocolsHealth = hm.cachedProtocols + return report + +proc onRelayMsg( + hm: NodeHealthMonitor, peer: PubSubPeer, msg: var RPCMsg +) {.gcsafe, raises: [].} = + ## Inspect Relay events for health-update relevance in Core (Relay) mode. + ## + ## For Core (Relay) mode, the connectivity health state is mostly determined + ## by the relay protocol state (it is the dominant factor), and we know + ## that a peer Relay can only affect this Relay's health if there is a + ## subscription change or a mesh (GRAFT/PRUNE) change. + ## + + if msg.subscriptions.len == 0: + if msg.control.isNone(): + return + let ctrl = msg.control.get() + if ctrl.graft.len == 0 and ctrl.prune.len == 0: + return + + hm.healthUpdateEvent.fire() + +proc healthLoop(hm: NodeHealthMonitor) {.async.} = + ## Re-evaluate the global health state of the node when notified of a potential change, + ## and call back the application if an actual change from the last notified state happened. + info "Health monitor loop start" + while true: + try: + await hm.healthUpdateEvent.wait() + hm.healthUpdateEvent.clear() + + hm.cachedProtocols = await hm.getAllProtocolHealthInfo() + let newConnectionStatus = hm.calculateConnectionState() + + if newConnectionStatus != hm.connectionStatus: + hm.connectionStatus = newConnectionStatus + + EventConnectionStatusChange.emit(hm.node.brokerCtx, newConnectionStatus) + + if not isNil(hm.onConnectionStatusChange): + await hm.onConnectionStatusChange(newConnectionStatus) + except CancelledError: + break + except Exception as e: + error "HealthMonitor: error in update loop", error = e.msg + + # safety cooldown to protect from edge cases + await sleepAsync(100.milliseconds) + + info "Health monitor loop end" + proc selectRandomPeersForKeepalive( node: WakuNode, outPeers: seq[PeerId], numRandomPeers: int ): Future[seq[PeerId]] {.async.} = ## Select peers for random keepalive, prioritizing mesh peers - if node.wakuRelay.isNil(): + if isNil(node.wakuRelay): return selectRandomPeers(outPeers, numRandomPeers) let meshPeers = node.wakuRelay.getPeersInMesh().valueOr: @@ -359,45 +683,55 @@ proc startKeepalive*( hm.keepAliveFut = hm.node.keepAliveLoop(randomPeersKeepalive, allPeersKeepalive) return ok() -proc getNodeHealthReport*(hm: NodeHealthMonitor): Future[HealthReport] {.async.} = - var report: HealthReport - report.nodeHealth = hm.nodeHealth - - let relayHealth = hm.getRelayHealth() - report.protocolsHealth.add(relayHealth) - report.protocolsHealth.add(await hm.getRlnRelayHealth()) - report.protocolsHealth.add(hm.getLightpushHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getLegacyLightpushHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getFilterHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getStoreHealth()) - report.protocolsHealth.add(hm.getLegacyStoreHealth()) - report.protocolsHealth.add(hm.getPeerExchangeHealth()) - report.protocolsHealth.add(hm.getRendezvousHealth()) - report.protocolsHealth.add(hm.getMixHealth()) - - report.protocolsHealth.add(hm.getLightpushClientHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getLegacyLightpushClientHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getStoreClientHealth()) - report.protocolsHealth.add(hm.getLegacyStoreClientHealth()) - report.protocolsHealth.add(hm.getFilterClientHealth(relayHealth.health)) - return report - proc setOverallHealth*(hm: NodeHealthMonitor, health: HealthStatus) = hm.nodeHealth = health proc startHealthMonitor*(hm: NodeHealthMonitor): Result[void, string] = hm.onlineMonitor.startOnlineMonitor() + + if isNil(hm.node.peerManager): + return err("startHealthMonitor: no node peerManager to monitor") + + if not isNil(hm.node.wakuRelay): + hm.relayObserver = PubSubObserver( + onRecv: proc(peer: PubSubPeer, msgs: var RPCMsg) {.gcsafe, raises: [].} = + hm.onRelayMsg(peer, msgs) + ) + hm.node.wakuRelay.addObserver(hm.relayObserver) + + hm.peerEventListener = EventWakuPeer.listen( + hm.node.brokerCtx, + proc(evt: EventWakuPeer): Future[void] {.async: (raises: []), gcsafe.} = + ## Recompute health on any peer changing anything (join, leave, identify, metadata update) + hm.healthUpdateEvent.fire(), + ).valueOr: + return err("Failed to subscribe to peer events: " & error) + + hm.healthUpdateEvent = newAsyncEvent() + hm.healthUpdateEvent.fire() + + hm.healthLoopFut = hm.healthLoop() + hm.startKeepalive().isOkOr: return err("startHealthMonitor: failed starting keep alive: " & error) return ok() proc stopHealthMonitor*(hm: NodeHealthMonitor) {.async.} = - if not hm.onlineMonitor.isNil(): + if not isNil(hm.onlineMonitor): await hm.onlineMonitor.stopOnlineMonitor() - if not hm.keepAliveFut.isNil(): + if not isNil(hm.keepAliveFut): await hm.keepAliveFut.cancelAndWait() + if not isNil(hm.healthLoopFut): + await hm.healthLoopFut.cancelAndWait() + + if hm.peerEventListener.id != 0: + EventWakuPeer.dropListener(hm.node.brokerCtx, hm.peerEventListener) + + if not isNil(hm.node.wakuRelay) and not isNil(hm.relayObserver): + hm.node.wakuRelay.removeObserver(hm.relayObserver) + proc new*( T: type NodeHealthMonitor, node: WakuNode, @@ -406,4 +740,10 @@ proc new*( let om = OnlineMonitor.init(dnsNameServers) om.setPeerStoreToOnlineMonitor(node.switch.peerStore) om.addOnlineStateObserver(node.peerManager.getOnlineStateObserver()) - T(nodeHealth: INITIALIZING, node: node, onlineMonitor: om) + T( + nodeHealth: INITIALIZING, + node: node, + onlineMonitor: om, + connectionStatus: ConnectionStatus.Disconnected, + strength: initTable[WakuProtocol, int](), + ) diff --git a/waku/node/health_monitor/protocol_health.nim b/waku/node/health_monitor/protocol_health.nim index 7bacea94b..4479888c8 100644 --- a/waku/node/health_monitor/protocol_health.nim +++ b/waku/node/health_monitor/protocol_health.nim @@ -1,5 +1,8 @@ import std/[options, strformat] import ./health_status +import waku/common/waku_protocol + +export waku_protocol type ProtocolHealth* = object protocol*: string @@ -39,8 +42,7 @@ proc shuttingDown*(p: var ProtocolHealth): ProtocolHealth = proc `$`*(p: ProtocolHealth): string = return fmt"protocol: {p.protocol}, health: {p.health}, description: {p.desc}" -proc init*(p: typedesc[ProtocolHealth], protocol: string): ProtocolHealth = - let p = ProtocolHealth( - protocol: protocol, health: HealthStatus.NOT_MOUNTED, desc: none[string]() +proc init*(p: typedesc[ProtocolHealth], protocol: WakuProtocol): ProtocolHealth = + return ProtocolHealth( + protocol: $protocol, health: HealthStatus.NOT_MOUNTED, desc: none[string]() ) - return p diff --git a/waku/node/peer_manager/peer_manager.nim b/waku/node/peer_manager/peer_manager.nim index bdb68905e..834fb19cf 100644 --- a/waku/node/peer_manager/peer_manager.nim +++ b/waku/node/peer_manager/peer_manager.nim @@ -1,27 +1,31 @@ {.push raises: [].} import - std/[options, sets, sequtils, times, strformat, strutils, math, random, tables], + std/ + [ + options, sets, sequtils, times, strformat, strutils, math, random, tables, + algorithm, + ], chronos, chronicles, metrics, - libp2p/multistream, - libp2p/muxers/muxer, - libp2p/nameresolving/nameresolver, - libp2p/peerstore - -import - ../../common/nimchronos, - ../../common/enr, - ../../common/callbacks, - ../../common/utils/parse_size_units, - ../../waku_core, - ../../waku_relay, - ../../waku_relay/protocol, - ../../waku_enr/sharding, - ../../waku_enr/capabilities, - ../../waku_metadata, - ../health_monitor/online_monitor, + libp2p/[multistream, muxers/muxer, nameresolving/nameresolver, peerstore], + waku/[ + waku_core, + waku_relay, + waku_metadata, + waku_core/topics/sharding, + waku_relay/protocol, + waku_enr/sharding, + waku_enr/capabilities, + events/peer_events, + common/nimchronos, + common/enr, + common/callbacks, + common/utils/parse_size_units, + common/broker/broker_context, + node/health_monitor/online_monitor, + ], ./peer_store/peer_storage, ./waku_peer_store @@ -84,6 +88,7 @@ type ConnectionChangeHandler* = proc( ): Future[void] {.gcsafe, raises: [Defect].} type PeerManager* = ref object of RootObj + brokerCtx: BrokerContext switch*: Switch wakuMetadata*: WakuMetadata initialBackoffInSec*: int @@ -483,8 +488,9 @@ proc canBeConnected*(pm: PeerManager, peerId: PeerId): bool = proc connectedPeers*( pm: PeerManager, protocol: string = "" ): (seq[PeerId], seq[PeerId]) = - ## Returns the peerIds of physical connections (in and out) - ## If a protocol is specified, only returns peers with at least one stream of that protocol + ## Returns the PeerIds of peers with an active socket connection. + ## If a protocol is specified, it returns peers that currently have one + ## or more active logical streams for that protocol. var inPeers: seq[PeerId] var outPeers: seq[PeerId] @@ -500,6 +506,65 @@ proc connectedPeers*( return (inPeers, outPeers) +proc capablePeers*(pm: PeerManager, protocol: string): (seq[PeerId], seq[PeerId]) = + ## Returns the PeerIds of peers with an active socket connection. + ## If a protocol is specified, it returns peers that have identified + ## themselves as supporting the protocol. + + var inPeers: seq[PeerId] + var outPeers: seq[PeerId] + + for peerId, muxers in pm.switch.connManager.getConnections(): + # filter out peers that don't have the capability registered in the peer store + if pm.switch.peerStore.hasPeer(peerId, protocol): + for peerConn in muxers: + if peerConn.connection.transportDir == Direction.In: + inPeers.add(peerId) + elif peerConn.connection.transportDir == Direction.Out: + outPeers.add(peerId) + + return (inPeers, outPeers) + +proc getConnectedPeersCount*(pm: PeerManager, protocol: string): int = + ## Returns the total number of unique connected peers (inbound + outbound) + ## with active streams for a specific protocol. + let (inPeers, outPeers) = pm.connectedPeers(protocol) + var peers = initHashSet[PeerId](nextPowerOfTwo(inPeers.len + outPeers.len)) + for p in inPeers: + peers.incl(p) + for p in outPeers: + peers.incl(p) + return peers.len + +proc getCapablePeersCount*(pm: PeerManager, protocol: string): int = + ## Returns the total number of unique connected peers (inbound + outbound) + ## who have identified themselves as supporting the given protocol. + let (inPeers, outPeers) = pm.capablePeers(protocol) + var peers = initHashSet[PeerId](nextPowerOfTwo(inPeers.len + outPeers.len)) + for p in inPeers: + peers.incl(p) + for p in outPeers: + peers.incl(p) + return peers.len + +proc getPeersForShard*(pm: PeerManager, protocolId: string, shard: PubsubTopic): int = + let (inPeers, outPeers) = pm.connectedPeers(protocolId) + let connectedProtocolPeers = inPeers & outPeers + if connectedProtocolPeers.len == 0: + return 0 + + let shardInfo = RelayShard.parse(shard).valueOr: + # count raw peers of the given protocol if for some reason we can't get + # a shard mapping out of the gossipsub topic string. + return connectedProtocolPeers.len + + var shardPeers = 0 + for peerId in connectedProtocolPeers: + if pm.switch.peerStore.hasShard(peerId, shardInfo.clusterId, shardInfo.shardId): + shardPeers.inc() + + return shardPeers + proc disconnectAllPeers*(pm: PeerManager) {.async.} = let (inPeerIds, outPeerIds) = pm.connectedPeers() let connectedPeers = concat(inPeerIds, outPeerIds) @@ -635,7 +700,7 @@ proc getPeerIp(pm: PeerManager, peerId: PeerId): Option[string] = # Event Handling # #~~~~~~~~~~~~~~~~~# -proc onPeerMetadata(pm: PeerManager, peerId: PeerId) {.async.} = +proc refreshPeerMetadata(pm: PeerManager, peerId: PeerId) {.async.} = let res = catch: await pm.switch.dial(peerId, WakuMetadataCodec) @@ -664,6 +729,10 @@ proc onPeerMetadata(pm: PeerManager, peerId: PeerId) {.async.} = let shards = metadata.shards.mapIt(it.uint16) pm.switch.peerStore.setShardInfo(peerId, shards) + # TODO: should only trigger an event if metadata actually changed + # should include the shard subscription delta in the event when + # it is a MetadataUpdated event + EventWakuPeer.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventMetadataUpdated) return info "disconnecting from peer", peerId = peerId, reason = reason @@ -673,14 +742,14 @@ proc onPeerMetadata(pm: PeerManager, peerId: PeerId) {.async.} = # called when a peer i) first connects to us ii) disconnects all connections from us proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = if not pm.wakuMetadata.isNil() and event.kind == PeerEventKind.Joined: - await pm.onPeerMetadata(peerId) + await pm.refreshPeerMetadata(peerId) var peerStore = pm.switch.peerStore var direction: PeerDirection var connectedness: Connectedness case event.kind - of Joined: + of PeerEventKind.Joined: direction = if event.initiator: Outbound else: Inbound connectedness = Connected @@ -708,10 +777,12 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = asyncSpawn(pm.switch.disconnect(peerId)) peerStore.delete(peerId) + EventWakuPeer.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventConnected) + if not pm.onConnectionChange.isNil(): # we don't want to await for the callback to finish asyncSpawn pm.onConnectionChange(peerId, Joined) - of Left: + of PeerEventKind.Left: direction = UnknownDirection connectedness = CanConnect @@ -723,12 +794,16 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = pm.ipTable.del(ip) break + EventWakuPeer.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventDisconnected) + if not pm.onConnectionChange.isNil(): # we don't want to await for the callback to finish asyncSpawn pm.onConnectionChange(peerId, Left) - of Identified: + of PeerEventKind.Identified: info "event identified", peerId = peerId + EventWakuPeer.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventIdentified) + peerStore[ConnectionBook][peerId] = connectedness peerStore[DirectionBook][peerId] = direction @@ -1085,8 +1160,11 @@ proc new*( error "Max backoff time can't be over 1 week", maxBackoff = backoff raise newException(Defect, "Max backoff time can't be over 1 week") + let brokerCtx = globalBrokerContext() + let pm = PeerManager( switch: switch, + brokerCtx: brokerCtx, wakuMetadata: wakuMetadata, storage: storage, initialBackoffInSec: initialBackoffInSec, diff --git a/waku/node/peer_manager/waku_peer_store.nim b/waku/node/peer_manager/waku_peer_store.nim index b7f2669e5..a03b5ae2e 100644 --- a/waku/node/peer_manager/waku_peer_store.nim +++ b/waku/node/peer_manager/waku_peer_store.nim @@ -162,7 +162,9 @@ proc connectedness*(peerStore: PeerStore, peerId: PeerId): Connectedness = peerStore[ConnectionBook].book.getOrDefault(peerId, NotConnected) proc hasShard*(peerStore: PeerStore, peerId: PeerID, cluster, shard: uint16): bool = - peerStore[ENRBook].book.getOrDefault(peerId).containsShard(cluster, shard) + return + peerStore[ENRBook].book.getOrDefault(peerId).containsShard(cluster, shard) or + peerStore[ShardBook].book.getOrDefault(peerId, @[]).contains(shard) proc hasCapability*(peerStore: PeerStore, peerId: PeerID, cap: Capabilities): bool = peerStore[ENRBook].book.getOrDefault(peerId).supportsCapability(cap) @@ -219,7 +221,8 @@ proc getPeersByShard*( peerStore: PeerStore, cluster, shard: uint16 ): seq[RemotePeerInfo] = return peerStore.peers.filterIt( - it.enr.isSome() and it.enr.get().containsShard(cluster, shard) + (it.enr.isSome() and it.enr.get().containsShard(cluster, shard)) or + it.shards.contains(shard) ) proc getPeersByCapability*( diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index d556811ac..cb3d81c7c 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -42,6 +42,7 @@ import waku_store/resume, waku_store_sync, waku_filter_v2, + waku_filter_v2/common as filter_common, waku_filter_v2/client as filter_client, waku_metadata, waku_rendezvous/protocol, @@ -57,12 +58,18 @@ import common/rate_limit/setting, common/callbacks, common/nimchronos, + common/broker/broker_context, + common/broker/request_broker, waku_mix, requests/node_requests, - common/broker/broker_context, + requests/health_requests, + events/health_events, + events/peer_events, ], ./net_config, - ./peer_manager + ./peer_manager, + ./health_monitor/health_status, + ./health_monitor/topic_health declarePublicCounter waku_node_messages, "number of messages received", ["type"] @@ -91,6 +98,9 @@ const clientId* = "Nimbus Waku v2 node" const WakuNodeVersionString* = "version / git commit hash: " & git_version +const EdgeTopicHealthyThreshold = 2 + ## Lightpush server and filter server requirement for a healthy topic in edge mode + # key and crypto modules different type # TODO: Move to application instance (e.g., `WakuNode2`) @@ -135,6 +145,10 @@ type topicSubscriptionQueue*: AsyncEventQueue[SubscriptionEvent] rateLimitSettings*: ProtocolRateLimitSettings wakuMix*: WakuMix + edgeTopicsHealth*: Table[PubsubTopic, TopicHealth] + edgeHealthEvent*: AsyncEvent + edgeHealthLoop: Future[void] + peerEventListener*: EventWakuPeerListener proc deduceRelayShard( node: WakuNode, @@ -469,7 +483,52 @@ proc updateAnnouncedAddrWithPrimaryIpAddr*(node: WakuNode): Result[void, string] return ok() -proc startProvidersAndListeners(node: WakuNode) = +proc calculateEdgeTopicHealth(node: WakuNode, shard: PubsubTopic): TopicHealth = + let filterPeers = + node.peerManager.getPeersForShard(filter_common.WakuFilterSubscribeCodec, shard) + let lightpushPeers = + node.peerManager.getPeersForShard(lightpush_protocol.WakuLightPushCodec, shard) + + if filterPeers >= EdgeTopicHealthyThreshold and + lightpushPeers >= EdgeTopicHealthyThreshold: + return TopicHealth.SUFFICIENTLY_HEALTHY + elif filterPeers > 0 and lightpushPeers > 0: + return TopicHealth.MINIMALLY_HEALTHY + + return TopicHealth.UNHEALTHY + +proc loopEdgeHealth(node: WakuNode) {.async.} = + while node.started: + await node.edgeHealthEvent.wait() + node.edgeHealthEvent.clear() + + try: + for shard in node.edgeTopicsHealth.keys: + if not node.wakuRelay.isNil and node.wakuRelay.isSubscribed(shard): + continue + + let oldHealth = node.edgeTopicsHealth.getOrDefault(shard, TopicHealth.UNHEALTHY) + let newHealth = node.calculateEdgeTopicHealth(shard) + if newHealth != oldHealth: + node.edgeTopicsHealth[shard] = newHealth + EventShardTopicHealthChange.emit(node.brokerCtx, shard, newHealth) + except CancelledError: + break + except CatchableError as e: + warn "Error in edge health check", error = e.msg + + # safety cooldown to protect from edge cases + await sleepAsync(100.milliseconds) + +proc startProvidersAndListeners*(node: WakuNode) = + node.peerEventListener = EventWakuPeer.listen( + node.brokerCtx, + proc(evt: EventWakuPeer) {.async: (raises: []), gcsafe.} = + node.edgeHealthEvent.fire(), + ).valueOr: + error "Failed to listen to peer events", error = error + return + RequestRelayShard.setProvider( node.brokerCtx, proc( @@ -481,8 +540,60 @@ proc startProvidersAndListeners(node: WakuNode) = ).isOkOr: error "Can't set provider for RequestRelayShard", error = error -proc stopProvidersAndListeners(node: WakuNode) = + RequestShardTopicsHealth.setProvider( + node.brokerCtx, + proc(topics: seq[PubsubTopic]): Result[RequestShardTopicsHealth, string] = + var response: RequestShardTopicsHealth + + for shard in topics: + var healthStatus = TopicHealth.UNHEALTHY + + if not node.wakuRelay.isNil: + healthStatus = + node.wakuRelay.topicsHealth.getOrDefault(shard, TopicHealth.NOT_SUBSCRIBED) + + if healthStatus == TopicHealth.NOT_SUBSCRIBED: + healthStatus = node.calculateEdgeTopicHealth(shard) + + response.topicHealth.add((shard, healthStatus)) + + return ok(response), + ).isOkOr: + error "Can't set provider for RequestShardTopicsHealth", error = error + + RequestContentTopicsHealth.setProvider( + node.brokerCtx, + proc(topics: seq[ContentTopic]): Result[RequestContentTopicsHealth, string] = + var response: RequestContentTopicsHealth + + for contentTopic in topics: + var topicHealth = TopicHealth.NOT_SUBSCRIBED + + let shardResult = node.deduceRelayShard(contentTopic, none[PubsubTopic]()) + + if shardResult.isOk(): + let shardObj = shardResult.get() + let pubsubTopic = $shardObj + if not isNil(node.wakuRelay): + topicHealth = node.wakuRelay.topicsHealth.getOrDefault( + pubsubTopic, TopicHealth.NOT_SUBSCRIBED + ) + + if topicHealth == TopicHealth.NOT_SUBSCRIBED and + pubsubTopic in node.edgeTopicsHealth: + topicHealth = node.calculateEdgeTopicHealth(pubsubTopic) + + response.contentTopicHealth.add((topic: contentTopic, health: topicHealth)) + + return ok(response), + ).isOkOr: + error "Can't set provider for RequestContentTopicsHealth", error = error + +proc stopProvidersAndListeners*(node: WakuNode) = + EventWakuPeer.dropListener(node.brokerCtx, node.peerEventListener) RequestRelayShard.clearProvider(node.brokerCtx) + RequestContentTopicsHealth.clearProvider(node.brokerCtx) + RequestShardTopicsHealth.clearProvider(node.brokerCtx) proc start*(node: WakuNode) {.async.} = ## Starts a created Waku Node and @@ -532,6 +643,9 @@ proc start*(node: WakuNode) {.async.} = ## The switch will update addresses after start using the addressMapper await node.switch.start() + node.edgeHealthEvent = newAsyncEvent() + node.edgeHealthLoop = loopEdgeHealth(node) + node.startProvidersAndListeners() node.started = true @@ -549,6 +663,10 @@ proc stop*(node: WakuNode) {.async.} = node.stopProvidersAndListeners() + if not node.edgeHealthLoop.isNil: + await node.edgeHealthLoop.cancelAndWait() + node.edgeHealthLoop = nil + await node.switch.stop() node.peerManager.stop() diff --git a/waku/requests/health_request.nim b/waku/requests/health_request.nim deleted file mode 100644 index 9f98eba67..000000000 --- a/waku/requests/health_request.nim +++ /dev/null @@ -1,21 +0,0 @@ -import waku/common/broker/[request_broker, multi_request_broker] - -import waku/api/types -import waku/node/health_monitor/[protocol_health, topic_health] -import waku/waku_core/topics - -export protocol_health, topic_health - -RequestBroker(sync): - type RequestNodeHealth* = object - healthStatus*: NodeHealth - -RequestBroker(sync): - type RequestRelayTopicsHealth* = object - topicHealth*: seq[tuple[topic: PubsubTopic, health: TopicHealth]] - - proc signature(topics: seq[PubsubTopic]): Result[RequestRelayTopicsHealth, string] - -MultiRequestBroker: - type RequestProtocolHealth* = object - healthStatus*: ProtocolHealth diff --git a/waku/requests/health_requests.nim b/waku/requests/health_requests.nim new file mode 100644 index 000000000..3554922b3 --- /dev/null +++ b/waku/requests/health_requests.nim @@ -0,0 +1,39 @@ +import waku/common/broker/request_broker + +import waku/api/types +import waku/node/health_monitor/[protocol_health, topic_health, health_report] +import waku/waku_core/topics +import waku/common/waku_protocol + +export protocol_health, topic_health + +# Get the overall node connectivity status +RequestBroker(sync): + type RequestConnectionStatus* = object + connectionStatus*: ConnectionStatus + +# Get the health status of a set of content topics +RequestBroker(sync): + type RequestContentTopicsHealth* = object + contentTopicHealth*: seq[tuple[topic: ContentTopic, health: TopicHealth]] + + proc signature(topics: seq[ContentTopic]): Result[RequestContentTopicsHealth, string] + +# Get a consolidated node health report +RequestBroker: + type RequestHealthReport* = object + healthReport*: HealthReport + +# Get the health status of a set of shards (pubsub topics) +RequestBroker(sync): + type RequestShardTopicsHealth* = object + topicHealth*: seq[tuple[topic: PubsubTopic, health: TopicHealth]] + + proc signature(topics: seq[PubsubTopic]): Result[RequestShardTopicsHealth, string] + +# Get the health status of a mounted protocol +RequestBroker: + type RequestProtocolHealth* = object + healthStatus*: ProtocolHealth + + proc signature(protocol: WakuProtocol): Future[Result[RequestProtocolHealth, string]] diff --git a/waku/requests/requests.nim b/waku/requests/requests.nim index 03e10f882..9225c0f3e 100644 --- a/waku/requests/requests.nim +++ b/waku/requests/requests.nim @@ -1,3 +1,3 @@ -import ./[health_request, rln_requests, node_requests] +import ./[health_requests, rln_requests, node_requests] -export health_request, rln_requests, node_requests +export health_requests, rln_requests, node_requests diff --git a/waku/rest_api/endpoint/health/types.nim b/waku/rest_api/endpoint/health/types.nim index 57f8b284c..88fa736a8 100644 --- a/waku/rest_api/endpoint/health/types.nim +++ b/waku/rest_api/endpoint/health/types.nim @@ -2,7 +2,8 @@ import results import chronicles, json_serialization, json_serialization/std/options -import ../../../waku_node, ../serdes +import ../serdes +import waku/[waku_node, api/types] #### Serialization and deserialization @@ -44,6 +45,7 @@ proc writeValue*( ) {.raises: [IOError].} = writer.beginRecord() writer.writeField("nodeHealth", $value.nodeHealth) + writer.writeField("connectionStatus", $value.connectionStatus) writer.writeField("protocolsHealth", value.protocolsHealth) writer.endRecord() @@ -52,6 +54,7 @@ proc readValue*( ) {.raises: [SerializationError, IOError].} = var nodeHealth: Option[HealthStatus] + connectionStatus: Option[ConnectionStatus] protocolsHealth: Option[seq[ProtocolHealth]] for fieldName in readObjectFields(reader): @@ -66,6 +69,16 @@ proc readValue*( reader.raiseUnexpectedValue("Invalid `health` value: " & $error) nodeHealth = some(health) + of "connectionStatus": + if connectionStatus.isSome(): + reader.raiseUnexpectedField( + "Multiple `connectionStatus` fields found", "HealthReport" + ) + + let state = ConnectionStatus.init(reader.readValue(string)).valueOr: + reader.raiseUnexpectedValue("Invalid `connectionStatus` value: " & $error) + + connectionStatus = some(state) of "protocolsHealth": if protocolsHealth.isSome(): reader.raiseUnexpectedField( @@ -79,5 +92,8 @@ proc readValue*( if nodeHealth.isNone(): reader.raiseUnexpectedValue("Field `nodeHealth` is missing") - value = - HealthReport(nodeHealth: nodeHealth.get, protocolsHealth: protocolsHealth.get(@[])) + value = HealthReport( + nodeHealth: nodeHealth.get, + connectionStatus: connectionStatus.get, + protocolsHealth: protocolsHealth.get(@[]), + ) diff --git a/waku/waku_relay/protocol.nim b/waku/waku_relay/protocol.nim index 3f343269a..17470af29 100644 --- a/waku/waku_relay/protocol.nim +++ b/waku/waku_relay/protocol.nim @@ -5,7 +5,7 @@ {.push raises: [].} import - std/[strformat, strutils], + std/[strformat, strutils, sets], stew/byteutils, results, sequtils, @@ -21,11 +21,13 @@ import import waku/waku_core, waku/node/health_monitor/topic_health, - waku/requests/health_request, + waku/requests/health_requests, + waku/events/health_events, ./message_id, - waku/common/broker/broker_context + waku/common/broker/broker_context, + waku/events/peer_events -from ../waku_core/codecs import WakuRelayCodec +from waku/waku_core/codecs import WakuRelayCodec export WakuRelayCodec type ShardMetrics = object @@ -154,6 +156,8 @@ type pubsubTopic: PubsubTopic, message: WakuMessage ): Future[ValidationResult] {.gcsafe, raises: [Defect].} WakuRelay* = ref object of GossipSub + brokerCtx: BrokerContext + peerEventListener: EventWakuPeerListener # seq of tuples: the first entry in the tuple contains the validators are called for every topic # the second entry contains the error messages to be returned when the validator fails wakuValidators: seq[tuple[handler: WakuValidatorHandler, errorMessage: string]] @@ -165,6 +169,11 @@ type topicsHealth*: Table[string, TopicHealth] onTopicHealthChange*: TopicHealthChangeHandler topicHealthLoopHandle*: Future[void] + topicHealthUpdateEvent: AsyncEvent + topicHealthDirty: HashSet[string] + # list of topics that need their health updated in the update event + topicHealthCheckAll: bool + # true if all topics need to have their health status refreshed in the update event msgMetricsPerShard*: Table[string, ShardMetrics] # predefinition for more detailed results from publishing new message @@ -287,6 +296,21 @@ proc initRelayObservers(w: WakuRelay) = ) proc onRecv(peer: PubSubPeer, msgs: var RPCMsg) = + if msgs.control.isSome(): + let ctrl = msgs.control.get() + var topicsChanged = false + + for graft in ctrl.graft: + w.topicHealthDirty.incl(graft.topicID) + topicsChanged = true + + for prune in ctrl.prune: + w.topicHealthDirty.incl(prune.topicID) + topicsChanged = true + + if topicsChanged: + w.topicHealthUpdateEvent.fire() + for msg in msgs.messages: let (msg_id_short, topic, wakuMessage, msgSize) = decodeRpcMessageInfo(peer, msg).valueOr: continue @@ -325,18 +349,6 @@ proc initRelayObservers(w: WakuRelay) = w.addObserver(administrativeObserver) -proc initRequestProviders(w: WakuRelay) = - RequestRelayTopicsHealth.setProvider( - globalBrokerContext(), - proc(topics: seq[PubsubTopic]): Result[RequestRelayTopicsHealth, string] = - var collectedRes: RequestRelayTopicsHealth - for topic in topics: - let health = w.topicsHealth.getOrDefault(topic, TopicHealth.NOT_SUBSCRIBED) - collectedRes.topicHealth.add((topic, health)) - return ok(collectedRes), - ).isOkOr: - error "Cannot set Relay Topics Health request provider", error = error - proc new*( T: type WakuRelay, switch: Switch, maxMessageSize = int(DefaultMaxWakuMessageSize) ): WakuRelayResult[T] = @@ -354,12 +366,25 @@ proc new*( maxMessageSize = maxMessageSize, parameters = GossipsubParameters, ) + w.brokerCtx = globalBrokerContext() procCall GossipSub(w).initPubSub() w.topicsHealth = initTable[string, TopicHealth]() + w.topicHealthUpdateEvent = newAsyncEvent() + w.topicHealthDirty = initHashSet[string]() + w.topicHealthCheckAll = false w.initProtocolHandler() w.initRelayObservers() - w.initRequestProviders() + + w.peerEventListener = EventWakuPeer.listen( + w.brokerCtx, + proc(evt: EventWakuPeer): Future[void] {.async: (raises: []), gcsafe.} = + if evt.kind == WakuPeerEventKind.EventDisconnected: + w.topicHealthCheckAll = true + w.topicHealthUpdateEvent.fire() + , + ).valueOr: + return err("Failed to subscribe to peer events: " & error) except InitializationError: return err("initialization error: " & getCurrentExceptionMsg()) @@ -437,38 +462,58 @@ proc calculateTopicHealth(wakuRelay: WakuRelay, topic: string): TopicHealth = return TopicHealth.MINIMALLY_HEALTHY return TopicHealth.SUFFICIENTLY_HEALTHY -proc updateTopicsHealth(wakuRelay: WakuRelay) {.async.} = - var futs = newSeq[Future[void]]() - for topic in toSeq(wakuRelay.topics.keys): - ## loop over all the topics I'm subscribed to - let - oldHealth = wakuRelay.topicsHealth.getOrDefault(topic) - currentHealth = wakuRelay.calculateTopicHealth(topic) +proc isSubscribed*(w: WakuRelay, topic: PubsubTopic): bool = + GossipSub(w).topics.hasKey(topic) - if oldHealth == currentHealth: - continue +proc subscribedTopics*(w: WakuRelay): seq[PubsubTopic] = + return toSeq(GossipSub(w).topics.keys()) - wakuRelay.topicsHealth[topic] = currentHealth - if not wakuRelay.onTopicHealthChange.isNil(): - let fut = wakuRelay.onTopicHealthChange(topic, currentHealth) - if not fut.completed(): # Fast path for successful sync handlers - futs.add(fut) +proc topicsHealthLoop(w: WakuRelay) {.async.} = + while true: + await w.topicHealthUpdateEvent.wait() + w.topicHealthUpdateEvent.clear() + + var topicsToCheck: seq[string] + + if w.topicHealthCheckAll: + topicsToCheck = toSeq(w.topics.keys) + else: + topicsToCheck = toSeq(w.topicHealthDirty) + + w.topicHealthCheckAll = false + w.topicHealthDirty.clear() + + var futs = newSeq[Future[void]]() + + for topic in topicsToCheck: + # guard against topic being unsubscribed since fire() + if not w.isSubscribed(topic): + continue + + let + oldHealth = w.topicsHealth.getOrDefault(topic, TopicHealth.UNHEALTHY) + currentHealth = w.calculateTopicHealth(topic) + + if oldHealth == currentHealth: + continue + + w.topicsHealth[topic] = currentHealth + + EventShardTopicHealthChange.emit(w.brokerCtx, topic, currentHealth) + + if not w.onTopicHealthChange.isNil(): + futs.add(w.onTopicHealthChange(topic, currentHealth)) if futs.len() > 0: - # slow path - we have to wait for the handlers to complete try: - futs = await allFinished(futs) + discard await allFinished(futs) except CancelledError: - # check for errors in futures - for fut in futs: - if fut.failed: - let err = fut.readError() - warn "Error in health change handler", description = err.msg + break + except CatchableError as e: + warn "Error in topic health callback", error = e.msg -proc topicsHealthLoop(wakuRelay: WakuRelay) {.async.} = - while true: - await wakuRelay.updateTopicsHealth() - await sleepAsync(10.seconds) + # safety cooldown to protect from edge cases + await sleepAsync(100.milliseconds) method start*(w: WakuRelay) {.async, base.} = info "start" @@ -478,15 +523,13 @@ method start*(w: WakuRelay) {.async, base.} = method stop*(w: WakuRelay) {.async, base.} = info "stop" await procCall GossipSub(w).stop() + + if w.peerEventListener.id != 0: + EventWakuPeer.dropListener(w.brokerCtx, w.peerEventListener) + if not w.topicHealthLoopHandle.isNil(): await w.topicHealthLoopHandle.cancelAndWait() -proc isSubscribed*(w: WakuRelay, topic: PubsubTopic): bool = - GossipSub(w).topics.hasKey(topic) - -proc subscribedTopics*(w: WakuRelay): seq[PubsubTopic] = - return toSeq(GossipSub(w).topics.keys()) - proc generateOrderedValidator(w: WakuRelay): ValidatorHandler {.gcsafe.} = # rejects messages that are not WakuMessage let wrappedValidator = proc( @@ -584,7 +627,8 @@ proc subscribe*(w: WakuRelay, pubsubTopic: PubsubTopic, handler: WakuRelayHandle procCall GossipSub(w).subscribe(pubsubTopic, topicHandler) w.topicHandlers[pubsubTopic] = topicHandler - asyncSpawn w.updateTopicsHealth() + w.topicHealthDirty.incl(pubsubTopic) + w.topicHealthUpdateEvent.fire() proc unsubscribeAll*(w: WakuRelay, pubsubTopic: PubsubTopic) = ## Unsubscribe all handlers on this pubsub topic @@ -594,6 +638,8 @@ proc unsubscribeAll*(w: WakuRelay, pubsubTopic: PubsubTopic) = procCall GossipSub(w).unsubscribeAll(pubsubTopic) w.topicValidator.del(pubsubTopic) w.topicHandlers.del(pubsubTopic) + w.topicsHealth.del(pubsubTopic) + w.topicHealthDirty.excl(pubsubTopic) proc unsubscribe*(w: WakuRelay, pubsubTopic: PubsubTopic) = if not w.topicValidator.hasKey(pubsubTopic): @@ -619,6 +665,8 @@ proc unsubscribe*(w: WakuRelay, pubsubTopic: PubsubTopic) = w.topicValidator.del(pubsubTopic) w.topicHandlers.del(pubsubTopic) + w.topicsHealth.del(pubsubTopic) + w.topicHealthDirty.excl(pubsubTopic) proc publish*( w: WakuRelay, pubsubTopic: PubsubTopic, wakuMessage: WakuMessage From 84f791100fcc4367ca17a9d999e9aa943359dd51 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:23:21 +0100 Subject: [PATCH 060/155] fix: peer selection by shard and rendezvous/metadata sharding initialization (#3718) * Fix peer selection for cases where ENR is not yet advertiesed but metadata exchange already adjusted supported shards. Fix initialization rendezvous protocol with configured and autoshards to let connect to relay nodes without having a valid subscribed shard already. This solves issue for autoshard nodes to connect ahead of subscribing. * Extend peer selection, rendezvous and metadata tests * Fix rendezvous test, fix metadata test failing due wrong setup, added it into all_tests --- tests/all_tests_waku.nim | 1 + tests/test_peer_manager.nim | 230 ++++++++++++++++++++++++ tests/test_waku_metadata.nim | 85 +++++++-- tests/test_waku_rendezvous.nim | 63 +++++++ waku/factory/node_factory.nim | 2 +- waku/node/peer_manager/peer_manager.nim | 14 +- waku/node/waku_node.nim | 28 ++- 7 files changed, 401 insertions(+), 22 deletions(-) diff --git a/tests/all_tests_waku.nim b/tests/all_tests_waku.nim index 3d22cd9c2..4d4225f9f 100644 --- a/tests/all_tests_waku.nim +++ b/tests/all_tests_waku.nim @@ -89,6 +89,7 @@ import ./test_waku_netconfig, ./test_waku_switch, ./test_waku_rendezvous, + ./test_waku_metadata, ./waku_discv5/test_waku_discv5 # Waku Keystore test suite diff --git a/tests/test_peer_manager.nim b/tests/test_peer_manager.nim index 97df39582..c96f21b6e 100644 --- a/tests/test_peer_manager.nim +++ b/tests/test_peer_manager.nim @@ -1207,3 +1207,233 @@ procSuite "Peer Manager": r = node1.peerManager.selectPeer(WakuPeerExchangeCodec) assert r.isSome(), "could not retrieve peer mounting WakuPeerExchangeCodec" + + asyncTest "selectPeer() filters peers by shard using ENR": + ## Given: A peer manager with 3 peers having different shards in their ENRs + let + clusterId = 0.uint16 + shardId0 = 0.uint16 + shardId1 = 1.uint16 + + # Create 3 nodes with different shards + let nodes = + @[ + newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId0], + ), + newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId1], + ), + newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId0], + ), + ] + + await allFutures(nodes.mapIt(it.start())) + for node in nodes: + discard await node.mountRelay() + + # Get peer infos with ENRs + let peerInfos = collect: + for node in nodes: + var peerInfo = node.switch.peerInfo.toRemotePeerInfo() + peerInfo.enr = some(node.enr) + peerInfo + + # Add all peers to node 0's peer manager and peerstore + for i in 1 .. 2: + nodes[0].peerManager.addPeer(peerInfos[i]) + nodes[0].peerManager.switch.peerStore[AddressBook][peerInfos[i].peerId] = + peerInfos[i].addrs + nodes[0].peerManager.switch.peerStore[ProtoBook][peerInfos[i].peerId] = + @[WakuRelayCodec] + + ## When: We select a peer for shard 0 + let shard0Topic = some(PubsubTopic("/waku/2/rs/0/0")) + let selectedPeer0 = nodes[0].peerManager.selectPeer(WakuRelayCodec, shard0Topic) + + ## Then: Only peers supporting shard 0 are considered (nodes 2, not node 1) + check: + selectedPeer0.isSome() + selectedPeer0.get().peerId != peerInfos[1].peerId # node1 has shard 1 + selectedPeer0.get().peerId == peerInfos[2].peerId # node2 has shard 0 + + ## When: We select a peer for shard 1 + let shard1Topic = some(PubsubTopic("/waku/2/rs/0/1")) + let selectedPeer1 = nodes[0].peerManager.selectPeer(WakuRelayCodec, shard1Topic) + + ## Then: Only peer with shard 1 is selected + check: + selectedPeer1.isSome() + selectedPeer1.get().peerId == peerInfos[1].peerId # node1 has shard 1 + + await allFutures(nodes.mapIt(it.stop())) + + asyncTest "selectPeer() filters peers by shard using shards field": + ## Given: A peer manager with peers having shards in RemotePeerInfo (no ENR) + let + clusterId = 0.uint16 + shardId0 = 0.uint16 + shardId1 = 1.uint16 + + # Create peer manager + let pm = PeerManager.new( + switch = SwitchBuilder.new().withRng(rng()).withMplex().withNoise().build(), + storage = nil, + ) + + # Create peer infos with shards field populated (simulating metadata exchange) + let basePeerId = "16Uiu2HAm7QGEZKujdSbbo1aaQyfDPQ6Bw3ybQnj6fruH5Dxwd7D" + let peers = toSeq(1 .. 3) + .mapIt(parsePeerInfo("/ip4/0.0.0.0/tcp/30300/p2p/" & basePeerId & $it)) + .filterIt(it.isOk()) + .mapIt(it.value) + require: + peers.len == 3 + + # Manually populate the shards field (ENR is not available) + var peerInfos: seq[RemotePeerInfo] = @[] + for i, peer in peers: + var peerInfo = RemotePeerInfo.init(peer.peerId, peer.addrs) + # Peer 0 and 2 have shard 0, peer 1 has shard 1 + peerInfo.shards = + if i == 1: + @[shardId1] + else: + @[shardId0] + # Note: ENR is intentionally left as none + peerInfos.add(peerInfo) + + # Add peers to peerstore + for peerInfo in peerInfos: + pm.switch.peerStore[AddressBook][peerInfo.peerId] = peerInfo.addrs + pm.switch.peerStore[ProtoBook][peerInfo.peerId] = @[WakuRelayCodec] + # simulate metadata exchange by setting shards field in peerstore + pm.switch.peerStore.setShardInfo(peerInfo.peerId, peerInfo.shards) + + ## When: We select a peer for shard 0 + let shard0Topic = some(PubsubTopic("/waku/2/rs/0/0")) + let selectedPeer0 = pm.selectPeer(WakuRelayCodec, shard0Topic) + + ## Then: Peers with shard 0 in shards field are selected + check: + selectedPeer0.isSome() + selectedPeer0.get().peerId in [peerInfos[0].peerId, peerInfos[2].peerId] + + ## When: We select a peer for shard 1 + let shard1Topic = some(PubsubTopic("/waku/2/rs/0/1")) + let selectedPeer1 = pm.selectPeer(WakuRelayCodec, shard1Topic) + + ## Then: Peer with shard 1 in shards field is selected + check: + selectedPeer1.isSome() + selectedPeer1.get().peerId == peerInfos[1].peerId + + asyncTest "selectPeer() handles invalid pubsub topic gracefully": + ## Given: A peer manager with valid peers + let node = newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = 0, + subscribeShards = @[0'u16], + ) + await node.start() + + # Add a peer + let peer = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + await peer.start() + discard await peer.mountRelay() + + var peerInfo = peer.switch.peerInfo.toRemotePeerInfo() + peerInfo.enr = some(peer.enr) + node.peerManager.addPeer(peerInfo) + node.peerManager.switch.peerStore[ProtoBook][peerInfo.peerId] = @[WakuRelayCodec] + + ## When: selectPeer is called with malformed pubsub topic + let invalidTopics = + @[ + some(PubsubTopic("invalid-topic")), + some(PubsubTopic("/waku/2/invalid")), + some(PubsubTopic("/waku/2/rs/abc/0")), # non-numeric cluster + some(PubsubTopic("")), # empty topic + ] + + ## Then: Returns none(RemotePeerInfo) without crashing + for invalidTopic in invalidTopics: + let result = node.peerManager.selectPeer(WakuRelayCodec, invalidTopic) + check: + result.isNone() + + await allFutures(node.stop(), peer.stop()) + + asyncTest "selectPeer() prioritizes ENR over shards field": + ## Given: A peer with both ENR and shards field populated + let + clusterId = 0.uint16 + shardId0 = 0.uint16 + shardId1 = 1.uint16 + + let node = newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId0], + ) + await node.start() + discard await node.mountRelay() + + # Create peer with ENR containing shard 0 + let peer = newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId0], + ) + await peer.start() + discard await peer.mountRelay() + + # Create peer info with ENR (shard 0) but set shards field to shard 1 + var peerInfo = peer.switch.peerInfo.toRemotePeerInfo() + peerInfo.enr = some(peer.enr) # ENR has shard 0 + peerInfo.shards = @[shardId1] # shards field has shard 1 + + node.peerManager.addPeer(peerInfo) + node.peerManager.switch.peerStore[ProtoBook][peerInfo.peerId] = @[WakuRelayCodec] + # simulate metadata exchange by setting shards field in peerstore + node.peerManager.switch.peerStore.setShardInfo(peerInfo.peerId, peerInfo.shards) + + ## When: We select for shard 0 + let shard0Topic = some(PubsubTopic("/waku/2/rs/0/0")) + let selectedPeer = node.peerManager.selectPeer(WakuRelayCodec, shard0Topic) + + ## Then: Peer is selected because ENR (shard 0) takes precedence + check: + selectedPeer.isSome() + selectedPeer.get().peerId == peerInfo.peerId + + ## When: We select for shard 1 + let shard1Topic = some(PubsubTopic("/waku/2/rs/0/1")) + let selectedPeer1 = node.peerManager.selectPeer(WakuRelayCodec, shard1Topic) + + ## Then: Peer is still selected because shards field is checked as fallback + check: + selectedPeer1.isSome() + selectedPeer1.get().peerId == peerInfo.peerId + + await allFutures(node.stop(), peer.stop()) diff --git a/tests/test_waku_metadata.nim b/tests/test_waku_metadata.nim index b30fd1712..cfceb89b5 100644 --- a/tests/test_waku_metadata.nim +++ b/tests/test_waku_metadata.nim @@ -13,14 +13,15 @@ import eth/keys, eth/p2p/discoveryv5/enr import - waku/ - [ - waku_node, - waku_core/topics, - node/peer_manager, - discovery/waku_discv5, - waku_metadata, - ], + waku/[ + waku_node, + waku_core/topics, + waku_core, + node/peer_manager, + discovery/waku_discv5, + waku_metadata, + waku_relay/protocol, + ], ./testlib/wakucore, ./testlib/wakunode @@ -41,26 +42,86 @@ procSuite "Waku Metadata Protocol": clusterId = clusterId, ) + # Mount metadata protocol on both nodes before starting + discard node1.mountMetadata(clusterId, @[]) + discard node2.mountMetadata(clusterId, @[]) + + # Mount relay so metadata can track subscriptions + discard await node1.mountRelay() + discard await node2.mountRelay() + # Start nodes await allFutures([node1.start(), node2.start()]) - node1.topicSubscriptionQueue.emit((kind: PubsubSub, topic: "/waku/2/rs/10/7")) - node1.topicSubscriptionQueue.emit((kind: PubsubSub, topic: "/waku/2/rs/10/6")) + # Subscribe to topics on node1 - relay will track these and metadata will report them + let noOpHandler: WakuRelayHandler = proc( + pubsubTopic: PubsubTopic, message: WakuMessage + ): Future[void] {.async.} = + discard + + node1.wakuRelay.subscribe("/waku/2/rs/10/7", noOpHandler) + node1.wakuRelay.subscribe("/waku/2/rs/10/6", noOpHandler) # Create connection let connOpt = await node2.peerManager.dialPeer( node1.switch.peerInfo.toRemotePeerInfo(), WakuMetadataCodec ) require: - connOpt.isSome + connOpt.isSome() # Request metadata let response1 = await node2.wakuMetadata.request(connOpt.get()) # Check the response or dont even continue require: - response1.isOk + response1.isOk() check: response1.get().clusterId.get() == clusterId response1.get().shards == @[uint32(6), uint32(7)] + + await allFutures([node1.stop(), node2.stop()]) + + asyncTest "Metadata reports configured shards before relay subscription": + ## Given: Node with configured shards but no relay subscriptions yet + let + clusterId = 10.uint16 + configuredShards = @[uint16(0), uint16(1)] + + let node1 = newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = configuredShards, + ) + let node2 = newTestWakuNode( + generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0), clusterId = clusterId + ) + + # Mount metadata with configured shards on node1 + discard node1.mountMetadata(clusterId, configuredShards) + # Mount metadata on node2 so it can make requests + discard node2.mountMetadata(clusterId, @[]) + + # Start nodes (relay is NOT mounted yet on node1) + await allFutures([node1.start(), node2.start()]) + + ## When: Node2 requests metadata from Node1 before relay is active + let connOpt = await node2.peerManager.dialPeer( + node1.switch.peerInfo.toRemotePeerInfo(), WakuMetadataCodec + ) + require: + connOpt.isSome + + let response = await node2.wakuMetadata.request(connOpt.get()) + + ## Then: Response contains configured shards even without relay subscriptions + require: + response.isOk() + + check: + response.get().clusterId.get() == clusterId + response.get().shards == @[uint32(0), uint32(1)] + + await allFutures([node1.stop(), node2.stop()]) diff --git a/tests/test_waku_rendezvous.nim b/tests/test_waku_rendezvous.nim index d3dd6f920..07113ca4a 100644 --- a/tests/test_waku_rendezvous.nim +++ b/tests/test_waku_rendezvous.nim @@ -10,6 +10,7 @@ import import waku/waku_core/peers, waku/waku_core/codecs, + waku/waku_core, waku/node/waku_node, waku/node/peer_manager/peer_manager, waku/waku_rendezvous/protocol, @@ -81,3 +82,65 @@ procSuite "Waku Rendezvous": records.len == 1 records[0].peerId == peerInfo1.peerId #records[0].mixPubKey == $node1.wakuMix.pubKey + + asyncTest "Rendezvous advertises configured shards before relay is active": + ## Given: A node with configured shards but no relay subscriptions yet + let + clusterId = 10.uint16 + configuredShards = @[RelayShard(clusterId: clusterId, shardId: 0)] + + let node = newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[0'u16], + ) + + ## When: Node mounts rendezvous with configured shards (before relay) + await node.mountRendezvous(clusterId, configuredShards) + await node.start() + + ## Then: The rendezvous protocol should be mounted successfully + check: + node.wakuRendezvous != nil + + # Verify that the protocol is running without errors + # (shards are used internally by the getShardsGetter closure) + let namespace = computeMixNamespace(clusterId) + check: + namespace.len > 0 + + await node.stop() + + asyncTest "Rendezvous uses configured shards when relay not mounted": + ## Given: A light client node with no relay protocol + let + clusterId = 10.uint16 + configuredShards = + @[ + RelayShard(clusterId: clusterId, shardId: 0), + RelayShard(clusterId: clusterId, shardId: 1), + ] + + let lightClient = newTestWakuNode( + generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0), clusterId = clusterId + ) + + ## When: Node mounts rendezvous with configured shards (no relay mounted) + await lightClient.mountRendezvous(clusterId, configuredShards) + await lightClient.start() + + ## Then: Rendezvous should be mounted successfully without relay + check: + lightClient.wakuRendezvous != nil + lightClient.wakuRelay == nil # Verify relay is not mounted + + # Verify the protocol is working (doesn't fail immediately) + # advertiseAll requires peers,so we just check the protocol is initialized + await sleepAsync(100.milliseconds) + + check: + lightClient.wakuRendezvous != nil + + await lightClient.stop() diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index 2cdfdb0d2..dc383e89d 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -337,7 +337,7 @@ proc setupProtocols( node.wakuRelay.addSignedShardsValidator(subscribedProtectedShards, conf.clusterId) if conf.rendezvous: - await node.mountRendezvous(conf.clusterId) + await node.mountRendezvous(conf.clusterId, shards) await node.mountRendezvousClient(conf.clusterId) # Keepalive mounted on all nodes diff --git a/waku/node/peer_manager/peer_manager.nim b/waku/node/peer_manager/peer_manager.nim index 834fb19cf..0c435468f 100644 --- a/waku/node/peer_manager/peer_manager.nim +++ b/waku/node/peer_manager/peer_manager.nim @@ -227,7 +227,19 @@ proc selectPeer*( protocol = proto, peers, address = cast[uint](pm.switch.peerStore) if shard.isSome(): - peers.keepItIf((it.enr.isSome() and it.enr.get().containsShard(shard.get()))) + # Parse the shard from the pubsub topic to get cluster and shard ID + let shardInfo = RelayShard.parse(shard.get()).valueOr: + trace "Failed to parse shard from pubsub topic", topic = shard.get() + return none(RemotePeerInfo) + + # Filter peers that support the requested shard + # Check both ENR (if present) and the shards field on RemotePeerInfo + peers.keepItIf( + # Check ENR if available + (it.enr.isSome() and it.enr.get().containsShard(shard.get())) or + # Otherwise check the shards field directly + (it.shards.len > 0 and it.shards.contains(shardInfo.shardId)) + ) shuffle(peers) diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index cb3d81c7c..53ce0349a 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -167,20 +167,28 @@ proc deduceRelayShard( return err("Invalid topic:" & pubsubTopic & " " & $error) return ok(shard) -proc getShardsGetter(node: WakuNode): GetShards = +proc getShardsGetter(node: WakuNode, configuredShards: seq[uint16]): GetShards = return proc(): seq[uint16] {.closure, gcsafe, raises: [].} = # fetch pubsubTopics subscribed to relay and convert them to shards if node.wakuRelay.isNil(): - return @[] + # If relay is not mounted, return configured shards + return configuredShards + let subscribedTopics = node.wakuRelay.subscribedTopics() + + # If relay hasn't subscribed to any topics yet, return configured shards + if subscribedTopics.len == 0: + return configuredShards + let relayShards = topicsToRelayShards(subscribedTopics).valueOr: error "could not convert relay topics to shards", error = $error, topics = subscribedTopics - return @[] + # Fall back to configured shards on error + return configuredShards if relayShards.isSome(): let shards = relayShards.get().shardIds return shards - return @[] + return configuredShards proc getCapabilitiesGetter(node: WakuNode): GetCapabilities = return proc(): seq[Capabilities] {.closure, gcsafe, raises: [].} = @@ -227,7 +235,7 @@ proc new*( rateLimitSettings: rateLimitSettings, ) - peerManager.setShardGetter(node.getShardsGetter()) + peerManager.setShardGetter(node.getShardsGetter(@[])) return node @@ -272,7 +280,7 @@ proc mountMetadata*( if not node.wakuMetadata.isNil(): return err("Waku metadata already mounted, skipping") - let metadata = WakuMetadata.new(clusterId, node.getShardsGetter()) + let metadata = WakuMetadata.new(clusterId, node.getShardsGetter(shards)) node.wakuMetadata = metadata node.peerManager.wakuMetadata = metadata @@ -413,14 +421,18 @@ proc mountRendezvousClient*(node: WakuNode, clusterId: uint16) {.async: (raises: if node.started: await node.wakuRendezvousClient.start() -proc mountRendezvous*(node: WakuNode, clusterId: uint16) {.async: (raises: []).} = +proc mountRendezvous*( + node: WakuNode, clusterId: uint16, shards: seq[RelayShard] = @[] +) {.async: (raises: []).} = info "mounting rendezvous discovery protocol" + let configuredShards = shards.mapIt(it.shardId) + node.wakuRendezvous = WakuRendezVous.new( node.switch, node.peerManager, clusterId, - node.getShardsGetter(), + node.getShardsGetter(configuredShards), node.getCapabilitiesGetter(), node.getWakuPeerRecordGetter(), ).valueOr: From eb0c34c553c4a0ba7f1806d14083e6ef1e49dc92 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:55:31 +0100 Subject: [PATCH 061/155] Adjust docker file to bsd (#3720) * add libbsd-dev into Dockerfile * add libstdc++ in Dockerfile to avoid runtime error loading shared library libstdc++.so.6: No such file or directory (needed by /usr/bin/wakunode) --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 90fb0a9c9..5b16b9eee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ ARG LOG_LEVEL=TRACE ARG HEAPTRACK_BUILD=0 # Get build tools and required header files -RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq +RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq libbsd-dev WORKDIR /app COPY . . @@ -46,7 +46,7 @@ LABEL version="unknown" EXPOSE 30303 60000 8545 # Referenced in the binary -RUN apk add --no-cache libgcc libpq-dev bind-tools +RUN apk add --no-cache libgcc libpq-dev bind-tools libstdc++ # Copy to separate location to accomodate different MAKE_TARGET values COPY --from=nim-build /app/build/$MAKE_TARGET /usr/local/bin/ From 8f29070dcfc7c621fdcd3941369fd2cdc81b69e7 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:49:35 +0100 Subject: [PATCH 062/155] fix avoid IndexDefect if DB error message is short (#3725) --- waku/common/databases/db_postgres/dbconn.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/waku/common/databases/db_postgres/dbconn.nim b/waku/common/databases/db_postgres/dbconn.nim index a6c237ae5..7ccf32099 100644 --- a/waku/common/databases/db_postgres/dbconn.nim +++ b/waku/common/databases/db_postgres/dbconn.nim @@ -48,8 +48,8 @@ proc check(db: DbConn): Result[void, string] = return err("exception in check: " & getCurrentExceptionMsg()) if message.len > 0: - let truncatedErr = message[0 .. 80] - ## libpq sometimes gives extremely long error messages + let truncatedErr = message[0 ..< min(80, message.len)] + error "postgres check issue. see truncated db error.", error = truncatedErr return err(truncatedErr) return ok() From b38b5aaea17e36c8f4dcccfa2e0030cb4a6619e7 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:18:46 +0100 Subject: [PATCH 063/155] force FINALIZE partition detach after detecting shorter error (#3728) --- .../waku_archive/driver/postgres_driver/postgres_driver.nim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim index 4877cb126..2f495ba5d 100644 --- a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim @@ -1347,8 +1347,10 @@ proc removePartition( (await self.performWriteQuery(detachPartitionQuery)).isOkOr: info "detected error when trying to detach partition", error - if ($error).contains("FINALIZE") or - ($error).contains("already pending detach in part"): + if ($error).contains("FINALIZE") or ($error).contains("already pending"): + ## We assume "already pending detach in partitioned table ..." as possible error + debug "enforce detach with FINALIZE because of detected error", error + ## We assume the database is suggesting to use FINALIZE when detaching a partition let detachPartitionFinalizeQuery = "ALTER TABLE messages DETACH PARTITION " & partitionName & " FINALIZE;" From 3603b838b92502db3208fb059c58868532aaf1c0 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:38:35 +0100 Subject: [PATCH 064/155] feat: liblogosdelivery FFI library of new API (#3714) * Initial for liblogosdelivery library (static & dynamic) based on current state of API. * nix build support added. * logosdelivery_example * Added support for missing logLevel/logFormat in new API create_node * Added full JSON to NodeConfig support * Added ctx and ctx.myLib check to avoid uninitialzed calls and crash. Adjusted logosdelivery_example with proper error handling and JSON config format * target aware install phase * Fix base64 decode of payload --- Makefile | 38 +- flake.nix | 7 + liblogosdelivery/BUILD.md | 123 +++ liblogosdelivery/MESSAGE_EVENTS.md | 148 ++++ liblogosdelivery/README.md | 262 +++++++ liblogosdelivery/declare_lib.nim | 24 + .../examples/logosdelivery_example.c | 193 +++++ liblogosdelivery/json_event.nim | 27 + liblogosdelivery/liblogosdelivery.h | 82 ++ liblogosdelivery/liblogosdelivery.nim | 29 + .../logos_delivery_api/messaging_api.nim | 91 +++ .../logos_delivery_api/node_api.nim | 111 +++ liblogosdelivery/nim.cfg | 27 + nix/default.nix | 40 +- nix/submodules.json | 247 ++++++ scripts/generate_nix_submodules.sh | 82 ++ tests/api/test_node_conf.nim | 708 ++++++++++++++++++ waku.nimble | 18 +- waku/api/api.nim | 1 + waku/api/api_conf.nim | 311 ++++++++ .../send_service/send_service.nim | 1 + 21 files changed, 2557 insertions(+), 13 deletions(-) create mode 100644 liblogosdelivery/BUILD.md create mode 100644 liblogosdelivery/MESSAGE_EVENTS.md create mode 100644 liblogosdelivery/README.md create mode 100644 liblogosdelivery/declare_lib.nim create mode 100644 liblogosdelivery/examples/logosdelivery_example.c create mode 100644 liblogosdelivery/json_event.nim create mode 100644 liblogosdelivery/liblogosdelivery.h create mode 100644 liblogosdelivery/liblogosdelivery.nim create mode 100644 liblogosdelivery/logos_delivery_api/messaging_api.nim create mode 100644 liblogosdelivery/logos_delivery_api/node_api.nim create mode 100644 liblogosdelivery/nim.cfg create mode 100644 nix/submodules.json create mode 100755 scripts/generate_nix_submodules.sh diff --git a/Makefile b/Makefile index 6457b3c0f..4fafd6310 100644 --- a/Makefile +++ b/Makefile @@ -434,10 +434,11 @@ docker-liteprotocoltester-push: ################ ## C Bindings ## ################ -.PHONY: cbindings cwaku_example libwaku +.PHONY: cbindings cwaku_example libwaku liblogosdelivery liblogosdelivery_example STATIC ?= 0 -BUILD_COMMAND ?= libwakuDynamic +LIBWAKU_BUILD_COMMAND ?= libwakuDynamic +LIBLOGOSDELIVERY_BUILD_COMMAND ?= liblogosdeliveryDynamic ifeq ($(detected_OS),Windows) LIB_EXT_DYNAMIC = dll @@ -453,11 +454,40 @@ endif LIB_EXT := $(LIB_EXT_DYNAMIC) ifeq ($(STATIC), 1) LIB_EXT = $(LIB_EXT_STATIC) - BUILD_COMMAND = libwakuStatic + LIBWAKU_BUILD_COMMAND = libwakuStatic + LIBLOGOSDELIVERY_BUILD_COMMAND = liblogosdeliveryStatic endif libwaku: | build deps librln - echo -e $(BUILD_MSG) "build/$@.$(LIB_EXT)" && $(ENV_SCRIPT) nim $(BUILD_COMMAND) $(NIM_PARAMS) waku.nims $@.$(LIB_EXT) + echo -e $(BUILD_MSG) "build/$@.$(LIB_EXT)" && $(ENV_SCRIPT) nim $(LIBWAKU_BUILD_COMMAND) $(NIM_PARAMS) waku.nims $@.$(LIB_EXT) + +liblogosdelivery: | build deps librln + echo -e $(BUILD_MSG) "build/$@.$(LIB_EXT)" && $(ENV_SCRIPT) nim $(LIBLOGOSDELIVERY_BUILD_COMMAND) $(NIM_PARAMS) waku.nims $@.$(LIB_EXT) + +logosdelivery_example: | build liblogosdelivery + @echo -e $(BUILD_MSG) "build/$@" +ifeq ($(detected_OS),Darwin) + gcc -o build/$@ \ + liblogosdelivery/examples/logosdelivery_example.c \ + -I./liblogosdelivery \ + -L./build \ + -llogosdelivery \ + -Wl,-rpath,./build +else ifeq ($(detected_OS),Linux) + gcc -o build/$@ \ + liblogosdelivery/examples/logosdelivery_example.c \ + -I./liblogosdelivery \ + -L./build \ + -llogosdelivery \ + -Wl,-rpath,'$$ORIGIN' +else ifeq ($(detected_OS),Windows) + gcc -o build/$@.exe \ + liblogosdelivery/examples/logosdelivery_example.c \ + -I./liblogosdelivery \ + -L./build \ + -llogosdelivery \ + -lws2_32 +endif ##################### ## Mobile Bindings ## diff --git a/flake.nix b/flake.nix index 88229a826..ee24c8f13 100644 --- a/flake.nix +++ b/flake.nix @@ -71,6 +71,13 @@ zerokitRln = zerokit.packages.${system}.rln; }; + liblogosdelivery = pkgs.callPackage ./nix/default.nix { + inherit stableSystems; + src = self; + targets = ["liblogosdelivery"]; + zerokitRln = zerokit.packages.${system}.rln; + }; + default = libwaku; }); diff --git a/liblogosdelivery/BUILD.md b/liblogosdelivery/BUILD.md new file mode 100644 index 000000000..011fbb438 --- /dev/null +++ b/liblogosdelivery/BUILD.md @@ -0,0 +1,123 @@ +# Building liblogosdelivery and Examples + +## Prerequisites + +- Nim 2.x compiler +- Rust toolchain (for RLN dependencies) +- GCC or Clang compiler +- Make + +## Building the Library + +### Dynamic Library + +```bash +make liblogosdelivery +``` + +This creates `build/liblogosdelivery.dylib` (macOS) or `build/liblogosdelivery.so` (Linux). + +### Static Library + +```bash +nim liblogosdelivery STATIC=1 +``` + +This creates `build/liblogosdelivery.a`. + +## Building Examples + +### liblogosdelivery Example + +Compile the C example that demonstrates all library features: + +```bash +# Using Make (recommended) +make liblogosdelivery_example + +## Running Examples + +```bash +./build/liblogosdelivery_example +``` + +The example will: +1. Create a Logos Messaging node +2. Register event callbacks for message events +3. Start the node +4. Subscribe to a content topic +5. Send a message +6. Show message delivery events (sent, propagated, or error) +7. Unsubscribe and cleanup + +## Build Artifacts + +After building, you'll have: + +``` +build/ +├── liblogosdelivery.dylib # Dynamic library (34MB) +├── liblogosdelivery.dylib.dSYM/ # Debug symbols +└── liblogosdelivery_example # Compiled example (34KB) +``` + +## Library Headers + +The main header file is: +- `liblogosdelivery/liblogosdelivery.h` - C API declarations + +## Troubleshooting + +### Library not found at runtime + +If you get "library not found" errors when running the example: + +**macOS:** +```bash +export DYLD_LIBRARY_PATH=/path/to/build:$DYLD_LIBRARY_PATH +./build/liblogosdelivery_example +``` + +**Linux:** +```bash +export LD_LIBRARY_PATH=/path/to/build:$LD_LIBRARY_PATH +./build/liblogosdelivery_example +``` +## Cross-Compilation + +For cross-compilation, you need to: +1. Build the Nim library for the target platform +2. Use the appropriate cross-compiler +3. Link against the target platform's liblogosdelivery + +Example for Linux from macOS: +```bash +# Build library for Linux (requires Docker or cross-compilation setup) +# Then compile with cross-compiler +``` + +## Integration with Your Project + +### CMake + +```cmake +find_library(LMAPI_LIBRARY NAMES lmapi PATHS ${PROJECT_SOURCE_DIR}/build) +include_directories(${PROJECT_SOURCE_DIR}/liblogosdelivery) +target_link_libraries(your_target ${LMAPI_LIBRARY}) +``` + +### Makefile + +```makefile +CFLAGS += -I/path/to/liblogosdelivery +LDFLAGS += -L/path/to/build -llmapi -Wl,-rpath,/path/to/build + +your_program: your_program.c + $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +``` + +## API Documentation + +See: +- [liblogosdelivery.h](liblogosdelivery/liblogosdelivery.h) - API function declarations +- [MESSAGE_EVENTS.md](liblogosdelivery/MESSAGE_EVENTS.md) - Message event handling guide diff --git a/liblogosdelivery/MESSAGE_EVENTS.md b/liblogosdelivery/MESSAGE_EVENTS.md new file mode 100644 index 000000000..60740fb62 --- /dev/null +++ b/liblogosdelivery/MESSAGE_EVENTS.md @@ -0,0 +1,148 @@ +# Message Event Handling in LMAPI + +## Overview + +The liblogosdelivery library emits three types of message delivery events that clients can listen to by registering an event callback using `logosdelivery_set_event_callback()`. + +## Event Types + +### 1. message_sent +Emitted when a message is successfully accepted by the send service and queued for delivery. + +**JSON Structure:** +```json +{ + "eventType": "message_sent", + "requestId": "unique-request-id", + "messageHash": "0x..." +} +``` + +**Fields:** +- `eventType`: Always "message_sent" +- `requestId`: Request ID returned from the send operation +- `messageHash`: Hash of the message that was sent + +### 2. message_propagated +Emitted when a message has been successfully propagated to neighboring nodes on the network. + +**JSON Structure:** +```json +{ + "eventType": "message_propagated", + "requestId": "unique-request-id", + "messageHash": "0x..." +} +``` + +**Fields:** +- `eventType`: Always "message_propagated" +- `requestId`: Request ID from the send operation +- `messageHash`: Hash of the message that was propagated + +### 3. message_error +Emitted when an error occurs during message sending or propagation. + +**JSON Structure:** +```json +{ + "eventType": "message_error", + "requestId": "unique-request-id", + "messageHash": "0x...", + "error": "error description" +} +``` + +**Fields:** +- `eventType`: Always "message_error" +- `requestId`: Request ID from the send operation +- `messageHash`: Hash of the message that failed +- `error`: Description of what went wrong + +## Usage + +### 1. Define an Event Callback + +```c +void event_callback(int ret, const char *msg, size_t len, void *userData) { + if (ret != RET_OK || msg == NULL || len == 0) { + return; + } + + // Parse the JSON message + // Extract eventType field + // Handle based on event type + + if (eventType == "message_sent") { + // Handle message sent + } else if (eventType == "message_propagated") { + // Handle message propagated + } else if (eventType == "message_error") { + // Handle message error + } +} +``` + +### 2. Register the Callback + +```c +void *ctx = logosdelivery_create_node(config, callback, userData); +logosdelivery_set_event_callback(ctx, event_callback, NULL); +``` + +### 3. Start the Node + +Once the node is started, events will be delivered to your callback: + +```c +logosdelivery_start_node(ctx, callback, userData); +``` + +## Event Flow + +For a typical successful message send: + +1. **send** → Returns request ID +2. **message_sent** → Message accepted and queued +3. **message_propagated** → Message delivered to peers + +For a failed message send: + +1. **send** → Returns request ID +2. **message_sent** → Message accepted and queued +3. **message_error** → Delivery failed with error description + +## Important Notes + +1. **Thread Safety**: The event callback is invoked from the FFI worker thread. Ensure your callback is thread-safe if it accesses shared state. + +2. **Non-Blocking**: Keep the callback fast and non-blocking. Do not perform long-running operations in the callback. + +3. **JSON Parsing**: The example uses a simple string-based parser. For production, use a proper JSON library like: + - [cJSON](https://github.com/DaveGamble/cJSON) + - [json-c](https://github.com/json-c/json-c) + - [Jansson](https://github.com/akheron/jansson) + +4. **Memory Management**: The message buffer is owned by the library. Copy any data you need to retain. + +5. **Event Order**: Events are delivered in the order they occur, but timing depends on network conditions. + +## Example Implementation + +See `examples/liblogosdelivery_example.c` for a complete working example that: +- Registers an event callback +- Sends a message +- Receives and prints all three event types +- Properly parses the JSON event structure + +## Debugging Events + +To see all events during development: + +```c +void debug_event_callback(int ret, const char *msg, size_t len, void *userData) { + printf("Event received: %.*s\n", (int)len, msg); +} +``` + +This will print the raw JSON for all events, helping you understand the event structure. diff --git a/liblogosdelivery/README.md b/liblogosdelivery/README.md new file mode 100644 index 000000000..f9909dd3d --- /dev/null +++ b/liblogosdelivery/README.md @@ -0,0 +1,262 @@ +# Logos Messaging API (LMAPI) Library + +A C FFI library providing a simplified interface to Logos Messaging functionality. + +## Overview + +This library wraps the high-level API functions from `waku/api/api.nim` and exposes them via a C FFI interface, making them accessible from C, C++, and other languages that support C FFI. + +## API Functions + +### Node Lifecycle + +#### `logosdelivery_create_node` +Creates a new instance of the node from the given configuration JSON. + +```c +void *logosdelivery_create_node( + const char *configJson, + FFICallBack callback, + void *userData +); +``` + +**Parameters:** +- `configJson`: JSON string containing node configuration +- `callback`: Callback function to receive the result +- `userData`: User data passed to the callback + +**Returns:** Pointer to the context needed by other API functions, or NULL on error. + +**Example configuration JSON:** +```json +{ + "mode": "Core", + "clusterId": 1, + "entryNodes": [ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im" + ], + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } +} +``` + +#### `logosdelivery_start_node` +Starts the node. + +```c +int logosdelivery_start_node( + void *ctx, + FFICallBack callback, + void *userData +); +``` + +#### `logosdelivery_stop_node` +Stops the node. + +```c +int logosdelivery_stop_node( + void *ctx, + FFICallBack callback, + void *userData +); +``` + +#### `logosdelivery_destroy` +Destroys a node instance and frees resources. + +```c +int logosdelivery_destroy( + void *ctx, + FFICallBack callback, + void *userData +); +``` + +### Messaging + +#### `logosdelivery_subscribe` +Subscribe to a content topic to receive messages. + +```c +int logosdelivery_subscribe( + void *ctx, + FFICallBack callback, + void *userData, + const char *contentTopic +); +``` + +**Parameters:** +- `ctx`: Context pointer from `logosdelivery_create_node` +- `callback`: Callback function to receive the result +- `userData`: User data passed to the callback +- `contentTopic`: Content topic string (e.g., "/myapp/1/chat/proto") + +#### `logosdelivery_unsubscribe` +Unsubscribe from a content topic. + +```c +int logosdelivery_unsubscribe( + void *ctx, + FFICallBack callback, + void *userData, + const char *contentTopic +); +``` + +#### `logosdelivery_send` +Send a message. + +```c +int logosdelivery_send( + void *ctx, + FFICallBack callback, + void *userData, + const char *messageJson +); +``` + +**Parameters:** +- `messageJson`: JSON string containing the message + +**Example message JSON:** +```json +{ + "contentTopic": "/myapp/1/chat/proto", + "payload": "SGVsbG8gV29ybGQ=", + "ephemeral": false +} +``` + +Note: The `payload` field should be base64-encoded. + +**Returns:** Request ID in the callback message that can be used to track message delivery. + +### Events + +#### `logosdelivery_set_event_callback` +Sets a callback that will be invoked whenever an event occurs (e.g., message received). + +```c +void logosdelivery_set_event_callback( + void *ctx, + FFICallBack callback, + void *userData +); +``` + +**Important:** The callback should be fast, non-blocking, and thread-safe. + +## Building + +The library follows the same build system as the main Logos Messaging project. + +### Build the library + +```bash +make liblogosdeliveryStatic # Build static library +# or +make liblogosdeliveryDynamic # Build dynamic library +``` + +## Return Codes + +All functions that return `int` use the following return codes: + +- `RET_OK` (0): Success +- `RET_ERR` (1): Error +- `RET_MISSING_CALLBACK` (2): Missing callback function + +## Callback Function + +All API functions use the following callback signature: + +```c +typedef void (*FFICallBack)( + int callerRet, + const char *msg, + size_t len, + void *userData +); +``` + +**Parameters:** +- `callerRet`: Return code (RET_OK, RET_ERR, etc.) +- `msg`: Response message (may be empty for success) +- `len`: Length of the message +- `userData`: User data passed in the original call + +## Example Usage + +```c +#include "liblogosdelivery.h" +#include + +void callback(int ret, const char *msg, size_t len, void *userData) { + if (ret == RET_OK) { + printf("Success: %.*s\n", (int)len, msg); + } else { + printf("Error: %.*s\n", (int)len, msg); + } +} + +int main() { + const char *config = "{" + "\"mode\": \"Core\"," + "\"clusterId\": 1" + "}"; + + // Create node + void *ctx = logosdelivery_create_node(config, callback, NULL); + if (ctx == NULL) { + return 1; + } + + // Start node + logosdelivery_start_node(ctx, callback, NULL); + + // Subscribe to a topic + logosdelivery_subscribe(ctx, callback, NULL, "/myapp/1/chat/proto"); + + // Send a message + const char *msg = "{" + "\"contentTopic\": \"/myapp/1/chat/proto\"," + "\"payload\": \"SGVsbG8gV29ybGQ=\"," + "\"ephemeral\": false" + "}"; + logosdelivery_send(ctx, callback, NULL, msg); + + // Clean up + logosdelivery_stop_node(ctx, callback, NULL); + logosdelivery_destroy(ctx, callback, NULL); + + return 0; +} +``` + +## Architecture + +The library is structured as follows: + +- `liblogosdelivery.h`: C header file with function declarations +- `liblogosdelivery.nim`: Main library entry point +- `declare_lib.nim`: Library declaration and initialization +- `lmapi/node_api.nim`: Node lifecycle API implementation +- `lmapi/messaging_api.nim`: Subscribe/send API implementation + +The library uses the nim-ffi framework for FFI infrastructure, which handles: +- Thread-safe request processing +- Async operation management +- Memory management between C and Nim +- Callback marshaling + +## See Also + +- Main API documentation: `waku/api/api.nim` +- Original libwaku library: `library/libwaku.nim` +- nim-ffi framework: `vendor/nim-ffi/` diff --git a/liblogosdelivery/declare_lib.nim b/liblogosdelivery/declare_lib.nim new file mode 100644 index 000000000..98209c649 --- /dev/null +++ b/liblogosdelivery/declare_lib.nim @@ -0,0 +1,24 @@ +import ffi +import waku/factory/waku + +declareLibrary("logosdelivery") + +template requireInitializedNode*( + ctx: ptr FFIContext[Waku], opName: string, onError: untyped +) = + if isNil(ctx): + let errMsg {.inject.} = opName & " failed: invalid context" + onError + elif isNil(ctx.myLib) or isNil(ctx.myLib[]): + let errMsg {.inject.} = opName & " failed: node is not initialized" + onError + +proc logosdelivery_set_event_callback( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.dynlib, exportc, cdecl.} = + if isNil(ctx): + echo "error: invalid context in logosdelivery_set_event_callback" + return + + ctx[].eventCallback = cast[pointer](callback) + ctx[].eventUserData = userData diff --git a/liblogosdelivery/examples/logosdelivery_example.c b/liblogosdelivery/examples/logosdelivery_example.c new file mode 100644 index 000000000..5437be427 --- /dev/null +++ b/liblogosdelivery/examples/logosdelivery_example.c @@ -0,0 +1,193 @@ +#include "../liblogosdelivery.h" +#include +#include +#include +#include + +static int create_node_ok = -1; + +// Helper function to extract a JSON string field value +// Very basic parser - for production use a proper JSON library +const char* extract_json_field(const char *json, const char *field, char *buffer, size_t bufSize) { + char searchStr[256]; + snprintf(searchStr, sizeof(searchStr), "\"%s\":\"", field); + + const char *start = strstr(json, searchStr); + if (!start) { + return NULL; + } + + start += strlen(searchStr); + const char *end = strchr(start, '"'); + if (!end) { + return NULL; + } + + size_t len = end - start; + if (len >= bufSize) { + len = bufSize - 1; + } + + memcpy(buffer, start, len); + buffer[len] = '\0'; + + return buffer; +} + +// Event callback that handles message events +void event_callback(int ret, const char *msg, size_t len, void *userData) { + if (ret != RET_OK || msg == NULL || len == 0) { + return; + } + + // Create null-terminated string for easier parsing + char *eventJson = malloc(len + 1); + if (!eventJson) { + return; + } + memcpy(eventJson, msg, len); + eventJson[len] = '\0'; + + // Extract eventType + char eventType[64]; + if (!extract_json_field(eventJson, "eventType", eventType, sizeof(eventType))) { + free(eventJson); + return; + } + + // Handle different event types + if (strcmp(eventType, "message_sent") == 0) { + char requestId[128]; + char messageHash[128]; + extract_json_field(eventJson, "requestId", requestId, sizeof(requestId)); + extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); + printf("📤 [EVENT] Message sent - RequestID: %s, Hash: %s\n", requestId, messageHash); + + } else if (strcmp(eventType, "message_error") == 0) { + char requestId[128]; + char messageHash[128]; + char error[256]; + extract_json_field(eventJson, "requestId", requestId, sizeof(requestId)); + extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); + extract_json_field(eventJson, "error", error, sizeof(error)); + printf("❌ [EVENT] Message error - RequestID: %s, Hash: %s, Error: %s\n", + requestId, messageHash, error); + + } else if (strcmp(eventType, "message_propagated") == 0) { + char requestId[128]; + char messageHash[128]; + extract_json_field(eventJson, "requestId", requestId, sizeof(requestId)); + extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); + printf("✅ [EVENT] Message propagated - RequestID: %s, Hash: %s\n", requestId, messageHash); + + } else { + printf("ℹ️ [EVENT] Unknown event type: %s\n", eventType); + } + + free(eventJson); +} + +// Simple callback that prints results +void simple_callback(int ret, const char *msg, size_t len, void *userData) { + const char *operation = (const char *)userData; + + if (operation != NULL && strcmp(operation, "create_node") == 0) { + create_node_ok = (ret == RET_OK) ? 1 : 0; + } + + if (ret == RET_OK) { + if (len > 0) { + printf("[%s] Success: %.*s\n", operation, (int)len, msg); + } else { + printf("[%s] Success\n", operation); + } + } else { + printf("[%s] Error: %.*s\n", operation, (int)len, msg); + } +} + +int main() { + printf("=== Logos Messaging API (LMAPI) Example ===\n\n"); + + // Configuration JSON for creating a node + const char *config = "{" + "\"logLevel\": \"DEBUG\"," + // "\"mode\": \"Edge\"," + "\"mode\": \"Core\"," + "\"protocolsConfig\": {" + "\"entryNodes\": [\"/dns4/node-01.do-ams3.misc.logos-chat.status.im/tcp/30303/p2p/16Uiu2HAkxoqUTud5LUPQBRmkeL2xP4iKx2kaABYXomQRgmLUgf78\"]," + "\"clusterId\": 42," + "\"autoShardingConfig\": {" + "\"numShardsInCluster\": 8" + "}" + "}," + "\"networkingConfig\": {" + "\"listenIpv4\": \"0.0.0.0\"," + "\"p2pTcpPort\": 60000," + "\"discv5UdpPort\": 9000" + "}" + "}"; + + printf("1. Creating node...\n"); + void *ctx = logosdelivery_create_node(config, simple_callback, (void *)"create_node"); + if (ctx == NULL) { + printf("Failed to create node\n"); + return 1; + } + + // Wait a bit for the callback + sleep(1); + + if (create_node_ok != 1) { + printf("Create node failed, stopping example early.\n"); + logosdelivery_destroy(ctx, simple_callback, (void *)"destroy"); + return 1; + } + + printf("\n2. Setting up event callback...\n"); + logosdelivery_set_event_callback(ctx, event_callback, NULL); + printf("Event callback registered for message events\n"); + + printf("\n3. Starting node...\n"); + logosdelivery_start_node(ctx, simple_callback, (void *)"start_node"); + + // Wait for node to start + sleep(2); + + printf("\n4. Subscribing to content topic...\n"); + const char *contentTopic = "/example/1/chat/proto"; + logosdelivery_subscribe(ctx, simple_callback, (void *)"subscribe", contentTopic); + + // Wait for subscription + sleep(1); + + printf("\n5. Sending a message...\n"); + printf("Watch for message events (sent, propagated, or error):\n"); + // Create base64-encoded payload: "Hello, Logos Messaging!" + const char *message = "{" + "\"contentTopic\": \"/example/1/chat/proto\"," + "\"payload\": \"SGVsbG8sIExvZ29zIE1lc3NhZ2luZyE=\"," + "\"ephemeral\": false" + "}"; + logosdelivery_send(ctx, simple_callback, (void *)"send", message); + + // Wait for message events to arrive + printf("Waiting for message delivery events...\n"); + sleep(60); + + printf("\n6. Unsubscribing from content topic...\n"); + logosdelivery_unsubscribe(ctx, simple_callback, (void *)"unsubscribe", contentTopic); + + sleep(1); + + printf("\n7. Stopping node...\n"); + logosdelivery_stop_node(ctx, simple_callback, (void *)"stop_node"); + + sleep(1); + + printf("\n8. Destroying context...\n"); + logosdelivery_destroy(ctx, simple_callback, (void *)"destroy"); + + printf("\n=== Example completed ===\n"); + return 0; +} diff --git a/liblogosdelivery/json_event.nim b/liblogosdelivery/json_event.nim new file mode 100644 index 000000000..389e29120 --- /dev/null +++ b/liblogosdelivery/json_event.nim @@ -0,0 +1,27 @@ +import std/[json, macros] + +type JsonEvent*[T] = ref object + eventType*: string + payload*: T + +macro toFlatJson*(event: JsonEvent): JsonNode = + ## Serializes JsonEvent[T] to flat JSON with eventType first, + ## followed by all fields from T's payload + result = quote: + var jsonObj = newJObject() + jsonObj["eventType"] = %`event`.eventType + + # Serialize payload fields into the same object (flattening) + let payloadJson = %`event`.payload + for key, val in payloadJson.pairs: + jsonObj[key] = val + + jsonObj + +proc `$`*[T](event: JsonEvent[T]): string = + $toFlatJson(event) + +proc newJsonEvent*[T](eventType: string, payload: T): JsonEvent[T] = + ## Creates a new JsonEvent with the given eventType and payload. + ## The payload's fields will be flattened into the JSON output. + JsonEvent[T](eventType: eventType, payload: payload) diff --git a/liblogosdelivery/liblogosdelivery.h b/liblogosdelivery/liblogosdelivery.h new file mode 100644 index 000000000..b014d6385 --- /dev/null +++ b/liblogosdelivery/liblogosdelivery.h @@ -0,0 +1,82 @@ + +// Generated manually and inspired by libwaku.h +// Header file for Logos Messaging API (LMAPI) library +#pragma once +#ifndef __liblogosdelivery__ +#define __liblogosdelivery__ + +#include +#include + +// The possible returned values for the functions that return int +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); + + // Creates a new instance of the node from the given configuration JSON. + // Returns a pointer to the Context needed by the rest of the API functions. + // Configuration should be in JSON format following the NodeConfig structure. + void *logosdelivery_create_node( + const char *configJson, + FFICallBack callback, + void *userData); + + // Starts the node. + int logosdelivery_start_node(void *ctx, + FFICallBack callback, + void *userData); + + // Stops the node. + int logosdelivery_stop_node(void *ctx, + FFICallBack callback, + void *userData); + + // Destroys an instance of a node created with logosdelivery_create_node + int logosdelivery_destroy(void *ctx, + FFICallBack callback, + void *userData); + + // Subscribe to a content topic. + // contentTopic: string representing the content topic (e.g., "/myapp/1/chat/proto") + int logosdelivery_subscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *contentTopic); + + // Unsubscribe from a content topic. + int logosdelivery_unsubscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *contentTopic); + + // Send a message. + // messageJson: JSON string with the following structure: + // { + // "contentTopic": "/myapp/1/chat/proto", + // "payload": "base64-encoded-payload", + // "ephemeral": false + // } + // Returns a request ID that can be used to track the message delivery. + int logosdelivery_send(void *ctx, + FFICallBack callback, + void *userData, + const char *messageJson); + + // Sets a callback that will be invoked whenever an event occurs. + // It is crucial that the passed callback is fast, non-blocking and potentially thread-safe. + void logosdelivery_set_event_callback(void *ctx, + FFICallBack callback, + void *userData); + +#ifdef __cplusplus +} +#endif + +#endif /* __liblogosdelivery__ */ diff --git a/liblogosdelivery/liblogosdelivery.nim b/liblogosdelivery/liblogosdelivery.nim new file mode 100644 index 000000000..7d068b065 --- /dev/null +++ b/liblogosdelivery/liblogosdelivery.nim @@ -0,0 +1,29 @@ +import std/[atomics, options] +import chronicles, chronos, chronos/threadsync, ffi +import waku/factory/waku, waku/node/waku_node, ./declare_lib + +################################################################################ +## Include different APIs, i.e. all procs with {.ffi.} pragma +include ./logos_delivery_api/node_api, ./logos_delivery_api/messaging_api + +################################################################################ +### Exported procs + +proc logosdelivery_destroy( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +): cint {.dynlib, exportc, cdecl.} = + initializeLibrary() + checkParams(ctx, callback, userData) + + ffi.destroyFFIContext(ctx).isOkOr: + let msg = "liblogosdelivery error: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return RET_ERR + + ## always need to invoke the callback although we don't retrieve value to the caller + callback(RET_OK, nil, 0, userData) + + return RET_OK + +# ### End of exported procs +# ################################################################################ diff --git a/liblogosdelivery/logos_delivery_api/messaging_api.nim b/liblogosdelivery/logos_delivery_api/messaging_api.nim new file mode 100644 index 000000000..cb2771034 --- /dev/null +++ b/liblogosdelivery/logos_delivery_api/messaging_api.nim @@ -0,0 +1,91 @@ +import std/[json] +import chronos, results, ffi +import stew/byteutils +import + waku/common/base64, + waku/factory/waku, + waku/waku_core/topics/content_topic, + waku/api/[api, types], + ../declare_lib + +proc logosdelivery_subscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + contentTopicStr: cstring, +) {.ffi.} = + requireInitializedNode(ctx, "Subscribe"): + return err(errMsg) + + # ContentTopic is just a string type alias + let contentTopic = ContentTopic($contentTopicStr) + + (await api.subscribe(ctx.myLib[], contentTopic)).isOkOr: + let errMsg = $error + return err("Subscribe failed: " & errMsg) + + return ok("") + +proc logosdelivery_unsubscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + contentTopicStr: cstring, +) {.ffi.} = + requireInitializedNode(ctx, "Unsubscribe"): + return err(errMsg) + + # ContentTopic is just a string type alias + let contentTopic = ContentTopic($contentTopicStr) + + api.unsubscribe(ctx.myLib[], contentTopic).isOkOr: + let errMsg = $error + return err("Unsubscribe failed: " & errMsg) + + return ok("") + +proc logosdelivery_send( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + messageJson: cstring, +) {.ffi.} = + requireInitializedNode(ctx, "Send"): + return err(errMsg) + + ## Parse the message JSON and send the message + var jsonNode: JsonNode + try: + jsonNode = parseJson($messageJson) + except Exception as e: + return err("Failed to parse message JSON: " & e.msg) + + # Extract content topic + if not jsonNode.hasKey("contentTopic"): + return err("Missing contentTopic field") + + # ContentTopic is just a string type alias + let contentTopic = ContentTopic(jsonNode["contentTopic"].getStr()) + + # Extract payload (expect base64 encoded string) + if not jsonNode.hasKey("payload"): + return err("Missing payload field") + + let payloadStr = jsonNode["payload"].getStr() + let payload = base64.decode(Base64String(payloadStr)).valueOr: + return err("invalid payload format: " & error) + + # Extract ephemeral flag + let ephemeral = jsonNode.getOrDefault("ephemeral").getBool(false) + + # Create message envelope + let envelope = MessageEnvelope.init( + contentTopic = contentTopic, payload = payload, ephemeral = ephemeral + ) + + # Send the message + let requestId = (await api.send(ctx.myLib[], envelope)).valueOr: + let errMsg = $error + return err("Send failed: " & errMsg) + + return ok($requestId) diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim new file mode 100644 index 000000000..6a0041857 --- /dev/null +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -0,0 +1,111 @@ +import std/json +import chronos, results, ffi +import + waku/factory/waku, + waku/node/waku_node, + waku/api/[api, api_conf, types], + waku/events/message_events, + ../declare_lib, + ../json_event + +# Add JSON serialization for RequestId +proc `%`*(id: RequestId): JsonNode = + %($id) + +registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): + proc(configJson: cstring): Future[Result[string, string]] {.async.} = + ## Parse the JSON configuration and create a node + let nodeConfig = + try: + decodeNodeConfigFromJson($configJson) + except SerializationError as e: + return err("Failed to parse config JSON: " & e.msg) + + # Create the node + ctx.myLib[] = (await api.createNode(nodeConfig)).valueOr: + let errMsg = $error + chronicles.error "CreateNodeRequest failed", err = errMsg + return err(errMsg) + + return ok("") + +proc logosdelivery_create_node( + configJson: cstring, callback: FFICallback, userData: pointer +): pointer {.dynlib, exportc, cdecl.} = + initializeLibrary() + + if isNil(callback): + echo "error: missing callback in logosdelivery_create_node" + return nil + + var ctx = ffi.createFFIContext[Waku]().valueOr: + let msg = "Error in createFFIContext: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return nil + + ctx.userData = userData + + ffi.sendRequestToFFIThread( + ctx, CreateNodeRequest.ffiNewReq(callback, userData, configJson) + ).isOkOr: + let msg = "error in sendRequestToFFIThread: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return nil + + return ctx + +proc logosdelivery_start_node( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + requireInitializedNode(ctx, "START_NODE"): + return err(errMsg) + + # setting up outgoing event listeners + let sentListener = MessageSentEvent.listen( + ctx.myLib[].brokerCtx, + proc(event: MessageSentEvent) {.async: (raises: []).} = + callEventCallback(ctx, "onMessageSent"): + $newJsonEvent("message_sent", event), + ).valueOr: + chronicles.error "MessageSentEvent.listen failed", err = $error + return err("MessageSentEvent.listen failed: " & $error) + + let errorListener = MessageErrorEvent.listen( + ctx.myLib[].brokerCtx, + proc(event: MessageErrorEvent) {.async: (raises: []).} = + callEventCallback(ctx, "onMessageError"): + $newJsonEvent("message_error", event), + ).valueOr: + chronicles.error "MessageErrorEvent.listen failed", err = $error + return err("MessageErrorEvent.listen failed: " & $error) + + let propagatedListener = MessagePropagatedEvent.listen( + ctx.myLib[].brokerCtx, + proc(event: MessagePropagatedEvent) {.async: (raises: []).} = + callEventCallback(ctx, "onMessagePropagated"): + $newJsonEvent("message_propagated", event), + ).valueOr: + chronicles.error "MessagePropagatedEvent.listen failed", err = $error + return err("MessagePropagatedEvent.listen failed: " & $error) + + (await startWaku(addr ctx.myLib[])).isOkOr: + let errMsg = $error + chronicles.error "START_NODE failed", err = errMsg + return err("failed to start: " & errMsg) + return ok("") + +proc logosdelivery_stop_node( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + requireInitializedNode(ctx, "STOP_NODE"): + return err(errMsg) + + MessageErrorEvent.dropAllListeners(ctx.myLib[].brokerCtx) + MessageSentEvent.dropAllListeners(ctx.myLib[].brokerCtx) + MessagePropagatedEvent.dropAllListeners(ctx.myLib[].brokerCtx) + + (await ctx.myLib[].stop()).isOkOr: + let errMsg = $error + chronicles.error "STOP_NODE failed", err = errMsg + return err("failed to stop: " & errMsg) + return ok("") diff --git a/liblogosdelivery/nim.cfg b/liblogosdelivery/nim.cfg new file mode 100644 index 000000000..3fd5adb32 --- /dev/null +++ b/liblogosdelivery/nim.cfg @@ -0,0 +1,27 @@ +# Nim configuration for liblogosdelivery + +# Ensure correct compiler configuration +--gc: + refc +--threads: + on + +# Include paths +--path: + "../vendor/nim-ffi" +--path: + "../" + +# Optimization and debugging +--opt: + speed +--debugger: + native + +# Export symbols for dynamic library +--app: + lib +--noMain + +# Enable FFI macro features when needed for debugging +# --define:ffiDumpMacros diff --git a/nix/default.nix b/nix/default.nix index d77862e8f..d532ec5b5 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -23,6 +23,10 @@ let tools = pkgs.callPackage ./tools.nix {}; version = tools.findKeyValue "^version = \"([a-f0-9.-]+)\"$" ../waku.nimble; revision = lib.substring 0 8 (src.rev or src.dirtyRev or "00000000"); + copyLibwaku = lib.elem "libwaku" targets; + copyLiblogosdelivery = lib.elem "liblogosdelivery" targets; + copyWakunode2 = lib.elem "wakunode2" targets; + hasKnownInstallTarget = copyLibwaku || copyLiblogosdelivery || copyWakunode2; in stdenv.mkDerivation { pname = "logos-messaging-nim"; @@ -91,11 +95,39 @@ in stdenv.mkDerivation { '' else '' mkdir -p $out/bin $out/include - # Copy library files - cp build/* $out/bin/ 2>/dev/null || true + # Copy artifacts from build directory (created by Make during buildPhase) + # Note: build/ is in the source tree, not result/ (which is a post-build symlink) + if [ -d build ]; then + ${lib.optionalString copyLibwaku '' + cp build/libwaku.{so,dylib,dll,a,lib} $out/bin/ 2>/dev/null || true + ''} - # Copy the header file - cp library/libwaku.h $out/include/ + ${lib.optionalString copyLiblogosdelivery '' + cp build/liblogosdelivery.{so,dylib,dll,a,lib} $out/bin/ 2>/dev/null || true + ''} + + ${lib.optionalString copyWakunode2 '' + cp build/wakunode2 $out/bin/ 2>/dev/null || true + ''} + + ${lib.optionalString (!hasKnownInstallTarget) '' + cp build/lib*.{so,dylib,dll,a,lib} $out/bin/ 2>/dev/null || true + ''} + fi + + # Copy header files + ${lib.optionalString copyLibwaku '' + cp library/libwaku.h $out/include/ 2>/dev/null || true + ''} + + ${lib.optionalString copyLiblogosdelivery '' + cp liblogosdelivery/liblogosdelivery.h $out/include/ 2>/dev/null || true + ''} + + ${lib.optionalString (!hasKnownInstallTarget) '' + cp library/libwaku.h $out/include/ 2>/dev/null || true + cp liblogosdelivery/liblogosdelivery.h $out/include/ 2>/dev/null || true + ''} ''; meta = with pkgs.lib; { diff --git a/nix/submodules.json b/nix/submodules.json new file mode 100644 index 000000000..2f94e5f2b --- /dev/null +++ b/nix/submodules.json @@ -0,0 +1,247 @@ +[ + { + "path": "vendor/db_connector", + "url": "https://github.com/nim-lang/db_connector.git", + "rev": "74aef399e5c232f95c9fc5c987cebac846f09d62" + } + , + { + "path": "vendor/dnsclient.nim", + "url": "https://github.com/ba0f3/dnsclient.nim.git", + "rev": "23214235d4784d24aceed99bbfe153379ea557c8" + } + , + { + "path": "vendor/nim-bearssl", + "url": "https://github.com/status-im/nim-bearssl.git", + "rev": "11e798b62b8e6beabe958e048e9e24c7e0f9ee63" + } + , + { + "path": "vendor/nim-chronicles", + "url": "https://github.com/status-im/nim-chronicles.git", + "rev": "54f5b726025e8c7385e3a6529d3aa27454c6e6ff" + } + , + { + "path": "vendor/nim-chronos", + "url": "https://github.com/status-im/nim-chronos.git", + "rev": "85af4db764ecd3573c4704139560df3943216cf1" + } + , + { + "path": "vendor/nim-confutils", + "url": "https://github.com/status-im/nim-confutils.git", + "rev": "e214b3992a31acece6a9aada7d0a1ad37c928f3b" + } + , + { + "path": "vendor/nim-dnsdisc", + "url": "https://github.com/status-im/nim-dnsdisc.git", + "rev": "b71d029f4da4ec56974d54c04518bada00e1b623" + } + , + { + "path": "vendor/nim-eth", + "url": "https://github.com/status-im/nim-eth.git", + "rev": "d9135e6c3c5d6d819afdfb566aa8d958756b73a8" + } + , + { + "path": "vendor/nim-faststreams", + "url": "https://github.com/status-im/nim-faststreams.git", + "rev": "c3ac3f639ed1d62f59d3077d376a29c63ac9750c" + } + , + { + "path": "vendor/nim-ffi", + "url": "https://github.com/logos-messaging/nim-ffi", + "rev": "06111de155253b34e47ed2aaed1d61d08d62cc1b" + } + , + { + "path": "vendor/nim-http-utils", + "url": "https://github.com/status-im/nim-http-utils.git", + "rev": "79cbab1460f4c0cdde2084589d017c43a3d7b4f1" + } + , + { + "path": "vendor/nim-json-rpc", + "url": "https://github.com/status-im/nim-json-rpc.git", + "rev": "9665c265035f49f5ff94bbffdeadde68e19d6221" + } + , + { + "path": "vendor/nim-json-serialization", + "url": "https://github.com/status-im/nim-json-serialization.git", + "rev": "b65fd6a7e64c864dabe40e7dfd6c7d07db0014ac" + } + , + { + "path": "vendor/nim-jwt", + "url": "https://github.com/vacp2p/nim-jwt.git", + "rev": "18f8378de52b241f321c1f9ea905456e89b95c6f" + } + , + { + "path": "vendor/nim-libbacktrace", + "url": "https://github.com/status-im/nim-libbacktrace.git", + "rev": "d8bd4ce5c46bb6d2f984f6b3f3d7380897d95ecb" + } + , + { + "path": "vendor/nim-libp2p", + "url": "https://github.com/vacp2p/nim-libp2p.git", + "rev": "eb7e6ff89889e41b57515f891ba82986c54809fb" + } + , + { + "path": "vendor/nim-lsquic", + "url": "https://github.com/vacp2p/nim-lsquic", + "rev": "f3fe33462601ea34eb2e8e9c357c92e61f8d121b" + } + , + { + "path": "vendor/nim-metrics", + "url": "https://github.com/status-im/nim-metrics.git", + "rev": "ecf64c6078d1276d3b7d9b3d931fbdb70004db11" + } + , + { + "path": "vendor/nim-minilru", + "url": "https://github.com/status-im/nim-minilru.git", + "rev": "0c4b2bce959591f0a862e9b541ba43c6d0cf3476" + } + , + { + "path": "vendor/nim-nat-traversal", + "url": "https://github.com/status-im/nim-nat-traversal.git", + "rev": "860e18c37667b5dd005b94c63264560c35d88004" + } + , + { + "path": "vendor/nim-presto", + "url": "https://github.com/status-im/nim-presto.git", + "rev": "92b1c7ff141e6920e1f8a98a14c35c1fa098e3be" + } + , + { + "path": "vendor/nim-regex", + "url": "https://github.com/nitely/nim-regex.git", + "rev": "4593305ed1e49731fc75af1dc572dd2559aad19c" + } + , + { + "path": "vendor/nim-results", + "url": "https://github.com/arnetheduck/nim-results.git", + "rev": "df8113dda4c2d74d460a8fa98252b0b771bf1f27" + } + , + { + "path": "vendor/nim-secp256k1", + "url": "https://github.com/status-im/nim-secp256k1.git", + "rev": "9dd3df62124aae79d564da636bb22627c53c7676" + } + , + { + "path": "vendor/nim-serialization", + "url": "https://github.com/status-im/nim-serialization.git", + "rev": "6f525d5447d97256750ca7856faead03e562ed20" + } + , + { + "path": "vendor/nim-sqlite3-abi", + "url": "https://github.com/arnetheduck/nim-sqlite3-abi.git", + "rev": "bdf01cf4236fb40788f0733466cdf6708783cbac" + } + , + { + "path": "vendor/nim-stew", + "url": "https://github.com/status-im/nim-stew.git", + "rev": "e5740014961438610d336cd81706582dbf2c96f0" + } + , + { + "path": "vendor/nim-stint", + "url": "https://github.com/status-im/nim-stint.git", + "rev": "470b7892561b5179ab20bd389a69217d6213fe58" + } + , + { + "path": "vendor/nim-taskpools", + "url": "https://github.com/status-im/nim-taskpools.git", + "rev": "9e8ccc754631ac55ac2fd495e167e74e86293edb" + } + , + { + "path": "vendor/nim-testutils", + "url": "https://github.com/status-im/nim-testutils.git", + "rev": "94d68e796c045d5b37cabc6be32d7bfa168f8857" + } + , + { + "path": "vendor/nim-toml-serialization", + "url": "https://github.com/status-im/nim-toml-serialization.git", + "rev": "fea85b27f0badcf617033ca1bc05444b5fd8aa7a" + } + , + { + "path": "vendor/nim-unicodedb", + "url": "https://github.com/nitely/nim-unicodedb.git", + "rev": "66f2458710dc641dd4640368f9483c8a0ec70561" + } + , + { + "path": "vendor/nim-unittest2", + "url": "https://github.com/status-im/nim-unittest2.git", + "rev": "8b51e99b4a57fcfb31689230e75595f024543024" + } + , + { + "path": "vendor/nim-web3", + "url": "https://github.com/status-im/nim-web3.git", + "rev": "81ee8ce479d86acb73be7c4f365328e238d9b4a3" + } + , + { + "path": "vendor/nim-websock", + "url": "https://github.com/status-im/nim-websock.git", + "rev": "ebe308a79a7b440a11dfbe74f352be86a3883508" + } + , + { + "path": "vendor/nim-zlib", + "url": "https://github.com/status-im/nim-zlib.git", + "rev": "daa8723fd32299d4ca621c837430c29a5a11e19a" + } + , + { + "path": "vendor/nimbus-build-system", + "url": "https://github.com/status-im/nimbus-build-system.git", + "rev": "e6c2c9da39c2d368d9cf420ac22692e99715d22c" + } + , + { + "path": "vendor/nimcrypto", + "url": "https://github.com/cheatfate/nimcrypto.git", + "rev": "721fb99ee099b632eb86dfad1f0d96ee87583774" + } + , + { + "path": "vendor/nph", + "url": "https://github.com/arnetheduck/nph.git", + "rev": "c6e03162dc2820d3088660f644818d7040e95791" + } + , + { + "path": "vendor/waku-rlnv2-contract", + "url": "https://github.com/logos-messaging/waku-rlnv2-contract.git", + "rev": "8a338f354481e8a3f3d64a72e38fad4c62e32dcd" + } + , + { + "path": "vendor/zerokit", + "url": "https://github.com/vacp2p/zerokit.git", + "rev": "70c79fbc989d4f87d9352b2f4bddcb60ebe55b19" + } +] diff --git a/scripts/generate_nix_submodules.sh b/scripts/generate_nix_submodules.sh new file mode 100755 index 000000000..51073294c --- /dev/null +++ b/scripts/generate_nix_submodules.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +# Generates nix/submodules.json from .gitmodules and git ls-tree. +# This allows Nix to fetch all git submodules without requiring +# locally initialized submodules or the '?submodules=1' URI flag. +# +# Usage: ./scripts/generate_nix_submodules.sh +# +# Run this script after: +# - Adding/removing submodules +# - Updating submodule commits (e.g. after 'make update') +# - Any change to .gitmodules +# +# Compatible with macOS bash 3.x (no associative arrays). + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUTPUT="${REPO_ROOT}/nix/submodules.json" + +cd "$REPO_ROOT" + +TMP_URLS=$(mktemp) +TMP_REVS=$(mktemp) +trap 'rm -f "$TMP_URLS" "$TMP_REVS"' EXIT + +# Parse .gitmodules: extract (path, url) pairs +current_path="" +while IFS= read -r line; do + case "$line" in + *"path = "*) + current_path="${line#*path = }" + ;; + *"url = "*) + if [ -n "$current_path" ]; then + url="${line#*url = }" + url="${url%/}" + printf '%s\t%s\n' "$current_path" "$url" >> "$TMP_URLS" + current_path="" + fi + ;; + esac +done < .gitmodules + +# Get pinned commit hashes from git tree +git ls-tree HEAD vendor/ | while IFS= read -r tree_line; do + mode=$(echo "$tree_line" | awk '{print $1}') + type=$(echo "$tree_line" | awk '{print $2}') + hash=$(echo "$tree_line" | awk '{print $3}') + path=$(echo "$tree_line" | awk '{print $4}') + if [ "$type" = "commit" ]; then + path="${path%/}" + printf '%s\t%s\n' "$path" "$hash" >> "$TMP_REVS" + fi +done + +# Generate JSON by joining urls and revs on path +printf '[\n' > "$OUTPUT" +first=true + +sort "$TMP_URLS" | while IFS="$(printf '\t')" read -r path url; do + rev=$(grep "^${path} " "$TMP_REVS" | cut -f2 || true) + + if [ -z "$rev" ]; then + echo "WARNING: No commit hash found for submodule '$path', skipping" >&2 + continue + fi + + if [ "$first" = true ]; then + first=false + else + printf ' ,\n' >> "$OUTPUT" + fi + + printf ' {\n "path": "%s",\n "url": "%s",\n "rev": "%s"\n }\n' \ + "$path" "$url" "$rev" >> "$OUTPUT" +done + +printf ']\n' >> "$OUTPUT" + +count=$(grep -c '"path"' "$OUTPUT" || echo 0) +echo "Generated $OUTPUT with $count submodule entries" diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index 4dfbd4b51..84bbfead3 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -1,7 +1,9 @@ {.used.} import std/options, results, stint, testutils/unittests +import json_serialization import waku/api/api_conf, waku/factory/waku_conf, waku/factory/networks_config +import waku/common/logging suite "LibWaku Conf - toWakuConf": test "Minimal configuration": @@ -298,3 +300,709 @@ suite "LibWaku Conf - toWakuConf": check: wakuConf.staticNodes.len == 1 wakuConf.staticNodes[0] == entryNodes[1] + +suite "NodeConfig JSON - complete format": + test "Full NodeConfig from complete JSON with field validation": + ## Given + let jsonStr = + """ + { + "mode": "Core", + "protocolsConfig": { + "entryNodes": ["enrtree://TREE@nodes.example.com"], + "staticStoreNodes": ["/ip4/1.2.3.4/tcp/80/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc"], + "clusterId": 10, + "autoShardingConfig": { + "numShardsInCluster": 4 + }, + "messageValidation": { + "maxMessageSize": "100 KiB", + "rlnConfig": null + } + }, + "networkingConfig": { + "listenIpv4": "192.168.1.1", + "p2pTcpPort": 7000, + "discv5UdpPort": 7001 + }, + "ethRpcEndpoints": ["http://localhost:8545"], + "p2pReliability": true, + "logLevel": "WARN", + "logFormat": "TEXT" + } + """ + + ## When + let config = decodeNodeConfigFromJson(jsonStr) + + ## Then — check every field + check: + config.mode == WakuMode.Core + config.ethRpcEndpoints == @["http://localhost:8545"] + config.p2pReliability == true + config.logLevel == LogLevel.WARN + config.logFormat == LogFormat.TEXT + + check: + config.networkingConfig.listenIpv4 == "192.168.1.1" + config.networkingConfig.p2pTcpPort == 7000 + config.networkingConfig.discv5UdpPort == 7001 + + let pc = config.protocolsConfig + check: + pc.entryNodes == @["enrtree://TREE@nodes.example.com"] + pc.staticStoreNodes == + @[ + "/ip4/1.2.3.4/tcp/80/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" + ] + pc.clusterId == 10 + pc.autoShardingConfig.numShardsInCluster == 4 + pc.messageValidation.maxMessageSize == "100 KiB" + pc.messageValidation.rlnConfig.isNone() + + test "Full NodeConfig with RlnConfig present": + ## Given + let jsonStr = + """ + { + "mode": "Edge", + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1, + "messageValidation": { + "maxMessageSize": "150 KiB", + "rlnConfig": { + "contractAddress": "0x1234567890ABCDEF1234567890ABCDEF12345678", + "chainId": 5, + "epochSizeSec": 600 + } + } + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + ## When + let config = decodeNodeConfigFromJson(jsonStr) + + ## Then + check config.mode == WakuMode.Edge + + let mv = config.protocolsConfig.messageValidation + check: + mv.maxMessageSize == "150 KiB" + mv.rlnConfig.isSome() + let rln = mv.rlnConfig.get() + check: + rln.contractAddress == "0x1234567890ABCDEF1234567890ABCDEF12345678" + rln.chainId == 5'u + rln.epochSizeSec == 600'u64 + + test "Round-trip encode/decode preserves all fields": + ## Given + let original = NodeConfig.init( + mode = Edge, + protocolsConfig = ProtocolsConfig.init( + entryNodes = @["enrtree://TREE@example.com"], + staticStoreNodes = + @[ + "/ip4/1.2.3.4/tcp/80/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" + ], + clusterId = 42, + autoShardingConfig = AutoShardingConfig(numShardsInCluster: 16), + messageValidation = MessageValidation( + maxMessageSize: "256 KiB", + rlnConfig: some( + RlnConfig( + contractAddress: "0xAABBCCDDEEFF00112233445566778899AABBCCDD", + chainId: 137, + epochSizeSec: 300, + ) + ), + ), + ), + networkingConfig = + NetworkingConfig(listenIpv4: "10.0.0.1", p2pTcpPort: 9090, discv5UdpPort: 9091), + ethRpcEndpoints = @["https://rpc.example.com"], + p2pReliability = true, + logLevel = LogLevel.DEBUG, + logFormat = LogFormat.JSON, + ) + + ## When + let decoded = decodeNodeConfigFromJson(Json.encode(original)) + + ## Then — check field by field + check: + decoded.mode == original.mode + decoded.ethRpcEndpoints == original.ethRpcEndpoints + decoded.p2pReliability == original.p2pReliability + decoded.logLevel == original.logLevel + decoded.logFormat == original.logFormat + decoded.networkingConfig.listenIpv4 == original.networkingConfig.listenIpv4 + decoded.networkingConfig.p2pTcpPort == original.networkingConfig.p2pTcpPort + decoded.networkingConfig.discv5UdpPort == original.networkingConfig.discv5UdpPort + decoded.protocolsConfig.entryNodes == original.protocolsConfig.entryNodes + decoded.protocolsConfig.staticStoreNodes == + original.protocolsConfig.staticStoreNodes + decoded.protocolsConfig.clusterId == original.protocolsConfig.clusterId + decoded.protocolsConfig.autoShardingConfig.numShardsInCluster == + original.protocolsConfig.autoShardingConfig.numShardsInCluster + decoded.protocolsConfig.messageValidation.maxMessageSize == + original.protocolsConfig.messageValidation.maxMessageSize + decoded.protocolsConfig.messageValidation.rlnConfig.isSome() + + let decodedRln = decoded.protocolsConfig.messageValidation.rlnConfig.get() + let originalRln = original.protocolsConfig.messageValidation.rlnConfig.get() + check: + decodedRln.contractAddress == originalRln.contractAddress + decodedRln.chainId == originalRln.chainId + decodedRln.epochSizeSec == originalRln.epochSizeSec + +suite "NodeConfig JSON - partial format with defaults": + test "Minimal NodeConfig - empty object uses all defaults": + ## Given + let config = decodeNodeConfigFromJson("{}") + let defaultConfig = NodeConfig.init() + + ## Then — compare field by field against defaults + check: + config.mode == defaultConfig.mode + config.ethRpcEndpoints == defaultConfig.ethRpcEndpoints + config.p2pReliability == defaultConfig.p2pReliability + config.logLevel == defaultConfig.logLevel + config.logFormat == defaultConfig.logFormat + config.networkingConfig.listenIpv4 == defaultConfig.networkingConfig.listenIpv4 + config.networkingConfig.p2pTcpPort == defaultConfig.networkingConfig.p2pTcpPort + config.networkingConfig.discv5UdpPort == + defaultConfig.networkingConfig.discv5UdpPort + config.protocolsConfig.entryNodes == defaultConfig.protocolsConfig.entryNodes + config.protocolsConfig.staticStoreNodes == + defaultConfig.protocolsConfig.staticStoreNodes + config.protocolsConfig.clusterId == defaultConfig.protocolsConfig.clusterId + config.protocolsConfig.autoShardingConfig.numShardsInCluster == + defaultConfig.protocolsConfig.autoShardingConfig.numShardsInCluster + config.protocolsConfig.messageValidation.maxMessageSize == + defaultConfig.protocolsConfig.messageValidation.maxMessageSize + config.protocolsConfig.messageValidation.rlnConfig.isSome() == + defaultConfig.protocolsConfig.messageValidation.rlnConfig.isSome() + + test "Minimal NodeConfig keeps network preset defaults": + ## Given + let config = decodeNodeConfigFromJson("{}") + + ## Then + check: + config.protocolsConfig.entryNodes == TheWakuNetworkPreset.entryNodes + config.protocolsConfig.messageValidation.rlnConfig.isSome() + + test "NodeConfig with only mode specified": + ## Given + let config = decodeNodeConfigFromJson("""{"mode": "Edge"}""") + + ## Then + check: + config.mode == WakuMode.Edge + ## Remaining fields get defaults + config.logLevel == LogLevel.INFO + config.logFormat == LogFormat.TEXT + config.p2pReliability == false + config.ethRpcEndpoints == newSeq[string]() + + test "ProtocolsConfig partial - optional fields get defaults": + ## Given — only entryNodes and clusterId provided + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": ["enrtree://X@y.com"], + "clusterId": 5 + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + ## When + let config = decodeNodeConfigFromJson(jsonStr) + + ## Then — required fields are set, optionals get defaults + check: + config.protocolsConfig.entryNodes == @["enrtree://X@y.com"] + config.protocolsConfig.clusterId == 5 + config.protocolsConfig.staticStoreNodes == newSeq[string]() + config.protocolsConfig.autoShardingConfig.numShardsInCluster == + DefaultAutoShardingConfig.numShardsInCluster + config.protocolsConfig.messageValidation.maxMessageSize == + DefaultMessageValidation.maxMessageSize + config.protocolsConfig.messageValidation.rlnConfig.isNone() + + test "MessageValidation partial - rlnConfig omitted defaults to none": + ## Given + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1, + "messageValidation": { + "maxMessageSize": "200 KiB" + } + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + ## When + let config = decodeNodeConfigFromJson(jsonStr) + + ## Then + check: + config.protocolsConfig.messageValidation.maxMessageSize == "200 KiB" + config.protocolsConfig.messageValidation.rlnConfig.isNone() + + test "logLevel and logFormat omitted use defaults": + ## Given + let jsonStr = + """ + { + "mode": "Core", + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1 + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + ## When + let config = decodeNodeConfigFromJson(jsonStr) + + ## Then + check: + config.logLevel == LogLevel.INFO + config.logFormat == LogFormat.TEXT + +suite "NodeConfig JSON - unsupported fields raise errors": + test "Unknown field at NodeConfig level raises": + let jsonStr = + """ + { + "mode": "Core", + "unknownTopLevel": true + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Typo in NodeConfig field name raises": + let jsonStr = + """ + { + "modes": "Core" + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Unknown field in ProtocolsConfig raises": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1, + "futureField": "something" + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Unknown field in NetworkingConfig raises": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1 + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000, + "futureNetworkField": "value" + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Unknown field in MessageValidation raises": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1, + "messageValidation": { + "maxMessageSize": "150 KiB", + "maxMesssageSize": "typo" + } + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Unknown field in RlnConfig raises": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1, + "messageValidation": { + "maxMessageSize": "150 KiB", + "rlnConfig": { + "contractAddress": "0xABCDEF1234567890ABCDEF1234567890ABCDEF12", + "chainId": 1, + "epochSizeSec": 600, + "unknownRlnField": true + } + } + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Unknown field in AutoShardingConfig raises": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1, + "autoShardingConfig": { + "numShardsInCluster": 8, + "shardPrefix": "extra" + } + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + +suite "NodeConfig JSON - missing required fields": + test "Missing 'entryNodes' in ProtocolsConfig": + let jsonStr = + """ + { + "protocolsConfig": { + "clusterId": 1 + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Missing 'clusterId' in ProtocolsConfig": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [] + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Missing required fields in NetworkingConfig": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1 + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0" + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Missing 'numShardsInCluster' in AutoShardingConfig": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1, + "autoShardingConfig": {} + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Missing required fields in RlnConfig": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1, + "messageValidation": { + "maxMessageSize": "150 KiB", + "rlnConfig": { + "contractAddress": "0xABCD" + } + } + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Missing 'maxMessageSize' in MessageValidation": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": 1, + "messageValidation": { + "rlnConfig": null + } + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + +suite "NodeConfig JSON - invalid values": + test "Invalid enum value for mode": + var raised = false + try: + discard decodeNodeConfigFromJson("""{"mode": "InvalidMode"}""") + except SerializationError: + raised = true + check raised + + test "Invalid enum value for logLevel": + var raised = false + try: + discard decodeNodeConfigFromJson("""{"logLevel": "SUPERVERBOSE"}""") + except SerializationError: + raised = true + check raised + + test "Wrong type for clusterId (string instead of number)": + let jsonStr = + """ + { + "protocolsConfig": { + "entryNodes": [], + "clusterId": "not-a-number" + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + } + } + """ + + var raised = false + try: + discard decodeNodeConfigFromJson(jsonStr) + except SerializationError: + raised = true + check raised + + test "Completely invalid JSON syntax": + var raised = false + try: + discard decodeNodeConfigFromJson("""{ not valid json at all }""") + except SerializationError: + raised = true + check raised + +suite "NodeConfig JSON -> WakuConf integration": + test "Decoded config translates to valid WakuConf": + ## Given + let jsonStr = + """ + { + "mode": "Core", + "protocolsConfig": { + "entryNodes": [ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im" + ], + "staticStoreNodes": [ + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" + ], + "clusterId": 55, + "autoShardingConfig": { + "numShardsInCluster": 6 + }, + "messageValidation": { + "maxMessageSize": "256 KiB", + "rlnConfig": null + } + }, + "networkingConfig": { + "listenIpv4": "0.0.0.0", + "p2pTcpPort": 60000, + "discv5UdpPort": 9000 + }, + "ethRpcEndpoints": ["http://localhost:8545"], + "p2pReliability": true, + "logLevel": "INFO", + "logFormat": "TEXT" + } + """ + + ## When + let nodeConfig = decodeNodeConfigFromJson(jsonStr) + let wakuConfRes = toWakuConf(nodeConfig) + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.clusterId == 55 + wakuConf.shardingConf.numShardsInCluster == 6 + wakuConf.maxMessageSizeBytes == 256'u64 * 1024'u64 + wakuConf.staticNodes.len == 1 + wakuConf.p2pReliability == true diff --git a/waku.nimble b/waku.nimble index 7368ba74b..e4c436e8d 100644 --- a/waku.nimble +++ b/waku.nimble @@ -64,7 +64,7 @@ proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = exec "nim " & lang & " --out:build/" & name & " --mm:refc " & extra_params & " " & srcDir & name & ".nim" -proc buildLibrary(lib_name: string, srcDir = "./", params = "", `type` = "static") = +proc buildLibrary(lib_name: string, srcDir = "./", params = "", `type` = "static", srcFile = "libwaku.nim", mainPrefix = "libwaku") = if not dirExists "build": mkDir "build" # allow something like "nim nimbus --verbosity:0 --hints:off nimbus.nims" @@ -73,12 +73,12 @@ proc buildLibrary(lib_name: string, srcDir = "./", params = "", `type` = "static extra_params &= " " & paramStr(i) if `type` == "static": 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" + " --threads:on --app:staticlib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:on -d:discv5_protocol_id=d5waku " & + extra_params & " " & srcDir & srcFile else: 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" + " --threads:on --app:lib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:off -d:discv5_protocol_id=d5waku " & + extra_params & " " & srcDir & srcFile proc buildMobileAndroid(srcDir = ".", params = "") = let cpu = getEnv("CPU") @@ -400,3 +400,11 @@ task libWakuIOS, "Build the mobile bindings for iOS": let srcDir = "./library" let extraParams = "-d:chronicles_log_level=ERROR" buildMobileIOS srcDir, extraParams + +task liblogosdeliveryStatic, "Build the liblogosdelivery (Logos Messaging Delivery API) static library": + let lib_name = paramStr(paramCount()) + buildLibrary lib_name, "liblogosdelivery/", chroniclesParams, "static", "liblogosdelivery.nim", "liblogosdelivery" + +task liblogosdeliveryDynamic, "Build the liblogosdelivery (Logos Messaging Delivery API) dynamic library": + let lib_name = paramStr(paramCount()) + buildLibrary lib_name, "liblogosdelivery/", chroniclesParams, "dynamic", "liblogosdelivery.nim", "liblogosdelivery" diff --git a/waku/api/api.nim b/waku/api/api.nim index 7f13919b3..3493513a3 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -4,6 +4,7 @@ import waku/factory/waku import waku/[requests/health_requests, waku_core, waku_node] import waku/node/delivery_service/send_service import waku/node/delivery_service/subscription_service +import libp2p/peerid import ./[api_conf, types] logScope: diff --git a/waku/api/api_conf.nim b/waku/api/api_conf.nim index 47aa9e7d8..7cac66426 100644 --- a/waku/api/api_conf.nim +++ b/waku/api/api_conf.nim @@ -1,14 +1,18 @@ import std/[net, options] import results +import json_serialization, json_serialization/std/options as json_options import waku/common/utils/parse_size_units, + waku/common/logging, waku/factory/waku_conf, waku/factory/conf_builder/conf_builder, waku/factory/networks_config, ./entry_nodes +export json_serialization, json_options + type AutoShardingConfig* {.requiresInit.} = object numShardsInCluster*: uint16 @@ -87,6 +91,8 @@ type NodeConfig* {.requiresInit.} = object networkingConfig: NetworkingConfig ethRpcEndpoints: seq[string] p2pReliability: bool + logLevel: LogLevel + logFormat: LogFormat proc init*( T: typedesc[NodeConfig], @@ -95,6 +101,8 @@ proc init*( networkingConfig: NetworkingConfig = DefaultNetworkingConfig, ethRpcEndpoints: seq[string] = @[], p2pReliability: bool = false, + logLevel: LogLevel = LogLevel.INFO, + logFormat: LogFormat = LogFormat.TEXT, ): T = return T( mode: mode, @@ -102,11 +110,57 @@ proc init*( networkingConfig: networkingConfig, ethRpcEndpoints: ethRpcEndpoints, p2pReliability: p2pReliability, + logLevel: logLevel, + logFormat: logFormat, ) +# -- Getters for ProtocolsConfig (private fields) - used for testing -- + +proc entryNodes*(c: ProtocolsConfig): seq[string] = + c.entryNodes + +proc staticStoreNodes*(c: ProtocolsConfig): seq[string] = + c.staticStoreNodes + +proc clusterId*(c: ProtocolsConfig): uint16 = + c.clusterId + +proc autoShardingConfig*(c: ProtocolsConfig): AutoShardingConfig = + c.autoShardingConfig + +proc messageValidation*(c: ProtocolsConfig): MessageValidation = + c.messageValidation + +# -- Getters for NodeConfig (private fields) - used for testing -- + +proc mode*(c: NodeConfig): WakuMode = + c.mode + +proc protocolsConfig*(c: NodeConfig): ProtocolsConfig = + c.protocolsConfig + +proc networkingConfig*(c: NodeConfig): NetworkingConfig = + c.networkingConfig + +proc ethRpcEndpoints*(c: NodeConfig): seq[string] = + c.ethRpcEndpoints + +proc p2pReliability*(c: NodeConfig): bool = + c.p2pReliability + +proc logLevel*(c: NodeConfig): LogLevel = + c.logLevel + +proc logFormat*(c: NodeConfig): LogFormat = + c.logFormat + proc toWakuConf*(nodeConfig: NodeConfig): Result[WakuConf, string] = var b = WakuConfBuilder.init() + # Apply log configuration + b.withLogLevel(nodeConfig.logLevel) + b.withLogFormat(nodeConfig.logFormat) + # Apply networking configuration let networkingConfig = nodeConfig.networkingConfig let ip = parseIpAddress(networkingConfig.listenIpv4) @@ -214,3 +268,260 @@ proc toWakuConf*(nodeConfig: NodeConfig): Result[WakuConf, string] = return err("Failed to validate configuration: " & error) return ok(wakuConf) + +# ---- JSON serialization (writeValue / readValue) ---- +# ---------- AutoShardingConfig ---------- + +proc writeValue*(w: var JsonWriter, val: AutoShardingConfig) {.raises: [IOError].} = + w.beginRecord() + w.writeField("numShardsInCluster", val.numShardsInCluster) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var AutoShardingConfig +) {.raises: [SerializationError, IOError].} = + var numShardsInCluster: Option[uint16] + + for fieldName in readObjectFields(r): + case fieldName + of "numShardsInCluster": + numShardsInCluster = some(r.readValue(uint16)) + else: + r.raiseUnexpectedField(fieldName, "AutoShardingConfig") + + if numShardsInCluster.isNone(): + r.raiseUnexpectedValue("Missing required field 'numShardsInCluster'") + + val = AutoShardingConfig(numShardsInCluster: numShardsInCluster.get()) + +# ---------- RlnConfig ---------- + +proc writeValue*(w: var JsonWriter, val: RlnConfig) {.raises: [IOError].} = + w.beginRecord() + w.writeField("contractAddress", val.contractAddress) + w.writeField("chainId", val.chainId) + w.writeField("epochSizeSec", val.epochSizeSec) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var RlnConfig +) {.raises: [SerializationError, IOError].} = + var + contractAddress: Option[string] + chainId: Option[uint] + epochSizeSec: Option[uint64] + + for fieldName in readObjectFields(r): + case fieldName + of "contractAddress": + contractAddress = some(r.readValue(string)) + of "chainId": + chainId = some(r.readValue(uint)) + of "epochSizeSec": + epochSizeSec = some(r.readValue(uint64)) + else: + r.raiseUnexpectedField(fieldName, "RlnConfig") + + if contractAddress.isNone(): + r.raiseUnexpectedValue("Missing required field 'contractAddress'") + if chainId.isNone(): + r.raiseUnexpectedValue("Missing required field 'chainId'") + if epochSizeSec.isNone(): + r.raiseUnexpectedValue("Missing required field 'epochSizeSec'") + + val = RlnConfig( + contractAddress: contractAddress.get(), + chainId: chainId.get(), + epochSizeSec: epochSizeSec.get(), + ) + +# ---------- NetworkingConfig ---------- + +proc writeValue*(w: var JsonWriter, val: NetworkingConfig) {.raises: [IOError].} = + w.beginRecord() + w.writeField("listenIpv4", val.listenIpv4) + w.writeField("p2pTcpPort", val.p2pTcpPort) + w.writeField("discv5UdpPort", val.discv5UdpPort) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var NetworkingConfig +) {.raises: [SerializationError, IOError].} = + var + listenIpv4: Option[string] + p2pTcpPort: Option[uint16] + discv5UdpPort: Option[uint16] + + for fieldName in readObjectFields(r): + case fieldName + of "listenIpv4": + listenIpv4 = some(r.readValue(string)) + of "p2pTcpPort": + p2pTcpPort = some(r.readValue(uint16)) + of "discv5UdpPort": + discv5UdpPort = some(r.readValue(uint16)) + else: + r.raiseUnexpectedField(fieldName, "NetworkingConfig") + + if listenIpv4.isNone(): + r.raiseUnexpectedValue("Missing required field 'listenIpv4'") + if p2pTcpPort.isNone(): + r.raiseUnexpectedValue("Missing required field 'p2pTcpPort'") + if discv5UdpPort.isNone(): + r.raiseUnexpectedValue("Missing required field 'discv5UdpPort'") + + val = NetworkingConfig( + listenIpv4: listenIpv4.get(), + p2pTcpPort: p2pTcpPort.get(), + discv5UdpPort: discv5UdpPort.get(), + ) + +# ---------- MessageValidation ---------- + +proc writeValue*(w: var JsonWriter, val: MessageValidation) {.raises: [IOError].} = + w.beginRecord() + w.writeField("maxMessageSize", val.maxMessageSize) + w.writeField("rlnConfig", val.rlnConfig) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var MessageValidation +) {.raises: [SerializationError, IOError].} = + var + maxMessageSize: Option[string] + rlnConfig: Option[Option[RlnConfig]] + + for fieldName in readObjectFields(r): + case fieldName + of "maxMessageSize": + maxMessageSize = some(r.readValue(string)) + of "rlnConfig": + rlnConfig = some(r.readValue(Option[RlnConfig])) + else: + r.raiseUnexpectedField(fieldName, "MessageValidation") + + if maxMessageSize.isNone(): + r.raiseUnexpectedValue("Missing required field 'maxMessageSize'") + + val = MessageValidation( + maxMessageSize: maxMessageSize.get(), rlnConfig: rlnConfig.get(none(RlnConfig)) + ) + +# ---------- ProtocolsConfig ---------- + +proc writeValue*(w: var JsonWriter, val: ProtocolsConfig) {.raises: [IOError].} = + w.beginRecord() + w.writeField("entryNodes", val.entryNodes) + w.writeField("staticStoreNodes", val.staticStoreNodes) + w.writeField("clusterId", val.clusterId) + w.writeField("autoShardingConfig", val.autoShardingConfig) + w.writeField("messageValidation", val.messageValidation) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var ProtocolsConfig +) {.raises: [SerializationError, IOError].} = + var + entryNodes: Option[seq[string]] + staticStoreNodes: Option[seq[string]] + clusterId: Option[uint16] + autoShardingConfig: Option[AutoShardingConfig] + messageValidation: Option[MessageValidation] + + for fieldName in readObjectFields(r): + case fieldName + of "entryNodes": + entryNodes = some(r.readValue(seq[string])) + of "staticStoreNodes": + staticStoreNodes = some(r.readValue(seq[string])) + of "clusterId": + clusterId = some(r.readValue(uint16)) + of "autoShardingConfig": + autoShardingConfig = some(r.readValue(AutoShardingConfig)) + of "messageValidation": + messageValidation = some(r.readValue(MessageValidation)) + else: + r.raiseUnexpectedField(fieldName, "ProtocolsConfig") + + if entryNodes.isNone(): + r.raiseUnexpectedValue("Missing required field 'entryNodes'") + if clusterId.isNone(): + r.raiseUnexpectedValue("Missing required field 'clusterId'") + + val = ProtocolsConfig.init( + entryNodes = entryNodes.get(), + staticStoreNodes = staticStoreNodes.get(@[]), + clusterId = clusterId.get(), + autoShardingConfig = autoShardingConfig.get(DefaultAutoShardingConfig), + messageValidation = messageValidation.get(DefaultMessageValidation), + ) + +# ---------- NodeConfig ---------- + +proc writeValue*(w: var JsonWriter, val: NodeConfig) {.raises: [IOError].} = + w.beginRecord() + w.writeField("mode", val.mode) + w.writeField("protocolsConfig", val.protocolsConfig) + w.writeField("networkingConfig", val.networkingConfig) + w.writeField("ethRpcEndpoints", val.ethRpcEndpoints) + w.writeField("p2pReliability", val.p2pReliability) + w.writeField("logLevel", val.logLevel) + w.writeField("logFormat", val.logFormat) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var NodeConfig +) {.raises: [SerializationError, IOError].} = + var + mode: Option[WakuMode] + protocolsConfig: Option[ProtocolsConfig] + networkingConfig: Option[NetworkingConfig] + ethRpcEndpoints: Option[seq[string]] + p2pReliability: Option[bool] + logLevel: Option[LogLevel] + logFormat: Option[LogFormat] + + for fieldName in readObjectFields(r): + case fieldName + of "mode": + mode = some(r.readValue(WakuMode)) + of "protocolsConfig": + protocolsConfig = some(r.readValue(ProtocolsConfig)) + of "networkingConfig": + networkingConfig = some(r.readValue(NetworkingConfig)) + of "ethRpcEndpoints": + ethRpcEndpoints = some(r.readValue(seq[string])) + of "p2pReliability": + p2pReliability = some(r.readValue(bool)) + of "logLevel": + logLevel = some(r.readValue(LogLevel)) + of "logFormat": + logFormat = some(r.readValue(LogFormat)) + else: + r.raiseUnexpectedField(fieldName, "NodeConfig") + + val = NodeConfig.init( + mode = mode.get(WakuMode.Core), + protocolsConfig = protocolsConfig.get(TheWakuNetworkPreset), + networkingConfig = networkingConfig.get(DefaultNetworkingConfig), + ethRpcEndpoints = ethRpcEndpoints.get(@[]), + p2pReliability = p2pReliability.get(false), + logLevel = logLevel.get(LogLevel.INFO), + logFormat = logFormat.get(LogFormat.TEXT), + ) + +# ---------- Decode helper ---------- +# Json.decode returns T via `result`, which conflicts with {.requiresInit.} +# on Nim 2.x. This helper avoids the issue by using readValue into a var. + +proc decodeNodeConfigFromJson*( + jsonStr: string +): NodeConfig {.raises: [SerializationError].} = + var val = NodeConfig.init() # default-initialized + try: + var stream = unsafeMemoryInput(jsonStr) + var reader = JsonReader[DefaultFlavor].init(stream) + reader.readValue(val) + except IOError as err: + raise (ref SerializationError)(msg: err.msg) + return val diff --git a/waku/node/delivery_service/send_service/send_service.nim b/waku/node/delivery_service/send_service/send_service.nim index f6a6ac94c..a41d07786 100644 --- a/waku/node/delivery_service/send_service/send_service.nim +++ b/waku/node/delivery_service/send_service/send_service.nim @@ -91,6 +91,7 @@ proc setupSendProcessorChain( for i in 1 ..< processors.len: currentProcessor.chain(processors[i]) currentProcessor = processors[i] + trace "Send processor chain", index = i, processor = type(processors[i]).name return ok(processors[0]) From 895f3e2d36c6265947a7f3aa3c44cd3fc4aa5fc5 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:59:45 +0100 Subject: [PATCH 065/155] update after rename to logos-delivery (#3729) --- .../ISSUE_TEMPLATE/prepare_beta_release.md | 20 +++++++++---------- .../ISSUE_TEMPLATE/prepare_full_release.md | 16 +++++++-------- .github/workflows/ci-daily.yml | 4 ++-- .github/workflows/ci.yml | 8 ++++---- .github/workflows/pre-release.yml | 8 ++++---- README.md | 2 +- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/prepare_beta_release.md b/.github/ISSUE_TEMPLATE/prepare_beta_release.md index 383d9018c..2e4226e67 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 @@ -22,11 +22,11 @@ All items below are to be completed by the owner of the given release. - [ ] Generate and edit release notes in CHANGELOG.md. - [ ] **Waku test and fleets validation** - - [ ] Ensure all the unit tests (specifically logos-messaging-js tests) are green against the release candidate. + - [ ] Ensure all the unit tests (specifically logos-delivery-js tests) are green against the release candidate. - [ ] Deploy the release candidate to `waku.test` only through [deploy-waku-test job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-test/) and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). - After completion, disable [deployment job](https://ci.infra.status.im/job/nim-waku/) so that its version is not updated on every merge to master. - Verify the deployed version at https://fleets.waku.org/. - - Confirm the container image exists on [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab). + - Confirm the container image exists on [Harbor](https://harbor.status.im/harbor/projects/9/repositories/logos-delivery/artifacts-tab). - [ ] Analyze Kibana logs from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test`. - Most relevant logs are `(fleet: "waku.test" AND message: "SIGSEGV")`. - [ ] Enable again the `waku.test` fleet to resume auto-deployment of the latest `master` commit. @@ -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/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). + - [ ] Update [logos-delivery-compose](https://github.com/logos-messaging/logos-delivery-compose) and [logos-delivery-simulator](https://github.com/logos-messaging/waku-simulator) according to the new release. + - [ ] Bump logos-delivery dependency in [logos-delivery-rust-bindings](https://github.com/logos-messaging/logos-delivery-rust-bindings) and make sure all examples and tests work. + - [ ] Bump logos-delivery dependency in [logos-delivery-go-bindings](https://github.com/logos-messaging/logos-delivery-go-bindings) and make sure all tests work. + - [ ] Create GitHub release (https://github.com/logos-messaging/logos-delivery/releases). - [ ] Submit a PR to merge the release branch back to `master`. Make sure you use the option "Merge pull request (Create a merge commit)" to perform the merge. Ping repo admin if this option is not available. - [ ] **Promote release to fleets** @@ -47,10 +47,10 @@ All items below are to be completed by the owner of the given release. ### Links -- [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) +- [Release process](https://github.com/logos-messaging/logos-delivery/blob/master/docs/contributors/release-process.md) +- [Release notes](https://github.com/logos-messaging/logos-delivery/blob/master/CHANGELOG.md) - [Fleet ownership](https://www.notion.so/Fleet-Ownership-7532aad8896d46599abac3c274189741?pvs=4#d2d2f0fe4b3c429fbd860a1d64f89a64) - [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) - [Jenkins](https://ci.infra.status.im/job/nim-waku/) - [Fleets](https://fleets.waku.org/) -- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) +- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/logos-delivery/artifacts-tab) diff --git a/.github/ISSUE_TEMPLATE/prepare_full_release.md b/.github/ISSUE_TEMPLATE/prepare_full_release.md index d7458a8e3..d919f9ed0 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 @@ -24,7 +24,7 @@ All items below are to be completed by the owner of the given release. - [ ] **Validation of release candidate** - [ ] **Automated testing** - - [ ] Ensure all the unit tests (specifically logos-messaging-js tests) are green against the release candidate. + - [ ] Ensure all the unit tests (specifically logos-delivery-js tests) are green against the release candidate. - [ ] Ask Vac-QA and Vac-DST to perform the available tests against the release candidate. - [ ] Vac-DST (an additional report is needed; see [this](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f)) @@ -55,10 +55,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`) 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). + - [ ] Update [logos-delivery-compose](https://github.com/logos-messaging/logos-delivery-compose) and [logos-delivery-simulator](https://github.com/logos-messaging/logos-delivery-simulator) according to the new release. + - [ ] Bump logos-delivery dependency in [logos-delivery-rust-bindings](https://github.com/logos-messaging/logos-delivery-rust-bindings) and make sure all examples and tests work. + - [ ] Bump logos-delivery dependency in [logos-delivery-go-bindings](https://github.com/logos-messaging/logos-delivery-go-bindings) and make sure all tests work. + - [ ] Create GitHub release (https://github.com/logos-messaging/logos-delivery/releases). - [ ] Submit a PR to merge the release branch back to `master`. Make sure you use the option "Merge pull request (Create a merge commit)" to perform the merge. Ping repo admin if this option is not available. - [ ] **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/logos-messaging/nwaku/blob/master/docs/contributors/release-process.md) -- [Release notes](https://github.com/logos-messaging/nwaku/blob/master/CHANGELOG.md) +- [Release process](https://github.com/logos-messaging/logos-delivery/blob/master/docs/contributors/release-process.md) +- [Release notes](https://github.com/logos-messaging/logos-delivery/blob/master/CHANGELOG.md) - [Fleet ownership](https://www.notion.so/Fleet-Ownership-7532aad8896d46599abac3c274189741?pvs=4#d2d2f0fe4b3c429fbd860a1d64f89a64) - [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) - [Jenkins](https://ci.infra.status.im/job/nim-waku/) diff --git a/.github/workflows/ci-daily.yml b/.github/workflows/ci-daily.yml index 236bd5216..b442014a6 100644 --- a/.github/workflows/ci-daily.yml +++ b/.github/workflows/ci-daily.yml @@ -1,4 +1,4 @@ -name: Daily logos-messaging-nim CI +name: Daily logos-delivery CI on: schedule: @@ -72,7 +72,7 @@ jobs: {\"name\": \"Status\", \"value\": \"$STATUS\", \"inline\": true} ], \"url\": \"$RUN_URL\", - \"footer\": {\"text\": \"Daily logos-messaging-nim CI\"} + \"footer\": {\"text\": \"Daily logos-delivery CI\"} }] }" \ "$DISCORD_WEBHOOK_URL" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3de6eb4f5..3c84f5c6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,12 +138,12 @@ 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@10dc3d3eb4b6a3d4313f7b2cc4a85a925e9ce039 + uses: logos-messaging/logos-delivery/.github/workflows/container-image.yml@10dc3d3eb4b6a3d4313f7b2cc4a85a925e9ce039 secrets: inherit nwaku-nwaku-interop-tests: needs: build-docker-image - uses: logos-messaging/logos-messaging-interop-tests/.github/workflows/nim_waku_PR.yml@SMOKE_TEST_STABLE + uses: logos-messaging/logos-delivery-interop-tests/.github/workflows/nim_waku_PR.yml@SMOKE_TEST_STABLE with: node_nwaku: ${{ needs.build-docker-image.outputs.image }} @@ -151,14 +151,14 @@ jobs: js-waku-node: needs: build-docker-image - uses: logos-messaging/logos-messaging-js/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-delivery-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node js-waku-node-optional: needs: build-docker-image - uses: logos-messaging/logos-messaging-js/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-delivery-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node-optional diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index faded198b..e145e28ae 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -91,14 +91,14 @@ jobs: build-docker-image: needs: tag-name - uses: logos-messaging/nwaku/.github/workflows/container-image.yml@master + uses: logos-messaging/logos-delivery/.github/workflows/container-image.yml@master with: image_tag: ${{ needs.tag-name.outputs.tag }} secrets: inherit js-waku-node: needs: build-docker-image - uses: logos-messaging/logos-messaging-js/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-delivery-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node @@ -106,7 +106,7 @@ jobs: js-waku-node-optional: needs: build-docker-image - uses: logos-messaging/logos-messaging-js/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-delivery-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node-optional @@ -150,7 +150,7 @@ jobs: -u $(id -u) \ docker.io/wakuorg/sv4git:latest \ release-notes ${RELEASE_NOTES_TAG} --previous $(git tag -l --sort -creatordate | grep -e "^v[0-9]*\.[0-9]*\.[0-9]*$") |\ - sed -E 's@#([0-9]+)@[#\1](https://github.com/logos-messaging/nwaku/issues/\1)@g' > release_notes.md + sed -E 's@#([0-9]+)@[#\1](https://github.com/logos-messaging/logos-delivery/issues/\1)@g' > release_notes.md sed -i "s/^## .*/Generated at $(date)/" release_notes.md diff --git a/README.md b/README.md index c64479738..8833ae131 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Introduction -The logos-messaging-nim, a.k.a. lmn or nwaku, repository implements a set of libp2p protocols aimed to bring +This repository implements a set of libp2p protocols aimed to bring private communications. - Nim implementation of [these specs](https://github.com/vacp2p/rfc-index/tree/main/waku). From f208cb79ed726eba5706d88e6183dfe751118124 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:00:51 +0100 Subject: [PATCH 066/155] adjust Dockerfile.lightpushWithMix.compile (#3724) --- Dockerfile.lightpushWithMix.compile | 7 +++---- .../Dockerfile.liteprotocoltester.compile | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile.lightpushWithMix.compile b/Dockerfile.lightpushWithMix.compile index 8006ec50b..82e076b41 100644 --- a/Dockerfile.lightpushWithMix.compile +++ b/Dockerfile.lightpushWithMix.compile @@ -7,7 +7,7 @@ ARG NIM_COMMIT ARG LOG_LEVEL=TRACE # Get build tools and required header files -RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq +RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq libbsd-dev WORKDIR /app COPY . . @@ -24,7 +24,6 @@ RUN make -j$(nproc) deps QUICK_AND_DIRTY_COMPILER=1 ${NIM_COMMIT} # Build the final node binary RUN make -j$(nproc) ${NIM_COMMIT} $MAKE_TARGET LOG_LEVEL=${LOG_LEVEL} NIMFLAGS="${NIMFLAGS}" - # REFERENCE IMAGE as BASE for specialized PRODUCTION IMAGES---------------------------------------- FROM alpine:3.18 AS base_lpt @@ -44,8 +43,8 @@ RUN apk add --no-cache libgcc libpq-dev \ wget \ iproute2 \ python3 \ - jq - + jq \ + libstdc++ COPY --from=nim-build /app/build/lightpush_publisher_mix /usr/bin/ RUN chmod +x /usr/bin/lightpush_publisher_mix diff --git a/apps/liteprotocoltester/Dockerfile.liteprotocoltester.compile b/apps/liteprotocoltester/Dockerfile.liteprotocoltester.compile index 9e2432051..dd7018cc0 100644 --- a/apps/liteprotocoltester/Dockerfile.liteprotocoltester.compile +++ b/apps/liteprotocoltester/Dockerfile.liteprotocoltester.compile @@ -7,7 +7,7 @@ ARG NIM_COMMIT ARG LOG_LEVEL=TRACE # Get build tools and required header files -RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq +RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq libbsd-dev WORKDIR /app COPY . . @@ -43,7 +43,8 @@ EXPOSE 30303 60000 8545 RUN apk add --no-cache libgcc libpq-dev \ wget \ iproute2 \ - python3 + python3 \ + libstdc++ COPY --from=nim-build /app/build/liteprotocoltester /usr/bin/ RUN chmod +x /usr/bin/liteprotocoltester From 2f3f56898f3251254523703cc3a3dec905eedbee Mon Sep 17 00:00:00 2001 From: Darshan <35736874+darshankabariya@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:51:15 +0530 Subject: [PATCH 067/155] fix: update release process (#3727) --- .../ISSUE_TEMPLATE/prepare_beta_release.md | 27 ++++++---- .../ISSUE_TEMPLATE/prepare_full_release.md | 53 +++++++++++-------- docs/contributors/release-process.md | 33 +++++++----- 3 files changed, 67 insertions(+), 46 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/prepare_beta_release.md b/.github/ISSUE_TEMPLATE/prepare_beta_release.md index 2e4226e67..3c4e76854 100644 --- a/.github/ISSUE_TEMPLATE/prepare_beta_release.md +++ b/.github/ISSUE_TEMPLATE/prepare_beta_release.md @@ -21,15 +21,21 @@ All items below are to be completed by the owner of the given release. - [ ] Assign release candidate tag to the release branch HEAD (e.g. `v0.X.0-beta-rc.0`, `v0.X.0-beta-rc.1`, ... `v0.X.0-beta-rc.N`). - [ ] Generate and edit release notes in CHANGELOG.md. -- [ ] **Waku test and fleets validation** - - [ ] Ensure all the unit tests (specifically logos-delivery-js tests) are green against the release candidate. - - [ ] Deploy the release candidate to `waku.test` only through [deploy-waku-test job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-test/) and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). - - After completion, disable [deployment job](https://ci.infra.status.im/job/nim-waku/) so that its version is not updated on every merge to master. - - Verify the deployed version at https://fleets.waku.org/. - - Confirm the container image exists on [Harbor](https://harbor.status.im/harbor/projects/9/repositories/logos-delivery/artifacts-tab). - - [ ] Analyze Kibana logs from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test`. - - Most relevant logs are `(fleet: "waku.test" AND message: "SIGSEGV")`. - - [ ] Enable again the `waku.test` fleet to resume auto-deployment of the latest `master` commit. +- [ ] **Validation of release candidate** + - [ ] **Automated testing** + - [ ] Ensure all the unit tests (specifically logos-messaging-js tests) are green against the release candidate. + - [ ] **Waku fleet testing** + - [ ] Deploy the release candidate to `waku.test` through [deploy-waku-test job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-test/) and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). + - After completion, disable fleet so that daily CI does not override your release candidate. + - Verify at https://fleets.waku.org/ that the fleet is locked to the release candidate image. + - Confirm the container image exists on [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab). + - [ ] Search [Kibana logs](https://kibana.infra.status.im/app/discover) from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test`. + - Set time range to "Last 30 days" (or since last release). + - Most relevant search query: `(fleet: "waku.test" AND message: "SIGSEGV")`, `(fleet: "waku.test" AND message: "exception")`, `(fleet: "waku.test" AND message: "error")`. + - Document any crashes or errors found. + - [ ] If `waku.test` validation is successful, deploy to `waku.sandbox` using the [deploy-waku-sandbox job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox/). + - [ ] Search [Kibana logs](https://kibana.infra.status.im/app/discover) for `waku.sandbox`: `(fleet: "waku.sandbox" AND message: "SIGSEGV")`, `(fleet: "waku.sandbox" AND message: "exception")`, `(fleet: "waku.sandbox" AND message: "error")`. most probably if there are no crashes or errors in `waku.test`, there will be no crashes or errors in `waku.sandbox`. + - [ ] Enable the `waku.test` fleet again to resume auto-deployment of the latest `master` commit. - [ ] **Proceed with release** @@ -53,4 +59,5 @@ All items below are to be completed by the owner of the given release. - [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) - [Jenkins](https://ci.infra.status.im/job/nim-waku/) - [Fleets](https://fleets.waku.org/) -- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/logos-delivery/artifacts-tab) +- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) +- [Kibana](https://kibana.infra.status.im/app/) diff --git a/.github/ISSUE_TEMPLATE/prepare_full_release.md b/.github/ISSUE_TEMPLATE/prepare_full_release.md index d919f9ed0..4df808bd4 100644 --- a/.github/ISSUE_TEMPLATE/prepare_full_release.md +++ b/.github/ISSUE_TEMPLATE/prepare_full_release.md @@ -24,33 +24,39 @@ All items below are to be completed by the owner of the given release. - [ ] **Validation of release candidate** - [ ] **Automated testing** - - [ ] Ensure all the unit tests (specifically logos-delivery-js tests) are green against the release candidate. - - [ ] Ask Vac-QA and Vac-DST to perform the available tests against the release candidate. - - [ ] Vac-DST (an additional report is needed; see [this](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f)) + - [ ] Ensure all the unit tests (specifically logos-messaging-js tests) are green against the release candidate. - [ ] **Waku fleet testing** - - [ ] Deploy the release candidate to `waku.test` and `waku.sandbox` fleets. - - Start the [deployment job](https://ci.infra.status.im/job/nim-waku/) for both fleets and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). - - After completion, disable [deployment job](https://ci.infra.status.im/job/nim-waku/) so that its version is not updated on every merge to `master`. - - Verify the deployed version at https://fleets.waku.org/. + - [ ] Deploy the release candidate to `waku.test` fleet. + - Start the [deployment job](https://ci.infra.status.im/job/nim-waku/) and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). + - After completion, disable fleet so that daily CI does not override your release candidate. + - Verify at https://fleets.waku.org/ that the fleet is locked to the release candidate image. - Confirm the container image exists on [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab). - - [ ] Search _Kibana_ logs from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test` and `waku.sandbox`. - - Most relevant logs are `(fleet: "waku.test" AND message: "SIGSEGV")` OR `(fleet: "waku.sandbox" AND message: "SIGSEGV")`. - - [ ] Enable again the `waku.test` fleet to resume auto-deployment of the latest `master` commit. + - [ ] Search [Kibana logs](https://kibana.infra.status.im/app/discover) from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test`. + - Set time range to "Last 30 days" (or since last release). + - Most relevant search query: `(fleet: "waku.test" AND message: "SIGSEGV")`, `(fleet: "waku.test" AND message: "exception")`, `(fleet: "waku.test" AND message: "error")`. + - Document any crashes or errors found. + - [ ] If `waku.test` validation is successful, deploy to `waku.sandbox` using the same [deployment job](https://ci.infra.status.im/job/nim-waku/). + - [ ] Search [Kibana logs](https://kibana.infra.status.im/app/discover) for `waku.sandbox`: `(fleet: "waku.sandbox" AND message: "SIGSEGV")`, `(fleet: "waku.sandbox" AND message: "exception")`, `(fleet: "waku.sandbox" AND message: "error")`. most probably if there are no crashes or errors in `waku.test`, there will be no crashes or errors in `waku.sandbox`. + - [ ] Enable the `waku.test` fleet again to resume auto-deployment of the latest `master` commit. -- [ ] **Status fleet testing** - - [ ] Deploy release candidate to `status.staging` - - [ ] Perform [sanity check](https://www.notion.so/How-to-test-Nwaku-on-Status-12c6e4b9bf06420ca868bd199129b425) and log results as comments in this issue. - - [ ] Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client. - - 1:1 Chats with each other - - Send and receive messages in a community - - Close one instance, send messages with second instance, reopen first instance and confirm messages sent while offline are retrieved from store - - [ ] Perform checks based on _end user impact_ - - [ ] Inform other (Waku and Status) CCs to point their instances to `status.staging` for a few days. Ping Status colleagues on their Discord server or in the [Status community](https://status.app/c/G3kAAMSQtb05kog3aGbr3kiaxN4tF5xy4BAGEkkLwILk2z3GcoYlm5hSJXGn7J3laft-tnTwDWmYJ18dP_3bgX96dqr_8E3qKAvxDf3NrrCMUBp4R9EYkQez9XSM4486mXoC3mIln2zc-TNdvjdfL9eHVZ-mGgs=#zQ3shZeEJqTC1xhGUjxuS4rtHSrhJ8vUYp64v6qWkLpvdy9L9) (this is not a blocking point.) - - [ ] Ask Status-QA to perform sanity checks (as described above) and checks based on _end user impact_; specify the version being tested - - [ ] Ask Status-QA or infra to run the automated Status e2e tests against `status.staging` - - [ ] Get other CCs' sign-off: they should comment on this PR, e.g., "Used the app for a week, no problem." If problems are reported, resolve them and create a new RC. - - [ ] **Get Status-QA sign-off**, ensuring that the `status.test` update will not disturb ongoing activities. + - [ ] **QA and DST testing** + - [ ] Ask Vac-QA and Vac-DST to run their available tests against the release candidate; share all release candidates with both teams. + - [ ] Vac-DST: An additional report is needed ([see this example](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f)). Inform DST team about what are the expectations for this rc. For example, if we expect higher or lower bandwidth consumption. + + - [ ] **Status fleet testing** + - [ ] Deploy release candidate to `status.staging` + - [ ] Perform [sanity check](https://www.notion.so/How-to-test-Nwaku-on-Status-12c6e4b9bf06420ca868bd199129b425) and log results as comments in this issue. + - [ ] Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client. + - 1:1 Chats with each other + - Send and receive messages in a community + - Close one instance, send messages with second instance, reopen first instance and confirm messages sent while offline are retrieved from store + - [ ] Perform checks based on _end user impact_ + - [ ] Inform other (Waku and Status) CCs to point their instances to `status.staging` for a few days. Ping Status colleagues on their Discord server or in the [Status community](https://status.app/c/G3kAAMSQtb05kog3aGbr3kiaxN4tF5xy4BAGEkkLwILk2z3GcoYlm5hSJXGn7J3laft-tnTwDWmYJ18dP_3bgX96dqr_8E3qKAvxDf3NrrCMUBp4R9EYkQez9XSM4486mXoC3mIln2zc-TNdvjdfL9eHVZ-mGgs=#zQ3shZeEJqTC1xhGUjxuS4rtHSrhJ8vUYp64v6qWkLpvdy9L9) (this is not a blocking point.) + - [ ] Ask Status-QA to perform sanity checks (as described above) and checks based on _end user impact_; specify the version being tested + - [ ] Ask Status-QA or infra to run the automated Status e2e tests against `status.staging` + - [ ] Get other CCs' sign-off: they should comment on this PR, e.g., "Used the app for a week, no problem." If problems are reported, resolve them and create a new RC. + - [ ] **Get Status-QA sign-off**, ensuring that the `status.test` update will not disturb ongoing activities. - [ ] **Proceed with release** @@ -74,3 +80,4 @@ All items below are to be completed by the owner of the given release. - [Jenkins](https://ci.infra.status.im/job/nim-waku/) - [Fleets](https://fleets.waku.org/) - [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) +- [Kibana](https://kibana.infra.status.im/app/) diff --git a/docs/contributors/release-process.md b/docs/contributors/release-process.md index bde63aa6f..8aa9282cd 100644 --- a/docs/contributors/release-process.md +++ b/docs/contributors/release-process.md @@ -20,7 +20,7 @@ For more context, see https://trunkbaseddevelopment.com/branch-for-release/ - **Full release**: follow the entire [Release process](#release-process--step-by-step). -- **Beta release**: skip just `6a` and `6c` steps from [Release process](#release-process--step-by-step). +- **Beta release**: skip just `6c` and `6d` steps from [Release process](#release-process--step-by-step). - Choose the appropriate release process based on the release type: - [Full Release](../../.github/ISSUE_TEMPLATE/prepare_full_release.md) @@ -70,20 +70,26 @@ For more context, see https://trunkbaseddevelopment.com/branch-for-release/ 6a. **Automated testing** - Ensure all the unit tests (specifically js-waku tests) are green against the release candidate. - - Ask Vac-QA and Vac-DST to run their available tests against the release candidate; share all release candidates with both teams. - - > We need an additional report like [this](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f) specifically from the DST team. 6b. **Waku fleet testing** - - Start job on `waku.sandbox` and `waku.test` [Deployment job](https://ci.infra.status.im/job/nim-waku/), wait for completion of the job. If it fails, then debug it. - - After completion, disable [deployment job](https://ci.infra.status.im/job/nim-waku/) so that its version is not updated on every merge to `master`. - - Verify at https://fleets.waku.org/ that the fleet is locked to the release candidate version. + - Start job on `waku.test` [Deployment job](https://ci.infra.status.im/job/nim-waku/), wait for completion of the job. If it fails, then debug it. + - After completion, disable fleet so that daily ci not override your release candidate. + - Verify at https://fleets.waku.org/ that the fleet is locked to the release candidate image. - Check if the image is created at [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab). - - Search _Kibana_ logs from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test` and `waku.sandbox`. - - Most relevant logs are `(fleet: "waku.test" AND message: "SIGSEGV")` OR `(fleet: "waku.sandbox" AND message: "SIGSEGV")`. + - Search [Kibana logs](https://kibana.infra.status.im/app/discover) from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test`. + - Set time range to "Last 30 days" (or since last release). + - Most relevant search query: `(fleet: "waku.test" AND message: "SIGSEGV")`, `(fleet: "waku.test" AND message: "exception")`, `(fleet: "waku.test" AND message: "error")`. + - Document any crashes or errors found. + - If `waku.test` validation is successful, deploy to `waku.sandbox` using the same [Deployment job](https://ci.infra.status.im/job/nim-waku/). + - Search [Kibana logs](https://kibana.infra.status.im/app/discover) for `waku.sandbox`: `(fleet: "waku.sandbox" AND message: "SIGSEGV")`, `(fleet: "waku.sandbox" AND message: "exception")`, `(fleet: "waku.sandbox" AND message: "error")`. most probably if there are no crashes or errors in `waku.test`, there will be no crashes or errors in `waku.sandbox`. - Enable the `waku.test` fleet again to resume auto-deployment of the latest `master` commit. - 6c. **Status fleet testing** + 6c. **QA and DST testing** + - Ask Vac-QA and Vac-DST to run their available tests against the release candidate; share all release candidates with both teams. + + > We need an additional report like [this](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f) specifically from the DST team. Inform DST team about what are the expectations for this rc. For example, if we expect higher or lower bandwidth consumption. + + 6d. **Status fleet testing** - Deploy release candidate to `status.staging` - Perform [sanity check](https://www.notion.so/How-to-test-Nwaku-on-Status-12c6e4b9bf06420ca868bd199129b425) and log results as comments in this issue. - Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client. @@ -120,10 +126,10 @@ We also need to merge the release branch back into master as a final step. 2. Deploy the release image to [Dockerhub](https://hub.docker.com/r/wakuorg/nwaku) by triggering [the manual Jenkins deployment job](https://ci.infra.status.im/job/nim-waku/job/docker-manual/). > Ensure the following build parameters are set: > - `MAKE_TARGET`: `wakunode2` - > - `IMAGE_TAG`: the release tag (e.g. `v0.36.0`) + > - `IMAGE_TAG`: the release tag (e.g. `v0.38.0`) > - `IMAGE_NAME`: `wakuorg/nwaku` > - `NIMFLAGS`: `--colors:off -d:disableMarchNative -d:chronicles_colors:none -d:postgres` - > - `GIT_REF` the release tag (e.g. `v0.36.0`) + > - `GIT_REF` the release tag (e.g. `v0.38.0`) ### Performing a patch release @@ -154,4 +160,5 @@ We also need to merge the release branch back into master as a final step. - [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) - [Jenkins](https://ci.infra.status.im/job/nim-waku/) - [Fleets](https://fleets.waku.org/) -- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) \ No newline at end of file +- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) +- [Kibana](https://kibana.infra.status.im/app/) \ No newline at end of file From 335600ebcb95a34f85a1e3c22d6cc736abc7559f Mon Sep 17 00:00:00 2001 From: Prem Chaitanya Prathi Date: Thu, 19 Feb 2026 10:26:17 +0530 Subject: [PATCH 068/155] feat: waku kademlia integration and mix updates (#3722) * feat: integrate mix protocol with extended kademlia discovery Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> --- apps/chat2mix/chat2mix.nim | 45 ++- apps/chat2mix/config_chat2mix.nim | 13 +- nix/default.nix | 13 +- simulations/mixnet/config.toml | 13 +- simulations/mixnet/config1.toml | 11 +- simulations/mixnet/config2.toml | 11 +- simulations/mixnet/config3.toml | 11 +- simulations/mixnet/config4.toml | 11 +- simulations/mixnet/run_chat_mix.sh | 2 +- simulations/mixnet/run_mix_node.sh | 2 +- simulations/mixnet/run_mix_node1.sh | 2 +- simulations/mixnet/run_mix_node2.sh | 2 +- simulations/mixnet/run_mix_node3.sh | 2 +- simulations/mixnet/run_mix_node4.sh | 2 +- tools/confutils/cli_args.nim | 17 ++ vendor/nim-libp2p | 2 +- waku.nimble | 2 +- waku/discovery/waku_kademlia.nim | 280 ++++++++++++++++++ waku/factory/conf_builder/conf_builder.nim | 6 +- .../kademlia_discovery_conf_builder.nim | 40 +++ .../conf_builder/waku_conf_builder.nim | 9 +- waku/factory/node_factory.nim | 42 ++- waku/factory/waku.nim | 3 + waku/factory/waku_conf.nim | 6 + waku/node/peer_manager/waku_peer_store.nim | 5 +- waku/node/waku_node.nim | 14 +- waku/waku_core/peers.nim | 1 + waku/waku_mix/protocol.nim | 151 ++-------- 28 files changed, 543 insertions(+), 175 deletions(-) create mode 100644 waku/discovery/waku_kademlia.nim create mode 100644 waku/factory/conf_builder/kademlia_discovery_conf_builder.nim diff --git a/apps/chat2mix/chat2mix.nim b/apps/chat2mix/chat2mix.nim index 45fd1fa2d..558454307 100644 --- a/apps/chat2mix/chat2mix.nim +++ b/apps/chat2mix/chat2mix.nim @@ -30,6 +30,7 @@ import protobuf/minprotobuf, # message serialisation/deserialisation from and to protobufs nameresolving/dnsresolver, protocols/mix/curve25519, + protocols/mix/mix_protocol, ] # define DNS resolution import waku/[ @@ -38,6 +39,7 @@ import waku_lightpush/rpc, waku_enr, discovery/waku_dnsdisc, + discovery/waku_kademlia, waku_node, node/waku_metrics, node/peer_manager, @@ -453,14 +455,48 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = (await node.mountMix(conf.clusterId, mixPrivKey, conf.mixnodes)).isOkOr: error "failed to mount waku mix protocol: ", error = $error quit(QuitFailure) - await node.mountRendezvousClient(conf.clusterId) + + # Setup extended kademlia discovery if bootstrap nodes are provided + if conf.kadBootstrapNodes.len > 0: + var kadBootstrapPeers: seq[(PeerId, seq[MultiAddress])] + for nodeStr in conf.kadBootstrapNodes: + let (peerId, ma) = parseFullAddress(nodeStr).valueOr: + error "Failed to parse kademlia bootstrap node", node = nodeStr, error = error + continue + kadBootstrapPeers.add((peerId, @[ma])) + + if kadBootstrapPeers.len > 0: + node.wakuKademlia = WakuKademlia.new( + node.switch, + ExtendedKademliaDiscoveryParams( + bootstrapNodes: kadBootstrapPeers, + mixPubKey: some(mixPubKey), + advertiseMix: false, + ), + node.peerManager, + getMixNodePoolSize = proc(): int {.gcsafe, raises: [].} = + if node.wakuMix.isNil(): + 0 + else: + node.getMixNodePoolSize(), + isNodeStarted = proc(): bool {.gcsafe, raises: [].} = + node.started, + ).valueOr: + error "failed to setup kademlia discovery", error = error + quit(QuitFailure) + + #await node.mountRendezvousClient(conf.clusterId) await node.start() node.peerManager.start() + if not node.wakuKademlia.isNil(): + (await node.wakuKademlia.start(minMixPeers = MinMixNodePoolSize)).isOkOr: + error "failed to start kademlia discovery", error = error + quit(QuitFailure) await node.mountLibp2pPing() - await node.mountPeerExchangeClient() + #await node.mountPeerExchangeClient() let pubsubTopic = conf.getPubsubTopic(node, conf.contentTopic) echo "pubsub topic is: " & pubsubTopic let nick = await readNick(transp) @@ -601,11 +637,6 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = node, pubsubTopic, conf.contentTopic, servicePeerInfo, false ) echo "waiting for mix nodes to be discovered..." - while true: - if node.getMixNodePoolSize() >= MinMixNodePoolSize: - break - discard await node.fetchPeerExchangePeers() - await sleepAsync(1000) while node.getMixNodePoolSize() < MinMixNodePoolSize: info "waiting for mix nodes to be discovered", diff --git a/apps/chat2mix/config_chat2mix.nim b/apps/chat2mix/config_chat2mix.nim index ddb7136cb..46cd481d7 100644 --- a/apps/chat2mix/config_chat2mix.nim +++ b/apps/chat2mix/config_chat2mix.nim @@ -203,13 +203,13 @@ type fleet* {. desc: "Select the fleet to connect to. This sets the DNS discovery URL to the selected fleet.", - defaultValue: Fleet.test, + defaultValue: Fleet.none, name: "fleet" .}: Fleet contentTopic* {. desc: "Content topic for chat messages.", - defaultValue: "/toy-chat-mix/2/huilong/proto", + defaultValue: "/toy-chat/2/baixa-chiado/proto", name: "content-topic" .}: string @@ -228,7 +228,14 @@ type desc: "WebSocket Secure Support.", defaultValue: false, name: "websocket-secure-support" - .}: bool ## rln-relay configuration + .}: bool + + ## Kademlia Discovery config + kadBootstrapNodes* {. + desc: + "Peer multiaddr for kademlia discovery bootstrap node (must include /p2p/). Argument may be repeated.", + name: "kad-bootstrap-node" + .}: seq[string] proc parseCmdArg*(T: type MixNodePubInfo, p: string): T = let elements = p.split(":") diff --git a/nix/default.nix b/nix/default.nix index d532ec5b5..ca91d0e2f 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -50,8 +50,8 @@ in stdenv.mkDerivation { ]; # Environment variables required for Android builds - ANDROID_SDK_ROOT="${pkgs.androidPkgs.sdk}"; - ANDROID_NDK_HOME="${pkgs.androidPkgs.ndk}"; + ANDROID_SDK_ROOT = "${pkgs.androidPkgs.sdk}"; + ANDROID_NDK_HOME = "${pkgs.androidPkgs.ndk}"; NIMFLAGS = "-d:disableMarchNative -d:git_revision_override=${revision}"; XDG_CACHE_HOME = "/tmp"; @@ -65,6 +65,15 @@ in stdenv.mkDerivation { configurePhase = '' patchShebangs . vendor/nimbus-build-system > /dev/null + + # build_nim.sh guards "rm -rf dist/checksums" with NIX_BUILD_TOP != "/build", + # but on macOS the nix sandbox uses /private/tmp/... so the check fails and + # dist/checksums (provided via preBuild) gets deleted. Fix the check to skip + # the removal whenever NIX_BUILD_TOP is set (i.e. any nix build). + substituteInPlace vendor/nimbus-build-system/scripts/build_nim.sh \ + --replace 'if [[ "''${NIX_BUILD_TOP}" != "/build" ]]; then' \ + 'if [[ -z "''${NIX_BUILD_TOP}" ]]; then' + make nimbus-build-system-paths make nimbus-build-system-nimble-dir ''; diff --git a/simulations/mixnet/config.toml b/simulations/mixnet/config.toml index 3719d8177..5cd1aa936 100644 --- a/simulations/mixnet/config.toml +++ b/simulations/mixnet/config.toml @@ -1,16 +1,17 @@ -log-level = "INFO" +log-level = "TRACE" relay = true mix = true filter = true -store = false +store = true lightpush = true max-connections = 150 -peer-exchange = true +peer-exchange = false metrics-logging = false cluster-id = 2 -discv5-discovery = true +discv5-discovery = false discv5-udp-port = 9000 discv5-enr-auto-update = true +enable-kad-discovery = true rest = true rest-admin = true ports-shift = 1 @@ -19,7 +20,9 @@ shard = [0] agent-string = "nwaku-mix" nodekey = "f98e3fba96c32e8d1967d460f1b79457380e1a895f7971cecc8528abe733781a" mixkey = "a87db88246ec0eedda347b9b643864bee3d6933eb15ba41e6d58cb678d813258" -rendezvous = true +rendezvous = false listen-address = "127.0.0.1" nat = "extip:127.0.0.1" +ext-multiaddr = ["/ip4/127.0.0.1/tcp/60001"] +ext-multiaddr-only = true ip-colocation-limit=0 diff --git a/simulations/mixnet/config1.toml b/simulations/mixnet/config1.toml index e06a527c1..73cccb8c6 100644 --- a/simulations/mixnet/config1.toml +++ b/simulations/mixnet/config1.toml @@ -1,17 +1,18 @@ -log-level = "INFO" +log-level = "TRACE" relay = true mix = true filter = true store = false lightpush = true max-connections = 150 -peer-exchange = true +peer-exchange = false metrics-logging = false cluster-id = 2 -discv5-discovery = true +discv5-discovery = false discv5-udp-port = 9001 discv5-enr-auto-update = true discv5-bootstrap-node = ["enr:-LG4QBaAbcA921hmu3IrreLqGZ4y3VWCjBCgNN9mpX9vqkkbSrM3HJHZTXnb5iVXgc5pPtDhWLxkB6F3yY25hSwMezkEgmlkgnY0gmlwhH8AAAGKbXVsdGlhZGRyc4oACATAqEQ-BuphgnJzhQACAQAAiXNlY3AyNTZrMaEDpEW1UlUGHRJg6g_zGuCddKWmIUBGZCQX13xGfh9J6KiDdGNwguphg3VkcIIjKYV3YWt1Mg0"] +kad-bootstrap-node = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o"] rest = true rest-admin = true ports-shift = 2 @@ -20,8 +21,10 @@ shard = [0] agent-string = "nwaku-mix" nodekey = "09e9d134331953357bd38bbfce8edb377f4b6308b4f3bfbe85c610497053d684" mixkey = "c86029e02c05a7e25182974b519d0d52fcbafeca6fe191fbb64857fb05be1a53" -rendezvous = true +rendezvous = false listen-address = "127.0.0.1" nat = "extip:127.0.0.1" +ext-multiaddr = ["/ip4/127.0.0.1/tcp/60002"] +ext-multiaddr-only = true ip-colocation-limit=0 #staticnode = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o", "/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA","/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f","/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu"] diff --git a/simulations/mixnet/config2.toml b/simulations/mixnet/config2.toml index 93822603b..c40e41103 100644 --- a/simulations/mixnet/config2.toml +++ b/simulations/mixnet/config2.toml @@ -1,17 +1,18 @@ -log-level = "INFO" +log-level = "TRACE" relay = true mix = true filter = true store = false lightpush = true max-connections = 150 -peer-exchange = true +peer-exchange = false metrics-logging = false cluster-id = 2 -discv5-discovery = true +discv5-discovery = false discv5-udp-port = 9002 discv5-enr-auto-update = true discv5-bootstrap-node = ["enr:-LG4QBaAbcA921hmu3IrreLqGZ4y3VWCjBCgNN9mpX9vqkkbSrM3HJHZTXnb5iVXgc5pPtDhWLxkB6F3yY25hSwMezkEgmlkgnY0gmlwhH8AAAGKbXVsdGlhZGRyc4oACATAqEQ-BuphgnJzhQACAQAAiXNlY3AyNTZrMaEDpEW1UlUGHRJg6g_zGuCddKWmIUBGZCQX13xGfh9J6KiDdGNwguphg3VkcIIjKYV3YWt1Mg0"] +kad-bootstrap-node = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o"] rest = false rest-admin = false ports-shift = 3 @@ -20,8 +21,10 @@ shard = [0] agent-string = "nwaku-mix" nodekey = "ed54db994682e857d77cd6fb81be697382dc43aa5cd78e16b0ec8098549f860e" mixkey = "b858ac16bbb551c4b2973313b1c8c8f7ea469fca03f1608d200bbf58d388ec7f" -rendezvous = true +rendezvous = false listen-address = "127.0.0.1" nat = "extip:127.0.0.1" +ext-multiaddr = ["/ip4/127.0.0.1/tcp/60003"] +ext-multiaddr-only = true ip-colocation-limit=0 #staticnode = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o", "/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF","/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f","/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu"] diff --git a/simulations/mixnet/config3.toml b/simulations/mixnet/config3.toml index 6f339dfff..80c19b34b 100644 --- a/simulations/mixnet/config3.toml +++ b/simulations/mixnet/config3.toml @@ -1,17 +1,18 @@ -log-level = "INFO" +log-level = "TRACE" relay = true mix = true filter = true store = false lightpush = true max-connections = 150 -peer-exchange = true +peer-exchange = false metrics-logging = false cluster-id = 2 -discv5-discovery = true +discv5-discovery = false discv5-udp-port = 9003 discv5-enr-auto-update = true discv5-bootstrap-node = ["enr:-LG4QBaAbcA921hmu3IrreLqGZ4y3VWCjBCgNN9mpX9vqkkbSrM3HJHZTXnb5iVXgc5pPtDhWLxkB6F3yY25hSwMezkEgmlkgnY0gmlwhH8AAAGKbXVsdGlhZGRyc4oACATAqEQ-BuphgnJzhQACAQAAiXNlY3AyNTZrMaEDpEW1UlUGHRJg6g_zGuCddKWmIUBGZCQX13xGfh9J6KiDdGNwguphg3VkcIIjKYV3YWt1Mg0"] +kad-bootstrap-node = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o"] rest = false rest-admin = false ports-shift = 4 @@ -20,8 +21,10 @@ shard = [0] agent-string = "nwaku-mix" nodekey = "42f96f29f2d6670938b0864aced65a332dcf5774103b4c44ec4d0ea4ef3c47d6" mixkey = "d8bd379bb394b0f22dd236d63af9f1a9bc45266beffc3fbbe19e8b6575f2535b" -rendezvous = true +rendezvous = false listen-address = "127.0.0.1" nat = "extip:127.0.0.1" +ext-multiaddr = ["/ip4/127.0.0.1/tcp/60004"] +ext-multiaddr-only = true ip-colocation-limit=0 #staticnode = ["/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF", "/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA","/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o","/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu"] diff --git a/simulations/mixnet/config4.toml b/simulations/mixnet/config4.toml index 23115ac03..ed5b2dad0 100644 --- a/simulations/mixnet/config4.toml +++ b/simulations/mixnet/config4.toml @@ -1,17 +1,18 @@ -log-level = "INFO" +log-level = "TRACE" relay = true mix = true filter = true store = false lightpush = true max-connections = 150 -peer-exchange = true +peer-exchange = false metrics-logging = false cluster-id = 2 -discv5-discovery = true +discv5-discovery = false discv5-udp-port = 9004 discv5-enr-auto-update = true discv5-bootstrap-node = ["enr:-LG4QBaAbcA921hmu3IrreLqGZ4y3VWCjBCgNN9mpX9vqkkbSrM3HJHZTXnb5iVXgc5pPtDhWLxkB6F3yY25hSwMezkEgmlkgnY0gmlwhH8AAAGKbXVsdGlhZGRyc4oACATAqEQ-BuphgnJzhQACAQAAiXNlY3AyNTZrMaEDpEW1UlUGHRJg6g_zGuCddKWmIUBGZCQX13xGfh9J6KiDdGNwguphg3VkcIIjKYV3YWt1Mg0"] +kad-bootstrap-node = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o"] rest = false rest-admin = false ports-shift = 5 @@ -20,8 +21,10 @@ shard = [0] agent-string = "nwaku-mix" nodekey = "3ce887b3c34b7a92dd2868af33941ed1dbec4893b054572cd5078da09dd923d4" mixkey = "780fff09e51e98df574e266bf3266ec6a3a1ddfcf7da826a349a29c137009d49" -rendezvous = true +rendezvous = false listen-address = "127.0.0.1" nat = "extip:127.0.0.1" +ext-multiaddr = ["/ip4/127.0.0.1/tcp/60005"] +ext-multiaddr-only = true ip-colocation-limit=0 #staticnode = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o", "/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA","/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f","/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF"] diff --git a/simulations/mixnet/run_chat_mix.sh b/simulations/mixnet/run_chat_mix.sh index 3dd6f5932..f711c055e 100755 --- a/simulations/mixnet/run_chat_mix.sh +++ b/simulations/mixnet/run_chat_mix.sh @@ -1,2 +1,2 @@ -../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE +../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE --kad-bootstrap-node="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" #--mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" diff --git a/simulations/mixnet/run_mix_node.sh b/simulations/mixnet/run_mix_node.sh index 1d005796e..2b293540c 100755 --- a/simulations/mixnet/run_mix_node.sh +++ b/simulations/mixnet/run_mix_node.sh @@ -1 +1 @@ -../../build/wakunode2 --config-file="config.toml" +../../build/wakunode2 --config-file="config.toml" 2>&1 | tee mix_node.log diff --git a/simulations/mixnet/run_mix_node1.sh b/simulations/mixnet/run_mix_node1.sh index 024eb3f99..617312122 100755 --- a/simulations/mixnet/run_mix_node1.sh +++ b/simulations/mixnet/run_mix_node1.sh @@ -1 +1 @@ -../../build/wakunode2 --config-file="config1.toml" +../../build/wakunode2 --config-file="config1.toml" 2>&1 | tee mix_node1.log diff --git a/simulations/mixnet/run_mix_node2.sh b/simulations/mixnet/run_mix_node2.sh index e55a9bac8..5fc2ef498 100755 --- a/simulations/mixnet/run_mix_node2.sh +++ b/simulations/mixnet/run_mix_node2.sh @@ -1 +1 @@ -../../build/wakunode2 --config-file="config2.toml" +../../build/wakunode2 --config-file="config2.toml" 2>&1 | tee mix_node2.log diff --git a/simulations/mixnet/run_mix_node3.sh b/simulations/mixnet/run_mix_node3.sh index dca8119a3..d77d04c02 100755 --- a/simulations/mixnet/run_mix_node3.sh +++ b/simulations/mixnet/run_mix_node3.sh @@ -1 +1 @@ -../../build/wakunode2 --config-file="config3.toml" +../../build/wakunode2 --config-file="config3.toml" 2>&1 | tee mix_node3.log diff --git a/simulations/mixnet/run_mix_node4.sh b/simulations/mixnet/run_mix_node4.sh index 9cf25158b..3a2b0299d 100755 --- a/simulations/mixnet/run_mix_node4.sh +++ b/simulations/mixnet/run_mix_node4.sh @@ -1 +1 @@ -../../build/wakunode2 --config-file="config4.toml" +../../build/wakunode2 --config-file="config4.toml" 2>&1 | tee mix_node4.log diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 6811e335f..5e4adacb2 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -621,6 +621,20 @@ with the drawback of consuming some more bandwidth.""", name: "mixnode" .}: seq[MixNodePubInfo] + # Kademlia Discovery config + enableKadDiscovery* {. + desc: + "Enable extended kademlia discovery. Can be enabled without bootstrap nodes for the first node in the network.", + defaultValue: false, + name: "enable-kad-discovery" + .}: bool + + kadBootstrapNodes* {. + desc: + "Peer multiaddr for kademlia discovery bootstrap node (must include /p2p/). Argument may be repeated.", + name: "kad-bootstrap-node" + .}: seq[string] + ## websocket config websocketSupport* {. desc: "Enable websocket: true|false", @@ -1057,4 +1071,7 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.rateLimitConf.withRateLimits(n.rateLimits) + b.kademliaDiscoveryConf.withEnabled(n.enableKadDiscovery) + b.kademliaDiscoveryConf.withBootstrapNodes(n.kadBootstrapNodes) + return b.build() diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index ca48c3718..ff8d51857 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit ca48c3718246bb411ff0e354a70cb82d9a28de0d +Subproject commit ff8d51857b4b79a68468e7bcc27b2026cca02996 diff --git a/waku.nimble b/waku.nimble index e4c436e8d..d879bc0e1 100644 --- a/waku.nimble +++ b/waku.nimble @@ -24,7 +24,7 @@ requires "nim >= 2.2.4", "stew", "stint", "metrics", - "libp2p >= 1.14.3", + "libp2p >= 1.15.0", "web3", "presto", "regex", diff --git a/waku/discovery/waku_kademlia.nim b/waku/discovery/waku_kademlia.nim new file mode 100644 index 000000000..94b63a321 --- /dev/null +++ b/waku/discovery/waku_kademlia.nim @@ -0,0 +1,280 @@ +{.push raises: [].} + +import std/[options, sequtils] +import + chronos, + chronicles, + results, + stew/byteutils, + libp2p/[peerid, multiaddress, switch], + libp2p/extended_peer_record, + libp2p/crypto/curve25519, + libp2p/protocols/[kademlia, kad_disco], + libp2p/protocols/kademlia_discovery/types as kad_types, + libp2p/protocols/mix/mix_protocol + +import waku/waku_core, waku/node/peer_manager + +logScope: + topics = "waku extended kademlia discovery" + +const + DefaultExtendedKademliaDiscoveryInterval* = chronos.seconds(5) + ExtendedKademliaDiscoveryStartupDelay* = chronos.seconds(5) + +type + MixNodePoolSizeProvider* = proc(): int {.gcsafe, raises: [].} + NodeStartedProvider* = proc(): bool {.gcsafe, raises: [].} + + ExtendedKademliaDiscoveryParams* = object + bootstrapNodes*: seq[(PeerId, seq[MultiAddress])] + mixPubKey*: Option[Curve25519Key] + advertiseMix*: bool = false + + WakuKademlia* = ref object + protocol*: KademliaDiscovery + peerManager: PeerManager + discoveryLoop: Future[void] + running*: bool + getMixNodePoolSize: MixNodePoolSizeProvider + isNodeStarted: NodeStartedProvider + +proc new*( + T: type WakuKademlia, + switch: Switch, + params: ExtendedKademliaDiscoveryParams, + peerManager: PeerManager, + getMixNodePoolSize: MixNodePoolSizeProvider = nil, + isNodeStarted: NodeStartedProvider = nil, +): Result[T, string] = + if params.bootstrapNodes.len == 0: + info "creating kademlia discovery as seed node (no bootstrap nodes)" + + let kademlia = KademliaDiscovery.new( + switch, + bootstrapNodes = params.bootstrapNodes, + config = KadDHTConfig.new( + validator = kad_types.ExtEntryValidator(), selector = kad_types.ExtEntrySelector() + ), + codec = ExtendedKademliaDiscoveryCodec, + ) + + try: + switch.mount(kademlia) + except CatchableError: + return err("failed to mount kademlia discovery: " & getCurrentExceptionMsg()) + + # Register services BEFORE starting kademlia so they are included in the + # initial self-signed peer record published to the DHT + if params.advertiseMix: + if params.mixPubKey.isSome(): + let alreadyAdvertising = kademlia.startAdvertising( + ServiceInfo(id: MixProtocolID, data: @(params.mixPubKey.get())) + ) + if alreadyAdvertising: + warn "mix service was already being advertised" + debug "extended kademlia advertising mix service", + keyHex = byteutils.toHex(params.mixPubKey.get()), + bootstrapNodes = params.bootstrapNodes.len + else: + warn "mix advertising enabled but no key provided" + + info "kademlia discovery created", + bootstrapNodes = params.bootstrapNodes.len, advertiseMix = params.advertiseMix + + return ok( + WakuKademlia( + protocol: kademlia, + peerManager: peerManager, + running: false, + getMixNodePoolSize: getMixNodePoolSize, + isNodeStarted: isNodeStarted, + ) + ) + +proc extractMixPubKey(service: ServiceInfo): Option[Curve25519Key] = + if service.id != MixProtocolID: + trace "service is not mix protocol", + serviceId = service.id, mixProtocolId = MixProtocolID + return none(Curve25519Key) + + if service.data.len != Curve25519KeySize: + warn "invalid mix pub key length from kademlia record", + expected = Curve25519KeySize, + actual = service.data.len, + dataHex = byteutils.toHex(service.data) + return none(Curve25519Key) + + debug "found mix protocol service", + dataLen = service.data.len, expectedLen = Curve25519KeySize + + let key = intoCurve25519Key(service.data) + debug "successfully extracted mix pub key", keyHex = byteutils.toHex(key) + return some(key) + +proc remotePeerInfoFrom(record: ExtendedPeerRecord): Option[RemotePeerInfo] = + debug "processing kademlia record", + peerId = record.peerId, + numAddresses = record.addresses.len, + numServices = record.services.len, + serviceIds = record.services.mapIt(it.id) + + if record.addresses.len == 0: + trace "kademlia record missing addresses", peerId = record.peerId + return none(RemotePeerInfo) + + let addrs = record.addresses.mapIt(it.address) + if addrs.len == 0: + trace "kademlia record produced no dialable addresses", peerId = record.peerId + return none(RemotePeerInfo) + + let protocols = record.services.mapIt(it.id) + + var mixPubKey = none(Curve25519Key) + for service in record.services: + debug "checking service", + peerId = record.peerId, serviceId = service.id, dataLen = service.data.len + mixPubKey = extractMixPubKey(service) + if mixPubKey.isSome(): + debug "extracted mix public key from service", peerId = record.peerId + break + + if record.services.len > 0 and mixPubKey.isNone(): + debug "record has services but no valid mix key", + peerId = record.peerId, services = record.services.mapIt(it.id) + return none(RemotePeerInfo) + return some( + RemotePeerInfo.init( + record.peerId, + addrs = addrs, + protocols = protocols, + origin = PeerOrigin.Kademlia, + mixPubKey = mixPubKey, + ) + ) + +proc lookupMixPeers*( + wk: WakuKademlia +): Future[Result[int, string]] {.async: (raises: []).} = + ## Lookup mix peers via kademlia and add them to the peer store. + ## Returns the number of mix peers found and added. + if wk.protocol.isNil(): + return err("cannot lookup mix peers: kademlia not mounted") + + let mixService = ServiceInfo(id: MixProtocolID, data: @[]) + var records: seq[ExtendedPeerRecord] + try: + records = await wk.protocol.lookup(mixService) + except CatchableError: + return err("mix peer lookup failed: " & getCurrentExceptionMsg()) + + debug "mix peer lookup returned records", numRecords = records.len + + var added = 0 + for record in records: + let peerOpt = remotePeerInfoFrom(record) + if peerOpt.isNone(): + continue + + let peerInfo = peerOpt.get() + if peerInfo.mixPubKey.isNone(): + continue + + wk.peerManager.addPeer(peerInfo, PeerOrigin.Kademlia) + info "mix peer added via kademlia lookup", + peerId = $peerInfo.peerId, mixPubKey = byteutils.toHex(peerInfo.mixPubKey.get()) + added.inc() + + info "mix peer lookup complete", found = added + return ok(added) + +proc runDiscoveryLoop( + wk: WakuKademlia, interval: Duration, minMixPeers: int +) {.async: (raises: []).} = + info "extended kademlia discovery loop started", interval = interval + + try: + while true: + # Wait for node to be started + if not wk.isNodeStarted.isNil() and not wk.isNodeStarted(): + await sleepAsync(ExtendedKademliaDiscoveryStartupDelay) + continue + + var records: seq[ExtendedPeerRecord] + try: + records = await wk.protocol.randomRecords() + except CatchableError as e: + warn "extended kademlia discovery failed", error = e.msg + await sleepAsync(interval) + continue + + debug "received random records from kademlia", numRecords = records.len + + var added = 0 + for record in records: + let peerOpt = remotePeerInfoFrom(record) + if peerOpt.isNone(): + continue + + let peerInfo = peerOpt.get() + wk.peerManager.addPeer(peerInfo, PeerOrigin.Kademlia) + debug "peer added via extended kademlia discovery", + peerId = $peerInfo.peerId, + addresses = peerInfo.addrs.mapIt($it), + protocols = peerInfo.protocols, + hasMixPubKey = peerInfo.mixPubKey.isSome() + added.inc() + + if added > 0: + info "added peers from extended kademlia discovery", count = added + + # Targeted mix peer lookup when pool is low + if minMixPeers > 0 and not wk.getMixNodePoolSize.isNil() and + wk.getMixNodePoolSize() < minMixPeers: + debug "mix node pool below threshold, performing targeted lookup", + currentPoolSize = wk.getMixNodePoolSize(), threshold = minMixPeers + let found = (await wk.lookupMixPeers()).valueOr: + warn "targeted mix peer lookup failed", error = error + 0 + if found > 0: + info "found mix peers via targeted kademlia lookup", count = found + + await sleepAsync(interval) + except CancelledError as e: + debug "extended kademlia discovery loop cancelled", error = e.msg + except CatchableError as e: + error "extended kademlia discovery loop failed", error = e.msg + +proc start*( + wk: WakuKademlia, + interval: Duration = DefaultExtendedKademliaDiscoveryInterval, + minMixPeers: int = 0, +): Future[Result[void, string]] {.async: (raises: []).} = + if wk.running: + return err("already running") + + try: + await wk.protocol.start() + except CatchableError as e: + return err("failed to start kademlia discovery: " & e.msg) + + wk.discoveryLoop = wk.runDiscoveryLoop(interval, minMixPeers) + + info "kademlia discovery started" + return ok() + +proc stop*(wk: WakuKademlia) {.async: (raises: []).} = + if not wk.running: + return + + info "Stopping kademlia discovery" + + wk.running = false + + if not wk.discoveryLoop.isNil(): + await wk.discoveryLoop.cancelAndWait() + wk.discoveryLoop = nil + + if not wk.protocol.isNil(): + await wk.protocol.stop() + info "Successfully stopped kademlia discovery" diff --git a/waku/factory/conf_builder/conf_builder.nim b/waku/factory/conf_builder/conf_builder.nim index 37cea76fe..b8d0316c3 100644 --- a/waku/factory/conf_builder/conf_builder.nim +++ b/waku/factory/conf_builder/conf_builder.nim @@ -10,10 +10,12 @@ import ./metrics_server_conf_builder, ./rate_limit_conf_builder, ./rln_relay_conf_builder, - ./mix_conf_builder + ./mix_conf_builder, + ./kademlia_discovery_conf_builder export waku_conf_builder, filter_service_conf_builder, store_sync_conf_builder, store_service_conf_builder, rest_server_conf_builder, dns_discovery_conf_builder, discv5_conf_builder, web_socket_conf_builder, metrics_server_conf_builder, - rate_limit_conf_builder, rln_relay_conf_builder, mix_conf_builder + rate_limit_conf_builder, rln_relay_conf_builder, mix_conf_builder, + kademlia_discovery_conf_builder diff --git a/waku/factory/conf_builder/kademlia_discovery_conf_builder.nim b/waku/factory/conf_builder/kademlia_discovery_conf_builder.nim new file mode 100644 index 000000000..916d71be1 --- /dev/null +++ b/waku/factory/conf_builder/kademlia_discovery_conf_builder.nim @@ -0,0 +1,40 @@ +import chronicles, std/options, results +import libp2p/[peerid, multiaddress, peerinfo] +import waku/factory/waku_conf + +logScope: + topics = "waku conf builder kademlia discovery" + +####################################### +## Kademlia Discovery Config Builder ## +####################################### +type KademliaDiscoveryConfBuilder* = object + enabled*: bool + bootstrapNodes*: seq[string] + +proc init*(T: type KademliaDiscoveryConfBuilder): KademliaDiscoveryConfBuilder = + KademliaDiscoveryConfBuilder() + +proc withEnabled*(b: var KademliaDiscoveryConfBuilder, enabled: bool) = + b.enabled = enabled + +proc withBootstrapNodes*( + b: var KademliaDiscoveryConfBuilder, bootstrapNodes: seq[string] +) = + b.bootstrapNodes = bootstrapNodes + +proc build*( + b: KademliaDiscoveryConfBuilder +): Result[Option[KademliaDiscoveryConf], string] = + # Kademlia is enabled if explicitly enabled OR if bootstrap nodes are provided + let enabled = b.enabled or b.bootstrapNodes.len > 0 + if not enabled: + return ok(none(KademliaDiscoveryConf)) + + var parsedNodes: seq[(PeerId, seq[MultiAddress])] + for nodeStr in b.bootstrapNodes: + let (peerId, ma) = parseFullAddress(nodeStr).valueOr: + return err("Failed to parse kademlia bootstrap node: " & error) + parsedNodes.add((peerId, @[ma])) + + return ok(some(KademliaDiscoveryConf(bootstrapNodes: parsedNodes))) diff --git a/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index b952e711e..e51f02dbd 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -25,7 +25,8 @@ import ./metrics_server_conf_builder, ./rate_limit_conf_builder, ./rln_relay_conf_builder, - ./mix_conf_builder + ./mix_conf_builder, + ./kademlia_discovery_conf_builder logScope: topics = "waku conf builder" @@ -80,6 +81,7 @@ type WakuConfBuilder* = object mixConf*: MixConfBuilder webSocketConf*: WebSocketConfBuilder rateLimitConf*: RateLimitConfBuilder + kademliaDiscoveryConf*: KademliaDiscoveryConfBuilder # End conf builders relay: Option[bool] lightPush: Option[bool] @@ -140,6 +142,7 @@ proc init*(T: type WakuConfBuilder): WakuConfBuilder = storeServiceConf: StoreServiceConfBuilder.init(), webSocketConf: WebSocketConfBuilder.init(), rateLimitConf: RateLimitConfBuilder.init(), + kademliaDiscoveryConf: KademliaDiscoveryConfBuilder.init(), ) proc withNetworkConf*(b: var WakuConfBuilder, networkConf: NetworkConf) = @@ -506,6 +509,9 @@ proc build*( let rateLimit = builder.rateLimitConf.build().valueOr: return err("Rate limits Conf building failed: " & $error) + let kademliaDiscoveryConf = builder.kademliaDiscoveryConf.build().valueOr: + return err("Kademlia Discovery Conf building failed: " & $error) + # End - Build sub-configs let logLevel = @@ -628,6 +634,7 @@ proc build*( restServerConf: restServerConf, dnsDiscoveryConf: dnsDiscoveryConf, mixConf: mixConf, + kademliaDiscoveryConf: kademliaDiscoveryConf, # end confs nodeKey: nodeKey, clusterId: clusterId, diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index dc383e89d..2f82440f6 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -6,7 +6,8 @@ import libp2p/protocols/pubsub/gossipsub, libp2p/protocols/connectivity/relay/relay, libp2p/nameresolving/dnsresolver, - libp2p/crypto/crypto + libp2p/crypto/crypto, + libp2p/crypto/curve25519 import ./internal_config, @@ -32,6 +33,7 @@ import ../waku_store_legacy/common as legacy_common, ../waku_filter_v2, ../waku_peer_exchange, + ../discovery/waku_kademlia, ../node/peer_manager, ../node/peer_manager/peer_store/waku_peer_storage, ../node/peer_manager/peer_store/migrations as peer_store_sqlite_migrations, @@ -165,13 +167,36 @@ proc setupProtocols( #mount mix if conf.mixConf.isSome(): - ( - await node.mountMix( - conf.clusterId, conf.mixConf.get().mixKey, conf.mixConf.get().mixnodes - ) - ).isOkOr: + let mixConf = conf.mixConf.get() + (await node.mountMix(conf.clusterId, mixConf.mixKey, mixConf.mixnodes)).isOkOr: return err("failed to mount waku mix protocol: " & $error) + # Setup extended kademlia discovery + if conf.kademliaDiscoveryConf.isSome(): + let mixPubKey = + if conf.mixConf.isSome(): + some(conf.mixConf.get().mixPubKey) + else: + none(Curve25519Key) + + node.wakuKademlia = WakuKademlia.new( + node.switch, + ExtendedKademliaDiscoveryParams( + bootstrapNodes: conf.kademliaDiscoveryConf.get().bootstrapNodes, + mixPubKey: mixPubKey, + advertiseMix: conf.mixConf.isSome(), + ), + node.peerManager, + getMixNodePoolSize = proc(): int {.gcsafe, raises: [].} = + if node.wakuMix.isNil(): + 0 + else: + node.getMixNodePoolSize(), + isNodeStarted = proc(): bool {.gcsafe, raises: [].} = + node.started, + ).valueOr: + return err("failed to setup kademlia discovery: " & error) + if conf.storeServiceConf.isSome(): let storeServiceConf = conf.storeServiceConf.get() if storeServiceConf.supportV2: @@ -477,6 +502,11 @@ proc startNode*( if conf.relay: node.peerManager.start() + if not node.wakuKademlia.isNil(): + let minMixPeers = if conf.mixConf.isSome(): 4 else: 0 + (await node.wakuKademlia.start(minMixPeers = minMixPeers)).isOkOr: + return err("failed to start kademlia discovery: " & error) + return ok() proc setupNode*( diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index dd253129c..9803a53a9 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -203,6 +203,9 @@ proc new*( else: nil + # Set the extMultiAddrsOnly flag so the node knows not to replace explicit addresses + node.extMultiAddrsOnly = wakuConf.endpointConf.extMultiAddrsOnly + node.setupAppCallbacks(wakuConf, appCallbacks, healthMonitor).isOkOr: error "Failed setting up app callbacks", error = error return err("Failed setting up app callbacks: " & $error) diff --git a/waku/factory/waku_conf.nim b/waku/factory/waku_conf.nim index 899008221..01574d067 100644 --- a/waku/factory/waku_conf.nim +++ b/waku/factory/waku_conf.nim @@ -4,6 +4,7 @@ import libp2p/crypto/crypto, libp2p/multiaddress, libp2p/crypto/curve25519, + libp2p/peerid, secp256k1, results @@ -51,6 +52,10 @@ type MixConf* = ref object mixPubKey*: Curve25519Key mixnodes*: seq[MixNodePubInfo] +type KademliaDiscoveryConf* = object + bootstrapNodes*: seq[(PeerId, seq[MultiAddress])] + ## Bootstrap nodes for extended kademlia discovery. + type StoreServiceConf* {.requiresInit.} = object dbMigration*: bool dbURl*: string @@ -109,6 +114,7 @@ type WakuConf* {.requiresInit.} = ref object metricsServerConf*: Option[MetricsServerConf] webSocketConf*: Option[WebSocketConf] mixConf*: Option[MixConf] + kademliaDiscoveryConf*: Option[KademliaDiscoveryConf] portsShift*: uint16 dnsAddrsNameServers*: seq[IpAddress] diff --git a/waku/node/peer_manager/waku_peer_store.nim b/waku/node/peer_manager/waku_peer_store.nim index a03b5ae2e..93ac9ad2e 100644 --- a/waku/node/peer_manager/waku_peer_store.nim +++ b/waku/node/peer_manager/waku_peer_store.nim @@ -43,9 +43,6 @@ type # Keeps track of peer shards ShardBook* = ref object of PeerBook[seq[uint16]] - # Keeps track of Mix protocol public keys of peers - MixPubKeyBook* = ref object of PeerBook[Curve25519Key] - proc getPeer*(peerStore: PeerStore, peerId: PeerId): RemotePeerInfo = let addresses = if peerStore[LastSeenBook][peerId].isSome(): @@ -85,7 +82,7 @@ proc delete*(peerStore: PeerStore, peerId: PeerId) = proc peers*(peerStore: PeerStore): seq[RemotePeerInfo] = let allKeys = concat( - toSeq(peerStore[LastSeenBook].book.keys()), + toSeq(peerStore[LastSeenOutboundBook].book.keys()), toSeq(peerStore[AddressBook].book.keys()), toSeq(peerStore[ProtoBook].book.keys()), toSeq(peerStore[KeyBook].book.keys()), diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 53ce0349a..254387c32 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -66,6 +66,7 @@ import events/health_events, events/peer_events, ], + waku/discovery/waku_kademlia, ./net_config, ./peer_manager, ./health_monitor/health_status, @@ -141,6 +142,7 @@ type wakuRendezvous*: WakuRendezVous wakuRendezvousClient*: rendezvous_client.WakuRendezVousClient announcedAddresses*: seq[MultiAddress] + extMultiAddrsOnly*: bool # When true, skip automatic IP address replacement started*: bool # Indicates that node has started listening topicSubscriptionQueue*: AsyncEventQueue[SubscriptionEvent] rateLimitSettings*: ProtocolRateLimitSettings @@ -149,6 +151,8 @@ type edgeHealthEvent*: AsyncEvent edgeHealthLoop: Future[void] peerEventListener*: EventWakuPeerListener + kademliaDiscoveryLoop*: Future[void] + wakuKademlia*: WakuKademlia proc deduceRelayShard( node: WakuNode, @@ -303,7 +307,7 @@ proc mountAutoSharding*( return ok() proc getMixNodePoolSize*(node: WakuNode): int = - return node.wakuMix.getNodePoolSize() + return node.wakuMix.poolSize() proc mountMix*( node: WakuNode, @@ -455,6 +459,11 @@ proc isBindIpWithZeroPort(inputMultiAdd: MultiAddress): bool = return false proc updateAnnouncedAddrWithPrimaryIpAddr*(node: WakuNode): Result[void, string] = + # Skip automatic IP replacement if extMultiAddrsOnly is set + # This respects the user's explicitly configured announced addresses + if node.extMultiAddrsOnly: + return ok() + let peerInfo = node.switch.peerInfo var announcedStr = "" var listenStr = "" @@ -705,6 +714,9 @@ proc stop*(node: WakuNode) {.async.} = not node.wakuPeerExchangeClient.pxLoopHandle.isNil(): await node.wakuPeerExchangeClient.pxLoopHandle.cancelAndWait() + if not node.wakuKademlia.isNil(): + await node.wakuKademlia.stop() + if not node.wakuRendezvous.isNil(): await node.wakuRendezvous.stopWait() diff --git a/waku/waku_core/peers.nim b/waku/waku_core/peers.nim index 48c994403..51a8e1157 100644 --- a/waku/waku_core/peers.nim +++ b/waku/waku_core/peers.nim @@ -38,6 +38,7 @@ type Static PeerExchange Dns + Kademlia PeerDirection* = enum UnknownDirection diff --git a/waku/waku_mix/protocol.nim b/waku/waku_mix/protocol.nim index 366d5da91..2c972bef6 100644 --- a/waku/waku_mix/protocol.nim +++ b/waku/waku_mix/protocol.nim @@ -1,22 +1,23 @@ {.push raises: [].} -import chronicles, std/[options, tables, sequtils], chronos, results, metrics, strutils +import chronicles, std/options, chronos, results, metrics import libp2p/crypto/curve25519, + libp2p/crypto/crypto, libp2p/protocols/mix, libp2p/protocols/mix/mix_node, libp2p/protocols/mix/mix_protocol, libp2p/protocols/mix/mix_metrics, - libp2p/[multiaddress, multicodec, peerid], + libp2p/protocols/mix/delay_strategy, + libp2p/[multiaddress, peerid], eth/common/keys import - ../node/peer_manager, - ../waku_core, - ../waku_enr, - ../node/peer_manager/waku_peer_store, - ../common/nimchronos + waku/node/peer_manager, + waku/waku_core, + waku/waku_enr, + waku/node/peer_manager/waku_peer_store logScope: topics = "waku mix" @@ -27,7 +28,6 @@ type WakuMix* = ref object of MixProtocol peerManager*: PeerManager clusterId: uint16 - nodePoolLoopHandle: Future[void] pubKey*: Curve25519Key WakuMixResult*[T] = Result[T, string] @@ -36,106 +36,10 @@ type multiAddr*: string pubKey*: Curve25519Key -proc filterMixNodes(cluster: Option[uint16], peer: RemotePeerInfo): bool = - # Note that origin based(discv5) filtering is not done intentionally - # so that more mix nodes can be discovered. - if peer.mixPubKey.isNone(): - trace "remote peer has no mix Pub Key", peer = $peer - return false - - if cluster.isSome() and peer.enr.isSome() and - peer.enr.get().isClusterMismatched(cluster.get()): - trace "peer has mismatching cluster", peer = $peer - return false - - return true - -proc appendPeerIdToMultiaddr*(multiaddr: MultiAddress, peerId: PeerId): MultiAddress = - if multiaddr.contains(multiCodec("p2p")).get(): - return multiaddr - - var maddrStr = multiaddr.toString().valueOr: - error "Failed to convert multiaddress to string.", err = error - return multiaddr - maddrStr.add("/p2p/" & $peerId) - var cleanAddr = MultiAddress.init(maddrStr).valueOr: - error "Failed to convert string to multiaddress.", err = error - return multiaddr - return cleanAddr - -func getIPv4Multiaddr*(maddrs: seq[MultiAddress]): Option[MultiAddress] = - for multiaddr in maddrs: - trace "checking multiaddr", addr = $multiaddr - if multiaddr.contains(multiCodec("ip4")).get(): - trace "found ipv4 multiaddr", addr = $multiaddr - return some(multiaddr) - trace "no ipv4 multiaddr found" - return none(MultiAddress) - -proc populateMixNodePool*(mix: WakuMix) = - # populate only peers that i) are reachable ii) share cluster iii) support mix - let remotePeers = mix.peerManager.switch.peerStore.peers().filterIt( - filterMixNodes(some(mix.clusterId), it) - ) - var mixNodes = initTable[PeerId, MixPubInfo]() - - for i in 0 ..< min(remotePeers.len, 100): - let ipv4addr = getIPv4Multiaddr(remotePeers[i].addrs).valueOr: - trace "peer has no ipv4 address", peer = $remotePeers[i] - continue - let maddrWithPeerId = appendPeerIdToMultiaddr(ipv4addr, remotePeers[i].peerId) - trace "remote peer info", info = remotePeers[i] - - if remotePeers[i].mixPubKey.isNone(): - trace "peer has no mix Pub Key", remotePeerId = $remotePeers[i] - continue - - let peerMixPubKey = remotePeers[i].mixPubKey.get() - var peerPubKey: crypto.PublicKey - if not remotePeers[i].peerId.extractPublicKey(peerPubKey): - warn "Failed to extract public key from peerId, skipping node", - remotePeerId = remotePeers[i].peerId - continue - - if peerPubKey.scheme != PKScheme.Secp256k1: - warn "Peer public key is not Secp256k1, skipping node", - remotePeerId = remotePeers[i].peerId, scheme = peerPubKey.scheme - continue - - let mixNodePubInfo = MixPubInfo.init( - remotePeers[i].peerId, - ipv4addr, - intoCurve25519Key(peerMixPubKey), - peerPubKey.skkey, - ) - trace "adding mix node to pool", - remotePeerId = remotePeers[i].peerId, multiAddr = $ipv4addr - mixNodes[remotePeers[i].peerId] = mixNodePubInfo - - # set the mix node pool - mix.setNodePool(mixNodes) - mix_pool_size.set(len(mixNodes)) - trace "mix node pool updated", poolSize = mix.getNodePoolSize() - -# Once mix protocol starts to use info from PeerStore, then this can be removed. -proc startMixNodePoolMgr*(mix: WakuMix) {.async.} = - info "starting mix node pool manager" - # try more aggressively to populate the pool at startup - var attempts = 50 - # TODO: make initial pool size configurable - while mix.getNodePoolSize() < 100 and attempts > 0: - attempts -= 1 - mix.populateMixNodePool() - await sleepAsync(1.seconds) - - # TODO: make interval configurable - heartbeat "Updating mix node pool", 5.seconds: - mix.populateMixNodePool() - proc processBootNodes( - bootnodes: seq[MixNodePubInfo], peermgr: PeerManager -): Table[PeerId, MixPubInfo] = - var mixNodes = initTable[PeerId, MixPubInfo]() + bootnodes: seq[MixNodePubInfo], peermgr: PeerManager, mix: WakuMix +) = + var count = 0 for node in bootnodes: let pInfo = parsePeerInfo(node.multiAddr).valueOr: error "Failed to get peer id from multiaddress: ", @@ -156,14 +60,15 @@ proc processBootNodes( error "Failed to parse multiaddress", multiAddr = node.multiAddr, error = error continue - mixNodes[peerId] = MixPubInfo.init(peerId, multiAddr, node.pubKey, peerPubKey.skkey) + let mixPubInfo = MixPubInfo.init(peerId, multiAddr, node.pubKey, peerPubKey.skkey) + mix.nodePool.add(mixPubInfo) + count.inc() peermgr.addPeer( RemotePeerInfo.init(peerId, @[multiAddr], mixPubKey = some(node.pubKey)) ) - mix_pool_size.set(len(mixNodes)) - info "using mix bootstrap nodes ", bootNodes = mixNodes - return mixNodes + mix_pool_size.set(count) + info "using mix bootstrap nodes ", count = count proc new*( T: type WakuMix, @@ -183,22 +88,28 @@ proc new*( ) if bootnodes.len < minMixPoolSize: warn "publishing with mix won't work until atleast 3 mix nodes in node pool" - let initTable = processBootNodes(bootnodes, peermgr) - if len(initTable) < minMixPoolSize: - warn "publishing with mix won't work until atleast 3 mix nodes in node pool" var m = WakuMix(peerManager: peermgr, clusterId: clusterId, pubKey: mixPubKey) - procCall MixProtocol(m).init(localMixNodeInfo, initTable, peermgr.switch) + procCall MixProtocol(m).init( + localMixNodeInfo, + peermgr.switch, + delayStrategy = + ExponentialDelayStrategy.new(meanDelayMs = 50, rng = crypto.newRng()), + ) + + processBootNodes(bootnodes, peermgr, m) + + if m.nodePool.len < minMixPoolSize: + warn "publishing with mix won't work until atleast 3 mix nodes in node pool" return ok(m) +proc poolSize*(mix: WakuMix): int = + mix.nodePool.len + method start*(mix: WakuMix) = info "starting waku mix protocol" - mix.nodePoolLoopHandle = mix.startMixNodePoolMgr() method stop*(mix: WakuMix) {.async.} = - if mix.nodePoolLoopHandle.isNil(): - return - await mix.nodePoolLoopHandle.cancelAndWait() - mix.nodePoolLoopHandle = nil + discard # Mix Protocol From b23e722cb416fa104097b4c6e38a8b9bf5aa7e50 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:11:33 +0100 Subject: [PATCH 069/155] bump nim-metrics to latest master (#3730) --- vendor/nim-metrics | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-metrics b/vendor/nim-metrics index 11d0cddfb..9b9afee96 160000 --- a/vendor/nim-metrics +++ b/vendor/nim-metrics @@ -1 +1 @@ -Subproject commit 11d0cddfb0e711aa2a8c75d1892ae24a64c299fc +Subproject commit 9b9afee96357ad82dabf4563cf292f89b50423df From a7872d59d1991c7f726c7845d8ac536c6bdf7fb4 Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Fri, 20 Feb 2026 00:10:33 +0100 Subject: [PATCH 070/155] add POSTGRES support in nix --- nix/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/default.nix b/nix/default.nix index ca91d0e2f..7df58df60 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -61,6 +61,7 @@ in stdenv.mkDerivation { "QUICK_AND_DIRTY_NIMBLE=${if quickAndDirty then "1" else "0"}" "USE_SYSTEM_NIM=${if useSystemNim then "1" else "0"}" "LIBRLN_FILE=${zerokitRln}/lib/librln.${if abidir != null then "so" else "a"}" + "POSTGRES=1" ]; configurePhase = '' From c7e0cc0eaaa7c5cbfe8cf20a39fb4ca5d0ba4681 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:24:26 +0100 Subject: [PATCH 071/155] bump nim-metrics to proper tagged commit v0.2.1 (#3734) --- vendor/nim-metrics | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-metrics b/vendor/nim-metrics index 9b9afee96..a1296caf3 160000 --- a/vendor/nim-metrics +++ b/vendor/nim-metrics @@ -1 +1 @@ -Subproject commit 9b9afee96357ad82dabf4563cf292f89b50423df +Subproject commit a1296caf3ebb5f30f51a5feae7749a30df2824c2 From ba85873f03a1da6ab04287949849815fd97b7bfd Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:55:31 +0100 Subject: [PATCH 072/155] health status event support for liblogosdelivery (#3737) --- liblogosdelivery/logos_delivery_api/node_api.nim | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim index 6a0041857..5c2d7885f 100644 --- a/liblogosdelivery/logos_delivery_api/node_api.nim +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -4,7 +4,7 @@ import waku/factory/waku, waku/node/waku_node, waku/api/[api, api_conf, types], - waku/events/message_events, + waku/events/[message_events, health_events], ../declare_lib, ../json_event @@ -88,6 +88,15 @@ proc logosdelivery_start_node( chronicles.error "MessagePropagatedEvent.listen failed", err = $error return err("MessagePropagatedEvent.listen failed: " & $error) + let ConnectionStatusChangeListener = EventConnectionStatusChange.listen( + ctx.myLib[].brokerCtx, + proc(event: EventConnectionStatusChange) {.async: (raises: []).} = + callEventCallback(ctx, "onConnectionStatusChange"): + $newJsonEvent("connection_status_change", event), + ).valueOr: + chronicles.error "ConnectionStatusChange.listen failed", err = $error + return err("ConnectionStatusChange.listen failed: " & $error) + (await startWaku(addr ctx.myLib[])).isOkOr: let errMsg = $error chronicles.error "START_NODE failed", err = errMsg @@ -103,6 +112,7 @@ proc logosdelivery_stop_node( MessageErrorEvent.dropAllListeners(ctx.myLib[].brokerCtx) MessageSentEvent.dropAllListeners(ctx.myLib[].brokerCtx) MessagePropagatedEvent.dropAllListeners(ctx.myLib[].brokerCtx) + EventConnectionStatusChange.dropAllListeners(ctx.myLib[].brokerCtx) (await ctx.myLib[].stop()).isOkOr: let errMsg = $error From 51ec09c39df1ce8b442381300d91ebdd3bfe9e15 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 2 Mar 2026 14:52:36 -0300 Subject: [PATCH 073/155] Implement stateful SubscriptionService for Core mode (#3732) * SubscriptionManager tracks shard and content topic interest * RecvService emits MessageReceivedEvent on subscribed content topics * Route MAPI through old Kernel API relay unique-handler infra to avoid code duplication * Encode current gen-zero network policy: on Core node boot, subscribe to all pubsub topics (all shards) * Add test_api_subscriptions.nim (basic relay/core testing only) * Removed any MAPI Edge sub/unsub/receive support code that was there (will add in next PR) * Hook MessageSeenEvent to Kernel API bus * Fix MAPI vs Kernel API unique relay handler support * RecvService delegating topic subs to SubscriptionManager * RecvService emits MessageReceivedEvent (fully filtered) * Rename old SubscriptionManager to LegacySubscriptionManager --- tests/api/test_all.nim | 7 +- tests/api/test_api_subscription.nim | 399 ++++++++++++++++++ tests/waku_store/test_wakunode_store.nim | 6 + waku/api/api.nim | 15 +- waku/events/message_events.nim | 10 +- waku/factory/waku.nim | 7 +- .../delivery_service/delivery_service.nim | 18 +- .../recv_service/recv_service.nim | 100 ++--- .../send_service/send_service.nim | 14 +- .../delivery_service/subscription_manager.nim | 164 +++++++ .../delivery_service/subscription_service.nim | 64 --- waku/node/kernel_api/relay.nim | 100 +++-- waku/node/waku_node.nim | 2 + .../subscription/subscription_manager.nim | 16 +- 14 files changed, 735 insertions(+), 187 deletions(-) create mode 100644 tests/api/test_api_subscription.nim create mode 100644 waku/node/delivery_service/subscription_manager.nim delete mode 100644 waku/node/delivery_service/subscription_service.nim diff --git a/tests/api/test_all.nim b/tests/api/test_all.nim index 57f7f37f2..4617c8cdb 100644 --- a/tests/api/test_all.nim +++ b/tests/api/test_all.nim @@ -1,3 +1,8 @@ {.used.} -import ./test_entry_nodes, ./test_node_conf, ./test_api_send, ./test_api_health +import + ./test_entry_nodes, + ./test_node_conf, + ./test_api_send, + ./test_api_subscription, + ./test_api_health diff --git a/tests/api/test_api_subscription.nim b/tests/api/test_api_subscription.nim new file mode 100644 index 000000000..8983c2934 --- /dev/null +++ b/tests/api/test_api_subscription.nim @@ -0,0 +1,399 @@ +{.used.} + +import std/[strutils, net, options, sets] +import chronos, testutils/unittests, stew/byteutils +import libp2p/[peerid, peerinfo, multiaddress, crypto/crypto] +import ../testlib/[common, wakucore, wakunode, testasync] + +import + waku, + waku/[ + waku_node, + waku_core, + common/broker/broker_context, + events/message_events, + waku_relay/protocol, + ] +import waku/api/api_conf, waku/factory/waku_conf + +# TODO: Edge testing (after MAPI edge support is completed) + +const TestTimeout = chronos.seconds(10) +const NegativeTestTimeout = chronos.seconds(2) + +type ReceiveEventListenerManager = ref object + brokerCtx: BrokerContext + receivedListener: MessageReceivedEventListener + receivedEvent: AsyncEvent + receivedMessages: seq[WakuMessage] + targetCount: int + +proc newReceiveEventListenerManager( + brokerCtx: BrokerContext, expectedCount: int = 1 +): ReceiveEventListenerManager = + let manager = ReceiveEventListenerManager( + brokerCtx: brokerCtx, receivedMessages: @[], targetCount: expectedCount + ) + manager.receivedEvent = newAsyncEvent() + + manager.receivedListener = MessageReceivedEvent + .listen( + brokerCtx, + proc(event: MessageReceivedEvent) {.async: (raises: []).} = + manager.receivedMessages.add(event.message) + + if manager.receivedMessages.len >= manager.targetCount: + manager.receivedEvent.fire() + , + ) + .expect("Failed to listen to MessageReceivedEvent") + + return manager + +proc teardown(manager: ReceiveEventListenerManager) = + MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener) + +proc waitForEvents( + manager: ReceiveEventListenerManager, timeout: Duration +): Future[bool] {.async.} = + return await manager.receivedEvent.wait().withTimeout(timeout) + +type TestNetwork = ref object + publisher: WakuNode + subscriber: Waku + publisherPeerInfo: RemotePeerInfo + +proc createApiNodeConf( + mode: WakuMode = WakuMode.Core, numShards: uint16 = 1 +): NodeConfig = + let netConf = NetworkingConfig(listenIpv4: "0.0.0.0", p2pTcpPort: 0, discv5UdpPort: 0) + result = NodeConfig.init( + mode = mode, + protocolsConfig = ProtocolsConfig.init( + entryNodes = @[], + clusterId = 1, + autoShardingConfig = AutoShardingConfig(numShardsInCluster: numShards), + ), + networkingConfig = netConf, + p2pReliability = true, + ) + +proc setupSubscriberNode(conf: NodeConfig): Future[Waku] {.async.} = + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(conf)).expect("Failed to create subscriber node") + (await startWaku(addr node)).expect("Failed to start subscriber node") + return node + +proc setupNetwork( + numShards: uint16 = 1, mode: WakuMode = WakuMode.Core +): Future[TestNetwork] {.async.} = + var net = TestNetwork() + + lockNewGlobalBrokerContext: + net.publisher = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + net.publisher.mountMetadata(1, @[0'u16]).expect("Failed to mount metadata") + (await net.publisher.mountRelay()).expect("Failed to mount relay") + await net.publisher.mountLibp2pPing() + await net.publisher.start() + + net.publisherPeerInfo = net.publisher.peerInfo.toRemotePeerInfo() + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + discard + + # Subscribe the publisher to all shards to guarantee a GossipSub mesh with the subscriber. + # Currently, Core/Relay nodes auto-subscribe to all network shards on boot, but if + # that changes, this will be needed to cause the publisher to have shard interest + # for any shards the subscriber may want to use, which is required for waitForMesh to work. + for i in 0 ..< numShards.int: + let shard = PubsubTopic("/waku/2/rs/1/" & $i) + net.publisher.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub publisher" + ) + + net.subscriber = await setupSubscriberNode(createApiNodeConf(mode, numShards)) + + await net.subscriber.node.connectToNodes(@[net.publisherPeerInfo]) + + return net + +proc teardown(net: TestNetwork) {.async.} = + if not isNil(net.subscriber): + (await net.subscriber.stop()).expect("Failed to stop subscriber node") + net.subscriber = nil + + if not isNil(net.publisher): + await net.publisher.stop() + net.publisher = nil + +proc getRelayShard(node: WakuNode, contentTopic: ContentTopic): PubsubTopic = + let autoSharding = node.wakuAutoSharding.get() + let shardObj = autoSharding.getShard(contentTopic).expect("Failed to get shard") + return PubsubTopic($shardObj) + +proc waitForMesh(node: WakuNode, shard: PubsubTopic) {.async.} = + for _ in 0 ..< 50: + if node.wakuRelay.getNumPeersInMesh(shard).valueOr(0) > 0: + return + await sleepAsync(100.milliseconds) + raise newException(ValueError, "GossipSub Mesh failed to stabilize on " & shard) + +proc publishToMesh( + net: TestNetwork, contentTopic: ContentTopic, payload: seq[byte] +): Future[Result[int, string]] {.async.} = + let shard = net.subscriber.node.getRelayShard(contentTopic) + + await waitForMesh(net.publisher, shard) + + let msg = WakuMessage( + payload: payload, contentTopic: contentTopic, version: 0, timestamp: now() + ) + return await net.publisher.publish(some(shard), msg) + +suite "Messaging API, SubscriptionManager": + asyncTest "Subscription API, relay node auto subscribe and receive message": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/test-content/proto") + (await net.subscriber.subscribe(testTopic)).expect( + "subscriberNode failed to subscribe" + ) + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + discard (await net.publishToMesh(testTopic, "Hello, world!".toBytes())).expect( + "Publish failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + require eventManager.receivedMessages.len == 1 + check eventManager.receivedMessages[0].contentTopic == testTopic + + asyncTest "Subscription API, relay node ignores unsubscribed content topics on same shard": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let subbedTopic = ContentTopic("/waku/2/subbed-topic/proto") + let ignoredTopic = ContentTopic("/waku/2/ignored-topic/proto") + (await net.subscriber.subscribe(subbedTopic)).expect("failed to subscribe") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + discard (await net.publishToMesh(ignoredTopic, "Ghost Msg".toBytes())).expect( + "Publish failed" + ) + + check not await eventManager.waitForEvents(NegativeTestTimeout) + check eventManager.receivedMessages.len == 0 + + asyncTest "Subscription API, relay node unsubscribe stops message receipt": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/unsub-test/proto") + + (await net.subscriber.subscribe(testTopic)).expect("failed to subscribe") + net.subscriber.unsubscribe(testTopic).expect("failed to unsubscribe") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + discard (await net.publishToMesh(testTopic, "Should be dropped".toBytes())).expect( + "Publish failed" + ) + + check not await eventManager.waitForEvents(NegativeTestTimeout) + check eventManager.receivedMessages.len == 0 + + asyncTest "Subscription API, overlapping topics on same shard maintain correct isolation": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let topicA = ContentTopic("/waku/2/topic-a/proto") + let topicB = ContentTopic("/waku/2/topic-b/proto") + (await net.subscriber.subscribe(topicA)).expect("failed to sub A") + (await net.subscriber.subscribe(topicB)).expect("failed to sub B") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + net.subscriber.unsubscribe(topicA).expect("failed to unsub A") + + discard (await net.publishToMesh(topicA, "Dropped Message".toBytes())).expect( + "Publish A failed" + ) + discard + (await net.publishToMesh(topicB, "Kept Msg".toBytes())).expect("Publish B failed") + + require await eventManager.waitForEvents(TestTimeout) + require eventManager.receivedMessages.len == 1 + check eventManager.receivedMessages[0].contentTopic == topicB + + asyncTest "Subscription API, redundant subs tolerated and subs are removed": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let glitchTopic = ContentTopic("/waku/2/glitch/proto") + + (await net.subscriber.subscribe(glitchTopic)).expect("failed to sub") + (await net.subscriber.subscribe(glitchTopic)).expect("failed to double sub") + net.subscriber.unsubscribe(glitchTopic).expect("failed to unsub") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + discard (await net.publishToMesh(glitchTopic, "Ghost Msg".toBytes())).expect( + "Publish failed" + ) + + check not await eventManager.waitForEvents(NegativeTestTimeout) + check eventManager.receivedMessages.len == 0 + + asyncTest "Subscription API, resubscribe to an unsubscribed topic": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/resub-test/proto") + + # Subscribe + (await net.subscriber.subscribe(testTopic)).expect("Initial sub failed") + + var eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + discard + (await net.publishToMesh(testTopic, "Msg 1".toBytes())).expect("Pub 1 failed") + + require await eventManager.waitForEvents(TestTimeout) + eventManager.teardown() + + # Unsubscribe and verify teardown + net.subscriber.unsubscribe(testTopic).expect("Unsub failed") + eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + + discard + (await net.publishToMesh(testTopic, "Ghost".toBytes())).expect("Ghost pub failed") + + check not await eventManager.waitForEvents(NegativeTestTimeout) + eventManager.teardown() + + # Resubscribe + (await net.subscriber.subscribe(testTopic)).expect("Resub failed") + eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + + discard + (await net.publishToMesh(testTopic, "Msg 2".toBytes())).expect("Pub 2 failed") + + require await eventManager.waitForEvents(TestTimeout) + check eventManager.receivedMessages[0].payload == "Msg 2".toBytes() + + asyncTest "Subscription API, two content topics in different shards": + let net = await setupNetwork(8) + defer: + await net.teardown() + + var topicA = ContentTopic("/appA/2/shard-test-a/proto") + var topicB = ContentTopic("/appB/2/shard-test-b/proto") + + # generate two content topics that land in two different shards + var i = 0 + while net.subscriber.node.getRelayShard(topicA) == + net.subscriber.node.getRelayShard(topicB): + topicB = ContentTopic("/appB" & $i & "/2/shard-test-b/proto") + inc i + + (await net.subscriber.subscribe(topicA)).expect("failed to sub A") + (await net.subscriber.subscribe(topicB)).expect("failed to sub B") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 2) + defer: + eventManager.teardown() + + discard (await net.publishToMesh(topicA, "Msg on Shard A".toBytes())).expect( + "Publish A failed" + ) + discard (await net.publishToMesh(topicB, "Msg on Shard B".toBytes())).expect( + "Publish B failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + require eventManager.receivedMessages.len == 2 + + asyncTest "Subscription API, many content topics in many shards": + let net = await setupNetwork(8) + defer: + await net.teardown() + + var allTopics: seq[ContentTopic] + for i in 0 ..< 100: + allTopics.add(ContentTopic("/stress-app-" & $i & "/2/state-test/proto")) + + var activeSubs: seq[ContentTopic] + + proc verifyNetworkState(expected: seq[ContentTopic]) {.async.} = + let eventManager = + newReceiveEventListenerManager(net.subscriber.brokerCtx, expected.len) + + for topic in allTopics: + discard (await net.publishToMesh(topic, "Stress Payload".toBytes())).expect( + "publish failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + + # here we just give a chance for any messages that we don't expect to arrive + await sleepAsync(1.seconds) + eventManager.teardown() + + # weak check (but catches most bugs) + require eventManager.receivedMessages.len == expected.len + + # strict expected receipt test + var receivedTopics = initHashSet[ContentTopic]() + for msg in eventManager.receivedMessages: + receivedTopics.incl(msg.contentTopic) + var expectedTopics = initHashSet[ContentTopic]() + for t in expected: + expectedTopics.incl(t) + + check receivedTopics == expectedTopics + + # subscribe to all content topics we generated + for t in allTopics: + (await net.subscriber.subscribe(t)).expect("sub failed") + activeSubs.add(t) + + await verifyNetworkState(activeSubs) + + # unsubscribe from some content topics + for i in 0 ..< 50: + let t = allTopics[i] + net.subscriber.unsubscribe(t).expect("unsub failed") + + let idx = activeSubs.find(t) + if idx >= 0: + activeSubs.del(idx) + + await verifyNetworkState(activeSubs) + + # re-subscribe to some content topics + for i in 0 ..< 25: + let t = allTopics[i] + (await net.subscriber.subscribe(t)).expect("resub failed") + activeSubs.add(t) + + await verifyNetworkState(activeSubs) diff --git a/tests/waku_store/test_wakunode_store.nim b/tests/waku_store/test_wakunode_store.nim index e30854906..9239435af 100644 --- a/tests/waku_store/test_wakunode_store.nim +++ b/tests/waku_store/test_wakunode_store.nim @@ -374,6 +374,12 @@ procSuite "WakuNode - Store": waitFor allFutures(client.stop(), server.stop()) test "Store protocol queries overrun request rate limitation": + when defined(macosx): + # on macos CI, this test is resulting a code 200 (OK) instead of a 429 error + # means the runner is somehow too slow to cause a request limit failure + skip() + return + ## Setup let serverKey = generateSecp256k1Key() diff --git a/waku/api/api.nim b/waku/api/api.nim index 3493513a3..ba6f83b78 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -3,7 +3,7 @@ import chronicles, chronos, results, std/strutils import waku/factory/waku import waku/[requests/health_requests, waku_core, waku_node] import waku/node/delivery_service/send_service -import waku/node/delivery_service/subscription_service +import waku/node/delivery_service/subscription_manager import libp2p/peerid import ./[api_conf, types] @@ -36,18 +36,27 @@ proc subscribe*( ): Future[Result[void, string]] {.async.} = ?checkApiAvailability(w) - return w.deliveryService.subscriptionService.subscribe(contentTopic) + return w.deliveryService.subscriptionManager.subscribe(contentTopic) proc unsubscribe*(w: Waku, contentTopic: ContentTopic): Result[void, string] = ?checkApiAvailability(w) - return w.deliveryService.subscriptionService.unsubscribe(contentTopic) + return w.deliveryService.subscriptionManager.unsubscribe(contentTopic) proc send*( w: Waku, envelope: MessageEnvelope ): Future[Result[RequestId, string]] {.async.} = ?checkApiAvailability(w) + let isSubbed = w.deliveryService.subscriptionManager + .isSubscribed(envelope.contentTopic) + .valueOr(false) + if not isSubbed: + info "Auto-subscribing to topic on send", contentTopic = envelope.contentTopic + w.deliveryService.subscriptionManager.subscribe(envelope.contentTopic).isOkOr: + warn "Failed to auto-subscribe", error = error + return err("Failed to auto-subscribe before sending: " & error) + let requestId = RequestId.new(w.rng) let deliveryTask = DeliveryTask.new(requestId, envelope, w.brokerCtx).valueOr: diff --git a/waku/events/message_events.nim b/waku/events/message_events.nim index cf3dac9b7..677a4a433 100644 --- a/waku/events/message_events.nim +++ b/waku/events/message_events.nim @@ -1,6 +1,4 @@ -import waku/common/broker/event_broker -import waku/api/types -import waku/waku_core/message +import waku/[api/types, waku_core/message, waku_core/topics, common/broker/event_broker] export types @@ -28,3 +26,9 @@ EventBroker: type MessageReceivedEvent* = object messageHash*: string message*: WakuMessage + +EventBroker: + # Internal event emitted when a message arrives from the network via any protocol + type MessageSeenEvent* = object + topic*: PubsubTopic + message*: WakuMessage diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 9803a53a9..ff0ab0568 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -35,6 +35,7 @@ import node/health_monitor, node/waku_metrics, node/delivery_service/delivery_service, + node/delivery_service/subscription_manager, rest_api/message_cache, rest_api/endpoint/server, rest_api/endpoint/builder as rest_server_builder, @@ -453,7 +454,7 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: ).isOkOr: error "Failed to set RequestProtocolHealth provider", error = error - ## Setup RequestHealthReport provider (The lost child) + ## Setup RequestHealthReport provider RequestHealthReport.setProvider( globalBrokerContext(), @@ -514,6 +515,10 @@ proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} = if not waku.wakuDiscv5.isNil(): await waku.wakuDiscv5.stop() + if not waku.deliveryService.isNil(): + await waku.deliveryService.stopDeliveryService() + waku.deliveryService = nil + if not waku.node.isNil(): await waku.node.stop() diff --git a/waku/node/delivery_service/delivery_service.nim b/waku/node/delivery_service/delivery_service.nim index 8106cba9f..258c01e95 100644 --- a/waku/node/delivery_service/delivery_service.nim +++ b/waku/node/delivery_service/delivery_service.nim @@ -5,7 +5,7 @@ import chronos import ./recv_service, ./send_service, - ./subscription_service, + ./subscription_manager, waku/[ waku_core, waku_node, @@ -18,29 +18,31 @@ import type DeliveryService* = ref object sendService*: SendService recvService: RecvService - subscriptionService*: SubscriptionService + subscriptionManager*: SubscriptionManager proc new*( T: type DeliveryService, useP2PReliability: bool, w: WakuNode ): Result[T, string] = ## storeClient is needed to give store visitility to DeliveryService ## wakuRelay and wakuLightpushClient are needed to give a mechanism to SendService to re-publish - let subscriptionService = SubscriptionService.new(w) - let sendService = ?SendService.new(useP2PReliability, w, subscriptionService) - let recvService = RecvService.new(w, subscriptionService) + let subscriptionManager = SubscriptionManager.new(w) + let sendService = ?SendService.new(useP2PReliability, w, subscriptionManager) + let recvService = RecvService.new(w, subscriptionManager) return ok( DeliveryService( sendService: sendService, recvService: recvService, - subscriptionService: subscriptionService, + subscriptionManager: subscriptionManager, ) ) proc startDeliveryService*(self: DeliveryService) = - self.sendService.startSendService() + self.subscriptionManager.startSubscriptionManager() self.recvService.startRecvService() + self.sendService.startSendService() proc stopDeliveryService*(self: DeliveryService) {.async.} = - self.sendService.stopSendService() + await self.sendService.stopSendService() await self.recvService.stopRecvService() + await self.subscriptionManager.stopSubscriptionManager() diff --git a/waku/node/delivery_service/recv_service/recv_service.nim b/waku/node/delivery_service/recv_service/recv_service.nim index 12780033a..0eba2c450 100644 --- a/waku/node/delivery_service/recv_service/recv_service.nim +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -2,9 +2,9 @@ ## receive and is backed by store-v3 requests to get an additional degree of certainty ## -import std/[tables, sequtils, options] +import std/[tables, sequtils, options, sets] import chronos, chronicles, libp2p/utility -import ../[subscription_service] +import ../[subscription_manager] import waku/[ waku_core, @@ -13,6 +13,7 @@ import waku_filter_v2/client, waku_core/topics, events/delivery_events, + events/message_events, waku_node, common/broker/broker_context, ] @@ -35,14 +36,9 @@ type RecvMessage = object type RecvService* = ref object of RootObj brokerCtx: BrokerContext - topicsInterest: Table[PubsubTopic, seq[ContentTopic]] - ## Tracks message verification requests and when was the last time a - ## pubsub topic was verified for missing messages - ## The key contains pubsub-topics node: WakuNode - onSubscribeListener: OnFilterSubscribeEventListener - onUnsubscribeListener: OnFilterUnsubscribeEventListener - subscriptionService: SubscriptionService + seenMsgListener: MessageSeenEventListener + subscriptionManager: SubscriptionManager recentReceivedMsgs: seq[RecvMessage] @@ -95,20 +91,20 @@ proc msgChecker(self: RecvService) {.async.} = self.endTimeToCheck = getNowInNanosecondTime() var msgHashesInStore = newSeq[WakuMessageHash](0) - for pubsubTopic, cTopics in self.topicsInterest.pairs: + for sub in self.subscriptionManager.getActiveSubscriptions(): let storeResp: StoreQueryResponse = ( await self.node.wakuStoreClient.queryToAny( StoreQueryRequest( includeData: false, - pubsubTopic: some(PubsubTopic(pubsubTopic)), - contentTopics: cTopics, + pubsubTopic: some(PubsubTopic(sub.pubsubTopic)), + contentTopics: sub.contentTopics, startTime: some(self.startTimeToCheck - DelayExtra.nanos), endTime: some(self.endTimeToCheck + DelayExtra.nanos), ) ) ).valueOr: error "msgChecker failed to get remote msgHashes", - pubsubTopic, cTopics, error = $error + pubsubTopic = sub.pubsubTopic, cTopics = sub.contentTopics, error = $error continue msgHashesInStore.add(storeResp.messages.mapIt(it.messageHash)) @@ -133,31 +129,20 @@ proc msgChecker(self: RecvService) {.async.} = ## update next check times self.startTimeToCheck = self.endTimeToCheck -proc onSubscribe( - self: RecvService, pubsubTopic: string, contentTopics: seq[string] -) {.gcsafe, raises: [].} = - info "onSubscribe", pubsubTopic, contentTopics - self.topicsInterest.withValue(pubsubTopic, contentTopicsOfInterest): - contentTopicsOfInterest[].add(contentTopics) - do: - self.topicsInterest[pubsubTopic] = contentTopics +proc processIncomingMessageOfInterest( + self: RecvService, pubsubTopic: string, message: WakuMessage +) = + ## Resolve an incoming network message that was already filtered by topic. + ## Deduplicate (by hash), store (saves in recently-seen messages) and emit + ## the MAPI MessageReceivedEvent for every unique incoming message. -proc onUnsubscribe( - self: RecvService, pubsubTopic: string, contentTopics: seq[string] -) {.gcsafe, raises: [].} = - info "onUnsubscribe", pubsubTopic, contentTopics + let msgHash = computeMessageHash(pubsubTopic, message) + if not self.recentReceivedMsgs.anyIt(it.msgHash == msgHash): + let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp) + self.recentReceivedMsgs.add(rxMsg) + MessageReceivedEvent.emit(self.brokerCtx, msgHash.to0xHex(), message) - self.topicsInterest.withValue(pubsubTopic, contentTopicsOfInterest): - let remainingCTopics = - contentTopicsOfInterest[].filterIt(not contentTopics.contains(it)) - contentTopicsOfInterest[] = remainingCTopics - - if remainingCTopics.len == 0: - self.topicsInterest.del(pubsubTopic) - do: - error "onUnsubscribe unsubscribing from wrong topic", pubsubTopic, contentTopics - -proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionService): T = +proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T = ## The storeClient will help to acquire any possible missed messages let now = getNowInNanosecondTime() @@ -165,22 +150,13 @@ proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionService): T = node: node, startTimeToCheck: now, brokerCtx: node.brokerCtx, - subscriptionService: s, - topicsInterest: initTable[PubsubTopic, seq[ContentTopic]](), + subscriptionManager: s, recentReceivedMsgs: @[], ) - if not node.wakuFilterClient.isNil(): - let filterPushHandler = proc( - pubsubTopic: PubsubTopic, message: WakuMessage - ) {.async, closure.} = - ## Captures all the messages received through filter - - let msgHash = computeMessageHash(pubSubTopic, message) - let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp) - recvService.recentReceivedMsgs.add(rxMsg) - - node.wakuFilterClient.registerPushHandler(filterPushHandler) + # TODO: For MAPI Edge support, either call node.wakuFilterClient.registerPushHandler + # so that the RecvService listens to incoming filter messages, + # or have the filter client emit MessageSeenEvent. return recvService @@ -194,26 +170,26 @@ proc startRecvService*(self: RecvService) = self.msgCheckerHandler = self.msgChecker() self.msgPrunerHandler = self.loopPruneOldMessages() - self.onSubscribeListener = OnFilterSubscribeEvent.listen( + self.seenMsgListener = MessageSeenEvent.listen( self.brokerCtx, - proc(subsEv: OnFilterSubscribeEvent) {.async: (raises: []).} = - self.onSubscribe(subsEv.pubsubTopic, subsEv.contentTopics), - ).valueOr: - error "Failed to set OnFilterSubscribeEvent listener", error = error - quit(QuitFailure) + proc(event: MessageSeenEvent) {.async: (raises: []).} = + if not self.subscriptionManager.isSubscribed( + event.topic, event.message.contentTopic + ): + trace "skipping message as I am not subscribed", + shard = event.topic, contenttopic = event.message.contentTopic + return - self.onUnsubscribeListener = OnFilterUnsubscribeEvent.listen( - self.brokerCtx, - proc(subsEv: OnFilterUnsubscribeEvent) {.async: (raises: []).} = - self.onUnsubscribe(subsEv.pubsubTopic, subsEv.contentTopics), + self.processIncomingMessageOfInterest(event.topic, event.message), ).valueOr: - error "Failed to set OnFilterUnsubscribeEvent listener", error = error + error "Failed to set MessageSeenEvent listener", error = error quit(QuitFailure) proc stopRecvService*(self: RecvService) {.async.} = - OnFilterSubscribeEvent.dropListener(self.brokerCtx, self.onSubscribeListener) - OnFilterUnsubscribeEvent.dropListener(self.brokerCtx, self.onUnsubscribeListener) + MessageSeenEvent.dropListener(self.brokerCtx, self.seenMsgListener) if not self.msgCheckerHandler.isNil(): await self.msgCheckerHandler.cancelAndWait() + self.msgCheckerHandler = nil if not self.msgPrunerHandler.isNil(): await self.msgPrunerHandler.cancelAndWait() + self.msgPrunerHandler = nil diff --git a/waku/node/delivery_service/send_service/send_service.nim b/waku/node/delivery_service/send_service/send_service.nim index a41d07786..a3c44bc0c 100644 --- a/waku/node/delivery_service/send_service/send_service.nim +++ b/waku/node/delivery_service/send_service/send_service.nim @@ -5,7 +5,7 @@ import std/[sequtils, tables, options] import chronos, chronicles, libp2p/utility import ./[send_processor, relay_processor, lightpush_processor, delivery_task], - ../[subscription_service], + ../[subscription_manager], waku/[ waku_core, node/waku_node, @@ -58,7 +58,7 @@ type SendService* = ref object of RootObj node: WakuNode checkStoreForMessages: bool - subscriptionService: SubscriptionService + subscriptionManager: SubscriptionManager proc setupSendProcessorChain( peerManager: PeerManager, @@ -99,7 +99,7 @@ proc new*( T: typedesc[SendService], preferP2PReliability: bool, w: WakuNode, - s: SubscriptionService, + s: SubscriptionManager, ): Result[T, string] = if w.wakuRelay.isNil() and w.wakuLightpushClient.isNil(): return err( @@ -120,7 +120,7 @@ proc new*( sendProcessor: sendProcessorChain, node: w, checkStoreForMessages: checkStoreForMessages, - subscriptionService: s, + subscriptionManager: s, ) return ok(sendService) @@ -250,9 +250,9 @@ proc serviceLoop(self: SendService) {.async.} = proc startSendService*(self: SendService) = self.serviceLoopHandle = self.serviceLoop() -proc stopSendService*(self: SendService) = +proc stopSendService*(self: SendService) {.async.} = if not self.serviceLoopHandle.isNil(): - discard self.serviceLoopHandle.cancelAndWait() + await self.serviceLoopHandle.cancelAndWait() proc send*(self: SendService, task: DeliveryTask) {.async.} = assert(not task.isNil(), "task for send must not be nil") @@ -260,7 +260,7 @@ proc send*(self: SendService, task: DeliveryTask) {.async.} = info "SendService.send: processing delivery task", requestId = task.requestId, msgHash = task.msgHash.to0xHex() - self.subscriptionService.subscribe(task.msg.contentTopic).isOkOr: + self.subscriptionManager.subscribe(task.msg.contentTopic).isOkOr: error "SendService.send: failed to subscribe to content topic", contentTopic = task.msg.contentTopic, error = error diff --git a/waku/node/delivery_service/subscription_manager.nim b/waku/node/delivery_service/subscription_manager.nim new file mode 100644 index 000000000..22df47413 --- /dev/null +++ b/waku/node/delivery_service/subscription_manager.nim @@ -0,0 +1,164 @@ +import std/[sets, tables, options, strutils], chronos, chronicles, results +import + waku/[ + waku_core, + waku_core/topics, + waku_core/topics/sharding, + waku_node, + waku_relay, + common/broker/broker_context, + events/delivery_events, + ] + +type SubscriptionManager* = ref object of RootObj + node: WakuNode + contentTopicSubs: Table[PubsubTopic, HashSet[ContentTopic]] + ## Map of Shard to ContentTopic needed because e.g. WakuRelay is PubsubTopic only. + ## A present key with an empty HashSet value means pubsubtopic already subscribed + ## (via subscribePubsubTopics()) but there's no specific content topic interest yet. + +proc new*(T: typedesc[SubscriptionManager], node: WakuNode): T = + SubscriptionManager( + node: node, contentTopicSubs: initTable[PubsubTopic, HashSet[ContentTopic]]() + ) + +proc addContentTopicInterest( + self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic +): Result[void, string] = + if not self.contentTopicSubs.hasKey(shard): + self.contentTopicSubs[shard] = initHashSet[ContentTopic]() + + self.contentTopicSubs.withValue(shard, cTopics): + if not cTopics[].contains(topic): + cTopics[].incl(topic) + + # TODO: Call a "subscribe(shard, topic)" on filter client here, + # so the filter client can know that subscriptions changed. + + return ok() + +proc removeContentTopicInterest( + self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic +): Result[void, string] = + self.contentTopicSubs.withValue(shard, cTopics): + if cTopics[].contains(topic): + cTopics[].excl(topic) + + if cTopics[].len == 0 and isNil(self.node.wakuRelay): + self.contentTopicSubs.del(shard) # We're done with cTopics here + + # TODO: Call a "unsubscribe(shard, topic)" on filter client here, + # so the filter client can know that subscriptions changed. + + return ok() + +proc subscribePubsubTopics( + self: SubscriptionManager, shards: seq[PubsubTopic] +): Result[void, string] = + if isNil(self.node.wakuRelay): + return err("subscribePubsubTopics requires a Relay") + + var errors: seq[string] = @[] + + for shard in shards: + if not self.contentTopicSubs.hasKey(shard): + self.node.subscribe((kind: PubsubSub, topic: shard), nil).isOkOr: + errors.add("shard " & shard & ": " & error) + continue + + self.contentTopicSubs[shard] = initHashSet[ContentTopic]() + + if errors.len > 0: + return err("subscribeShard errors: " & errors.join("; ")) + + return ok() + +proc startSubscriptionManager*(self: SubscriptionManager) = + if isNil(self.node.wakuRelay): + return + + if self.node.wakuAutoSharding.isSome(): + # Subscribe relay to all shards in autosharding. + let autoSharding = self.node.wakuAutoSharding.get() + let clusterId = autoSharding.clusterId + let numShards = autoSharding.shardCountGenZero + + if numShards > 0: + var clusterPubsubTopics = newSeqOfCap[PubsubTopic](numShards) + + for i in 0 ..< numShards: + let shardObj = RelayShard(clusterId: clusterId, shardId: uint16(i)) + clusterPubsubTopics.add(PubsubTopic($shardObj)) + + self.subscribePubsubTopics(clusterPubsubTopics).isOkOr: + error "Failed to auto-subscribe Relay to cluster shards: ", error = error + else: + info "SubscriptionManager has no AutoSharding configured; skipping auto-subscribe." + +proc stopSubscriptionManager*(self: SubscriptionManager) {.async.} = + discard + +proc getActiveSubscriptions*( + self: SubscriptionManager +): seq[tuple[pubsubTopic: string, contentTopics: seq[ContentTopic]]] = + var activeSubs: seq[tuple[pubsubTopic: string, contentTopics: seq[ContentTopic]]] = + @[] + + for pubsub, cTopicSet in self.contentTopicSubs.pairs: + if cTopicSet.len > 0: + var cTopicSeq = newSeqOfCap[ContentTopic](cTopicSet.len) + for t in cTopicSet: + cTopicSeq.add(t) + activeSubs.add((pubsub, cTopicSeq)) + + return activeSubs + +proc getShardForContentTopic( + self: SubscriptionManager, topic: ContentTopic +): Result[PubsubTopic, string] = + if self.node.wakuAutoSharding.isSome(): + let shardObj = ?self.node.wakuAutoSharding.get().getShard(topic) + return ok($shardObj) + + return err("SubscriptionManager requires AutoSharding") + +proc isSubscribed*( + self: SubscriptionManager, topic: ContentTopic +): Result[bool, string] = + let shard = ?self.getShardForContentTopic(topic) + return ok( + self.contentTopicSubs.hasKey(shard) and self.contentTopicSubs[shard].contains(topic) + ) + +proc isSubscribed*( + self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic +): bool {.raises: [].} = + self.contentTopicSubs.withValue(shard, cTopics): + return cTopics[].contains(contentTopic) + return false + +proc subscribe*(self: SubscriptionManager, topic: ContentTopic): Result[void, string] = + if isNil(self.node.wakuRelay) and isNil(self.node.wakuFilterClient): + return err("SubscriptionManager requires either Relay or Filter Client.") + + let shard = ?self.getShardForContentTopic(topic) + + if not isNil(self.node.wakuRelay) and not self.contentTopicSubs.hasKey(shard): + ?self.subscribePubsubTopics(@[shard]) + + ?self.addContentTopicInterest(shard, topic) + + return ok() + +proc unsubscribe*( + self: SubscriptionManager, topic: ContentTopic +): Result[void, string] = + if isNil(self.node.wakuRelay) and isNil(self.node.wakuFilterClient): + return err("SubscriptionManager requires either Relay or Filter Client.") + + let shard = ?self.getShardForContentTopic(topic) + + if self.isSubscribed(shard, topic): + ?self.removeContentTopicInterest(shard, topic) + + return ok() diff --git a/waku/node/delivery_service/subscription_service.nim b/waku/node/delivery_service/subscription_service.nim deleted file mode 100644 index 78763161b..000000000 --- a/waku/node/delivery_service/subscription_service.nim +++ /dev/null @@ -1,64 +0,0 @@ -import chronos, chronicles -import - waku/[ - waku_core, - waku_core/topics, - events/message_events, - waku_node, - common/broker/broker_context, - ] - -type SubscriptionService* = ref object of RootObj - brokerCtx: BrokerContext - node: WakuNode - -proc new*(T: typedesc[SubscriptionService], node: WakuNode): T = - ## The storeClient will help to acquire any possible missed messages - - return SubscriptionService(brokerCtx: node.brokerCtx, node: node) - -proc isSubscribed*( - self: SubscriptionService, topic: ContentTopic -): Result[bool, string] = - var isSubscribed = false - if self.node.wakuRelay.isNil() == false: - return self.node.isSubscribed((kind: ContentSub, topic: topic)) - - # TODO: Add support for edge mode with Filter subscription management - return ok(isSubscribed) - -#TODO: later PR may consider to refactor or place this function elsewhere -# The only important part is that it emits MessageReceivedEvent -proc getReceiveHandler(self: SubscriptionService): WakuRelayHandler = - return proc(topic: PubsubTopic, msg: WakuMessage): Future[void] {.async, gcsafe.} = - let msgHash = computeMessageHash(topic, msg).to0xHex() - info "API received message", - pubsubTopic = topic, contentTopic = msg.contentTopic, msgHash = msgHash - - MessageReceivedEvent.emit(self.brokerCtx, msgHash, msg) - -proc subscribe*(self: SubscriptionService, topic: ContentTopic): Result[void, string] = - let isSubscribed = self.isSubscribed(topic).valueOr: - error "Failed to check subscription status: ", error = error - return err("Failed to check subscription status: " & error) - - if isSubscribed == false: - if self.node.wakuRelay.isNil() == false: - self.node.subscribe((kind: ContentSub, topic: topic), self.getReceiveHandler()).isOkOr: - error "Failed to subscribe: ", error = error - return err("Failed to subscribe: " & error) - - # TODO: Add support for edge mode with Filter subscription management - - return ok() - -proc unsubscribe*( - self: SubscriptionService, topic: ContentTopic -): Result[void, string] = - if self.node.wakuRelay.isNil() == false: - self.node.unsubscribe((kind: ContentSub, topic: topic)).isOkOr: - error "Failed to unsubscribe: ", error = error - return err("Failed to unsubscribe: " & error) - - # TODO: Add support for edge mode with Filter subscription management - return ok() diff --git a/waku/node/kernel_api/relay.nim b/waku/node/kernel_api/relay.nim index a0a128449..ec4d05ddd 100644 --- a/waku/node/kernel_api/relay.nim +++ b/waku/node/kernel_api/relay.nim @@ -19,16 +19,20 @@ import libp2p/utility import - ../waku_node, - ../../waku_relay, - ../../waku_core, - ../../waku_core/topics/sharding, - ../../waku_filter_v2, - ../../waku_archive_legacy, - ../../waku_archive, - ../../waku_store_sync, - ../peer_manager, - ../../waku_rln_relay + waku/[ + waku_relay, + waku_core, + waku_core/topics/sharding, + waku_filter_v2, + waku_archive_legacy, + waku_archive, + waku_store_sync, + waku_rln_relay, + node/waku_node, + node/peer_manager, + common/broker/broker_context, + events/message_events, + ] export waku_relay.WakuRelayHandler @@ -44,14 +48,25 @@ logScope: ## Waku relay proc registerRelayHandler( - node: WakuNode, topic: PubsubTopic, appHandler: WakuRelayHandler -) = + node: WakuNode, topic: PubsubTopic, appHandler: WakuRelayHandler = nil +): bool = ## Registers the only handler for the given topic. ## Notice that this handler internally calls other handlers, such as filter, ## archive, etc, plus the handler provided by the application. + ## Returns `true` if a mesh subscription was created or `false` if the relay + ## was already subscribed to the topic. - if node.wakuRelay.isSubscribed(topic): - return + let alreadySubscribed = node.wakuRelay.isSubscribed(topic) + + if not appHandler.isNil(): + if not alreadySubscribed or not node.legacyAppHandlers.hasKey(topic): + node.legacyAppHandlers[topic] = appHandler + else: + debug "Legacy appHandler already exists for active PubsubTopic, ignoring new handler", + topic = topic + + if alreadySubscribed: + return false proc traceHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = let msgSizeKB = msg.payload.len / 1000 @@ -82,6 +97,9 @@ proc registerRelayHandler( node.wakuStoreReconciliation.messageIngress(topic, msg) + proc internalHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + MessageSeenEvent.emit(node.brokerCtx, topic, msg) + let uniqueTopicHandler = proc( topic: PubsubTopic, msg: WakuMessage ): Future[void] {.async, gcsafe.} = @@ -89,7 +107,15 @@ proc registerRelayHandler( await filterHandler(topic, msg) await archiveHandler(topic, msg) await syncHandler(topic, msg) - await appHandler(topic, msg) + await internalHandler(topic, msg) + + # Call the legacy (kernel API) app handler if it exists. + # Normally, hasKey is false and the MessageSeenEvent bus (new API) is used instead. + # But we need to support legacy behavior (kernel API use), hence this. + # NOTE: We can delete `legacyAppHandlers` if instead we refactor WakuRelay to support multiple + # PubsubTopic handlers, since that's actually supported by libp2p PubSub (bigger refactor...) + if node.legacyAppHandlers.hasKey(topic) and not node.legacyAppHandlers[topic].isNil(): + await node.legacyAppHandlers[topic](topic, msg) node.wakuRelay.subscribe(topic, uniqueTopicHandler) @@ -115,8 +141,11 @@ proc subscribe*( ): Result[void, string] = ## Subscribes to a PubSub or Content topic. Triggers handler when receiving messages on ## this topic. WakuRelayHandler is a method that takes a topic and a Waku message. + ## If `handler` is nil, the API call will subscribe to the topic in the relay mesh + ## but no app handler will be registered at this time (it can be registered later with + ## another call to this proc for the same gossipsub topic). - if node.wakuRelay.isNil(): + if isNil(node.wakuRelay): error "Invalid API call to `subscribe`. WakuRelay not mounted." return err("Invalid API call to `subscribe`. WakuRelay not mounted.") @@ -124,13 +153,15 @@ proc subscribe*( error "Failed to decode subscription event", error = error return err("Failed to decode subscription event: " & error) - if node.wakuRelay.isSubscribed(pubsubTopic): - warn "No-effect API call to subscribe. Already subscribed to topic", pubsubTopic - return ok() - - info "subscribe", pubsubTopic, contentTopicOp - node.registerRelayHandler(pubsubTopic, handler) - node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: pubsubTopic)) + if node.registerRelayHandler(pubsubTopic, handler): + info "subscribe", pubsubTopic, contentTopicOp + node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: pubsubTopic)) + else: + if isNil(handler): + warn "No-effect API call to subscribe. Already subscribed to topic", pubsubTopic + else: + info "subscribe (was already subscribed in the mesh; appHandler set)", + pubsubTopic = pubsubTopic return ok() @@ -138,8 +169,10 @@ proc unsubscribe*( node: WakuNode, subscription: SubscriptionEvent ): Result[void, string] = ## Unsubscribes from a specific PubSub or Content topic. + ## This will both unsubscribe from the relay mesh and remove the app handler, if any. + ## NOTE: This works because using MAPI and Kernel API at the same time is unsupported. - if node.wakuRelay.isNil(): + if isNil(node.wakuRelay): error "Invalid API call to `unsubscribe`. WakuRelay not mounted." return err("Invalid API call to `unsubscribe`. WakuRelay not mounted.") @@ -147,13 +180,20 @@ proc unsubscribe*( error "Failed to decode unsubscribe event", error = error return err("Failed to decode unsubscribe event: " & error) - if not node.wakuRelay.isSubscribed(pubsubTopic): - warn "No-effect API call to `unsubscribe`. Was not subscribed", pubsubTopic - return ok() + let hadHandler = node.legacyAppHandlers.hasKey(pubsubTopic) + if hadHandler: + node.legacyAppHandlers.del(pubsubTopic) - info "unsubscribe", pubsubTopic, contentTopicOp - node.wakuRelay.unsubscribe(pubsubTopic) - node.topicSubscriptionQueue.emit((kind: PubsubUnsub, topic: pubsubTopic)) + if node.wakuRelay.isSubscribed(pubsubTopic): + info "unsubscribe", pubsubTopic, contentTopicOp + node.wakuRelay.unsubscribe(pubsubTopic) + node.topicSubscriptionQueue.emit((kind: PubsubUnsub, topic: pubsubTopic)) + else: + if not hadHandler: + warn "No-effect API call to `unsubscribe`. Was not subscribed", pubsubTopic + else: + info "unsubscribe (was not subscribed in the mesh; appHandler removed)", + pubsubTopic = pubsubTopic return ok() diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 254387c32..0cef4cc5d 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -146,6 +146,8 @@ type started*: bool # Indicates that node has started listening topicSubscriptionQueue*: AsyncEventQueue[SubscriptionEvent] rateLimitSettings*: ProtocolRateLimitSettings + legacyAppHandlers*: Table[PubsubTopic, WakuRelayHandler] + ## Kernel API Relay appHandlers (if any) wakuMix*: WakuMix edgeTopicsHealth*: Table[PubsubTopic, TopicHealth] edgeHealthEvent*: AsyncEvent diff --git a/waku/waku_core/subscription/subscription_manager.nim b/waku/waku_core/subscription/subscription_manager.nim index 1b950b3b4..ccade763b 100644 --- a/waku/waku_core/subscription/subscription_manager.nim +++ b/waku/waku_core/subscription/subscription_manager.nim @@ -5,19 +5,19 @@ import std/tables, results, chronicles, chronos import ./push_handler, ../topics, ../message ## Subscription manager -type SubscriptionManager* = object +type LegacySubscriptionManager* = object subscriptions: TableRef[(string, ContentTopic), FilterPushHandler] -proc init*(T: type SubscriptionManager): T = - SubscriptionManager( +proc init*(T: type LegacySubscriptionManager): T = + LegacySubscriptionManager( subscriptions: newTable[(string, ContentTopic), FilterPushHandler]() ) -proc clear*(m: var SubscriptionManager) = +proc clear*(m: var LegacySubscriptionManager) = m.subscriptions.clear() proc registerSubscription*( - m: SubscriptionManager, + m: LegacySubscriptionManager, pubsubTopic: PubsubTopic, contentTopic: ContentTopic, handler: FilterPushHandler, @@ -29,12 +29,12 @@ proc registerSubscription*( error "failed to register filter subscription", error = getCurrentExceptionMsg() proc removeSubscription*( - m: SubscriptionManager, pubsubTopic: PubsubTopic, contentTopic: ContentTopic + m: LegacySubscriptionManager, pubsubTopic: PubsubTopic, contentTopic: ContentTopic ) = m.subscriptions.del((pubsubTopic, contentTopic)) proc notifySubscriptionHandler*( - m: SubscriptionManager, + m: LegacySubscriptionManager, pubsubTopic: PubsubTopic, contentTopic: ContentTopic, message: WakuMessage, @@ -48,5 +48,5 @@ proc notifySubscriptionHandler*( except CatchableError: discard -proc getSubscriptionsCount*(m: SubscriptionManager): int = +proc getSubscriptionsCount*(m: LegacySubscriptionManager): int = m.subscriptions.len() From db19da9254994b8363f0daa6a8869bfb9544e8a6 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:56:39 +0100 Subject: [PATCH 074/155] move destroy api to node_api, add some security checks and fix a possible resource leak (#3736) --- liblogosdelivery/declare_lib.nim | 9 ++++++++ liblogosdelivery/liblogosdelivery.nim | 22 ------------------- .../logos_delivery_api/node_api.nim | 19 ++++++++++++++++ 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/liblogosdelivery/declare_lib.nim b/liblogosdelivery/declare_lib.nim index 98209c649..5087a0dee 100644 --- a/liblogosdelivery/declare_lib.nim +++ b/liblogosdelivery/declare_lib.nim @@ -1,8 +1,12 @@ import ffi +import std/locks import waku/factory/waku declareLibrary("logosdelivery") +var eventCallbackLock: Lock +initLock(eventCallbackLock) + template requireInitializedNode*( ctx: ptr FFIContext[Waku], opName: string, onError: untyped ) = @@ -20,5 +24,10 @@ proc logosdelivery_set_event_callback( echo "error: invalid context in logosdelivery_set_event_callback" return + # prevent race conditions that might happen due incorrect usage. + eventCallbackLock.acquire() + defer: + eventCallbackLock.release() + ctx[].eventCallback = cast[pointer](callback) ctx[].eventUserData = userData diff --git a/liblogosdelivery/liblogosdelivery.nim b/liblogosdelivery/liblogosdelivery.nim index 7d068b065..b6a4c0bda 100644 --- a/liblogosdelivery/liblogosdelivery.nim +++ b/liblogosdelivery/liblogosdelivery.nim @@ -5,25 +5,3 @@ import waku/factory/waku, waku/node/waku_node, ./declare_lib ################################################################################ ## Include different APIs, i.e. all procs with {.ffi.} pragma include ./logos_delivery_api/node_api, ./logos_delivery_api/messaging_api - -################################################################################ -### Exported procs - -proc logosdelivery_destroy( - ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer -): cint {.dynlib, exportc, cdecl.} = - initializeLibrary() - checkParams(ctx, callback, userData) - - ffi.destroyFFIContext(ctx).isOkOr: - let msg = "liblogosdelivery error: " & $error - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - ## always need to invoke the callback although we don't retrieve value to the caller - callback(RET_OK, nil, 0, userData) - - return RET_OK - -# ### End of exported procs -# ################################################################################ diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim index 5c2d7885f..2d6c8d0de 100644 --- a/liblogosdelivery/logos_delivery_api/node_api.nim +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -29,6 +29,22 @@ registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): return ok("") +proc logosdelivery_destroy( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +): cint {.dynlib, exportc, cdecl.} = + initializeLibrary() + checkParams(ctx, callback, userData) + + ffi.destroyFFIContext(ctx).isOkOr: + let msg = "liblogosdelivery error: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return RET_ERR + + ## always need to invoke the callback although we don't retrieve value to the caller + callback(RET_OK, nil, 0, userData) + + return RET_OK + proc logosdelivery_create_node( configJson: cstring, callback: FFICallback, userData: pointer ): pointer {.dynlib, exportc, cdecl.} = @@ -50,6 +66,9 @@ proc logosdelivery_create_node( ).isOkOr: let msg = "error in sendRequestToFFIThread: " & $error callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + # free allocated resources as they won't be available + ffi.destroyFFIContext(ctx).isOkOr: + chronicles.error "Error in destroyFFIContext after sendRequestToFFIThread during creation", err = $error return nil return ctx From 7e36e268676bbc0fe6d81664732de870d8352c7d Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Tue, 3 Mar 2026 08:11:16 -0300 Subject: [PATCH 075/155] Fix NodeHealthMonitor logspam (#3743) --- waku/node/health_monitor/node_health_monitor.nim | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/waku/node/health_monitor/node_health_monitor.nim b/waku/node/health_monitor/node_health_monitor.nim index ba0518e61..ddba47ccb 100644 --- a/waku/node/health_monitor/node_health_monitor.nim +++ b/waku/node/health_monitor/node_health_monitor.nim @@ -405,13 +405,8 @@ proc calculateConnectionState*( elif kind in FilterClientProtocols: filterCount = max(filterCount, strength) - debug "calculateConnectionState", - protocol = kind, - strength = strength, - relayCount = relayCount, - storeClientCount = storeClientCount, - lightpushCount = lightpushCount, - filterCount = filterCount + debug "calculateConnectionState", + relayCount, storeClientCount, lightpushCount, filterCount # Relay connectivity should be a sufficient check in Core mode. # "Store peers" are relay peers because incoming messages in @@ -528,6 +523,9 @@ proc healthLoop(hm: NodeHealthMonitor) {.async.} = let newConnectionStatus = hm.calculateConnectionState() if newConnectionStatus != hm.connectionStatus: + debug "connectionStatus change", + oldstatus = hm.connectionStatus, newstatus = newConnectionStatus + hm.connectionStatus = newConnectionStatus EventConnectionStatusChange.emit(hm.node.brokerCtx, newConnectionStatus) From 09618a2656195f3c2d529e97d0447ec13d4f118a Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:32:45 +0100 Subject: [PATCH 076/155] Add debug API in liblogosdelivery (#3742) --- .../examples/logosdelivery_example.c | 24 +++++++-- liblogosdelivery/liblogosdelivery.h | 16 ++++++ liblogosdelivery/liblogosdelivery.nim | 6 ++- .../logos_delivery_api/debug_api.nim | 46 +++++++++++++++++ tests/wakunode2/test_app.nim | 2 +- waku/factory/waku.nim | 10 ++-- waku/factory/waku_state_info.nim | 50 +++++++++++++++++++ 7 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 liblogosdelivery/logos_delivery_api/debug_api.nim create mode 100644 waku/factory/waku_state_info.nim diff --git a/liblogosdelivery/examples/logosdelivery_example.c b/liblogosdelivery/examples/logosdelivery_example.c index 5437be427..c0929e650 100644 --- a/liblogosdelivery/examples/logosdelivery_example.c +++ b/liblogosdelivery/examples/logosdelivery_example.c @@ -161,7 +161,23 @@ int main() { // Wait for subscription sleep(1); - printf("\n5. Sending a message...\n"); + printf("\n5. Retrieving all possibl node info ids...\n"); + logosdelivery_get_available_node_info_ids(ctx, simple_callback, (void *)"get_available_node_info_ids"); + + printf("\nRetrieving node info for a specific invalid ID...\n"); + logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "WrongNodeInfoId"); + + printf("\nRetrieving several node info for specific correct IDs...\n"); + logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "Version"); + // logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "Metrics"); + logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "MyMultiaddresses"); + logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "MyENR"); + logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "MyPeerId"); + + printf("\nRetrieving available configs...\n"); + logosdelivery_get_available_configs(ctx, simple_callback, (void *)"get_available_configs"); + + printf("\n6. Sending a message...\n"); printf("Watch for message events (sent, propagated, or error):\n"); // Create base64-encoded payload: "Hello, Logos Messaging!" const char *message = "{" @@ -175,17 +191,17 @@ int main() { printf("Waiting for message delivery events...\n"); sleep(60); - printf("\n6. Unsubscribing from content topic...\n"); + printf("\n7. Unsubscribing from content topic...\n"); logosdelivery_unsubscribe(ctx, simple_callback, (void *)"unsubscribe", contentTopic); sleep(1); - printf("\n7. Stopping node...\n"); + printf("\n8. Stopping node...\n"); logosdelivery_stop_node(ctx, simple_callback, (void *)"stop_node"); sleep(1); - printf("\n8. Destroying context...\n"); + printf("\n9. Destroying context...\n"); logosdelivery_destroy(ctx, simple_callback, (void *)"destroy"); printf("\n=== Example completed ===\n"); diff --git a/liblogosdelivery/liblogosdelivery.h b/liblogosdelivery/liblogosdelivery.h index b014d6385..0d318b691 100644 --- a/liblogosdelivery/liblogosdelivery.h +++ b/liblogosdelivery/liblogosdelivery.h @@ -75,6 +75,22 @@ extern "C" FFICallBack callback, void *userData); + // Retrieves the list of available node info IDs. + int logosdelivery_get_available_node_info_ids(void *ctx, + FFICallBack callback, + void *userData); + + // Given a node info ID, retrieves the corresponding info. + int logosdelivery_get_node_info(void *ctx, + FFICallBack callback, + void *userData, + const char *nodeInfoId); + + // Retrieves the list of available configurations. + int logosdelivery_get_available_configs(void *ctx, + FFICallBack callback, + void *userData); + #ifdef __cplusplus } #endif diff --git a/liblogosdelivery/liblogosdelivery.nim b/liblogosdelivery/liblogosdelivery.nim index b6a4c0bda..fc907498a 100644 --- a/liblogosdelivery/liblogosdelivery.nim +++ b/liblogosdelivery/liblogosdelivery.nim @@ -4,4 +4,8 @@ import waku/factory/waku, waku/node/waku_node, ./declare_lib ################################################################################ ## Include different APIs, i.e. all procs with {.ffi.} pragma -include ./logos_delivery_api/node_api, ./logos_delivery_api/messaging_api + +include + ./logos_delivery_api/node_api, + ./logos_delivery_api/messaging_api, + ./logos_delivery_api/debug_api diff --git a/liblogosdelivery/logos_delivery_api/debug_api.nim b/liblogosdelivery/logos_delivery_api/debug_api.nim new file mode 100644 index 000000000..bee8ab537 --- /dev/null +++ b/liblogosdelivery/logos_delivery_api/debug_api.nim @@ -0,0 +1,46 @@ +import std/[json, strutils] +import waku/factory/waku_state_info + +proc logosdelivery_get_available_node_info_ids( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## Returns the list of all available node info item ids that + ## can be queried with `get_node_info_item`. + requireInitializedNode(ctx, "GetNodeInfoIds"): + return err(errMsg) + + return ok($ctx.myLib[].stateInfo.getAllPossibleInfoItemIds()) + +proc logosdelivery_get_node_info( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + nodeInfoId: cstring, +) {.ffi.} = + ## Returns the content of the node info item with the given id if it exists. + requireInitializedNode(ctx, "GetNodeInfoItem"): + return err(errMsg) + + let infoItemIdEnum = + try: + parseEnum[NodeInfoId]($nodeInfoId) + except ValueError: + return err("Invalid node info id: " & $nodeInfoId) + + return ok(ctx.myLib[].stateInfo.getNodeInfoItem(infoItemIdEnum)) + +proc logosdelivery_get_available_configs( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## Returns information about the accepted config items. + ## For analogy with a CLI app, this is the info when typing --help for a command. + requireInitializedNode(ctx, "GetAvailableConfigs"): + return err(errMsg) + + ## TODO: we are now returning a simple default value for NodeConfig. + ## The NodeConfig struct is too complex and we need to have a flattened simpler config. + ## The expected returned value for this is a list of possible config items with their + ## description, accepted values, default value, etc. + + let defaultConfig = NodeConfig.init() + return ok($(%*defaultConfig)) diff --git a/tests/wakunode2/test_app.nim b/tests/wakunode2/test_app.nim index e94a3b21d..6ec6043fe 100644 --- a/tests/wakunode2/test_app.nim +++ b/tests/wakunode2/test_app.nim @@ -21,7 +21,7 @@ suite "Wakunode2 - Waku": raiseAssert error ## When - let version = waku.version + let version = waku.stateInfo.getNodeInfoItem(NodeInfoId.Version) ## Then check: diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index ff0ab0568..dbee8d093 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -47,7 +47,8 @@ import factory/internal_config, factory/app_callbacks, ], - ./waku_conf + ./waku_conf, + ./waku_state_info logScope: topics = "wakunode waku" @@ -56,7 +57,7 @@ logScope: const git_version* {.strdefine.} = "n/a" type Waku* = ref object - version: string + stateInfo*: WakuStateInfo conf*: WakuConf rng*: ref HmacDrbgContext @@ -79,9 +80,6 @@ type Waku* = ref object brokerCtx*: BrokerContext -func version*(waku: Waku): string = - waku.version - proc setupSwitchServices( waku: Waku, conf: WakuConf, circuitRelay: Relay, rng: ref HmacDrbgContext ) = @@ -216,7 +214,7 @@ proc new*( return err("could not create delivery service: " & $error) var waku = Waku( - version: git_version, + stateInfo: WakuStateInfo.init(node), conf: wakuConf, rng: rng, key: wakuConf.nodeKey, diff --git a/waku/factory/waku_state_info.nim b/waku/factory/waku_state_info.nim new file mode 100644 index 000000000..5dc72a693 --- /dev/null +++ b/waku/factory/waku_state_info.nim @@ -0,0 +1,50 @@ +## This module is aimed to collect and provide information about the state of the node, +## such as its version, metrics values, etc. +## It has been originally designed to be used by the debug API, which acts as a consumer of +## this information, but any other module can populate the information it needs to be +## accessible through the debug API. + +import std/[tables, sequtils, strutils] +import metrics, eth/p2p/discoveryv5/enr, libp2p/peerid +import waku/waku_node + +type + NodeInfoId* {.pure.} = enum + Version + Metrics + MyMultiaddresses + MyENR + MyPeerId + + WakuStateInfo* {.requiresInit.} = object + node: WakuNode + +proc getAllPossibleInfoItemIds*(self: WakuStateInfo): seq[NodeInfoId] = + ## Returns all possible options that can be queried to learn about the node's information. + var ret = newSeq[NodeInfoId](0) + for item in NodeInfoId: + ret.add(item) + return ret + +proc getMetrics(): string = + {.gcsafe.}: + return defaultRegistry.toText() ## defaultRegistry is {.global.} in metrics module + +proc getNodeInfoItem*(self: WakuStateInfo, infoItemId: NodeInfoId): string = + ## Returns the content of the info item with the given id if it exists. + case infoItemId + of NodeInfoId.Version: + return git_version + of NodeInfoId.Metrics: + return getMetrics() + of NodeInfoId.MyMultiaddresses: + return self.node.info().listenAddresses.join(",") + of NodeInfoId.MyENR: + return self.node.enr.toURI() + of NodeInfoId.MyPeerId: + return $PeerId(self.node.peerId()) + else: + return "unknown info item id" + +proc init*(T: typedesc[WakuStateInfo], node: WakuNode): T = + return WakuStateInfo(node: node) From 1f9c4cb8cc4cac2347d19bac6eecb169775ca366 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:17:54 +0100 Subject: [PATCH 077/155] Chore: adapt cli args for delivery api (#3744) * LogosDeliveryAPI: NodeConfig -> WakluNodeConf + mode selector and logos.dev preset * Adjustment made on test, logos.dev preset * change default agentString from nwaku to logos-delivery * Add p2pReliability switch to presets and make it default to true. * Borrow entryNode idea from NodeConfig to WakuNodeConf to easy shortcut among diffrent bootstrap node list all needs different formats * Fix rateLimit assignment for builder * Remove Core mode default as we already have a defaul, user must override * Removed obsolate API createNode with NodeConfig - tests are refactored for WakuNodeConf usage * Fix failing test due to twn-clusterid(1) default has overwrite for maxMessagSize. Fix readme. --- examples/api_example/api_example.nim | 27 +- liblogosdelivery/README.md | 20 +- .../examples/logosdelivery_example.c | 34 +- liblogosdelivery/liblogosdelivery.h | 4 +- .../logos_delivery_api/node_api.nim | 38 +- tests/api/test_api_health.nim | 44 +- tests/api/test_api_send.nim | 45 +- tests/api/test_api_subscription.nim | 37 +- tests/api/test_entry_nodes.nim | 2 +- tests/api/test_node_conf.nim | 1206 ++++------------- tests/test_waku.nim | 71 +- tools/confutils/cli_args.nim | 80 +- {waku/api => tools/confutils}/entry_nodes.nim | 0 waku/api.nim | 3 +- waku/api/api.nim | 10 +- waku/api/api_conf.nim | 15 +- .../filter_service_conf_builder.nim | 6 + .../conf_builder/rate_limit_conf_builder.nim | 6 + .../conf_builder/waku_conf_builder.nim | 41 +- waku/factory/networks_config.nim | 40 + waku/rest_api/endpoint/builder.nim | 4 +- 21 files changed, 641 insertions(+), 1092 deletions(-) rename {waku/api => tools/confutils}/entry_nodes.nim (100%) diff --git a/examples/api_example/api_example.nim b/examples/api_example/api_example.nim index 37dd5d34b..4a7cde5db 100644 --- a/examples/api_example/api_example.nim +++ b/examples/api_example/api_example.nim @@ -59,19 +59,24 @@ when isMainModule: echo "Starting Waku node..." - let config = - if (args.ethRpcEndpoint == ""): - # Create a basic configuration for the Waku node - # No RLN as we don't have an ETH RPC Endpoint - NodeConfig.init( - protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 42) - ) - else: - # Connect to TWN, use ETH RPC Endpoint for RLN - NodeConfig.init(mode = WakuMode.Core, ethRpcEndpoints = @[args.ethRpcEndpoint]) + # Use WakuNodeConf (the CLI configuration type) for node setup + var conf = defaultWakuNodeConf().valueOr: + echo "Failed to create default config: ", error + quit(QuitFailure) + + if args.ethRpcEndpoint == "": + # Create a basic configuration for the Waku node + # No RLN as we don't have an ETH RPC Endpoint + conf.mode = Core + conf.preset = "logos.dev" + else: + # Connect to TWN, use ETH RPC Endpoint for RLN + conf.mode = Core + conf.preset = "twn" + conf.ethClientUrls = @[EthRpcUrl(args.ethRpcEndpoint)] # Create the node using the library API's createNode function - let node = (waitFor createNode(config)).valueOr: + let node = (waitFor createNode(conf)).valueOr: echo "Failed to create node: ", error quit(QuitFailure) diff --git a/liblogosdelivery/README.md b/liblogosdelivery/README.md index f9909dd3d..e8352c611 100644 --- a/liblogosdelivery/README.md +++ b/liblogosdelivery/README.md @@ -32,18 +32,17 @@ void *logosdelivery_create_node( ```json { "mode": "Core", - "clusterId": 1, - "entryNodes": [ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im" - ], - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } + "preset": "logos.dev", + "listenAddress": "0.0.0.0", + "tcpPort": 60000, + "discv5UdpPort": 9000 } ``` +Configuration uses flat field names matching `WakuNodeConf` in `tools/confutils/cli_args.nim`. +Use `"preset"` to select a network preset (e.g., `"twn"`, `"logos.dev"`) which auto-configures +entry nodes, cluster ID, sharding, and other network-specific settings. + #### `logosdelivery_start_node` Starts the node. @@ -207,8 +206,9 @@ void callback(int ret, const char *msg, size_t len, void *userData) { int main() { const char *config = "{" + "\"logLevel\": \"INFO\"," "\"mode\": \"Core\"," - "\"clusterId\": 1" + "\"preset\": \"logos.dev\"" "}"; // Create node diff --git a/liblogosdelivery/examples/logosdelivery_example.c b/liblogosdelivery/examples/logosdelivery_example.c index c0929e650..61333f84d 100644 --- a/liblogosdelivery/examples/logosdelivery_example.c +++ b/liblogosdelivery/examples/logosdelivery_example.c @@ -61,7 +61,7 @@ void event_callback(int ret, const char *msg, size_t len, void *userData) { char messageHash[128]; extract_json_field(eventJson, "requestId", requestId, sizeof(requestId)); extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); - printf("📤 [EVENT] Message sent - RequestID: %s, Hash: %s\n", requestId, messageHash); + printf("[EVENT] Message sent - RequestID: %s, Hash: %s\n", requestId, messageHash); } else if (strcmp(eventType, "message_error") == 0) { char requestId[128]; @@ -70,7 +70,7 @@ void event_callback(int ret, const char *msg, size_t len, void *userData) { extract_json_field(eventJson, "requestId", requestId, sizeof(requestId)); extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); extract_json_field(eventJson, "error", error, sizeof(error)); - printf("❌ [EVENT] Message error - RequestID: %s, Hash: %s, Error: %s\n", + printf("[EVENT] Message error - RequestID: %s, Hash: %s, Error: %s\n", requestId, messageHash, error); } else if (strcmp(eventType, "message_propagated") == 0) { @@ -78,10 +78,15 @@ void event_callback(int ret, const char *msg, size_t len, void *userData) { char messageHash[128]; extract_json_field(eventJson, "requestId", requestId, sizeof(requestId)); extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); - printf("✅ [EVENT] Message propagated - RequestID: %s, Hash: %s\n", requestId, messageHash); + printf("[EVENT] Message propagated - RequestID: %s, Hash: %s\n", requestId, messageHash); + + } else if (strcmp(eventType, "connection_status_change") == 0) { + char connectionStatus[256]; + extract_json_field(eventJson, "connectionStatus", connectionStatus, sizeof(connectionStatus)); + printf("[EVENT] Connection status change - Status: %s\n", connectionStatus); } else { - printf("ℹ️ [EVENT] Unknown event type: %s\n", eventType); + printf("[EVENT] Unknown event type: %s\n", eventType); } free(eventJson); @@ -109,23 +114,12 @@ void simple_callback(int ret, const char *msg, size_t len, void *userData) { int main() { printf("=== Logos Messaging API (LMAPI) Example ===\n\n"); - // Configuration JSON for creating a node + // Configuration JSON using WakuNodeConf field names (flat structure). + // Field names match Nim identifiers from WakuNodeConf in tools/confutils/cli_args.nim. const char *config = "{" - "\"logLevel\": \"DEBUG\"," - // "\"mode\": \"Edge\"," + "\"logLevel\": \"INFO\"," "\"mode\": \"Core\"," - "\"protocolsConfig\": {" - "\"entryNodes\": [\"/dns4/node-01.do-ams3.misc.logos-chat.status.im/tcp/30303/p2p/16Uiu2HAkxoqUTud5LUPQBRmkeL2xP4iKx2kaABYXomQRgmLUgf78\"]," - "\"clusterId\": 42," - "\"autoShardingConfig\": {" - "\"numShardsInCluster\": 8" - "}" - "}," - "\"networkingConfig\": {" - "\"listenIpv4\": \"0.0.0.0\"," - "\"p2pTcpPort\": 60000," - "\"discv5UdpPort\": 9000" - "}" + "\"preset\": \"logos.dev\"" "}"; printf("1. Creating node...\n"); @@ -152,7 +146,7 @@ int main() { logosdelivery_start_node(ctx, simple_callback, (void *)"start_node"); // Wait for node to start - sleep(2); + sleep(10); printf("\n4. Subscribing to content topic...\n"); const char *contentTopic = "/example/1/chat/proto"; diff --git a/liblogosdelivery/liblogosdelivery.h b/liblogosdelivery/liblogosdelivery.h index 0d318b691..5092db9f2 100644 --- a/liblogosdelivery/liblogosdelivery.h +++ b/liblogosdelivery/liblogosdelivery.h @@ -22,7 +22,9 @@ extern "C" // Creates a new instance of the node from the given configuration JSON. // Returns a pointer to the Context needed by the rest of the API functions. - // Configuration should be in JSON format following the NodeConfig structure. + // Configuration should be in JSON format using WakuNodeConf field names. + // Field names match Nim identifiers from WakuNodeConf (camelCase). + // Example: {"mode": "Core", "clusterId": 42, "relay": true} void *logosdelivery_create_node( const char *configJson, FFICallBack callback, diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim index 2d6c8d0de..1835f75b5 100644 --- a/liblogosdelivery/logos_delivery_api/node_api.nim +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -1,10 +1,11 @@ -import std/json -import chronos, results, ffi +import std/[json, strutils] +import chronos, chronicles, results, confutils, confutils/std/net, ffi import waku/factory/waku, waku/node/waku_node, - waku/api/[api, api_conf, types], + waku/api/[api, types], waku/events/[message_events, health_events], + tools/confutils/cli_args, ../declare_lib, ../json_event @@ -14,15 +15,32 @@ proc `%`*(id: RequestId): JsonNode = registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): proc(configJson: cstring): Future[Result[string, string]] {.async.} = - ## Parse the JSON configuration and create a node - let nodeConfig = - try: - decodeNodeConfigFromJson($configJson) - except SerializationError as e: - return err("Failed to parse config JSON: " & e.msg) + ## Parse the JSON configuration using fieldPairs approach (WakuNodeConf) + var conf = defaultWakuNodeConf().valueOr: + return err("Failed creating default conf: " & error) + + var jsonNode: JsonNode + try: + jsonNode = parseJson($configJson) + except Exception: + return err( + "Failed to parse config JSON: " & getCurrentExceptionMsg() & + " configJson string: " & $configJson + ) + + for confField, confValue in fieldPairs(conf): + if jsonNode.contains(confField): + let formattedString = ($jsonNode[confField]).strip(chars = {'\"'}) + try: + confValue = parseCmdArg(typeof(confValue), formattedString) + except Exception: + return err( + "Failed to parse field '" & confField & "': " & + getCurrentExceptionMsg() & ". Value: " & formattedString + ) # Create the node - ctx.myLib[] = (await api.createNode(nodeConfig)).valueOr: + ctx.myLib[] = (await api.createNode(conf)).valueOr: let errMsg = $error chronicles.error "CreateNodeRequest failed", err = errMsg return err(errMsg) diff --git a/tests/api/test_api_health.nim b/tests/api/test_api_health.nim index b7aab43f9..f3dd340af 100644 --- a/tests/api/test_api_health.nim +++ b/tests/api/test_api_health.nim @@ -13,9 +13,10 @@ import waku/events/health_events, waku/common/waku_protocol, waku/factory/waku_conf +import tools/confutils/cli_args const TestTimeout = chronos.seconds(10) -const DefaultShard = PubsubTopic("/waku/2/rs/1/0") +const DefaultShard = PubsubTopic("/waku/2/rs/3/0") const TestContentTopic = ContentTopic("/waku/2/default-content/proto") proc dummyHandler( @@ -80,7 +81,7 @@ suite "LM API health checking": newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) (await serviceNode.mountRelay()).isOkOr: raiseAssert error - serviceNode.mountMetadata(1, @[0'u16]).isOkOr: + serviceNode.mountMetadata(3, @[0'u16]).isOkOr: raiseAssert error await serviceNode.mountLibp2pPing() await serviceNode.start() @@ -89,16 +90,15 @@ suite "LM API health checking": serviceNode.wakuRelay.subscribe(DefaultShard, dummyHandler) lockNewGlobalBrokerContext: - let conf = NodeConfig.init( - mode = WakuMode.Core, - networkingConfig = - NetworkingConfig(listenIpv4: "0.0.0.0", p2pTcpPort: 0, discv5UdpPort: 0), - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], - clusterId = 1'u16, - autoShardingConfig = AutoShardingConfig(numShardsInCluster: 1), - ), - ) + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = Core + conf.listenAddress = parseIpAddress("0.0.0.0") + conf.tcpPort = Port(0) + conf.discv5UdpPort = Port(0) + conf.clusterId = 3'u16 + conf.numShardsInNetwork = 1 + conf.rest = false client = (await createNode(conf)).valueOr: raiseAssert error @@ -267,17 +267,15 @@ suite "LM API health checking": var edgeWaku: Waku lockNewGlobalBrokerContext: - let edgeConf = NodeConfig.init( - mode = WakuMode.Edge, - networkingConfig = - NetworkingConfig(listenIpv4: "0.0.0.0", p2pTcpPort: 0, discv5UdpPort: 0), - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], - clusterId = 1'u16, - messageValidation = - MessageValidation(maxMessageSize: "150 KiB", rlnConfig: none(RlnConfig)), - ), - ) + var edgeConf = defaultWakuNodeConf().valueOr: + raiseAssert error + edgeConf.mode = Edge + edgeConf.listenAddress = parseIpAddress("0.0.0.0") + edgeConf.tcpPort = Port(0) + edgeConf.discv5UdpPort = Port(0) + edgeConf.clusterId = 3'u16 + edgeConf.maxMessageSize = "150 KiB" + edgeConf.rest = false edgeWaku = (await createNode(edgeConf)).valueOr: raiseAssert "Failed to create edge node: " & error diff --git a/tests/api/test_api_send.nim b/tests/api/test_api_send.nim index 7343fc655..30a176119 100644 --- a/tests/api/test_api_send.nim +++ b/tests/api/test_api_send.nim @@ -6,7 +6,8 @@ import ../testlib/[common, wakucore, wakunode, testasync] import ../waku_archive/archive_utils import waku, waku/[waku_node, waku_core, waku_relay/protocol, common/broker/broker_context] -import waku/api/api_conf, waku/factory/waku_conf +import waku/factory/waku_conf +import tools/confutils/cli_args type SendEventOutcome {.pure.} = enum Sent @@ -116,20 +117,18 @@ proc validate( for requestId in manager.errorRequestIds: check requestId == expectedRequestId -proc createApiNodeConf(mode: WakuMode = WakuMode.Core): NodeConfig = - # allocate random ports to avoid port-already-in-use errors - let netConf = NetworkingConfig(listenIpv4: "0.0.0.0", p2pTcpPort: 0, discv5UdpPort: 0) - - result = NodeConfig.init( - mode = mode, - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], - clusterId = 1, - autoShardingConfig = AutoShardingConfig(numShardsInCluster: 1), - ), - networkingConfig = netConf, - p2pReliability = true, - ) +proc createApiNodeConf(mode: cli_args.WakuMode = cli_args.WakuMode.Core): WakuNodeConf = + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = mode + conf.listenAddress = parseIpAddress("0.0.0.0") + conf.tcpPort = Port(0) + conf.discv5UdpPort = Port(0) + conf.clusterId = 3'u16 + conf.numShardsInNetwork = 1 + conf.reliabilityEnabled = true + conf.rest = false + result = conf suite "Waku API - Send": var @@ -153,7 +152,7 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: relayNode1 = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) - relayNode1.mountMetadata(1, @[0'u16]).isOkOr: + relayNode1.mountMetadata(3, @[0'u16]).isOkOr: raiseAssert "Failed to mount metadata: " & error (await relayNode1.mountRelay()).isOkOr: raiseAssert "Failed to mount relay" @@ -163,7 +162,7 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: relayNode2 = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) - relayNode2.mountMetadata(1, @[0'u16]).isOkOr: + relayNode2.mountMetadata(3, @[0'u16]).isOkOr: raiseAssert "Failed to mount metadata: " & error (await relayNode2.mountRelay()).isOkOr: raiseAssert "Failed to mount relay" @@ -173,7 +172,7 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: lightpushNode = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) - lightpushNode.mountMetadata(1, @[0'u16]).isOkOr: + lightpushNode.mountMetadata(3, @[0'u16]).isOkOr: raiseAssert "Failed to mount metadata: " & error (await lightpushNode.mountRelay()).isOkOr: raiseAssert "Failed to mount relay" @@ -185,7 +184,7 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: storeNode = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) - storeNode.mountMetadata(1, @[0'u16]).isOkOr: + storeNode.mountMetadata(3, @[0'u16]).isOkOr: raiseAssert "Failed to mount metadata: " & error (await storeNode.mountRelay()).isOkOr: raiseAssert "Failed to mount relay" @@ -210,7 +209,7 @@ suite "Waku API - Send": storeNodePeerId = storeNode.peerInfo.peerId # Subscribe all relay nodes to the default shard topic - const testPubsubTopic = PubsubTopic("/waku/2/rs/1/0") + const testPubsubTopic = PubsubTopic("/waku/2/rs/3/0") proc dummyHandler( topic: PubsubTopic, msg: WakuMessage ): Future[void] {.async, gcsafe.} = @@ -387,7 +386,7 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: fakeLightpushNode = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) - fakeLightpushNode.mountMetadata(1, @[0'u16]).isOkOr: + fakeLightpushNode.mountMetadata(3, @[0'u16]).isOkOr: raiseAssert "Failed to mount metadata: " & error (await fakeLightpushNode.mountRelay()).isOkOr: raiseAssert "Failed to mount relay" @@ -402,13 +401,13 @@ suite "Waku API - Send": discard fakeLightpushNode.subscribe( - (kind: PubsubSub, topic: PubsubTopic("/waku/2/rs/1/0")), dummyHandler + (kind: PubsubSub, topic: PubsubTopic("/waku/2/rs/3/0")), dummyHandler ).isOkOr: raiseAssert "Failed to subscribe fakeLightpushNode: " & error var node: Waku lockNewGlobalBrokerContext: - node = (await createNode(createApiNodeConf(WakuMode.Edge))).valueOr: + node = (await createNode(createApiNodeConf(cli_args.WakuMode.Edge))).valueOr: raiseAssert error (await startWaku(addr node)).isOkOr: raiseAssert "Failed to start Waku node: " & error diff --git a/tests/api/test_api_subscription.nim b/tests/api/test_api_subscription.nim index 8983c2934..6639e3dea 100644 --- a/tests/api/test_api_subscription.nim +++ b/tests/api/test_api_subscription.nim @@ -14,7 +14,8 @@ import events/message_events, waku_relay/protocol, ] -import waku/api/api_conf, waku/factory/waku_conf +import waku/factory/waku_conf +import tools/confutils/cli_args # TODO: Edge testing (after MAPI edge support is completed) @@ -64,21 +65,21 @@ type TestNetwork = ref object publisherPeerInfo: RemotePeerInfo proc createApiNodeConf( - mode: WakuMode = WakuMode.Core, numShards: uint16 = 1 -): NodeConfig = - let netConf = NetworkingConfig(listenIpv4: "0.0.0.0", p2pTcpPort: 0, discv5UdpPort: 0) - result = NodeConfig.init( - mode = mode, - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], - clusterId = 1, - autoShardingConfig = AutoShardingConfig(numShardsInCluster: numShards), - ), - networkingConfig = netConf, - p2pReliability = true, - ) + mode: cli_args.WakuMode = cli_args.WakuMode.Core, numShards: uint16 = 1 +): WakuNodeConf = + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = mode + conf.listenAddress = parseIpAddress("0.0.0.0") + conf.tcpPort = Port(0) + conf.discv5UdpPort = Port(0) + conf.clusterId = 3'u16 + conf.numShardsInNetwork = numShards + conf.reliabilityEnabled = true + conf.rest = false + result = conf -proc setupSubscriberNode(conf: NodeConfig): Future[Waku] {.async.} = +proc setupSubscriberNode(conf: WakuNodeConf): Future[Waku] {.async.} = var node: Waku lockNewGlobalBrokerContext: node = (await createNode(conf)).expect("Failed to create subscriber node") @@ -86,14 +87,14 @@ proc setupSubscriberNode(conf: NodeConfig): Future[Waku] {.async.} = return node proc setupNetwork( - numShards: uint16 = 1, mode: WakuMode = WakuMode.Core + numShards: uint16 = 1, mode: cli_args.WakuMode = cli_args.WakuMode.Core ): Future[TestNetwork] {.async.} = var net = TestNetwork() lockNewGlobalBrokerContext: net.publisher = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) - net.publisher.mountMetadata(1, @[0'u16]).expect("Failed to mount metadata") + net.publisher.mountMetadata(3, @[0'u16]).expect("Failed to mount metadata") (await net.publisher.mountRelay()).expect("Failed to mount relay") await net.publisher.mountLibp2pPing() await net.publisher.start() @@ -108,7 +109,7 @@ proc setupNetwork( # that changes, this will be needed to cause the publisher to have shard interest # for any shards the subscriber may want to use, which is required for waitForMesh to work. for i in 0 ..< numShards.int: - let shard = PubsubTopic("/waku/2/rs/1/" & $i) + let shard = PubsubTopic("/waku/2/rs/3/" & $i) net.publisher.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( "Failed to sub publisher" ) diff --git a/tests/api/test_entry_nodes.nim b/tests/api/test_entry_nodes.nim index 136a49b2b..38dc38ba4 100644 --- a/tests/api/test_entry_nodes.nim +++ b/tests/api/test_entry_nodes.nim @@ -2,7 +2,7 @@ import std/options, results, testutils/unittests -import waku/api/entry_nodes +import tools/confutils/entry_nodes # Since classifyEntryNode is internal, we test it indirectly through processEntryNodes behavior # The enum is exported so we can test against it diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index 84bbfead3..d0b3d433c 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -1,36 +1,64 @@ {.used.} -import std/options, results, stint, testutils/unittests +import std/[options, json, strutils], results, stint, testutils/unittests import json_serialization -import waku/api/api_conf, waku/factory/waku_conf, waku/factory/networks_config +import confutils, confutils/std/net +import tools/confutils/cli_args +import waku/factory/waku_conf, waku/factory/networks_config import waku/common/logging -suite "LibWaku Conf - toWakuConf": - test "Minimal configuration": +# Helper: parse JSON into WakuNodeConf using fieldPairs (same as liblogosdelivery) +proc parseWakuNodeConfFromJson(jsonStr: string): Result[WakuNodeConf, string] = + var conf = defaultWakuNodeConf().valueOr: + return err(error) + var jsonNode: JsonNode + try: + jsonNode = parseJson(jsonStr) + except Exception: + return err("JSON parse error: " & getCurrentExceptionMsg()) + for confField, confValue in fieldPairs(conf): + if jsonNode.contains(confField): + let formattedString = ($jsonNode[confField]).strip(chars = {'\"'}) + try: + confValue = parseCmdArg(typeof(confValue), formattedString) + except Exception: + return err( + "Field '" & confField & "' parse error: " & getCurrentExceptionMsg() & + ". Value: " & formattedString + ) + return ok(conf) + +suite "WakuNodeConf - mode-driven toWakuConf": + test "Core mode enables service protocols": ## Given - let nodeConfig = NodeConfig.init(ethRpcEndpoints = @["http://someaddress"]) + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = Core + conf.clusterId = 1 ## When - let wakuConfRes = toWakuConf(nodeConfig) + let wakuConfRes = conf.toWakuConf() ## Then - let wakuConf = wakuConfRes.valueOr: - raiseAssert error - wakuConf.validate().isOkOr: - raiseAssert error + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() check: + wakuConf.relay == true + wakuConf.lightPush == true + wakuConf.peerExchangeService == true + wakuConf.rendezvous == true wakuConf.clusterId == 1 - wakuConf.shardingConf.numShardsInCluster == 8 - wakuConf.staticNodes.len == 0 - test "Edge mode configuration": + test "Edge mode disables service protocols": ## Given - let protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 1) - - let nodeConfig = NodeConfig.init(mode = Edge, protocolsConfig = protocolsConfig) + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = Edge + conf.clusterId = 1 ## When - let wakuConfRes = toWakuConf(nodeConfig) + let wakuConfRes = conf.toWakuConf() ## Then require wakuConfRes.isOk() @@ -42,16 +70,175 @@ suite "LibWaku Conf - toWakuConf": wakuConf.filterServiceConf.isSome() == false wakuConf.storeServiceConf.isSome() == false wakuConf.peerExchangeService == true - wakuConf.clusterId == 1 - test "Core mode configuration": + test "noMode uses explicit CLI flags as-is": ## Given - let protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 1) - - let nodeConfig = NodeConfig.init(mode = Core, protocolsConfig = protocolsConfig) + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = WakuMode.noMode + conf.relay = true + conf.lightpush = false + conf.clusterId = 5 ## When - let wakuConfRes = toWakuConf(nodeConfig) + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == true + wakuConf.lightPush == false + wakuConf.clusterId == 5 + + test "Core mode overrides individual protocol flags": + ## Given - user sets relay=false but mode=Core should override + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = Core + conf.relay = false # will be overridden by Core mode + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == true # mode overrides + +suite "WakuNodeConf - JSON parsing with fieldPairs": + test "Empty JSON produces valid default conf": + ## Given / When + let confRes = parseWakuNodeConfFromJson("{}") + + ## Then + require confRes.isOk() + let conf = confRes.get() + check: + conf.mode == WakuMode.noMode + conf.clusterId == 0 + conf.logLevel == logging.LogLevel.INFO + + test "JSON with mode and clusterId": + ## Given / When + let confRes = parseWakuNodeConfFromJson("""{"mode": "Core", "clusterId": 42}""") + + ## Then + require confRes.isOk() + let conf = confRes.get() + check: + conf.mode == Core + conf.clusterId == 42 + + test "JSON with Edge mode": + ## Given / When + let confRes = parseWakuNodeConfFromJson("""{"mode": "Edge"}""") + + ## Then + require confRes.isOk() + let conf = confRes.get() + check: + conf.mode == Edge + + test "JSON with logLevel": + ## Given / When + let confRes = parseWakuNodeConfFromJson("""{"logLevel": "DEBUG"}""") + + ## Then + require confRes.isOk() + let conf = confRes.get() + check: + conf.logLevel == logging.LogLevel.DEBUG + + test "JSON with sharding config": + ## Given / When + let confRes = + parseWakuNodeConfFromJson("""{"clusterId": 99, "numShardsInNetwork": 16}""") + + ## Then + require confRes.isOk() + let conf = confRes.get() + check: + conf.clusterId == 99 + conf.numShardsInNetwork == 16 + + test "JSON with unknown fields is silently ignored": + ## Given / When + let confRes = + parseWakuNodeConfFromJson("""{"unknownField": true, "clusterId": 5}""") + + ## Then - unknown fields are just ignored (not in fieldPairs) + require confRes.isOk() + let conf = confRes.get() + check: + conf.clusterId == 5 + + test "Invalid JSON syntax returns error": + ## Given / When + let confRes = parseWakuNodeConfFromJson("{ not valid json }") + + ## Then + check confRes.isErr() + +suite "WakuNodeConf - preset integration": + test "TWN preset applies TheWakuNetworkConf": + ## Given + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.preset = "twn" + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.clusterId == 1 + + test "LogosDev preset applies LogosDevConf": + ## Given + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.preset = "logosdev" + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.clusterId == 2 + + test "Invalid preset returns error": + ## Given + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.preset = "nonexistent" + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + check wakuConfRes.isErr() + +suite "WakuNodeConf JSON -> WakuConf integration": + test "Core mode JSON config produces valid WakuConf": + ## Given + let confRes = parseWakuNodeConfFromJson( + """{"mode": "Core", "clusterId": 55, "numShardsInNetwork": 6}""" + ) + require confRes.isOk() + let conf = confRes.get() + + ## When + let wakuConfRes = conf.toWakuConf() ## Then require wakuConfRes.isOk() @@ -61,93 +248,72 @@ suite "LibWaku Conf - toWakuConf": wakuConf.relay == true wakuConf.lightPush == true wakuConf.peerExchangeService == true - wakuConf.clusterId == 1 + wakuConf.clusterId == 55 + wakuConf.shardingConf.numShardsInCluster == 6 - test "Auto-sharding configuration": + test "Edge mode JSON config produces valid WakuConf": ## Given - let nodeConfig = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], - staticStoreNodes = @[], - clusterId = 42, - autoShardingConfig = AutoShardingConfig(numShardsInCluster: 16), - ), - ) + let confRes = parseWakuNodeConfFromJson("""{"mode": "Edge", "clusterId": 1}""") + require confRes.isOk() + let conf = confRes.get() ## When - let wakuConfRes = toWakuConf(nodeConfig) + let wakuConfRes = conf.toWakuConf() ## Then require wakuConfRes.isOk() let wakuConf = wakuConfRes.get() require wakuConf.validate().isOk() check: - wakuConf.clusterId == 42 - wakuConf.shardingConf.numShardsInCluster == 16 + wakuConf.relay == false + wakuConf.lightPush == false + wakuConf.peerExchangeService == true - test "Bootstrap nodes configuration": + test "JSON with preset produces valid WakuConf": ## Given - let entryNodes = - @[ - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g", - "enr:-QEkuECnZ3IbVAgkOzv-QLnKC4dRKAPRY80m1-R7G8jZ7yfT3ipEfBrhKN7ARcQgQ-vg-h40AQzyvAkPYlHPaFKk6u9MBgmlkgnY0iXNlY3AyNTZrMaEDk49D8JjMSns4p1XVNBvJquOUzT4PENSJknkROspfAFGg3RjcIJ2X4N1ZHCCd2g", - ] - let libConf = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = entryNodes, staticStoreNodes = @[], clusterId = 1 - ), - ) + let confRes = + parseWakuNodeConfFromJson("""{"mode": "Core", "preset": "logosdev"}""") + require confRes.isOk() + let conf = confRes.get() ## When - let wakuConfRes = toWakuConf(libConf) - - ## Then - require wakuConfRes.isOk() - let wakuConf = wakuConfRes.get() - require wakuConf.validate().isOk() - require wakuConf.discv5Conf.isSome() - check: - wakuConf.discv5Conf.get().bootstrapNodes == entryNodes - - test "Static store nodes configuration": - ## Given - let staticStoreNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "/ip4/192.168.1.1/tcp/60001/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYd", - ] - let nodeConf = NodeConfig.init( - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], staticStoreNodes = staticStoreNodes, clusterId = 1 - ) - ) - - ## When - let wakuConfRes = toWakuConf(nodeConf) + let wakuConfRes = conf.toWakuConf() ## Then require wakuConfRes.isOk() let wakuConf = wakuConfRes.get() require wakuConf.validate().isOk() check: - wakuConf.staticNodes == staticStoreNodes + wakuConf.clusterId == 2 + wakuConf.relay == true - test "Message validation with max message size": + test "JSON with static nodes": ## Given - let nodeConfig = NodeConfig.init( - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], - staticStoreNodes = @[], - clusterId = 1, - messageValidation = - MessageValidation(maxMessageSize: "100KiB", rlnConfig: none(RlnConfig)), - ) + let confRes = parseWakuNodeConfFromJson( + """{"mode": "Core", "clusterId": 42, "staticnodes": ["/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc"]}""" ) + require confRes.isOk() + let conf = confRes.get() ## When - let wakuConfRes = toWakuConf(nodeConfig) + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.staticNodes.len == 1 + + test "JSON with max message size": + ## Given + let confRes = + parseWakuNodeConfFromJson("""{"clusterId": 42, "maxMessageSize": "100KiB"}""") + require confRes.isOk() + let conf = confRes.get() + + ## When + let wakuConfRes = conf.toWakuConf() ## Then require wakuConfRes.isOk() @@ -156,853 +322,49 @@ suite "LibWaku Conf - toWakuConf": check: wakuConf.maxMessageSizeBytes == 100'u64 * 1024'u64 - test "Message validation with RLN config": - ## Given - let nodeConfig = NodeConfig.init( - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], - clusterId = 1, - messageValidation = MessageValidation( - maxMessageSize: "150 KiB", - rlnConfig: some( - RlnConfig( - contractAddress: "0x1234567890123456789012345678901234567890", - chainId: 1'u, - epochSizeSec: 600'u64, - ) - ), - ), - ), - ethRpcEndpoints = @["http://127.0.0.1:1111"], - ) +# ---- Deprecated NodeConfig tests (kept for backward compatibility) ---- - ## When - let wakuConf = toWakuConf(nodeConfig).valueOr: - raiseAssert error +{.push warning[Deprecated]: off.} - wakuConf.validate().isOkOr: - raiseAssert error +import waku/api/api_conf - check: - wakuConf.maxMessageSizeBytes == 150'u64 * 1024'u64 - - require wakuConf.rlnRelayConf.isSome() - let rlnConf = wakuConf.rlnRelayConf.get() - check: - rlnConf.dynamic == true - rlnConf.ethContractAddress == "0x1234567890123456789012345678901234567890" - rlnConf.chainId == 1'u256 - rlnConf.epochSizeSec == 600'u64 - - test "Full Core mode configuration with all fields": - ## Given - let nodeConfig = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = - @[ - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g" - ], - staticStoreNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" - ], - clusterId = 99, - autoShardingConfig = AutoShardingConfig(numShardsInCluster: 12), - messageValidation = MessageValidation( - maxMessageSize: "512KiB", - rlnConfig: some( - RlnConfig( - contractAddress: "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - chainId: 5'u, # Goerli - epochSizeSec: 300'u64, - ) - ), - ), - ), - ethRpcEndpoints = @["https://127.0.0.1:8333"], - ) - - ## When - let wakuConfRes = toWakuConf(nodeConfig) - - ## Then +suite "NodeConfig (deprecated) - toWakuConf": + test "Minimal configuration": + let nodeConfig = NodeConfig.init(ethRpcEndpoints = @["http://someaddress"]) + let wakuConfRes = api_conf.toWakuConf(nodeConfig) let wakuConf = wakuConfRes.valueOr: raiseAssert error wakuConf.validate().isOkOr: raiseAssert error + check: + wakuConf.clusterId == 1 + wakuConf.shardingConf.numShardsInCluster == 8 + wakuConf.staticNodes.len == 0 - # Check basic settings + test "Edge mode configuration": + let protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 1) + let nodeConfig = + NodeConfig.init(mode = api_conf.WakuMode.Edge, protocolsConfig = protocolsConfig) + let wakuConfRes = api_conf.toWakuConf(nodeConfig) + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == false + wakuConf.lightPush == false + wakuConf.peerExchangeService == true + + test "Core mode configuration": + let protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 1) + let nodeConfig = + NodeConfig.init(mode = api_conf.WakuMode.Core, protocolsConfig = protocolsConfig) + let wakuConfRes = api_conf.toWakuConf(nodeConfig) + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() check: wakuConf.relay == true wakuConf.lightPush == true wakuConf.peerExchangeService == true - wakuConf.rendezvous == true - wakuConf.clusterId == 99 - # Check sharding - check: - wakuConf.shardingConf.numShardsInCluster == 12 - - # Check bootstrap nodes - require wakuConf.discv5Conf.isSome() - check: - wakuConf.discv5Conf.get().bootstrapNodes.len == 1 - - # Check static nodes - check: - wakuConf.staticNodes.len == 1 - wakuConf.staticNodes[0] == - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" - - # Check message validation - check: - wakuConf.maxMessageSizeBytes == 512'u64 * 1024'u64 - - # Check RLN config - require wakuConf.rlnRelayConf.isSome() - let rlnConf = wakuConf.rlnRelayConf.get() - check: - rlnConf.dynamic == true - rlnConf.ethContractAddress == "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - rlnConf.chainId == 5'u256 - rlnConf.epochSizeSec == 300'u64 - - test "NodeConfig with mixed entry nodes (integration test)": - ## Given - let entryNodes = - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - ] - - let nodeConfig = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = entryNodes, staticStoreNodes = @[], clusterId = 1 - ), - ) - - ## When - let wakuConfRes = toWakuConf(nodeConfig) - - ## Then - require wakuConfRes.isOk() - let wakuConf = wakuConfRes.get() - require wakuConf.validate().isOk() - - # Check that ENRTree went to DNS discovery - require wakuConf.dnsDiscoveryConf.isSome() - check: - wakuConf.dnsDiscoveryConf.get().enrTreeUrl == entryNodes[0] - - # Check that multiaddr went to static nodes - check: - wakuConf.staticNodes.len == 1 - wakuConf.staticNodes[0] == entryNodes[1] - -suite "NodeConfig JSON - complete format": - test "Full NodeConfig from complete JSON with field validation": - ## Given - let jsonStr = - """ - { - "mode": "Core", - "protocolsConfig": { - "entryNodes": ["enrtree://TREE@nodes.example.com"], - "staticStoreNodes": ["/ip4/1.2.3.4/tcp/80/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc"], - "clusterId": 10, - "autoShardingConfig": { - "numShardsInCluster": 4 - }, - "messageValidation": { - "maxMessageSize": "100 KiB", - "rlnConfig": null - } - }, - "networkingConfig": { - "listenIpv4": "192.168.1.1", - "p2pTcpPort": 7000, - "discv5UdpPort": 7001 - }, - "ethRpcEndpoints": ["http://localhost:8545"], - "p2pReliability": true, - "logLevel": "WARN", - "logFormat": "TEXT" - } - """ - - ## When - let config = decodeNodeConfigFromJson(jsonStr) - - ## Then — check every field - check: - config.mode == WakuMode.Core - config.ethRpcEndpoints == @["http://localhost:8545"] - config.p2pReliability == true - config.logLevel == LogLevel.WARN - config.logFormat == LogFormat.TEXT - - check: - config.networkingConfig.listenIpv4 == "192.168.1.1" - config.networkingConfig.p2pTcpPort == 7000 - config.networkingConfig.discv5UdpPort == 7001 - - let pc = config.protocolsConfig - check: - pc.entryNodes == @["enrtree://TREE@nodes.example.com"] - pc.staticStoreNodes == - @[ - "/ip4/1.2.3.4/tcp/80/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" - ] - pc.clusterId == 10 - pc.autoShardingConfig.numShardsInCluster == 4 - pc.messageValidation.maxMessageSize == "100 KiB" - pc.messageValidation.rlnConfig.isNone() - - test "Full NodeConfig with RlnConfig present": - ## Given - let jsonStr = - """ - { - "mode": "Edge", - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1, - "messageValidation": { - "maxMessageSize": "150 KiB", - "rlnConfig": { - "contractAddress": "0x1234567890ABCDEF1234567890ABCDEF12345678", - "chainId": 5, - "epochSizeSec": 600 - } - } - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - ## When - let config = decodeNodeConfigFromJson(jsonStr) - - ## Then - check config.mode == WakuMode.Edge - - let mv = config.protocolsConfig.messageValidation - check: - mv.maxMessageSize == "150 KiB" - mv.rlnConfig.isSome() - let rln = mv.rlnConfig.get() - check: - rln.contractAddress == "0x1234567890ABCDEF1234567890ABCDEF12345678" - rln.chainId == 5'u - rln.epochSizeSec == 600'u64 - - test "Round-trip encode/decode preserves all fields": - ## Given - let original = NodeConfig.init( - mode = Edge, - protocolsConfig = ProtocolsConfig.init( - entryNodes = @["enrtree://TREE@example.com"], - staticStoreNodes = - @[ - "/ip4/1.2.3.4/tcp/80/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" - ], - clusterId = 42, - autoShardingConfig = AutoShardingConfig(numShardsInCluster: 16), - messageValidation = MessageValidation( - maxMessageSize: "256 KiB", - rlnConfig: some( - RlnConfig( - contractAddress: "0xAABBCCDDEEFF00112233445566778899AABBCCDD", - chainId: 137, - epochSizeSec: 300, - ) - ), - ), - ), - networkingConfig = - NetworkingConfig(listenIpv4: "10.0.0.1", p2pTcpPort: 9090, discv5UdpPort: 9091), - ethRpcEndpoints = @["https://rpc.example.com"], - p2pReliability = true, - logLevel = LogLevel.DEBUG, - logFormat = LogFormat.JSON, - ) - - ## When - let decoded = decodeNodeConfigFromJson(Json.encode(original)) - - ## Then — check field by field - check: - decoded.mode == original.mode - decoded.ethRpcEndpoints == original.ethRpcEndpoints - decoded.p2pReliability == original.p2pReliability - decoded.logLevel == original.logLevel - decoded.logFormat == original.logFormat - decoded.networkingConfig.listenIpv4 == original.networkingConfig.listenIpv4 - decoded.networkingConfig.p2pTcpPort == original.networkingConfig.p2pTcpPort - decoded.networkingConfig.discv5UdpPort == original.networkingConfig.discv5UdpPort - decoded.protocolsConfig.entryNodes == original.protocolsConfig.entryNodes - decoded.protocolsConfig.staticStoreNodes == - original.protocolsConfig.staticStoreNodes - decoded.protocolsConfig.clusterId == original.protocolsConfig.clusterId - decoded.protocolsConfig.autoShardingConfig.numShardsInCluster == - original.protocolsConfig.autoShardingConfig.numShardsInCluster - decoded.protocolsConfig.messageValidation.maxMessageSize == - original.protocolsConfig.messageValidation.maxMessageSize - decoded.protocolsConfig.messageValidation.rlnConfig.isSome() - - let decodedRln = decoded.protocolsConfig.messageValidation.rlnConfig.get() - let originalRln = original.protocolsConfig.messageValidation.rlnConfig.get() - check: - decodedRln.contractAddress == originalRln.contractAddress - decodedRln.chainId == originalRln.chainId - decodedRln.epochSizeSec == originalRln.epochSizeSec - -suite "NodeConfig JSON - partial format with defaults": - test "Minimal NodeConfig - empty object uses all defaults": - ## Given - let config = decodeNodeConfigFromJson("{}") - let defaultConfig = NodeConfig.init() - - ## Then — compare field by field against defaults - check: - config.mode == defaultConfig.mode - config.ethRpcEndpoints == defaultConfig.ethRpcEndpoints - config.p2pReliability == defaultConfig.p2pReliability - config.logLevel == defaultConfig.logLevel - config.logFormat == defaultConfig.logFormat - config.networkingConfig.listenIpv4 == defaultConfig.networkingConfig.listenIpv4 - config.networkingConfig.p2pTcpPort == defaultConfig.networkingConfig.p2pTcpPort - config.networkingConfig.discv5UdpPort == - defaultConfig.networkingConfig.discv5UdpPort - config.protocolsConfig.entryNodes == defaultConfig.protocolsConfig.entryNodes - config.protocolsConfig.staticStoreNodes == - defaultConfig.protocolsConfig.staticStoreNodes - config.protocolsConfig.clusterId == defaultConfig.protocolsConfig.clusterId - config.protocolsConfig.autoShardingConfig.numShardsInCluster == - defaultConfig.protocolsConfig.autoShardingConfig.numShardsInCluster - config.protocolsConfig.messageValidation.maxMessageSize == - defaultConfig.protocolsConfig.messageValidation.maxMessageSize - config.protocolsConfig.messageValidation.rlnConfig.isSome() == - defaultConfig.protocolsConfig.messageValidation.rlnConfig.isSome() - - test "Minimal NodeConfig keeps network preset defaults": - ## Given - let config = decodeNodeConfigFromJson("{}") - - ## Then - check: - config.protocolsConfig.entryNodes == TheWakuNetworkPreset.entryNodes - config.protocolsConfig.messageValidation.rlnConfig.isSome() - - test "NodeConfig with only mode specified": - ## Given - let config = decodeNodeConfigFromJson("""{"mode": "Edge"}""") - - ## Then - check: - config.mode == WakuMode.Edge - ## Remaining fields get defaults - config.logLevel == LogLevel.INFO - config.logFormat == LogFormat.TEXT - config.p2pReliability == false - config.ethRpcEndpoints == newSeq[string]() - - test "ProtocolsConfig partial - optional fields get defaults": - ## Given — only entryNodes and clusterId provided - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": ["enrtree://X@y.com"], - "clusterId": 5 - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - ## When - let config = decodeNodeConfigFromJson(jsonStr) - - ## Then — required fields are set, optionals get defaults - check: - config.protocolsConfig.entryNodes == @["enrtree://X@y.com"] - config.protocolsConfig.clusterId == 5 - config.protocolsConfig.staticStoreNodes == newSeq[string]() - config.protocolsConfig.autoShardingConfig.numShardsInCluster == - DefaultAutoShardingConfig.numShardsInCluster - config.protocolsConfig.messageValidation.maxMessageSize == - DefaultMessageValidation.maxMessageSize - config.protocolsConfig.messageValidation.rlnConfig.isNone() - - test "MessageValidation partial - rlnConfig omitted defaults to none": - ## Given - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1, - "messageValidation": { - "maxMessageSize": "200 KiB" - } - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - ## When - let config = decodeNodeConfigFromJson(jsonStr) - - ## Then - check: - config.protocolsConfig.messageValidation.maxMessageSize == "200 KiB" - config.protocolsConfig.messageValidation.rlnConfig.isNone() - - test "logLevel and logFormat omitted use defaults": - ## Given - let jsonStr = - """ - { - "mode": "Core", - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1 - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - ## When - let config = decodeNodeConfigFromJson(jsonStr) - - ## Then - check: - config.logLevel == LogLevel.INFO - config.logFormat == LogFormat.TEXT - -suite "NodeConfig JSON - unsupported fields raise errors": - test "Unknown field at NodeConfig level raises": - let jsonStr = - """ - { - "mode": "Core", - "unknownTopLevel": true - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Typo in NodeConfig field name raises": - let jsonStr = - """ - { - "modes": "Core" - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Unknown field in ProtocolsConfig raises": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1, - "futureField": "something" - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Unknown field in NetworkingConfig raises": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1 - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000, - "futureNetworkField": "value" - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Unknown field in MessageValidation raises": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1, - "messageValidation": { - "maxMessageSize": "150 KiB", - "maxMesssageSize": "typo" - } - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Unknown field in RlnConfig raises": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1, - "messageValidation": { - "maxMessageSize": "150 KiB", - "rlnConfig": { - "contractAddress": "0xABCDEF1234567890ABCDEF1234567890ABCDEF12", - "chainId": 1, - "epochSizeSec": 600, - "unknownRlnField": true - } - } - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Unknown field in AutoShardingConfig raises": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1, - "autoShardingConfig": { - "numShardsInCluster": 8, - "shardPrefix": "extra" - } - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - -suite "NodeConfig JSON - missing required fields": - test "Missing 'entryNodes' in ProtocolsConfig": - let jsonStr = - """ - { - "protocolsConfig": { - "clusterId": 1 - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Missing 'clusterId' in ProtocolsConfig": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [] - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Missing required fields in NetworkingConfig": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1 - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0" - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Missing 'numShardsInCluster' in AutoShardingConfig": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1, - "autoShardingConfig": {} - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Missing required fields in RlnConfig": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1, - "messageValidation": { - "maxMessageSize": "150 KiB", - "rlnConfig": { - "contractAddress": "0xABCD" - } - } - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Missing 'maxMessageSize' in MessageValidation": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": 1, - "messageValidation": { - "rlnConfig": null - } - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - -suite "NodeConfig JSON - invalid values": - test "Invalid enum value for mode": - var raised = false - try: - discard decodeNodeConfigFromJson("""{"mode": "InvalidMode"}""") - except SerializationError: - raised = true - check raised - - test "Invalid enum value for logLevel": - var raised = false - try: - discard decodeNodeConfigFromJson("""{"logLevel": "SUPERVERBOSE"}""") - except SerializationError: - raised = true - check raised - - test "Wrong type for clusterId (string instead of number)": - let jsonStr = - """ - { - "protocolsConfig": { - "entryNodes": [], - "clusterId": "not-a-number" - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - } - } - """ - - var raised = false - try: - discard decodeNodeConfigFromJson(jsonStr) - except SerializationError: - raised = true - check raised - - test "Completely invalid JSON syntax": - var raised = false - try: - discard decodeNodeConfigFromJson("""{ not valid json at all }""") - except SerializationError: - raised = true - check raised - -suite "NodeConfig JSON -> WakuConf integration": - test "Decoded config translates to valid WakuConf": - ## Given - let jsonStr = - """ - { - "mode": "Core", - "protocolsConfig": { - "entryNodes": [ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im" - ], - "staticStoreNodes": [ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" - ], - "clusterId": 55, - "autoShardingConfig": { - "numShardsInCluster": 6 - }, - "messageValidation": { - "maxMessageSize": "256 KiB", - "rlnConfig": null - } - }, - "networkingConfig": { - "listenIpv4": "0.0.0.0", - "p2pTcpPort": 60000, - "discv5UdpPort": 9000 - }, - "ethRpcEndpoints": ["http://localhost:8545"], - "p2pReliability": true, - "logLevel": "INFO", - "logFormat": "TEXT" - } - """ - - ## When - let nodeConfig = decodeNodeConfigFromJson(jsonStr) - let wakuConfRes = toWakuConf(nodeConfig) - - ## Then - require wakuConfRes.isOk() - let wakuConf = wakuConfRes.get() - require wakuConf.validate().isOk() - check: - wakuConf.clusterId == 55 - wakuConf.shardingConf.numShardsInCluster == 6 - wakuConf.maxMessageSizeBytes == 256'u64 * 1024'u64 - wakuConf.staticNodes.len == 1 - wakuConf.p2pReliability == true +{.pop.} diff --git a/tests/test_waku.nim b/tests/test_waku.nim index b8e2b26b1..dabd65af7 100644 --- a/tests/test_waku.nim +++ b/tests/test_waku.nim @@ -3,49 +3,49 @@ import chronos, testutils/unittests, std/options import waku +import tools/confutils/cli_args suite "Waku API - Create node": asyncTest "Create node with minimal configuration": ## Given - let nodeConfig = NodeConfig.init( - protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 1) - ) + var nodeConf = defaultWakuNodeConf().valueOr: + raiseAssert error + nodeConf.mode = Core + nodeConf.clusterId = 3'u16 + nodeConf.rest = false # This is the actual minimal config but as the node auto-start, it is not suitable for tests - # NodeConfig.init(ethRpcEndpoints = @["http://someaddress"]) ## When - let node = (await createNode(nodeConfig)).valueOr: + let node = (await createNode(nodeConf)).valueOr: raiseAssert error ## Then check: not node.isNil() - node.conf.clusterId == 1 + node.conf.clusterId == 3 node.conf.relay == true asyncTest "Create node with full configuration": ## Given - let nodeConfig = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = - @[ - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g" - ], - staticStoreNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" - ], - clusterId = 99, - autoShardingConfig = AutoShardingConfig(numShardsInCluster: 16), - messageValidation = - MessageValidation(maxMessageSize: "1024 KiB", rlnConfig: none(RlnConfig)), - ), - ) + var nodeConf = defaultWakuNodeConf().valueOr: + raiseAssert error + nodeConf.mode = Core + nodeConf.clusterId = 99'u16 + nodeConf.rest = false + nodeConf.numShardsInNetwork = 16 + nodeConf.maxMessageSize = "1024 KiB" + nodeConf.entryNodes = + @[ + "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g" + ] + nodeConf.staticnodes = + @[ + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" + ] ## When - let node = (await createNode(nodeConfig)).valueOr: + let node = (await createNode(nodeConf)).valueOr: raiseAssert error ## Then @@ -62,20 +62,19 @@ suite "Waku API - Create node": asyncTest "Create node with mixed entry nodes (enrtree, multiaddr)": ## Given - let nodeConfig = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - ], - clusterId = 42, - ), - ) + var nodeConf = defaultWakuNodeConf().valueOr: + raiseAssert error + nodeConf.mode = Core + nodeConf.clusterId = 42'u16 + nodeConf.rest = false + nodeConf.entryNodes = + @[ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + ] ## When - let node = (await createNode(nodeConfig)).valueOr: + let node = (await createNode(nodeConf)).valueOr: raiseAssert error ## Then diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 5e4adacb2..d8bd9b7f5 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -30,7 +30,8 @@ import waku_core/message/default_values, waku_mix, ], - ../../tools/rln_keystore_generator/rln_keystore_generator + ../../tools/rln_keystore_generator/rln_keystore_generator, + ./entry_nodes import ./envvar as confEnvvarDefs, ./envvar_net as confEnvvarNet @@ -52,6 +53,11 @@ type StartUpCommand* = enum noCommand # default, runs waku generateRlnKeystore # generates a new RLN keystore +type WakuMode* {.pure.} = enum + noMode # default - use explicit CLI flags as-is + Core # full service node + Edge # client-only node + type WakuNodeConf* = object configFile* {. desc: "Loads configuration from a TOML file (cmd-line parameters take precedence)", @@ -150,9 +156,16 @@ type WakuNodeConf* = object .}: seq[ProtectedShard] ## General node config + mode* {. + desc: + "Node operation mode. 'Core' enables relay+service protocols. 'Edge' enables client-only protocols. Default: explicit CLI flags used.", + defaultValue: WakuMode.noMode, + name: "mode" + .}: WakuMode + preset* {. desc: - "Network preset to use. 'twn' is The RLN-protected Waku Network (cluster 1). Overrides other values.", + "Network preset to use. 'twn' is The RLN-protected Waku Network (cluster 1). 'logos.dev' is the Logos Dev Network (cluster 2). Overrides other values.", defaultValue: "", name: "preset" .}: string @@ -165,7 +178,7 @@ type WakuNodeConf* = object .}: uint16 agentString* {. - defaultValue: "nwaku-" & cli_args.git_version, + defaultValue: "logos-delivery-" & cli_args.git_version, desc: "Node agent string which is used as identifier in network", name: "agent-string" .}: string @@ -293,6 +306,14 @@ hence would have reachability issues.""", name: "rln-relay-dynamic" .}: bool + entryNodes* {. + desc: + "Entry node address (enrtree:, enr:, or multiaddr). " & + "Automatically classified and distributed to DNS discovery, discv5 bootstrap, " & + "and static nodes. Argument may be repeated.", + name: "entry-node" + .}: seq[string] + staticnodes* {. desc: "Peer multiaddr to directly connect with. Argument may be repeated.", name: "staticnode" @@ -453,7 +474,7 @@ hence would have reachability issues.""", desc: """Adds an extra effort in the delivery/reception of messages by leveraging store-v3 requests. with the drawback of consuming some more bandwidth.""", - defaultValue: false, + defaultValue: true, name: "reliability" .}: bool @@ -907,12 +928,19 @@ proc toNetworkConf( "TWN - The Waku Network configuration will not be applied when `--cluster-id=1` is passed in future releases. Use `--preset=twn` instead." ) lcPreset = "twn" + if clusterId.isSome() and clusterId.get() == 2: + warn( + "Logos.dev - Logos.dev configuration will not be applied when `--cluster-id=2` is passed in future releases. Use `--preset=logos.dev` instead." + ) + lcPreset = "logos.dev" case lcPreset of "": ok(none(NetworkConf)) of "twn": ok(some(NetworkConf.TheWakuNetworkConf())) + of "logos.dev", "logosdev": + ok(some(NetworkConf.LogosDevConf())) else: err("Invalid --preset value passed: " & lcPreset) @@ -982,6 +1010,26 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.withRelayShardedPeerManagement(n.relayShardedPeerManagement) b.withStaticNodes(n.staticNodes) + # Process entry nodes - supports enrtree:, enr:, and multiaddress formats + if n.entryNodes.len > 0: + let (enrTreeUrls, bootstrapEnrs, staticNodesFromEntry) = processEntryNodes( + n.entryNodes + ).valueOr: + return err("Failed to process entry nodes: " & error) + + # Set ENRTree URLs for DNS discovery + if enrTreeUrls.len > 0: + for url in enrTreeUrls: + b.dnsDiscoveryConf.withEnrTreeUrl(url) + + # Set ENR records as bootstrap nodes for discv5 + if bootstrapEnrs.len > 0: + b.discv5Conf.withBootstrapNodes(bootstrapEnrs) + + # Add static nodes (multiaddrs and those extracted from ENR entries) + if staticNodesFromEntry.len > 0: + b.withStaticNodes(staticNodesFromEntry) + if n.numShardsInNetwork != 0: b.withNumShardsInCluster(n.numShardsInNetwork) b.withShardingConf(AutoSharding) @@ -1069,9 +1117,31 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.webSocketConf.withKeyPath(n.websocketSecureKeyPath) b.webSocketConf.withCertPath(n.websocketSecureCertPath) - b.rateLimitConf.withRateLimits(n.rateLimits) + if n.rateLimits.len > 0: + b.rateLimitConf.withRateLimits(n.rateLimits) b.kademliaDiscoveryConf.withEnabled(n.enableKadDiscovery) b.kademliaDiscoveryConf.withBootstrapNodes(n.kadBootstrapNodes) + # Mode-driven configuration overrides + case n.mode + of WakuMode.Core: + b.withRelay(true) + b.filterServiceConf.withEnabled(true) + b.withLightPush(true) + b.discv5Conf.withEnabled(true) + b.withPeerExchange(true) + b.withRendezvous(true) + b.rateLimitConf.withRateLimitsIfNotAssigned( + @["filter:100/1s", "lightpush:5/1s", "px:5/1s"] + ) + of WakuMode.Edge: + b.withPeerExchange(true) + b.withRelay(false) + b.filterServiceConf.withEnabled(false) + b.withLightPush(false) + b.storeServiceConf.withEnabled(false) + of WakuMode.noMode: + discard # use explicit CLI flags as-is + return b.build() diff --git a/waku/api/entry_nodes.nim b/tools/confutils/entry_nodes.nim similarity index 100% rename from waku/api/entry_nodes.nim rename to tools/confutils/entry_nodes.nim diff --git a/waku/api.nim b/waku/api.nim index 110a8f431..a977a062a 100644 --- a/waku/api.nim +++ b/waku/api.nim @@ -1,4 +1,5 @@ -import ./api/[api, api_conf, entry_nodes] +import ./api/[api, api_conf] import ./events/message_events +import tools/confutils/entry_nodes export api, api_conf, entry_nodes, message_events diff --git a/waku/api/api.nim b/waku/api/api.nim index ba6f83b78..1eee982fd 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -1,18 +1,20 @@ -import chronicles, chronos, results, std/strutils +import chronicles, chronos, results import waku/factory/waku import waku/[requests/health_requests, waku_core, waku_node] import waku/node/delivery_service/send_service import waku/node/delivery_service/subscription_manager import libp2p/peerid +import ../../tools/confutils/cli_args import ./[api_conf, types] +export cli_args + logScope: topics = "api" -# TODO: Specs says it should return a `WakuNode`. As `send` and other APIs are defined, we can align. -proc createNode*(config: NodeConfig): Future[Result[Waku, string]] {.async.} = - let wakuConf = toWakuConf(config).valueOr: +proc createNode*(conf: WakuNodeConf): Future[Result[Waku, string]] {.async.} = + let wakuConf = conf.toWakuConf().valueOr: return err("Failed to handle the configuration: " & error) ## We are not defining app callbacks at node creation diff --git a/waku/api/api_conf.nim b/waku/api/api_conf.nim index 7cac66426..70bb02af3 100644 --- a/waku/api/api_conf.nim +++ b/waku/api/api_conf.nim @@ -9,7 +9,7 @@ import waku/factory/waku_conf, waku/factory/conf_builder/conf_builder, waku/factory/networks_config, - ./entry_nodes + tools/confutils/entry_nodes export json_serialization, json_options @@ -85,7 +85,9 @@ type WakuMode* {.pure.} = enum Edge Core -type NodeConfig* {.requiresInit.} = object +type NodeConfig* {. + requiresInit, deprecated: "Use WakuNodeConf from tools/confutils/cli_args instead" +.} = object mode: WakuMode protocolsConfig: ProtocolsConfig networkingConfig: NetworkingConfig @@ -154,7 +156,9 @@ proc logLevel*(c: NodeConfig): LogLevel = proc logFormat*(c: NodeConfig): LogFormat = c.logFormat -proc toWakuConf*(nodeConfig: NodeConfig): Result[WakuConf, string] = +proc toWakuConf*( + nodeConfig: NodeConfig +): Result[WakuConf, string] {.deprecated: "Use WakuNodeConf.toWakuConf instead".} = var b = WakuConfBuilder.init() # Apply log configuration @@ -516,7 +520,10 @@ proc readValue*( proc decodeNodeConfigFromJson*( jsonStr: string -): NodeConfig {.raises: [SerializationError].} = +): NodeConfig {. + raises: [SerializationError], + deprecated: "Use WakuNodeConf with fieldPairs-based JSON parsing instead" +.} = var val = NodeConfig.init() # default-initialized try: var stream = unsafeMemoryInput(jsonStr) diff --git a/waku/factory/conf_builder/filter_service_conf_builder.nim b/waku/factory/conf_builder/filter_service_conf_builder.nim index a3f056b01..0a6617430 100644 --- a/waku/factory/conf_builder/filter_service_conf_builder.nim +++ b/waku/factory/conf_builder/filter_service_conf_builder.nim @@ -22,6 +22,12 @@ proc withEnabled*(b: var FilterServiceConfBuilder, enabled: bool) = proc withMaxPeersToServe*(b: var FilterServiceConfBuilder, maxPeersToServe: uint32) = b.maxPeersToServe = some(maxPeersToServe) +proc withMaxPeersToServeIfNotAssigned*( + b: var FilterServiceConfBuilder, maxPeersToServe: uint32 +) = + if b.maxPeersToServe.isNone(): + b.maxPeersToServe = some(maxPeersToServe) + proc withSubscriptionTimeout*( b: var FilterServiceConfBuilder, subscriptionTimeout: uint16 ) = diff --git a/waku/factory/conf_builder/rate_limit_conf_builder.nim b/waku/factory/conf_builder/rate_limit_conf_builder.nim index 0d466a132..b2edbef03 100644 --- a/waku/factory/conf_builder/rate_limit_conf_builder.nim +++ b/waku/factory/conf_builder/rate_limit_conf_builder.nim @@ -14,6 +14,12 @@ proc init*(T: type RateLimitConfBuilder): RateLimitConfBuilder = proc withRateLimits*(b: var RateLimitConfBuilder, rateLimits: seq[string]) = b.strValue = some(rateLimits) +proc withRateLimitsIfNotAssigned*( + b: var RateLimitConfBuilder, rateLimits: seq[string] +) = + if b.strValue.isNone() or b.strValue.get().len == 0: + b.strValue = some(rateLimits) + proc build*(b: RateLimitConfBuilder): Result[ProtocolRateLimitSettings, string] = if b.strValue.isSome() and b.objValue.isSome(): return err("Rate limits conf must only be set once on the builder") diff --git a/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index e51f02dbd..2c427918d 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -12,7 +12,8 @@ import ../networks_config, ../../common/logging, ../../common/utils/parse_size_units, - ../../waku_enr/capabilities + ../../waku_enr/capabilities, + tools/confutils/entry_nodes import ./filter_service_conf_builder, @@ -393,6 +394,42 @@ proc applyNetworkConf(builder: var WakuConfBuilder) = discarded = builder.discv5Conf.bootstrapNodes builder.discv5Conf.withBootstrapNodes(networkConf.discv5BootstrapNodes) + if networkConf.enableKadDiscovery: + if not builder.kademliaDiscoveryConf.enabled: + builder.kademliaDiscoveryConf.withEnabled(networkConf.enableKadDiscovery) + + if builder.kademliaDiscoveryConf.bootstrapNodes.len == 0 and + networkConf.kadBootstrapNodes.len > 0: + builder.kademliaDiscoveryConf.withBootstrapNodes(networkConf.kadBootstrapNodes) + + if networkConf.mix: + if builder.mix.isNone: + builder.mix = some(networkConf.mix) + + if builder.p2pReliability.isNone: + builder.withP2pReliability(networkConf.p2pReliability) + + # Process entry nodes from network config - classify and distribute + if networkConf.entryNodes.len > 0: + let processed = processEntryNodes(networkConf.entryNodes) + if processed.isOk(): + let (enrTreeUrls, bootstrapEnrs, staticNodesFromEntry) = processed.get() + + # Set ENRTree URLs for DNS discovery + if enrTreeUrls.len > 0: + for url in enrTreeUrls: + builder.dnsDiscoveryConf.withEnrTreeUrl(url) + + # Set ENR records as bootstrap nodes for discv5 + if bootstrapEnrs.len > 0: + builder.discv5Conf.withBootstrapNodes(bootstrapEnrs) + + # Add static nodes (multiaddrs and those extracted from ENR entries) + if staticNodesFromEntry.len > 0: + builder.withStaticNodes(staticNodesFromEntry) + else: + warn "Failed to process entry nodes from network conf", error = processed.error() + proc build*( builder: var WakuConfBuilder, rng: ref HmacDrbgContext = crypto.newRng() ): Result[WakuConf, string] = @@ -606,7 +643,7 @@ proc build*( provided = maxConnections, recommended = DefaultMaxConnections # TODO: Do the git version thing here - let agentString = builder.agentString.get("nwaku") + let agentString = builder.agentString.get("logos-delivery") # TODO: use `DefaultColocationLimit`. the user of this value should # probably be defining a config object diff --git a/waku/factory/networks_config.nim b/waku/factory/networks_config.nim index c7193aa9c..94856fb21 100644 --- a/waku/factory/networks_config.nim +++ b/waku/factory/networks_config.nim @@ -29,6 +29,11 @@ type NetworkConf* = object shardingConf*: ShardingConf discv5Discovery*: bool discv5BootstrapNodes*: seq[string] + enableKadDiscovery*: bool + kadBootstrapNodes*: seq[string] + entryNodes*: seq[string] + mix*: bool + p2pReliability*: bool # cluster-id=1 (aka The Waku Network) # Cluster configuration corresponding to The Waku Network. Note that it @@ -45,6 +50,11 @@ proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf = rlnEpochSizeSec: 600, rlnRelayUserMessageLimit: 100, shardingConf: ShardingConf(kind: AutoSharding, numShardsInCluster: 8), + enableKadDiscovery: false, + kadBootstrapNodes: @[], + entryNodes: @[], + mix: false, + p2pReliability: false, discv5Discovery: true, discv5BootstrapNodes: @[ @@ -54,6 +64,36 @@ proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf = ], ) +# cluster-id=2 (Logos Dev Network) +# Cluster configuration for the Logos Dev Network. +proc LogosDevConf*(T: type NetworkConf): NetworkConf = + const ZeroChainId = 0'u256 + return NetworkConf( + maxMessageSize: "150KiB", + clusterId: 2, + rlnRelay: false, + rlnRelayEthContractAddress: "", + rlnRelayDynamic: false, + rlnRelayChainId: ZeroChainId, + rlnEpochSizeSec: 0, + rlnRelayUserMessageLimit: 0, + shardingConf: ShardingConf(kind: AutoSharding, numShardsInCluster: 8), + enableKadDiscovery: true, + mix: true, + p2pReliability: true, + discv5Discovery: true, + discv5BootstrapNodes: @[], + entryNodes: + @[ + "/dns4/delivery-01.do-ams3.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby", + "/dns4/delivery-02.do-ams3.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAmMK7PYygBtKUQ8EHp7EfaD3bCEsJrkFooK8RQ2PVpJprH", + "/dns4/delivery-01.gc-us-central1-a.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm4S1JYkuzDKLKQvwgAhZKs9otxXqt8SCGtB4hoJP1S397", + "/dns4/delivery-02.gc-us-central1-a.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm8Y9kgBNtjxvCnf1X6gnZJW5EGE4UwwCL3CCm55TwqBiH", + "/dns4/delivery-01.ac-cn-hongkong-c.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm8YokiNun9BkeA1ZRmhLbtNUvcwRr64F69tYj9fkGyuEP", + "/dns4/delivery-02.ac-cn-hongkong-c.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAkvwhGHKNry6LACrB8TmEFoCJKEX29XR5dDUzk3UT3UNSE", + ], + ) + proc validateShards*( shardingConf: ShardingConf, shards: seq[uint16] ): Result[void, string] = diff --git a/waku/rest_api/endpoint/builder.nim b/waku/rest_api/endpoint/builder.nim index bbd8de422..41ab7e06b 100644 --- a/waku/rest_api/endpoint/builder.nim +++ b/waku/rest_api/endpoint/builder.nim @@ -28,7 +28,6 @@ import # It will always be called from main thread anyway. # Ref: https://nim-lang.org/docs/manual.html#threads-gc-safety var restServerNotInstalledTab {.threadvar.}: TableRef[string, string] -restServerNotInstalledTab = newTable[string, string]() export WakuRestServerRef @@ -42,6 +41,9 @@ type RestServerConf* = object proc startRestServerEssentials*( nodeHealthMonitor: NodeHealthMonitor, conf: RestServerConf, portsShift: uint16 ): Result[WakuRestServerRef, string] = + if restServerNotInstalledTab.isNil: + restServerNotInstalledTab = newTable[string, string]() + let requestErrorHandler: RestRequestErrorHandler = proc( error: RestRequestError, request: HttpRequestRef ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = From 4a6ad732356c99d00c5f0143df8a470b84b6281d Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:48:00 +0100 Subject: [PATCH 078/155] Chore: adapt debugapi to wakonodeconf (#3745) * logosdelivery_get_available_configs collects and format WakuNodeConf options * simplify debug config output --- .../logos_delivery_api/debug_api.nim | 22 ++- tools/confutils/cli_args.nim | 4 +- tools/confutils/config_option_meta.nim | 143 ++++++++++++++++++ 3 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 tools/confutils/config_option_meta.nim diff --git a/liblogosdelivery/logos_delivery_api/debug_api.nim b/liblogosdelivery/logos_delivery_api/debug_api.nim index bee8ab537..623b3b08f 100644 --- a/liblogosdelivery/logos_delivery_api/debug_api.nim +++ b/liblogosdelivery/logos_delivery_api/debug_api.nim @@ -1,5 +1,6 @@ import std/[json, strutils] import waku/factory/waku_state_info +import tools/confutils/[cli_args, config_option_meta] proc logosdelivery_get_available_node_info_ids( ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer @@ -33,14 +34,21 @@ proc logosdelivery_get_available_configs( ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer ) {.ffi.} = ## Returns information about the accepted config items. - ## For analogy with a CLI app, this is the info when typing --help for a command. requireInitializedNode(ctx, "GetAvailableConfigs"): return err(errMsg) - ## TODO: we are now returning a simple default value for NodeConfig. - ## The NodeConfig struct is too complex and we need to have a flattened simpler config. - ## The expected returned value for this is a list of possible config items with their - ## description, accepted values, default value, etc. + let optionMetas: seq[ConfigOptionMeta] = extractConfigOptionMeta(WakuNodeConf) + var configOptionDetails = newJArray() - let defaultConfig = NodeConfig.init() - return ok($(%*defaultConfig)) + # for confField, confValue in fieldPairs(conf): + # defaultConfig[confField] = $confValue + + for meta in optionMetas: + configOptionDetails.add( + %*{meta.fieldName: meta.typeName & "(" & meta.defaultValue & ")", "desc": meta.desc} + ) + + var jsonNode = newJObject() + jsonNode["configOptions"] = configOptionDetails + let asString = pretty(jsonNode) + return ok(pretty(jsonNode)) diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index d8bd9b7f5..4a6e8c618 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -480,7 +480,9 @@ with the drawback of consuming some more bandwidth.""", ## REST HTTP config rest* {. - desc: "Enable Waku REST HTTP server: true|false", defaultValue: true, name: "rest" + desc: "Enable Waku REST HTTP server: true|false", + defaultValue: false, + name: "rest" .}: bool restAddress* {. diff --git a/tools/confutils/config_option_meta.nim b/tools/confutils/config_option_meta.nim new file mode 100644 index 000000000..1880fdef5 --- /dev/null +++ b/tools/confutils/config_option_meta.nim @@ -0,0 +1,143 @@ +import std/[macros] + +type ConfigOptionMeta* = object + fieldName*: string + typeName*: string + cliName*: string + desc*: string + defaultValue*: string + command*: string + +proc getPragmaValue(pragmaNode: NimNode, pragmaName: string): string {.compileTime.} = + if pragmaNode.kind != nnkPragma: + return "" + + for item in pragmaNode: + if item.kind == nnkExprColonExpr and item[0].eqIdent(pragmaName): + return item[1].repr + + return "" + +proc getFieldName(fieldNode: NimNode): string {.compileTime.} = + case fieldNode.kind + of nnkPragmaExpr: + if fieldNode.len >= 1: + return getFieldName(fieldNode[0]) + of nnkPostfix: + if fieldNode.len >= 2: + return getFieldName(fieldNode[1]) + of nnkIdent, nnkSym: + return fieldNode.strVal + else: + discard + + return fieldNode.repr + +proc getFieldAndPragma( + fieldDef: NimNode +): tuple[fieldName, typeName: string, pragmaNode: NimNode] {.compileTime.} = + if fieldDef.kind != nnkIdentDefs: + return ("", "", newNimNode(nnkEmpty)) + + let declaredField = fieldDef[0] + var typeNode = fieldDef[1] + var pragmaNode = newNimNode(nnkEmpty) + + if declaredField.kind == nnkPragmaExpr: + pragmaNode = declaredField[1] + elif typeNode.kind == nnkPragmaExpr: + pragmaNode = typeNode[1] + typeNode = typeNode[0] + + return (getFieldName(declaredField), typeNode.repr, pragmaNode) + +proc makeMetaNode( + fieldName, typeName, cliName, desc, defaultValue, command: string +): NimNode {.compileTime.} = + result = newTree( + nnkObjConstr, + ident("ConfigOptionMeta"), + newTree(nnkExprColonExpr, ident("fieldName"), newLit(fieldName)), + newTree(nnkExprColonExpr, ident("typeName"), newLit(typeName)), + newTree(nnkExprColonExpr, ident("cliName"), newLit(cliName)), + newTree(nnkExprColonExpr, ident("desc"), newLit(desc)), + newTree(nnkExprColonExpr, ident("defaultValue"), newLit(defaultValue)), + newTree(nnkExprColonExpr, ident("command"), newLit(command)), + ) + +macro extractConfigOptionMeta*(T: typedesc): untyped = + proc findFirstRecList(n: NimNode): NimNode {.compileTime.} = + if n.kind == nnkRecList: + return n + for child in n: + let found = findFirstRecList(child) + if not found.isNil: + return found + return nil + + proc collectRecList( + recList: NimNode, metas: var seq[NimNode], commandCtx: string + ) {.compileTime.} = + for child in recList: + case child.kind + of nnkIdentDefs: + let (fieldName, typeName, pragmaNode) = getFieldAndPragma(child) + if fieldName.len == 0: + continue + let cliName = block: + let n = getPragmaValue(pragmaNode, "name") + if n.len > 0: n else: fieldName + let desc = getPragmaValue(pragmaNode, "desc") + let defaultValue = getPragmaValue(pragmaNode, "defaultValue") + metas.add( + makeMetaNode(fieldName, typeName, cliName, desc, defaultValue, commandCtx) + ) + of nnkRecCase: + let discriminator = child[0] + if discriminator.kind == nnkIdentDefs: + let (fieldName, typeName, pragmaNode) = getFieldAndPragma(discriminator) + if fieldName.len > 0: + let cliName = block: + let n = getPragmaValue(pragmaNode, "name") + if n.len > 0: n else: fieldName + let desc = getPragmaValue(pragmaNode, "desc") + let defaultValue = getPragmaValue(pragmaNode, "defaultValue") + metas.add( + makeMetaNode(fieldName, typeName, cliName, desc, defaultValue, commandCtx) + ) + + for i in 1 ..< child.len: + let branch = child[i] + case branch.kind + of nnkOfBranch: + let branchCtx = branch[0].repr + for j in 1 ..< branch.len: + if branch[j].kind == nnkRecList: + collectRecList(branch[j], metas, branchCtx) + of nnkElse: + for j in 0 ..< branch.len: + if branch[j].kind == nnkRecList: + collectRecList(branch[j], metas, commandCtx) + else: + discard + else: + discard + + let typeInst = getTypeInst(T) + var targetType = T + if typeInst.kind == nnkBracketExpr and typeInst.len >= 2: + targetType = typeInst[1] + + let typeImpl = getImpl(targetType) + let recList = findFirstRecList(typeImpl) + if recList.isNil: + return newTree(nnkPrefix, ident("@"), newNimNode(nnkBracket)) + + var metas: seq[NimNode] = @[] + collectRecList(recList, metas, "") + + let bracket = newNimNode(nnkBracket) + for node in metas: + bracket.add(node) + + result = newTree(nnkPrefix, ident("@"), bracket) From 0ad55159b39ed74ba6b88b5a9d72db766b4b0f08 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:48:48 +0100 Subject: [PATCH 079/155] liblogosdelivery supports MessageReceivedEvent propagation over FFI. Adjusted example. (#3747) --- Makefile | 3 + liblogosdelivery/examples/json_utils.c | 96 +++++++++++++++++++ liblogosdelivery/examples/json_utils.h | 21 ++++ .../examples/logosdelivery_example.c | 84 ++++++++++------ .../logos_delivery_api/node_api.nim | 17 +++- 5 files changed, 188 insertions(+), 33 deletions(-) create mode 100644 liblogosdelivery/examples/json_utils.c create mode 100644 liblogosdelivery/examples/json_utils.h diff --git a/Makefile b/Makefile index 4fafd6310..8f98e90bd 100644 --- a/Makefile +++ b/Makefile @@ -469,6 +469,7 @@ logosdelivery_example: | build liblogosdelivery ifeq ($(detected_OS),Darwin) gcc -o build/$@ \ liblogosdelivery/examples/logosdelivery_example.c \ + liblogosdelivery/examples/json_utils.c \ -I./liblogosdelivery \ -L./build \ -llogosdelivery \ @@ -476,6 +477,7 @@ ifeq ($(detected_OS),Darwin) else ifeq ($(detected_OS),Linux) gcc -o build/$@ \ liblogosdelivery/examples/logosdelivery_example.c \ + liblogosdelivery/examples/json_utils.c \ -I./liblogosdelivery \ -L./build \ -llogosdelivery \ @@ -483,6 +485,7 @@ else ifeq ($(detected_OS),Linux) else ifeq ($(detected_OS),Windows) gcc -o build/$@.exe \ liblogosdelivery/examples/logosdelivery_example.c \ + liblogosdelivery/examples/json_utils.c \ -I./liblogosdelivery \ -L./build \ -llogosdelivery \ diff --git a/liblogosdelivery/examples/json_utils.c b/liblogosdelivery/examples/json_utils.c new file mode 100644 index 000000000..8b33bb648 --- /dev/null +++ b/liblogosdelivery/examples/json_utils.c @@ -0,0 +1,96 @@ +#include "json_utils.h" +#include +#include + +const char* extract_json_field(const char *json, const char *field, char *buffer, size_t bufSize) { + char searchStr[256]; + snprintf(searchStr, sizeof(searchStr), "\"%s\":\"", field); + + const char *start = strstr(json, searchStr); + if (!start) { + return NULL; + } + + start += strlen(searchStr); + const char *end = strchr(start, '"'); + if (!end) { + return NULL; + } + + size_t len = end - start; + if (len >= bufSize) { + len = bufSize - 1; + } + + memcpy(buffer, start, len); + buffer[len] = '\0'; + + return buffer; +} + +const char* extract_json_object(const char *json, const char *field, size_t *outLen) { + char searchStr[256]; + snprintf(searchStr, sizeof(searchStr), "\"%s\":{", field); + + const char *start = strstr(json, searchStr); + if (!start) { + return NULL; + } + + // Advance to the opening brace + start = strchr(start, '{'); + if (!start) { + return NULL; + } + + // Find the matching closing brace (handles nested braces) + int depth = 0; + const char *p = start; + while (*p) { + if (*p == '{') depth++; + else if (*p == '}') { + depth--; + if (depth == 0) { + *outLen = (size_t)(p - start + 1); + return start; + } + } + p++; + } + return NULL; +} + +int decode_json_byte_array(const char *json, const char *field, char *buffer, size_t bufSize) { + char searchStr[256]; + snprintf(searchStr, sizeof(searchStr), "\"%s\":[", field); + + const char *start = strstr(json, searchStr); + if (!start) { + return -1; + } + + // Advance to the opening bracket + start = strchr(start, '['); + if (!start) { + return -1; + } + start++; // skip '[' + + size_t pos = 0; + const char *p = start; + while (*p && *p != ']' && pos < bufSize - 1) { + // Skip whitespace and commas + while (*p == ' ' || *p == ',' || *p == '\n' || *p == '\r' || *p == '\t') p++; + if (*p == ']') break; + + // Parse integer + int val = 0; + while (*p >= '0' && *p <= '9') { + val = val * 10 + (*p - '0'); + p++; + } + buffer[pos++] = (char)val; + } + buffer[pos] = '\0'; + return (int)pos; +} diff --git a/liblogosdelivery/examples/json_utils.h b/liblogosdelivery/examples/json_utils.h new file mode 100644 index 000000000..4039ca4f6 --- /dev/null +++ b/liblogosdelivery/examples/json_utils.h @@ -0,0 +1,21 @@ +#ifndef JSON_UTILS_H +#define JSON_UTILS_H + +#include + +// Extract a JSON string field value into buffer. +// Returns pointer to buffer on success, NULL on failure. +// Very basic parser - for production use a proper JSON library. +const char* extract_json_field(const char *json, const char *field, char *buffer, size_t bufSize); + +// Extract a nested JSON object as a raw string. +// Returns a pointer into `json` at the start of the object, and sets `outLen`. +// Handles nested braces. +const char* extract_json_object(const char *json, const char *field, size_t *outLen); + +// Decode a JSON array of integers (byte values) into a buffer. +// Parses e.g. [72,101,108,108,111] into "Hello". +// Returns number of bytes decoded, or -1 on error. +int decode_json_byte_array(const char *json, const char *field, char *buffer, size_t bufSize); + +#endif // JSON_UTILS_H diff --git a/liblogosdelivery/examples/logosdelivery_example.c b/liblogosdelivery/examples/logosdelivery_example.c index 61333f84d..729f7f0dc 100644 --- a/liblogosdelivery/examples/logosdelivery_example.c +++ b/liblogosdelivery/examples/logosdelivery_example.c @@ -1,4 +1,5 @@ #include "../liblogosdelivery.h" +#include "json_utils.h" #include #include #include @@ -6,33 +7,10 @@ static int create_node_ok = -1; -// Helper function to extract a JSON string field value -// Very basic parser - for production use a proper JSON library -const char* extract_json_field(const char *json, const char *field, char *buffer, size_t bufSize) { - char searchStr[256]; - snprintf(searchStr, sizeof(searchStr), "\"%s\":\"", field); - - const char *start = strstr(json, searchStr); - if (!start) { - return NULL; - } - - start += strlen(searchStr); - const char *end = strchr(start, '"'); - if (!end) { - return NULL; - } - - size_t len = end - start; - if (len >= bufSize) { - len = bufSize - 1; - } - - memcpy(buffer, start, len); - buffer[len] = '\0'; - - return buffer; -} +// Flags set by event callback, polled by main thread +static volatile int got_message_sent = 0; +static volatile int got_message_error = 0; +static volatile int got_message_received = 0; // Event callback that handles message events void event_callback(int ret, const char *msg, size_t len, void *userData) { @@ -62,6 +40,7 @@ void event_callback(int ret, const char *msg, size_t len, void *userData) { extract_json_field(eventJson, "requestId", requestId, sizeof(requestId)); extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); printf("[EVENT] Message sent - RequestID: %s, Hash: %s\n", requestId, messageHash); + got_message_sent = 1; } else if (strcmp(eventType, "message_error") == 0) { char requestId[128]; @@ -72,6 +51,7 @@ void event_callback(int ret, const char *msg, size_t len, void *userData) { extract_json_field(eventJson, "error", error, sizeof(error)); printf("[EVENT] Message error - RequestID: %s, Hash: %s, Error: %s\n", requestId, messageHash, error); + got_message_error = 1; } else if (strcmp(eventType, "message_propagated") == 0) { char requestId[128]; @@ -85,6 +65,41 @@ void event_callback(int ret, const char *msg, size_t len, void *userData) { extract_json_field(eventJson, "connectionStatus", connectionStatus, sizeof(connectionStatus)); printf("[EVENT] Connection status change - Status: %s\n", connectionStatus); + } else if (strcmp(eventType, "message_received") == 0) { + char messageHash[128]; + extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); + + // Extract the nested "message" object + size_t msgObjLen = 0; + const char *msgObj = extract_json_object(eventJson, "message", &msgObjLen); + if (msgObj) { + // Make a null-terminated copy of the message object + char *msgJson = malloc(msgObjLen + 1); + if (msgJson) { + memcpy(msgJson, msgObj, msgObjLen); + msgJson[msgObjLen] = '\0'; + + char contentTopic[256]; + extract_json_field(msgJson, "contentTopic", contentTopic, sizeof(contentTopic)); + + // Decode payload from JSON byte array to string + char payload[4096]; + int payloadLen = decode_json_byte_array(msgJson, "payload", payload, sizeof(payload)); + + printf("[EVENT] Message received - Hash: %s, ContentTopic: %s\n", messageHash, contentTopic); + if (payloadLen > 0) { + printf(" Payload (%d bytes): %.*s\n", payloadLen, payloadLen, payload); + } else { + printf(" Payload: (empty or could not decode)\n"); + } + + free(msgJson); + } + } else { + printf("[EVENT] Message received - Hash: %s (could not parse message)\n", messageHash); + } + got_message_received = 1; + } else { printf("[EVENT] Unknown event type: %s\n", eventType); } @@ -146,7 +161,7 @@ int main() { logosdelivery_start_node(ctx, simple_callback, (void *)"start_node"); // Wait for node to start - sleep(10); + sleep(5); printf("\n4. Subscribing to content topic...\n"); const char *contentTopic = "/example/1/chat/proto"; @@ -181,9 +196,18 @@ int main() { "}"; logosdelivery_send(ctx, simple_callback, (void *)"send", message); - // Wait for message events to arrive + // Poll for terminal message events (sent, error, or received) with timeout printf("Waiting for message delivery events...\n"); - sleep(60); + int timeout_sec = 60; + int elapsed = 0; + while (!(got_message_sent || got_message_error || got_message_received) + && elapsed < timeout_sec) { + usleep(100000); // 100ms + elapsed++; + } + if (elapsed >= timeout_sec) { + printf("Timed out waiting for message events after %d seconds\n", timeout_sec); + } printf("\n7. Unsubscribing from content topic...\n"); logosdelivery_unsubscribe(ctx, simple_callback, (void *)"unsubscribe", contentTopic); diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim index 1835f75b5..cd644abd7 100644 --- a/liblogosdelivery/logos_delivery_api/node_api.nim +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -35,8 +35,8 @@ registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): confValue = parseCmdArg(typeof(confValue), formattedString) except Exception: return err( - "Failed to parse field '" & confField & "': " & - getCurrentExceptionMsg() & ". Value: " & formattedString + "Failed to parse field '" & confField & "': " & getCurrentExceptionMsg() & + ". Value: " & formattedString ) # Create the node @@ -86,7 +86,8 @@ proc logosdelivery_create_node( callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) # free allocated resources as they won't be available ffi.destroyFFIContext(ctx).isOkOr: - chronicles.error "Error in destroyFFIContext after sendRequestToFFIThread during creation", err = $error + chronicles.error "Error in destroyFFIContext after sendRequestToFFIThread during creation", + err = $error return nil return ctx @@ -125,6 +126,15 @@ proc logosdelivery_start_node( chronicles.error "MessagePropagatedEvent.listen failed", err = $error return err("MessagePropagatedEvent.listen failed: " & $error) + let receivedListener = MessageReceivedEvent.listen( + ctx.myLib[].brokerCtx, + proc(event: MessageReceivedEvent) {.async: (raises: []).} = + callEventCallback(ctx, "onMessageReceived"): + $newJsonEvent("message_received", event), + ).valueOr: + chronicles.error "MessageReceivedEvent.listen failed", err = $error + return err("MessageReceivedEvent.listen failed: " & $error) + let ConnectionStatusChangeListener = EventConnectionStatusChange.listen( ctx.myLib[].brokerCtx, proc(event: EventConnectionStatusChange) {.async: (raises: []).} = @@ -149,6 +159,7 @@ proc logosdelivery_stop_node( MessageErrorEvent.dropAllListeners(ctx.myLib[].brokerCtx) MessageSentEvent.dropAllListeners(ctx.myLib[].brokerCtx) MessagePropagatedEvent.dropAllListeners(ctx.myLib[].brokerCtx) + MessageReceivedEvent.dropAllListeners(ctx.myLib[].brokerCtx) EventConnectionStatusChange.dropAllListeners(ctx.myLib[].brokerCtx) (await ctx.myLib[].stop()).isOkOr: From dedc2501db5a6cd2d2a64a9cbfe52ce066c89abb Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:49:35 +0100 Subject: [PATCH 080/155] fix avoid IndexDefect if DB error message is short (#3725) --- waku/common/databases/db_postgres/dbconn.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/waku/common/databases/db_postgres/dbconn.nim b/waku/common/databases/db_postgres/dbconn.nim index a6c237ae5..7ccf32099 100644 --- a/waku/common/databases/db_postgres/dbconn.nim +++ b/waku/common/databases/db_postgres/dbconn.nim @@ -48,8 +48,8 @@ proc check(db: DbConn): Result[void, string] = return err("exception in check: " & getCurrentExceptionMsg()) if message.len > 0: - let truncatedErr = message[0 .. 80] - ## libpq sometimes gives extremely long error messages + let truncatedErr = message[0 ..< min(80, message.len)] + error "postgres check issue. see truncated db error.", error = truncatedErr return err(truncatedErr) return ok() From 4654975e66381f63ce7d82f035500e826b38f1a6 Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Thu, 12 Mar 2026 09:31:10 +0100 Subject: [PATCH 081/155] update changelog to avoid IndexDefect exception --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a00228e..edc4a705c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ -## v0.37.1-beta (2025-12-10) +## v0.37.1 (2026-03-12) ### Bug Fixes +- Avoid IndexDefect if DB error message is short ([#3725](https://github.com/logos-messaging/logos-delivery/pull/3725)) - Remove ENR cache from peer exchange ([#3652](https://github.com/logos-messaging/logos-messaging-nim/pull/3652)) ([7920368a](https://github.com/logos-messaging/logos-messaging-nim/commit/7920368a36687cd5f12afa52d59866792d8457ca)) ## v0.37.0 (2025-10-01) From ff723a80c5526677851d5d3ec429feab80ec5894 Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Thu, 12 Mar 2026 16:03:17 +0100 Subject: [PATCH 082/155] adapt CHANGELOG to the actual recent releases --- CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7213337ed..1339e1e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,6 @@ ### Bug Fixes - Avoid IndexDefect if DB error message is short ([#3725](https://github.com/logos-messaging/logos-delivery/pull/3725)) - -## 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) From 1ace0154d3c351207ec1423361802342bf291747 Mon Sep 17 00:00:00 2001 From: Darshan <35736874+darshankabariya@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:17:47 +0530 Subject: [PATCH 083/155] chore: correct dynamic library extension on mac and update OS detection (#3754) --- examples/python/waku.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/examples/python/waku.py b/examples/python/waku.py index 65eb5d750..b2303e5e3 100644 --- a/examples/python/waku.py +++ b/examples/python/waku.py @@ -1,23 +1,32 @@ -from flask import Flask import ctypes import argparse +import sys + +if sys.platform == "darwin": + _lib_ext = "dylib" +elif sys.platform == "win32": + _lib_ext = "dll" +else: + _lib_ext = "so" + +_lib_path = f"build/libwaku.{_lib_ext}" libwaku = object try: # This python script should be run from the root repo folder - libwaku = ctypes.CDLL("build/libwaku.so") -except Exception as e: - print("Exception: ", e) - print(""" -The 'libwaku.so' library can be created with the next command from + libwaku = ctypes.CDLL(_lib_path) +except OSError as e: + print(f"Exception: {e}") + print(f""" +The '{_lib_path}' library can be created with the next command from the repo's root folder: `make libwaku`. -And it should build the library in 'build/libwaku.so'. +And it should build the library in '{_lib_path}'. -Therefore, make sure the LD_LIBRARY_PATH env var points at the location that -contains the 'libwaku.so' library. +Therefore, make sure the library path env var points at the location that +contains the '{_lib_path}' library. """) - exit(-1) + exit(1) def handle_event(ret, msg, user_data): print("Event received: %s" % msg) From a77870782a46115161c8bd4780e79f92d0579067 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:13:09 +0100 Subject: [PATCH 084/155] Change release process (#3750) * Simplify release process and leave the DST validation for deployment process * Rename prepare_full_release.md to prepare_release.md * Remove release-process.md as it duplicates info and causes confusion --- .../ISSUE_TEMPLATE/prepare_beta_release.md | 63 ------- ...are_full_release.md => prepare_release.md} | 39 ++--- docs/contributors/release-process.md | 164 ------------------ 3 files changed, 17 insertions(+), 249 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/prepare_beta_release.md rename .github/ISSUE_TEMPLATE/{prepare_full_release.md => prepare_release.md} (71%) delete mode 100644 docs/contributors/release-process.md diff --git a/.github/ISSUE_TEMPLATE/prepare_beta_release.md b/.github/ISSUE_TEMPLATE/prepare_beta_release.md deleted file mode 100644 index 3c4e76854..000000000 --- a/.github/ISSUE_TEMPLATE/prepare_beta_release.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: Prepare Beta Release -about: Execute tasks for the creation and publishing of a new beta release -title: 'Prepare beta release 0.0.0' -labels: beta-release -assignees: '' - ---- - - - -### Items to complete - -All items below are to be completed by the owner of the given release. - -- [ ] Create release branch with major and minor only ( e.g. release/v0.X ) if it doesn't exist. -- [ ] Assign release candidate tag to the release branch HEAD (e.g. `v0.X.0-beta-rc.0`, `v0.X.0-beta-rc.1`, ... `v0.X.0-beta-rc.N`). -- [ ] Generate and edit release notes in CHANGELOG.md. - -- [ ] **Validation of release candidate** - - [ ] **Automated testing** - - [ ] Ensure all the unit tests (specifically logos-messaging-js tests) are green against the release candidate. - - [ ] **Waku fleet testing** - - [ ] Deploy the release candidate to `waku.test` through [deploy-waku-test job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-test/) and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). - - After completion, disable fleet so that daily CI does not override your release candidate. - - Verify at https://fleets.waku.org/ that the fleet is locked to the release candidate image. - - Confirm the container image exists on [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab). - - [ ] Search [Kibana logs](https://kibana.infra.status.im/app/discover) from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test`. - - Set time range to "Last 30 days" (or since last release). - - Most relevant search query: `(fleet: "waku.test" AND message: "SIGSEGV")`, `(fleet: "waku.test" AND message: "exception")`, `(fleet: "waku.test" AND message: "error")`. - - Document any crashes or errors found. - - [ ] If `waku.test` validation is successful, deploy to `waku.sandbox` using the [deploy-waku-sandbox job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox/). - - [ ] Search [Kibana logs](https://kibana.infra.status.im/app/discover) for `waku.sandbox`: `(fleet: "waku.sandbox" AND message: "SIGSEGV")`, `(fleet: "waku.sandbox" AND message: "exception")`, `(fleet: "waku.sandbox" AND message: "error")`. most probably if there are no crashes or errors in `waku.test`, there will be no crashes or errors in `waku.sandbox`. - - [ ] Enable the `waku.test` fleet again to resume auto-deployment of the latest `master` commit. - -- [ ] **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 [logos-delivery-compose](https://github.com/logos-messaging/logos-delivery-compose) and [logos-delivery-simulator](https://github.com/logos-messaging/waku-simulator) according to the new release. - - [ ] Bump logos-delivery dependency in [logos-delivery-rust-bindings](https://github.com/logos-messaging/logos-delivery-rust-bindings) and make sure all examples and tests work. - - [ ] Bump logos-delivery dependency in [logos-delivery-go-bindings](https://github.com/logos-messaging/logos-delivery-go-bindings) and make sure all tests work. - - [ ] Create GitHub release (https://github.com/logos-messaging/logos-delivery/releases). - - [ ] Submit a PR to merge the release branch back to `master`. Make sure you use the option "Merge pull request (Create a merge commit)" to perform the merge. Ping repo admin if this option is not available. - -- [ ] **Promote release to fleets** - - [ ] Ask the PM lead to announce the release. - - [ ] Update infra config with any deprecated arguments or changed options. - - [ ] Update waku.sandbox with [this deployment job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox/). - -### Links - -- [Release process](https://github.com/logos-messaging/logos-delivery/blob/master/docs/contributors/release-process.md) -- [Release notes](https://github.com/logos-messaging/logos-delivery/blob/master/CHANGELOG.md) -- [Fleet ownership](https://www.notion.so/Fleet-Ownership-7532aad8896d46599abac3c274189741?pvs=4#d2d2f0fe4b3c429fbd860a1d64f89a64) -- [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) -- [Jenkins](https://ci.infra.status.im/job/nim-waku/) -- [Fleets](https://fleets.waku.org/) -- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) -- [Kibana](https://kibana.infra.status.im/app/) diff --git a/.github/ISSUE_TEMPLATE/prepare_full_release.md b/.github/ISSUE_TEMPLATE/prepare_release.md similarity index 71% rename from .github/ISSUE_TEMPLATE/prepare_full_release.md rename to .github/ISSUE_TEMPLATE/prepare_release.md index 4df808bd4..83456e79a 100644 --- a/.github/ISSUE_TEMPLATE/prepare_full_release.md +++ b/.github/ISSUE_TEMPLATE/prepare_release.md @@ -1,7 +1,7 @@ --- -name: Prepare Full Release +name: Prepare Release about: Execute tasks for the creation and publishing of a new full release -title: 'Prepare full release 0.0.0' +title: 'Prepare release 0.0.0' labels: full-release assignees: '' @@ -26,6 +26,9 @@ All items below are to be completed by the owner of the given release. - [ ] **Automated testing** - [ ] Ensure all the unit tests (specifically logos-messaging-js tests) are green against the release candidate. + - [ ] **QA testing** + - [ ] Ask QA to run their available tests against the release candidate. + - [ ] **Waku fleet testing** - [ ] Deploy the release candidate to `waku.test` fleet. - Start the [deployment job](https://ci.infra.status.im/job/nim-waku/) and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). @@ -36,40 +39,32 @@ All items below are to be completed by the owner of the given release. - Set time range to "Last 30 days" (or since last release). - Most relevant search query: `(fleet: "waku.test" AND message: "SIGSEGV")`, `(fleet: "waku.test" AND message: "exception")`, `(fleet: "waku.test" AND message: "error")`. - Document any crashes or errors found. - - [ ] If `waku.test` validation is successful, deploy to `waku.sandbox` using the same [deployment job](https://ci.infra.status.im/job/nim-waku/). - - [ ] Search [Kibana logs](https://kibana.infra.status.im/app/discover) for `waku.sandbox`: `(fleet: "waku.sandbox" AND message: "SIGSEGV")`, `(fleet: "waku.sandbox" AND message: "exception")`, `(fleet: "waku.sandbox" AND message: "error")`. most probably if there are no crashes or errors in `waku.test`, there will be no crashes or errors in `waku.sandbox`. + - [ ] Ask QA to perform tests against `waku.test`, if any. Then, after that, review Kibana for possible issues or unexpected restart. - [ ] Enable the `waku.test` fleet again to resume auto-deployment of the latest `master` commit. - - [ ] **QA and DST testing** - - [ ] Ask Vac-QA and Vac-DST to run their available tests against the release candidate; share all release candidates with both teams. - - [ ] Vac-DST: An additional report is needed ([see this example](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f)). Inform DST team about what are the expectations for this rc. For example, if we expect higher or lower bandwidth consumption. - - - [ ] **Status fleet testing** - - [ ] Deploy release candidate to `status.staging` + - [ ] **Status testing** + - [ ] Get QA approval to deploy a new version in `status.staging`. + - [ ] Deploy release candidate to `status.staging`. - [ ] Perform [sanity check](https://www.notion.so/How-to-test-Nwaku-on-Status-12c6e4b9bf06420ca868bd199129b425) and log results as comments in this issue. - - [ ] Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client. - - 1:1 Chats with each other - - Send and receive messages in a community - - Close one instance, send messages with second instance, reopen first instance and confirm messages sent while offline are retrieved from store + - [ ] Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client mode. + - 1:1 Chats with each other. + - Send and receive messages in a community. + - Close one instance, send messages with second instance, reopen first instance and confirm messages sent while offline are retrieved from store. - [ ] Perform checks based on _end user impact_ - [ ] Inform other (Waku and Status) CCs to point their instances to `status.staging` for a few days. Ping Status colleagues on their Discord server or in the [Status community](https://status.app/c/G3kAAMSQtb05kog3aGbr3kiaxN4tF5xy4BAGEkkLwILk2z3GcoYlm5hSJXGn7J3laft-tnTwDWmYJ18dP_3bgX96dqr_8E3qKAvxDf3NrrCMUBp4R9EYkQez9XSM4486mXoC3mIln2zc-TNdvjdfL9eHVZ-mGgs=#zQ3shZeEJqTC1xhGUjxuS4rtHSrhJ8vUYp64v6qWkLpvdy9L9) (this is not a blocking point.) - - [ ] Ask Status-QA to perform sanity checks (as described above) and checks based on _end user impact_; specify the version being tested - - [ ] Ask Status-QA or infra to run the automated Status e2e tests against `status.staging` + - [ ] Ask QA to perform sanity checks (as described above) and checks based on _end user impact_; specify the version being tested + - [ ] Ask QA or infra to run the automated Status e2e tests against `status.staging` - [ ] Get other CCs' sign-off: they should comment on this PR, e.g., "Used the app for a week, no problem." If problems are reported, resolve them and create a new RC. - - [ ] **Get Status-QA sign-off**, ensuring that the `status.test` update will not disturb ongoing activities. - [ ] **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`). + - [ ] Assign a final release tag (`v0.X.0`) to the same commit that contains the validated release-candidate tag (e.g. `git tag -as v0.X.0 -m "final release."`). - [ ] Update [logos-delivery-compose](https://github.com/logos-messaging/logos-delivery-compose) and [logos-delivery-simulator](https://github.com/logos-messaging/logos-delivery-simulator) according to the new release. - [ ] Bump logos-delivery dependency in [logos-delivery-rust-bindings](https://github.com/logos-messaging/logos-delivery-rust-bindings) and make sure all examples and tests work. - [ ] Bump logos-delivery dependency in [logos-delivery-go-bindings](https://github.com/logos-messaging/logos-delivery-go-bindings) and make sure all tests work. - [ ] Create GitHub release (https://github.com/logos-messaging/logos-delivery/releases). - [ ] Submit a PR to merge the release branch back to `master`. Make sure you use the option "Merge pull request (Create a merge commit)" to perform the merge. Ping repo admin if this option is not available. - -- [ ] **Promote release to fleets** - - [ ] Ask the PM lead to announce the release. - - [ ] Update infra config with any deprecated arguments or changed options. + - [ ] Create a deployment issue with the recently created release. ### Links diff --git a/docs/contributors/release-process.md b/docs/contributors/release-process.md deleted file mode 100644 index 8aa9282cd..000000000 --- a/docs/contributors/release-process.md +++ /dev/null @@ -1,164 +0,0 @@ -# Release Process - -How to do releases. - -For more context, see https://trunkbaseddevelopment.com/branch-for-release/ - -## How to do releases - -### Prerequisites - -- All issues under the corresponding release [milestone](https://github.com/waku-org/nwaku/milestones) have been closed or, after consultation, deferred to the next release. -- All submodules are up to date. - > Updating submodules requires a PR (and very often several "fixes" to maintain compatibility with the changes in submodules). That PR process must be done and merged a couple of days before the release. - - > In case the submodules update has a low effort and/or risk for the release, follow the ["Update submodules"](./git-submodules.md) instructions. - - > If the effort or risk is too high, consider postponing the submodules upgrade for the subsequent release or delaying the current release until the submodules updates are included in the release candidate. - -### Release types - -- **Full release**: follow the entire [Release process](#release-process--step-by-step). - -- **Beta release**: skip just `6c` and `6d` steps from [Release process](#release-process--step-by-step). - -- Choose the appropriate release process based on the release type: - - [Full Release](../../.github/ISSUE_TEMPLATE/prepare_full_release.md) - - [Beta Release](../../.github/ISSUE_TEMPLATE/prepare_beta_release.md) - -### Release process ( step by step ) - -1. Checkout a release branch from master - - ``` - git checkout -b release/v0.X.0 - ``` - -2. Update `CHANGELOG.md` and ensure it is up to date. Use the helper Make target to get PR based release-notes/changelog update. - - ``` - make release-notes - ``` - -3. Create a release-candidate tag with the same name as release and `-rc.N` suffix a few days before the official release and push it - - ``` - git tag -as v0.X.0-rc.0 -m "Initial release." - git push origin v0.X.0-rc.0 - ``` - - This will trigger a [workflow](../../.github/workflows/pre-release.yml) which will build RC artifacts and create and publish a GitHub release - -4. Open a PR from the release branch for others to review the included changes and the release-notes - -5. In case additional changes are needed, create a new RC tag - - Make sure the new tag is associated - with CHANGELOG update. - - ``` - # Make changes, rebase and create new tag - # Squash to one commit and make a nice commit message - git rebase -i origin/master - git tag -as v0.X.0-rc.1 -m "Initial release." - git push origin v0.X.0-rc.1 - ``` - - Similarly use v0.X.0-rc.2, v0.X.0-rc.3 etc. for additional RC tags. - -6. **Validation of release candidate** - - 6a. **Automated testing** - - Ensure all the unit tests (specifically js-waku tests) are green against the release candidate. - - 6b. **Waku fleet testing** - - Start job on `waku.test` [Deployment job](https://ci.infra.status.im/job/nim-waku/), wait for completion of the job. If it fails, then debug it. - - After completion, disable fleet so that daily ci not override your release candidate. - - Verify at https://fleets.waku.org/ that the fleet is locked to the release candidate image. - - Check if the image is created at [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab). - - Search [Kibana logs](https://kibana.infra.status.im/app/discover) from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test`. - - Set time range to "Last 30 days" (or since last release). - - Most relevant search query: `(fleet: "waku.test" AND message: "SIGSEGV")`, `(fleet: "waku.test" AND message: "exception")`, `(fleet: "waku.test" AND message: "error")`. - - Document any crashes or errors found. - - If `waku.test` validation is successful, deploy to `waku.sandbox` using the same [Deployment job](https://ci.infra.status.im/job/nim-waku/). - - Search [Kibana logs](https://kibana.infra.status.im/app/discover) for `waku.sandbox`: `(fleet: "waku.sandbox" AND message: "SIGSEGV")`, `(fleet: "waku.sandbox" AND message: "exception")`, `(fleet: "waku.sandbox" AND message: "error")`. most probably if there are no crashes or errors in `waku.test`, there will be no crashes or errors in `waku.sandbox`. - - Enable the `waku.test` fleet again to resume auto-deployment of the latest `master` commit. - - 6c. **QA and DST testing** - - Ask Vac-QA and Vac-DST to run their available tests against the release candidate; share all release candidates with both teams. - - > We need an additional report like [this](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f) specifically from the DST team. Inform DST team about what are the expectations for this rc. For example, if we expect higher or lower bandwidth consumption. - - 6d. **Status fleet testing** - - Deploy release candidate to `status.staging` - - Perform [sanity check](https://www.notion.so/How-to-test-Nwaku-on-Status-12c6e4b9bf06420ca868bd199129b425) and log results as comments in this issue. - - Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client. - - 1:1 Chats with each other - - Send and receive messages in a community - - Close one instance, send messages with second instance, reopen first instance and confirm messages sent while offline are retrieved from store - - Perform checks based on _end-user impact_. - - Inform other (Waku and Status) CCs to point their instances to `status.staging` for a few days. Ping Status colleagues from their Discord server or [Status community](https://status.app) (not a blocking point). - - Ask Status-QA to perform sanity checks (as described above) and checks based on _end user impact_; specify the version being tested. - - Ask Status-QA or infra to run the automated Status e2e tests against `status.staging`. - - Get other CCs' sign-off: they should comment on this PR, e.g., "Used the app for a week, no problem." If problems are reported, resolve them and create a new RC. - - **Get Status-QA sign-off**, ensuring that the `status.test` update will not disturb ongoing activities. - -7. Once the release-candidate has been validated, create a final release tag and push it. -We also need to merge the release branch back into master as a final step. - - ``` - git checkout release/v0.X.0 - git tag -as v0.X.0 -m "final release." (use v0.X.0-beta as the tag if you are creating a beta release) - git push origin v0.X.0 - git switch master - git pull - git merge release/v0.X.0 - ``` -8. Update `waku-rust-bindings`, `waku-simulator` and `nwaku-compose` to use the new release. - -9. Create a [GitHub release](https://github.com/waku-org/nwaku/releases) from the release tag. - - * Add binaries produced by the ["Upload Release Asset"](https://github.com/waku-org/nwaku/actions/workflows/release-assets.yml) workflow. Where possible, test the binaries before uploading to the release. - -### After the release - -1. Announce the release on Twitter, Discord and other channels. -2. Deploy the release image to [Dockerhub](https://hub.docker.com/r/wakuorg/nwaku) by triggering [the manual Jenkins deployment job](https://ci.infra.status.im/job/nim-waku/job/docker-manual/). - > Ensure the following build parameters are set: - > - `MAKE_TARGET`: `wakunode2` - > - `IMAGE_TAG`: the release tag (e.g. `v0.38.0`) - > - `IMAGE_NAME`: `wakuorg/nwaku` - > - `NIMFLAGS`: `--colors:off -d:disableMarchNative -d:chronicles_colors:none -d:postgres` - > - `GIT_REF` the release tag (e.g. `v0.38.0`) - -### Performing a patch release - -1. Cherry-pick the relevant commits from master to the release branch - - ``` - git cherry-pick - ``` - -2. Create a release-candidate tag with the same name as release and `-rc.N` suffix - -3. Update `CHANGELOG.md`. From the release branch, use the helper Make target after having cherry-picked the commits. - - ``` - make release-notes - ``` - Create a new branch and raise a PR with the changelog updates to master. - -4. Once the release-candidate has been validated and changelog PR got merged, cherry-pick the changelog update from master to the release branch. Create a final release tag and push it. - -5. Create a [GitHub release](https://github.com/waku-org/nwaku/releases) from the release tag and follow the same post-release process as usual. - -### 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) -- [Fleet ownership](https://www.notion.so/Fleet-Ownership-7532aad8896d46599abac3c274189741?pvs=4#d2d2f0fe4b3c429fbd860a1d64f89a64) -- [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) -- [Jenkins](https://ci.infra.status.im/job/nim-waku/) -- [Fleets](https://fleets.waku.org/) -- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) -- [Kibana](https://kibana.infra.status.im/app/) \ No newline at end of file From 03249df715aa04dbe689649145ae7e059eda8e6d Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:13:40 +0100 Subject: [PATCH 085/155] Add deployment process (#3751) --- .github/ISSUE_TEMPLATE/deploy_release.md | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/deploy_release.md diff --git a/.github/ISSUE_TEMPLATE/deploy_release.md b/.github/ISSUE_TEMPLATE/deploy_release.md new file mode 100644 index 000000000..68557bf46 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/deploy_release.md @@ -0,0 +1,50 @@ +--- +name: Deploy Release +about: Execute tasks for deploying a new version in a fleet +title: 'Deploy release vX.X.X in waku.sandbox and/or status.prod fleet' +labels: deploy-release +assignees: '' + +--- + + + +### Link to the Release PR + + + +### Items to complete, in order + + + +- [ ] Receive sign-off from DST. + - [ ] Inform DST team about what are the expectations for this release. For example, if we expect higher, same or lower bandwidth consumption. Or a new protocol appears, etc. + - [ ] Ask DST to add a comment approving this deployment and add a link to the analysis report. + +- [ ] Update waku.sandbox with [this deployment job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox/). + +- [ ] Deploy to status.prod + - [ ] Ask Status admin to add a comment approving that this deployment to happen now. + - [ ] Update status.prod with [this deployment job](https://ci.infra.status.im/job/nim-waku/job/deploy-status-prod/). + +- [ ] Update infra config + - [ ] Submit PRs into infra repos to adjust deprecated or changed arguments (review CHANGELOG.md for that release). And confirm the fleet can run after that. This requires coordination with infra team. + +### Reference Links + +- [Release process](https://github.com/logos-messaging/logos-delivery/blob/master/docs/contributors/release-process.md) +- [Release notes](https://github.com/logos-messaging/logos-delivery/blob/master/CHANGELOG.md) +- [Infra-role-nim-waku](https://github.com/status-im/infra-role-nim-waku) +- [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) +- [Infra-Status](https://github.com/status-im/infra-status) +- [Jenkins](https://ci.infra.status.im/job/nim-waku/) +- [Fleets](https://fleets.waku.org/) +- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) +- [Kibana](https://kibana.infra.status.im/app/) From bc9454db5e0258eaa964f36c37d7204244ece9a5 Mon Sep 17 00:00:00 2001 From: Tanya S <120410716+stubbsta@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:27:50 +0200 Subject: [PATCH 086/155] Chore: Simplify on chain group manager error handling (#3678) --- tests/node/test_wakunode_legacy_lightpush.nim | 7 +- tests/node/test_wakunode_lightpush.nim | 7 +- .../test_rln_group_manager_onchain.nim | 99 ++--- tests/waku_rln_relay/test_waku_rln_relay.nim | 26 +- .../test_wakunode_rln_relay.nim | 49 +-- tests/wakunode_rest/test_rest_relay.nim | 39 +- .../rln_keystore_generator.nim | 4 +- .../group_manager/group_manager_base.nim | 4 +- .../group_manager/on_chain/group_manager.nim | 358 +++++++++--------- .../group_manager/on_chain/retry_wrapper.nim | 41 +- waku/waku_rln_relay/rln_relay.nim | 4 +- 11 files changed, 288 insertions(+), 350 deletions(-) diff --git a/tests/node/test_wakunode_legacy_lightpush.nim b/tests/node/test_wakunode_legacy_lightpush.nim index 902464bcd..68c6cacde 100644 --- a/tests/node/test_wakunode_legacy_lightpush.nim +++ b/tests/node/test_wakunode_legacy_lightpush.nim @@ -134,11 +134,8 @@ suite "RLN Proofs as a Lightpush Service": let manager1 = cast[OnchainGroupManager](server.wakuRlnRelay.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated1 = waitFor manager1.updateRoots() info "Updated root for node1", rootUpdated1 diff --git a/tests/node/test_wakunode_lightpush.nim b/tests/node/test_wakunode_lightpush.nim index 66b87b85e..b407327e3 100644 --- a/tests/node/test_wakunode_lightpush.nim +++ b/tests/node/test_wakunode_lightpush.nim @@ -137,11 +137,8 @@ suite "RLN Proofs as a Lightpush Service": let manager1 = cast[OnchainGroupManager](server.wakuRlnRelay.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated1 = waitFor manager1.updateRoots() info "Updated root for node1", rootUpdated1 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 aac900911..29da94129 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -74,10 +74,11 @@ suite "Onchain group manager": raiseAssert "Expected error when keystore file doesn't exist" test "trackRootChanges: should guard against uninitialized state": - try: - discard manager.trackRootChanges() - except CatchableError: - check getCurrentExceptionMsg().len == 38 + let initializedResult = waitFor manager.trackRootChanges() + + check: + initializedResult.isErr() + initializedResult.error == "OnchainGroupManager is not initialized" test "trackRootChanges: should sync to the state of the group": let credentials = generateCredentials() @@ -86,10 +87,8 @@ suite "Onchain group manager": let merkleRootBefore = waitFor manager.fetchMerkleRoot() - try: - waitFor manager.register(credentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + (waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error discard waitFor withTimeout(trackRootChanges(manager), 15.seconds) @@ -110,13 +109,11 @@ suite "Onchain group manager": let merkleRootBefore = waitFor manager.fetchMerkleRoot() - try: - for i in 0 ..< credentials.len(): - info "Registering credential", index = i, credential = credentials[i] - waitFor manager.register(credentials[i], UserMessageLimit(20)) - discard waitFor manager.updateRoots() - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + for i in 0 ..< credentials.len(): + info "Registering credential", index = i, credential = credentials[i] + (waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr: + assert false, "Failed to register credential " & $i & ": " & error + discard waitFor manager.updateRoots() let merkleRootAfter = waitFor manager.fetchMerkleRoot() @@ -127,16 +124,15 @@ suite "Onchain group manager": test "register: should guard against uninitialized state": let dummyCommitment = default(IDCommitment) - try: - waitFor manager.register( - RateCommitment( - idCommitment: dummyCommitment, userMessageLimit: UserMessageLimit(20) - ) + let res = waitFor manager.register( + RateCommitment( + idCommitment: dummyCommitment, userMessageLimit: UserMessageLimit(20) ) - except CatchableError: - assert true - except Exception: - assert false, "exception raised: " & getCurrentExceptionMsg() + ) + + check: + res.isErr() + res.error == "OnchainGroupManager is not initialized" test "register: should register successfully": # TODO :- similar to ```trackRootChanges: should fetch history correctly``` @@ -146,11 +142,8 @@ suite "Onchain group manager": let idCredentials = generateCredentials() let merkleRootBefore = waitFor manager.fetchMerkleRoot() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let merkleRootAfter = waitFor manager.fetchMerkleRoot() @@ -177,26 +170,25 @@ suite "Onchain group manager": manager.onRegister(callback) - try: + ( waitFor manager.register( RateCommitment( idCommitment: idCommitment, userMessageLimit: UserMessageLimit(20) ) ) - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + ).isOkOr: + assert false, "error returned when calling register: " & error waitFor fut test "withdraw: should guard against uninitialized state": let idSecretHash = generateCredentials().idSecretHash - try: - waitFor manager.withdraw(idSecretHash) - except CatchableError: - assert true - except Exception: - assert false, "exception raised: " & getCurrentExceptionMsg() + let res = waitFor manager.withdraw(idSecretHash) + + check: + res.isErr() + res.error == "OnchainGroupManager is not initialized" test "validateRoot: should validate good root": let idCredentials = generateCredentials() @@ -217,10 +209,8 @@ suite "Onchain group manager": (waitFor manager.init()).isOkOr: raiseAssert $error - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned : " & getCurrentExceptionMsg() waitFor fut @@ -299,10 +289,8 @@ suite "Onchain group manager": manager.onRegister(callback) - try: - waitFor manager.register(credentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + (waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error waitFor fut let rootUpdated = waitFor manager.updateRoots() @@ -337,11 +325,8 @@ suite "Onchain group manager": let idCredential = generateCredentials() - try: - waitFor manager.register(idCredential, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling startGroupSync: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredential, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let messageBytes = "Hello".toBytes() @@ -395,14 +380,12 @@ suite "Onchain group manager": return callback - try: - manager.onRegister(generateCallback(futures, credentials)) + manager.onRegister(generateCallback(futures, credentials)) - for i in 0 ..< credentials.len(): - waitFor manager.register(credentials[i], UserMessageLimit(20)) - discard waitFor manager.updateRoots() - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + for i in 0 ..< credentials.len(): + (waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr: + assert false, "Failed to register credential " & $i & ": " & error + discard waitFor manager.updateRoots() waitFor allFutures(futures) diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index d9fe0d890..e41b79608 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -242,11 +242,8 @@ suite "Waku rln relay": let manager = cast[OnchainGroupManager](wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let epoch1 = wakuRlnRelay.getCurrentEpoch() @@ -301,11 +298,8 @@ suite "Waku rln relay": let manager = cast[OnchainGroupManager](wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error # usually it's 20 seconds but we set it to 1 for testing purposes which make the test faster wakuRlnRelay.rlnMaxTimestampGap = 1 @@ -353,11 +347,9 @@ suite "Waku rln relay": let manager1 = cast[OnchainGroupManager](wakuRlnRelay1.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + "error returned when calling register: " & error let index2 = MembershipIndex(6) let rlnConf2 = getWakuRlnConfig(manager = manager, index = index2) @@ -369,11 +361,9 @@ suite "Waku rln relay": let manager2 = cast[OnchainGroupManager](wakuRlnRelay2.groupManager) let idCredentials2 = generateCredentials() - try: - waitFor manager2.register(idCredentials2, UserMessageLimit(20)) - except Exception, CatchableError: + (waitFor manager2.register(idCredentials2, UserMessageLimit(20))).isOkOr: assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + "error returned when calling register: " & error # get the current epoch time let epoch = wakuRlnRelay1.getCurrentEpoch() diff --git a/tests/waku_rln_relay/test_wakunode_rln_relay.nim b/tests/waku_rln_relay/test_wakunode_rln_relay.nim index fcf97a671..79a4d6711 100644 --- a/tests/waku_rln_relay/test_wakunode_rln_relay.nim +++ b/tests/waku_rln_relay/test_wakunode_rln_relay.nim @@ -58,11 +58,8 @@ procSuite "WakuNode - RLN relay": let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated1 = waitFor manager1.updateRoots() info "Updated root for node1", rootUpdated1 @@ -172,11 +169,8 @@ procSuite "WakuNode - RLN relay": let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated1 = waitFor manager1.updateRoots() info "Updated root for node", node = 1, rootUpdated = rootUpdated1 @@ -192,11 +186,8 @@ procSuite "WakuNode - RLN relay": let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) let idCredentials2 = generateCredentials() - try: - waitFor manager2.register(idCredentials2, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager2.register(idCredentials2, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated2 = waitFor manager2.updateRoots() info "Updated root for node", node = 2, rootUpdated = rootUpdated2 @@ -212,11 +203,8 @@ procSuite "WakuNode - RLN relay": let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) let idCredentials3 = generateCredentials() - try: - waitFor manager3.register(idCredentials3, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager3.register(idCredentials3, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated3 = waitFor manager3.updateRoots() info "Updated root for node", node = 3, rootUpdated = rootUpdated3 @@ -333,11 +321,8 @@ procSuite "WakuNode - RLN relay": let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated1 = waitFor manager1.updateRoots() info "Updated root for node1", rootUpdated1 @@ -448,11 +433,8 @@ procSuite "WakuNode - RLN relay": let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated1 = waitFor manager1.updateRoots() info "Updated root for node1", rootUpdated1 @@ -620,11 +602,8 @@ procSuite "WakuNode - RLN relay": let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated1 = waitFor manager1.updateRoots() info "Updated root for node1", rootUpdated1 diff --git a/tests/wakunode_rest/test_rest_relay.nim b/tests/wakunode_rest/test_rest_relay.nim index f16e5c4f4..efdd597ba 100644 --- a/tests/wakunode_rest/test_rest_relay.nim +++ b/tests/wakunode_rest/test_rest_relay.nim @@ -42,8 +42,8 @@ suite "Waku v2 Rest API - Relay": var manager {.threadVar.}: OnchainGroupManager setup: - anvilProc = runAnvil() - manager = waitFor setupOnchainGroupManager() + anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH)) + manager = waitFor setupOnchainGroupManager(deployContracts = false) teardown: stopAnvil(anvilProc) @@ -268,11 +268,8 @@ suite "Waku v2 Rest API - Relay": let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "Failed to register identity credentials" & getCurrentExceptionMsg() let rootUpdated = waitFor manager.updateRoots() info "Updated root for node", rootUpdated @@ -545,11 +542,8 @@ suite "Waku v2 Rest API - Relay": let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "Failed to register identity credentials" & getCurrentExceptionMsg() let rootUpdated = waitFor manager.updateRoots() info "Updated root for node", rootUpdated @@ -617,11 +611,8 @@ suite "Waku v2 Rest API - Relay": let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "Failed to register identity credentials" & getCurrentExceptionMsg() let rootUpdated = waitFor manager.updateRoots() info "Updated root for node", rootUpdated @@ -679,11 +670,8 @@ suite "Waku v2 Rest API - Relay": let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "Failed to register identity credentials" & getCurrentExceptionMsg() let rootUpdated = waitFor manager.updateRoots() info "Updated root for node", rootUpdated @@ -754,11 +742,8 @@ suite "Waku v2 Rest API - Relay": let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "Failed to register identity credentials" & getCurrentExceptionMsg() let rootUpdated = waitFor manager.updateRoots() info "Updated root for node", rootUpdated diff --git a/tools/rln_keystore_generator/rln_keystore_generator.nim b/tools/rln_keystore_generator/rln_keystore_generator.nim index 85df37982..503e8d58e 100644 --- a/tools/rln_keystore_generator/rln_keystore_generator.nim +++ b/tools/rln_keystore_generator/rln_keystore_generator.nim @@ -73,7 +73,9 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = # 4. register on-chain try: - waitFor groupManager.register(credential, conf.userMessageLimit) + (waitFor groupManager.register(credential, conf.userMessageLimit)).isOkOr: + error "Failed to register on-chain", error = error + quit(QuitFailure) except Exception, CatchableError: error "failure while registering credentials on-chain", error = getCurrentExceptionMsg() diff --git a/waku/waku_rln_relay/group_manager/group_manager_base.nim b/waku/waku_rln_relay/group_manager/group_manager_base.nim index de2962e42..9c088d4c5 100644 --- a/waku/waku_rln_relay/group_manager/group_manager_base.nim +++ b/waku/waku_rln_relay/group_manager/group_manager_base.nim @@ -144,6 +144,4 @@ method generateProof*( return err("generateProof is not implemented") method isReady*(g: GroupManager): Future[bool] {.base, async.} = - raise newException( - CatchableError, "isReady proc for " & $g.type & " is not implemented yet" - ) + return true diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 2ce7d4423..2e4882891 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 @@ -50,109 +50,85 @@ type proc fetchMerkleProofElements*( g: OnchainGroupManager ): Future[Result[seq[byte], string]] {.async.} = - try: - let membershipIndex = g.membershipIndex.get() - let index40 = stuint(membershipIndex, 40) + let membershipIndex = g.membershipIndex.get() + let index40 = stuint(membershipIndex, 40) - let methodSig = "getMerkleProof(uint40)" - var paddedParam = newSeq[byte](32) - let indexBytes = index40.toBytesBE() - for i in 0 ..< min(indexBytes.len, paddedParam.len): - paddedParam[paddedParam.len - indexBytes.len + i] = indexBytes[i] + let methodSig = "getMerkleProof(uint40)" + var paddedParam = newSeq[byte](32) + let indexBytes = index40.toBytesBE() + for i in 0 ..< min(indexBytes.len, paddedParam.len): + paddedParam[paddedParam.len - indexBytes.len + i] = indexBytes[i] - let response = await sendEthCallWithParams( - ethRpc = g.ethRpc.get(), - functionSignature = methodSig, - params = paddedParam, - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) + let response = await sendEthCallWithParams( + ethRpc = g.ethRpc.get(), + functionSignature = methodSig, + params = paddedParam, + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) - return response - except CatchableError: - error "Failed to fetch Merkle proof elements", error = getCurrentExceptionMsg() - return err("Failed to fetch merkle proof elements: " & getCurrentExceptionMsg()) + return response proc fetchMerkleRoot*( g: OnchainGroupManager ): Future[Result[UInt256, string]] {.async.} = - try: - let merkleRoot = await sendEthCallWithoutParams( - ethRpc = g.ethRpc.get(), - functionSignature = "root()", - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) - return merkleRoot - except CatchableError: - error "Failed to fetch Merkle root", error = getCurrentExceptionMsg() - return err("Failed to fetch merkle root: " & getCurrentExceptionMsg()) + let merkleRoot = await sendEthCallWithoutParams( + ethRpc = g.ethRpc.get(), + functionSignature = "root()", + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + return merkleRoot proc fetchNextFreeIndex*( g: OnchainGroupManager ): Future[Result[UInt256, string]] {.async.} = - try: - let nextFreeIndex = await sendEthCallWithoutParams( - ethRpc = g.ethRpc.get(), - functionSignature = "nextFreeIndex()", - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) - return nextFreeIndex - except CatchableError: - error "Failed to fetch next free index", error = getCurrentExceptionMsg() - return err("Failed to fetch next free index: " & getCurrentExceptionMsg()) + let nextFreeIndex = await sendEthCallWithoutParams( + ethRpc = g.ethRpc.get(), + functionSignature = "nextFreeIndex()", + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + return nextFreeIndex proc fetchMembershipStatus*( g: OnchainGroupManager, idCommitment: IDCommitment ): Future[Result[bool, string]] {.async.} = - try: - let params = idCommitment.reversed() - let responseBytes = ( - await sendEthCallWithParams( - ethRpc = g.ethRpc.get(), - functionSignature = "isInMembershipSet(uint256)", - params = params, - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) - ).valueOr: - return err("Failed to check membership: " & error) - - return ok(responseBytes.len == 32 and responseBytes[^1] == 1'u8) - except CatchableError: - error "Failed to fetch membership set membership", error = getCurrentExceptionMsg() - return err("Failed to fetch membership set membership: " & getCurrentExceptionMsg()) - -proc fetchMaxMembershipRateLimit*( - g: OnchainGroupManager -): Future[Result[UInt256, string]] {.async.} = - try: - let maxMembershipRateLimit = await sendEthCallWithoutParams( + let params = idCommitment.reversed() + let responseBytes = ( + await sendEthCallWithParams( ethRpc = g.ethRpc.get(), - functionSignature = "maxMembershipRateLimit()", + functionSignature = "isInMembershipSet(uint256)", + params = params, fromAddress = g.ethRpc.get().defaultAccount, toAddress = fromHex(Address, g.ethContractAddress), chainId = g.chainId, ) - return maxMembershipRateLimit - except CatchableError: - error "Failed to fetch max membership rate limit", error = getCurrentExceptionMsg() - return err("Failed to fetch max membership rate limit: " & getCurrentExceptionMsg()) + ).valueOr: + return err("Failed to check membership: " & error) -template initializedGuard(g: OnchainGroupManager): untyped = + return ok(responseBytes.len == 32 and responseBytes[^1] == 1'u8) + +proc fetchMaxMembershipRateLimit*( + g: OnchainGroupManager +): Future[Result[UInt256, string]] {.async.} = + let maxMembershipRateLimit = await sendEthCallWithoutParams( + ethRpc = g.ethRpc.get(), + functionSignature = "maxMembershipRateLimit()", + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + + return maxMembershipRateLimit + +proc checkInitialized(g: OnchainGroupManager): Result[void, string] = if not g.initialized: - raise newException(CatchableError, "OnchainGroupManager is not initialized") - -template retryWrapper( - g: OnchainGroupManager, res: auto, errStr: string, body: untyped -): auto = - retryWrapper(res, RetryStrategy.new(), errStr, g.onFatalErrorAction): - body + return err("OnchainGroupManager is not initialized") + return ok() proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = let rootRes = (await g.fetchMerkleRoot()).valueOr: @@ -172,40 +148,37 @@ proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = return false -proc trackRootChanges*(g: OnchainGroupManager) {.async: (raises: [CatchableError]).} = - try: - initializedGuard(g) - const rpcDelay = 5.seconds +proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.async.} = + ?checkInitialized(g) - while true: - await sleepAsync(rpcDelay) - let rootUpdated = await g.updateRoots() + const rpcDelay = 5.seconds - if rootUpdated: - ## The membership set on-chain has changed (some new members have joined or some members have left) - if g.membershipIndex.isSome(): - ## A membership index exists only if the node has registered with RLN. - ## Non-registered nodes cannot have Merkle proof elements. - let proofResult = await g.fetchMerkleProofElements() - if proofResult.isErr(): - error "Failed to fetch Merkle proof", error = proofResult.error - else: - g.merkleProofCache = proofResult.get() + while true: + await sleepAsync(rpcDelay) + let rootUpdated = await g.updateRoots() - let nextFreeIndex = (await g.fetchNextFreeIndex()).valueOr: - error "Failed to fetch next free index", error = error - raise - newException(CatchableError, "Failed to fetch next free index: " & error) + if rootUpdated: + ## The membership set on-chain has changed (some new members have joined or some members have left) + if g.membershipIndex.isSome(): + ## A membership index exists only if the node has registered with RLN. + ## Non-registered nodes cannot have Merkle proof elements. + let proofResult = await g.fetchMerkleProofElements() + if proofResult.isErr(): + error "Failed to fetch Merkle proof", error = proofResult.error + else: + g.merkleProofCache = proofResult.get() - let memberCount = cast[int64](nextFreeIndex) - waku_rln_number_registered_memberships.set(float64(memberCount)) - except CatchableError: - error "Fatal error in trackRootChanges", error = getCurrentExceptionMsg() + let nextFreeIndex = (await g.fetchNextFreeIndex()).valueOr: + error "Failed to fetch next free index", error = error + return err("Failed to fetch next free index: " & error) + + let memberCount = cast[int64](nextFreeIndex) + waku_rln_number_registered_memberships.set(float64(memberCount)) method register*( g: OnchainGroupManager, rateCommitment: RateCommitment -): Future[void] {.async: (raises: [Exception]).} = - initializedGuard(g) +): Future[Result[void, string]] {.async.} = + ?checkInitialized(g) try: let leaf = rateCommitment.toLeaf().get() @@ -214,33 +187,40 @@ method register*( info "registering member via callback", rateCommitment = leaf, index = idx await g.registerCb.get()(@[Membership(rateCommitment: leaf, index: idx)]) g.latestIndex.inc() - except CatchableError: - raise newException(ValueError, getCurrentExceptionMsg()) + except Exception as e: + return err("Failed to call register callback: " & e.msg) + + return ok() method register*( g: OnchainGroupManager, identityCredential: IdentityCredential, userMessageLimit: UserMessageLimit, -): Future[void] {.async: (raises: [Exception]).} = - initializedGuard(g) +): Future[Result[void, string]] {.async.} = + ?checkInitialized(g) let ethRpc = g.ethRpc.get() let wakuRlnContract = g.wakuRlnContract.get() - var gasPrice: int - g.retryWrapper(gasPrice, "Failed to get gas price"): - 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 gasPrice = ( + await retryWrapper( + RetryStrategy.new(), + "Failed to get gas price", + proc(): Future[int] {.async.} = + let fetchedGasPrice = uint64(await ethRpc.provider.eth_gasPrice()) + if fetchedGasPrice > uint64(high(int) div 2): + warn "Gas price overflow detected, capping at maximum int value", + fetchedGasPrice = fetchedGasPrice, maxInt = high(int) + return high(int) + else: + let calculatedGasPrice = int(fetchedGasPrice) * 2 + debug "Gas price calculated", + fetchedGasPrice = fetchedGasPrice, gasPrice = calculatedGasPrice + return calculatedGasPrice, + ) + ).valueOr: + return err("Failed to get gas price: " & error) + let idCommitmentHex = identityCredential.idCommitment.inHex() debug "identityCredential idCommitmentHex", idCommitment = idCommitmentHex let idCommitment = identityCredential.idCommitment.toUInt256() @@ -249,27 +229,37 @@ method register*( idCommitment = idCommitment, userMessageLimit = userMessageLimit, idCommitmentsToErase = idCommitmentsToErase - var txHash: TxHash - g.retryWrapper(txHash, "Failed to register the member"): - await wakuRlnContract - .register(idCommitment, userMessageLimit.stuint(32), idCommitmentsToErase) - .send(gasPrice = gasPrice) + let txHash = ( + await retryWrapper( + RetryStrategy.new(), + "Failed to register the member", + proc(): Future[TxHash] {.async.} = + return await wakuRlnContract + .register(idCommitment, userMessageLimit.stuint(32), idCommitmentsToErase) + .send(gasPrice = gasPrice), + ) + ).valueOr: + return err("Failed to register member: " & error) # wait for the transaction to be mined - var tsReceipt: ReceiptObject - g.retryWrapper(tsReceipt, "Failed to get the transaction receipt"): - await ethRpc.getMinedTransactionReceipt(txHash) + let tsReceipt = ( + await retryWrapper( + RetryStrategy.new(), + "Failed to get the transaction receipt", + proc(): Future[ReceiptObject] {.async.} = + return await ethRpc.getMinedTransactionReceipt(txHash), + ) + ).valueOr: + return err("Failed to get transaction receipt: " & error) debug "registration transaction mined", txHash = txHash g.registrationTxHash = some(txHash) # the receipt topic holds the hash of signature of the raised events debug "ts receipt", receipt = tsReceipt[] if tsReceipt.status.isNone(): - raise newException(ValueError, "Transaction failed: status is None") + return err("Transaction failed: status is None") if tsReceipt.status.get() != 1.Quantity: - raise newException( - ValueError, "Transaction failed with status: " & $tsReceipt.status.get() - ) + return err("Transaction failed with status: " & $tsReceipt.status.get()) ## Search through all transaction logs to find the MembershipRegistered event let expectedEventSignature = cast[FixedBytes[32]](keccak.keccak256.digest( @@ -283,9 +273,7 @@ method register*( break if membershipRegisteredLog.isNone(): - raise newException( - ValueError, "register: MembershipRegistered event not found in transaction logs" - ) + return err("register: MembershipRegistered event not found in transaction logs") let registrationLog = membershipRegisteredLog.get() @@ -309,20 +297,28 @@ method register*( if g.registerCb.isSome(): let member = Membership(rateCommitment: rateCommitment, index: g.latestIndex) - await g.registerCb.get()(@[member]) + try: + await g.registerCb.get()(@[member]) + except Exception as e: + return err("Failed to call register callback: " & e.msg) g.latestIndex.inc() - return + return ok() method withdraw*( g: OnchainGroupManager, idCommitment: IDCommitment -): Future[void] {.async: (raises: [Exception]).} = - initializedGuard(g) # TODO: after slashing is enabled on the contract +): Future[Result[void, string]] {.async.} = + checkInitialized(g).isOkOr: + return err(error) + return ok() method withdrawBatch*( g: OnchainGroupManager, idCommitments: seq[IDCommitment] -): Future[void] {.async: (raises: [Exception]).} = - initializedGuard(g) +): Future[Result[void, string]] {.async.} = + checkInitialized(g).isOkOr: + return err(error) + + return ok() proc getRootFromProofAndIndex( g: OnchainGroupManager, elements: seq[byte], bits: seq[byte] @@ -354,7 +350,7 @@ method generateProof*( epoch: Epoch, messageId: MessageId, rlnIdentifier = DefaultRlnIdentifier, -): GroupManagerResult[RateLimitProof] {.gcsafe, raises: [].} = +): GroupManagerResult[RateLimitProof] {.gcsafe.} = ## Generates an RLN proof using the cached Merkle proof and custom witness # Ensure identity credentials and membership index are set if g.idCredentials.isNone(): @@ -452,7 +448,7 @@ method generateProof*( method verifyProof*( g: OnchainGroupManager, input: seq[byte], proof: RateLimitProof -): GroupManagerResult[bool] {.gcsafe, raises: [].} = +): GroupManagerResult[bool] {.gcsafe.} = ## -- Verifies an RLN rate-limit proof against the set of valid Merkle roots -- var normalizedProof = proof @@ -492,25 +488,31 @@ method onWithdraw*(g: OnchainGroupManager, cb: OnWithdrawCallback) {.gcsafe.} = proc establishConnection( g: OnchainGroupManager ): Future[GroupManagerResult[Web3]] {.async.} = - var ethRpc: Web3 + let ethRpc = ( + await retryWrapper( + RetryStrategy.new(), + "Failed to connect to the Ethereum client", + proc(): Future[Web3] {.async.} = + var innerEthRpc: Web3 + var connected = false + for clientUrl in g.ethClientUrls: + ## We give a chance to the user to provide multiple clients + ## and we try to connect to each of them + try: + innerEthRpc = await newWeb3(clientUrl) + connected = true + break + except CatchableError: + error "failed connect Eth client", error = getCurrentExceptionMsg() - g.retryWrapper(ethRpc, "Failed to connect to the Ethereum client"): - var innerEthRpc: Web3 - var connected = false - for clientUrl in g.ethClientUrls: - ## We give a chance to the user to provide multiple clients - ## and we try to connect to each of them - try: - innerEthRpc = await newWeb3(clientUrl) - connected = true - break - except CatchableError: - error "failed connect Eth client", error = getCurrentExceptionMsg() + ## this exception is handled by the retrywrapper + if not connected: + raise newException(CatchableError, "all failed") - if not connected: - raise newException(CatchableError, "all failed") - - innerEthRpc + return innerEthRpc, + ) + ).valueOr: + return err("Failed to establish Ethereum connection: " & error) return ok(ethRpc) @@ -519,9 +521,15 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} let ethRpc: Web3 = (await establishConnection(g)).valueOr: return err("failed to connect to Ethereum clients: " & $error) - var fetchedChainId: UInt256 - g.retryWrapper(fetchedChainId, "Failed to get the chain id"): - await ethRpc.provider.eth_chainId() + let fetchedChainId = ( + await retryWrapper( + RetryStrategy.new(), + "Failed to get the chain id", + proc(): Future[UInt256] {.async.} = + return await ethRpc.provider.eth_chainId(), + ) + ).valueOr: + return err("Failed to get chain id: " & error) # Set the chain id if g.chainId == 0: @@ -595,8 +603,10 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} proc onDisconnect() {.async.} = error "Ethereum client disconnected" - var newEthRpc: Web3 = (await g.establishConnection()).valueOr: - g.onFatalErrorAction("failed to connect to Ethereum clients onDisconnect") + let newEthRpc: Web3 = (await g.establishConnection()).valueOr: + error "Fatal: failed to reconnect to Ethereum clients after disconnect", + error = error + g.onFatalErrorAction("failed to reconnect to Ethereum clients: " & error) return newEthRpc.ondisconnect = ethRpc.ondisconnect @@ -616,12 +626,14 @@ method stop*(g: OnchainGroupManager): Future[void] {.async, gcsafe.} = g.initialized = false method isReady*(g: OnchainGroupManager): Future[bool] {.async.} = - initializedGuard(g) + checkInitialized(g).isOkOr: + return false if g.ethRpc.isNone(): + error "Ethereum RPC client is not configured" return false if g.wakuRlnContract.isNone(): + error "Waku RLN contract is not configured" return false - return true diff --git a/waku/waku_rln_relay/group_manager/on_chain/retry_wrapper.nim b/waku/waku_rln_relay/group_manager/on_chain/retry_wrapper.nim index df8716279..97bc0c435 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/retry_wrapper.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/retry_wrapper.nim @@ -1,36 +1,31 @@ -import ../../../common/error_handling import chronos import results +const + DefaultRetryDelay* = 4000.millis + DefaultRetryCount* = 15'u + type RetryStrategy* = object - shouldRetry*: bool retryDelay*: Duration retryCount*: uint proc new*(T: type RetryStrategy): RetryStrategy = - return RetryStrategy(shouldRetry: true, retryDelay: 4000.millis, retryCount: 15) + return RetryStrategy(retryDelay: DefaultRetryDelay, retryCount: DefaultRetryCount) -template retryWrapper*( - res: auto, - retryStrategy: RetryStrategy, - errStr: string, - errCallback: OnFatalErrorHandler, - body: untyped, -): auto = - if errCallback == nil: - raise newException(CatchableError, "Ensure that the errCallback is set") +proc retryWrapper*[T]( + retryStrategy: RetryStrategy, errStr: string, body: proc(): Future[T] {.async.} +): Future[Result[T, string]] {.async.} = var retryCount = retryStrategy.retryCount - var shouldRetry = retryStrategy.shouldRetry - var exceptionMessage = "" + var lastError = "" - while shouldRetry and retryCount > 0: + while retryCount > 0: try: - res = body - shouldRetry = false - except: + let value = await body() + return ok(value) + except CatchableError as e: retryCount -= 1 - exceptionMessage = getCurrentExceptionMsg() - await sleepAsync(retryStrategy.retryDelay) - if shouldRetry: - errCallback(errStr & ": " & exceptionMessage) - return + lastError = e.msg + if retryCount > 0: + await sleepAsync(retryStrategy.retryDelay) + + return err(errStr & ": " & lastError) diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index 5c893e2a2..8559dcd66 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -68,7 +68,7 @@ type WakuRLNRelay* = ref object of RootObj onFatalErrorAction*: OnFatalErrorHandler nonceManager*: NonceManager epochMonitorFuture*: Future[void] - rootChangesFuture*: Future[void] + rootChangesFuture*: Future[Result[void, string]] brokerCtx*: BrokerContext proc calcEpoch*(rlnPeer: WakuRLNRelay, t: float64): Epoch = @@ -467,7 +467,7 @@ proc mount( return ok(wakuRlnRelay) -proc isReady*(rlnPeer: WakuRLNRelay): Future[bool] {.async: (raises: [Exception]).} = +proc isReady*(rlnPeer: WakuRLNRelay): Future[bool] {.async.} = ## returns true if the rln-relay protocol is ready to relay messages ## returns false otherwise From 6a1cf578ef65e427a66a1e4eec24b57b35900ef5 Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Fri, 13 Mar 2026 12:10:40 +0100 Subject: [PATCH 087/155] Revert "Release : patch release v0.37.1-beta (#3661)" We are going to update the CHANGELOG with another PR today This reverts commit 868d43164e9b5ad0c3a856e872448e9e80531e0c. --- CHANGELOG.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c80a3b79..61e818afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,4 @@ -## 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) +## v0.37.0 (2025-10-01) ### Notes From 69ede0b7dfd198a0ddae9f208b19a991f10e4a99 Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Fri, 13 Mar 2026 12:41:03 +0100 Subject: [PATCH 088/155] rm leftovrs in CHANGELOG.md --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b80e6bd30..edc4a705c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,3 @@ -<<<<<<< update-changelog-v0.37.1-master ## v0.37.1 (2026-03-12) ### Bug Fixes @@ -6,10 +5,7 @@ - Avoid IndexDefect if DB error message is short ([#3725](https://github.com/logos-messaging/logos-delivery/pull/3725)) - Remove ENR cache from peer exchange ([#3652](https://github.com/logos-messaging/logos-messaging-nim/pull/3652)) ([7920368a](https://github.com/logos-messaging/logos-messaging-nim/commit/7920368a36687cd5f12afa52d59866792d8457ca)) -## v0.37.0-beta (2025-10-01) -======= ## v0.37.0 (2025-10-01) ->>>>>>> master ### Notes From 96f1c40ab300db2eabdf3d2c4547288ebe241d58 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:33:24 +0100 Subject: [PATCH 089/155] simple refactor in waku mix protocol mostly to rm duplicated log (#3752) --- waku/waku_mix/protocol.nim | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/waku/waku_mix/protocol.nim b/waku/waku_mix/protocol.nim index 2c972bef6..e31929b71 100644 --- a/waku/waku_mix/protocol.nim +++ b/waku/waku_mix/protocol.nim @@ -71,7 +71,7 @@ proc processBootNodes( info "using mix bootstrap nodes ", count = count proc new*( - T: type WakuMix, + T: typedesc[WakuMix], nodeAddr: string, peermgr: PeerManager, clusterId: uint16, @@ -86,8 +86,6 @@ proc new*( peermgr.switch.peerInfo.peerId, nodeMultiAddr, mixPubKey, mixPrivKey, peermgr.switch.peerInfo.publicKey.skkey, peermgr.switch.peerInfo.privateKey.skkey, ) - if bootnodes.len < minMixPoolSize: - warn "publishing with mix won't work until atleast 3 mix nodes in node pool" var m = WakuMix(peerManager: peermgr, clusterId: clusterId, pubKey: mixPubKey) procCall MixProtocol(m).init( From fdf4e839ffcd40542b4def2207290f405948131c Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Fri, 13 Mar 2026 23:22:53 +0100 Subject: [PATCH 090/155] use fix branch from nim-http-utils --- vendor/nim-http-utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-http-utils b/vendor/nim-http-utils index 79cbab146..08db60946 160000 --- a/vendor/nim-http-utils +++ b/vendor/nim-http-utils @@ -1 +1 @@ -Subproject commit 79cbab1460f4c0cdde2084589d017c43a3d7b4f1 +Subproject commit 08db609467a0e2b5a6e8ce118bb83dba7a8a9375 From 6030983a83479eaa93f216bd43e8afa316e7b558 Mon Sep 17 00:00:00 2001 From: darshankabariya Date: Mon, 16 Mar 2026 14:27:43 +0530 Subject: [PATCH 091/155] chore: add v0.38.0 changelog --- CHANGELOG.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index edc4a705c..bc5155b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,73 @@ +## v0.38.0 (2026-03-16) + +### Notes + +- **liblogosdelivery**: Major new FFI API with debug API, health status events, message received events, stateful SubscriptionService, and improved resource management. +- Waku Kademlia discovery integrated with Mix protocol. +- Context-aware and event-driven broker architecture introduced. +- REST Store API now defaults to page size 20 with max 100. +- Lightpush no longer mounts without relay enabled. +- Repository renamed from `logos-messaging-nim` to `logos-delivery`. + +### Features + +- liblogosdelivery: FFI library of new API ([#3714](https://github.com/logos-messaging/logos-delivery/pull/3714)) ([3603b838](https://github.com/logos-messaging/logos-delivery/commit/3603b838)) +- liblogosdelivery: health status event support ([#3737](https://github.com/logos-messaging/logos-delivery/pull/3737)) ([ba85873f](https://github.com/logos-messaging/logos-delivery/commit/ba85873f)) +- liblogosdelivery: MessageReceivedEvent propagation over FFI ([#3747](https://github.com/logos-messaging/logos-delivery/pull/3747)) ([0ad55159](https://github.com/logos-messaging/logos-delivery/commit/0ad55159)) +- liblogosdelivery: add debug API ([#3742](https://github.com/logos-messaging/logos-delivery/pull/3742)) ([09618a26](https://github.com/logos-messaging/logos-delivery/commit/09618a26)) +- liblogosdelivery: implement stateful SubscriptionService for Core mode ([#3732](https://github.com/logos-messaging/logos-delivery/pull/3732)) ([51ec09c3](https://github.com/logos-messaging/logos-delivery/commit/51ec09c3)) +- Waku Kademlia integration and Mix protocol updates ([#3722](https://github.com/logos-messaging/logos-delivery/pull/3722)) ([335600eb](https://github.com/logos-messaging/logos-delivery/commit/335600eb)) +- Waku API: implement Health spec ([#3689](https://github.com/logos-messaging/logos-delivery/pull/3689)) ([1fb4d1ea](https://github.com/logos-messaging/logos-delivery/commit/1fb4d1ea)) +- Waku API: send ([#3669](https://github.com/logos-messaging/logos-delivery/pull/3669)) ([1fd25355](https://github.com/logos-messaging/logos-delivery/commit/1fd25355)) +- iOS compilation support (WIP) ([#3668](https://github.com/logos-messaging/logos-delivery/pull/3668)) ([96196ab8](https://github.com/logos-messaging/logos-delivery/commit/96196ab8)) +- Distribute libwaku binaries ([#3612](https://github.com/logos-messaging/logos-delivery/pull/3612)) ([9e2b3830](https://github.com/logos-messaging/logos-delivery/commit/9e2b3830)) +- Rendezvous: broadcast and discover WakuPeerRecords ([#3617](https://github.com/logos-messaging/logos-delivery/pull/3617)) ([b0cd75f4](https://github.com/logos-messaging/logos-delivery/commit/b0cd75f4)) +- New postgres metric to estimate payload stats ([#3596](https://github.com/logos-messaging/logos-delivery/pull/3596)) ([454b098a](https://github.com/logos-messaging/logos-delivery/commit/454b098a)) + +### Bug Fixes + +- Fix NodeHealthMonitor logspam ([#3743](https://github.com/logos-messaging/logos-delivery/pull/3743)) ([7e36e268](https://github.com/logos-messaging/logos-delivery/commit/7e36e268)) +- Fix peer selection by shard and rendezvous/metadata sharding initialization ([#3718](https://github.com/logos-messaging/logos-delivery/pull/3718)) ([84f79110](https://github.com/logos-messaging/logos-delivery/commit/84f79110)) +- Correct dynamic library extension on mac and update OS detection ([#3754](https://github.com/logos-messaging/logos-delivery/pull/3754)) ([1ace0154](https://github.com/logos-messaging/logos-delivery/commit/1ace0154)) +- Force FINALIZE partition detach after detecting shorter error ([#3728](https://github.com/logos-messaging/logos-delivery/pull/3728)) ([b38b5aae](https://github.com/logos-messaging/logos-delivery/commit/b38b5aae)) +- Fix store protocol issue in v0.37.0 ([#3657](https://github.com/logos-messaging/logos-delivery/pull/3657)) ([91b4c5f5](https://github.com/logos-messaging/logos-delivery/commit/91b4c5f5)) +- Fix hash inputs for external nullifier, remove length prefix for sha256 ([#3660](https://github.com/logos-messaging/logos-delivery/pull/3660)) ([2d40cb9d](https://github.com/logos-messaging/logos-delivery/commit/2d40cb9d)) +- Fix admin API peer shards field from metadata protocol ([#3594](https://github.com/logos-messaging/logos-delivery/pull/3594)) ([e54851d9](https://github.com/logos-messaging/logos-delivery/commit/e54851d9)) +- Wakucanary exits with error if ping fails ([#3595](https://github.com/logos-messaging/logos-delivery/pull/3595), [#3711](https://github.com/logos-messaging/logos-delivery/pull/3711)) +- Force epoll in chronos for Android ([#3705](https://github.com/logos-messaging/logos-delivery/pull/3705)) ([beb1dde1](https://github.com/logos-messaging/logos-delivery/commit/beb1dde1)) +- Fix build_rln.sh script ([#3704](https://github.com/logos-messaging/logos-delivery/pull/3704)) ([09034837](https://github.com/logos-messaging/logos-delivery/commit/09034837)) +- liblogosdelivery: move destroy API to node_api, add security checks and fix possible resource leak ([#3736](https://github.com/logos-messaging/logos-delivery/pull/3736)) ([db19da92](https://github.com/logos-messaging/logos-delivery/commit/db19da92)) + +### Changes + +- Context-aware brokers architecture ([#3674](https://github.com/logos-messaging/logos-delivery/pull/3674)) ([c27405b1](https://github.com/logos-messaging/logos-delivery/commit/c27405b1)) +- Introduce EventBroker, RequestBroker and MultiRequestBroker ([#3644](https://github.com/logos-messaging/logos-delivery/pull/3644)) ([ae74b901](https://github.com/logos-messaging/logos-delivery/commit/ae74b901)) +- Use chronos' TokenBucket ([#3670](https://github.com/logos-messaging/logos-delivery/pull/3670)) ([284a0816](https://github.com/logos-messaging/logos-delivery/commit/284a0816)) +- REST Store API constraints: default page size 20, max 100 ([#3602](https://github.com/logos-messaging/logos-delivery/pull/3602)) ([8c30a8e1](https://github.com/logos-messaging/logos-delivery/commit/8c30a8e1)) +- Do not mount lightpush without relay ([#3540](https://github.com/logos-messaging/logos-delivery/pull/3540)) ([7d1c6aba](https://github.com/logos-messaging/logos-delivery/commit/7d1c6aba)) +- Mix: use exit==dest approach ([#3642](https://github.com/logos-messaging/logos-delivery/pull/3642)) ([088e3108](https://github.com/logos-messaging/logos-delivery/commit/088e3108)) +- Mix: simple refactor to reduce duplicated logs ([#3752](https://github.com/logos-messaging/logos-delivery/pull/3752)) ([96f1c40a](https://github.com/logos-messaging/logos-delivery/commit/96f1c40a)) +- Simplify NodeHealthMonitor creation ([#3716](https://github.com/logos-messaging/logos-delivery/pull/3716)) ([a8bdbca9](https://github.com/logos-messaging/logos-delivery/commit/a8bdbca9)) +- Adapt CLI args for delivery API ([#3744](https://github.com/logos-messaging/logos-delivery/pull/3744)) ([1f9c4cb8](https://github.com/logos-messaging/logos-delivery/commit/1f9c4cb8)) +- Adapt debugapi to WakoNodeConf ([#3745](https://github.com/logos-messaging/logos-delivery/pull/3745)) ([4a6ad732](https://github.com/logos-messaging/logos-delivery/commit/4a6ad732)) +- Bump nim-ffi to v0.1.3 ([#3696](https://github.com/logos-messaging/logos-delivery/pull/3696)) ([a02aaab5](https://github.com/logos-messaging/logos-delivery/commit/a02aaab5)) +- Bump nim-metrics to v0.2.1 ([#3734](https://github.com/logos-messaging/logos-delivery/pull/3734)) ([c7e0cc0e](https://github.com/logos-messaging/logos-delivery/commit/c7e0cc0e)) +- Add gasprice overflow check ([#3636](https://github.com/logos-messaging/logos-delivery/pull/3636)) ([a8590a0a](https://github.com/logos-messaging/logos-delivery/commit/a8590a0a)) +- Pin RLN dependencies to specific version ([#3649](https://github.com/logos-messaging/logos-delivery/pull/3649)) ([834eea94](https://github.com/logos-messaging/logos-delivery/commit/834eea94)) +- Update CI/README references after repository rename to logos-delivery ([#3729](https://github.com/logos-messaging/logos-delivery/pull/3729)) ([895f3e2d](https://github.com/logos-messaging/logos-delivery/commit/895f3e2d)) + +### This release supports the following [libp2p protocols](https://docs.libp2p.io/concepts/protocols/): + +| Protocol | Spec status | Protocol id | +| ---: | :---: | :--- | +| [`11/WAKU2-RELAY`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/11/relay.md) | `stable` | `/vac/waku/relay/2.0.0` | +| [`12/WAKU2-FILTER`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/12/filter.md) | `draft` | `/vac/waku/filter/2.0.0-beta1`
`/vac/waku/filter-subscribe/2.0.0-beta1`
`/vac/waku/filter-push/2.0.0-beta1` | +| [`13/WAKU2-STORE`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/13/store.md) | `draft` | `/vac/waku/store/2.0.0-beta4` | +| [`19/WAKU2-LIGHTPUSH`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/19/lightpush.md) | `draft` | `/vac/waku/lightpush/2.0.0-beta1` | +| [`WAKU2-LIGHTPUSH v3`](https://github.com/waku-org/specs/blob/master/standards/core/lightpush.md) | `draft` | `/vac/waku/lightpush/3.0.0` | +| [`66/WAKU2-METADATA`](https://github.com/waku-org/specs/blob/master/standards/core/metadata.md) | `raw` | `/vac/waku/metadata/1.0.0` | +| [`WAKU-SYNC`](https://github.com/waku-org/specs/blob/master/standards/core/sync.md) | `draft` | `/vac/waku/sync/1.0.0` | + ## v0.37.1 (2026-03-12) ### Bug Fixes From 614f17162638eb178b1b119c86c6c7fa040d018c Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Tue, 17 Mar 2026 10:15:35 -0300 Subject: [PATCH 092/155] nim nph 0.7.0 formatting (#3759) --- apps/chat2/chat2.nim | 23 +- apps/chat2bridge/chat2bridge.nim | 14 +- apps/chat2mix/chat2mix.nim | 23 +- apps/chat2mix/config_chat2mix.nim | 21 +- apps/wakucanary/wakucanary.nim | 9 +- .../logos_delivery_api/debug_api.nim | 4 +- .../json_connection_status_change_event.nim | 10 +- library/libwaku.nim | 2 +- .../content_script_version_1.nim | 3 +- .../content_script_version_2.nim | 3 +- .../content_script_version_3.nim | 3 +- .../content_script_version_4.nim | 3 +- .../content_script_version_5.nim | 3 +- .../content_script_version_6.nim | 3 +- .../content_script_version_7.nim | 3 +- .../pg_migration_manager.nim | 19 +- tests/api/test_api_send.nim | 2 +- tests/api/test_entry_nodes.nim | 64 +- tests/common/test_base64_codec.nim | 31 +- tests/common/test_request_broker.nim | 378 ++--- tests/common/test_sqlite_migrations.nim | 33 +- .../peer_store/test_waku_peer_storage.nim | 77 +- tests/node/test_wakunode_health_monitor.nim | 68 +- tests/node/test_wakunode_legacy_store.nim | 112 +- tests/node/test_wakunode_relay_rln.nim | 48 +- tests/node/test_wakunode_sharding.nim | 44 +- tests/node/test_wakunode_store.nim | 138 +- tests/resources/payloads.nim | 6 +- tests/test_peer_manager.nim | 60 +- tests/test_waku.nim | 23 +- tests/test_waku_keystore_keyfile.nim | 43 +- tests/test_waku_noise.nim | 9 +- tests/test_waku_rendezvous.nim | 9 +- tests/test_waku_switch.nim | 16 +- .../test_driver_postgres_query.nim | 1273 ++++++++--------- .../waku_archive/test_driver_queue_query.nim | 1145 +++++++-------- .../waku_archive/test_driver_sqlite_query.nim | 1180 +++++++-------- tests/waku_archive/test_retention_policy.nim | 19 +- tests/waku_archive/test_waku_archive.nim | 105 +- .../test_driver_postgres_query.nim | 1273 ++++++++--------- .../test_driver_queue_query.nim | 1135 +++++++-------- .../test_driver_sqlite_query.nim | 1180 +++++++-------- .../waku_archive_legacy/test_waku_archive.nim | 105 +- tests/waku_enr/test_sharding.nim | 15 +- tests/waku_rln_relay/test_waku_rln_relay.nim | 17 +- .../test_wakunode_rln_relay.nim | 15 +- tests/waku_rln_relay/utils_onchain.nim | 31 +- tests/waku_store/test_client.nim | 35 +- tests/waku_store/test_resume.nim | 25 +- tests/waku_store/test_wakunode_store.nim | 25 +- tests/waku_store_legacy/test_resume.nim | 178 ++- tests/waku_store_legacy/test_rpc_codec.nim | 9 +- .../waku_store_legacy/test_wakunode_store.nim | 25 +- tests/waku_store_sync/test_range_split.nim | 42 +- .../waku_store_sync/test_state_transition.nim | 40 +- tests/wakunode_rest/test_rest_filter.nim | 11 +- tests/wakunode_rest/test_rest_relay.nim | 48 +- tests/wakunode_rest/test_rest_store.nim | 151 +- tools/confutils/cli_args.nim | 3 +- vendor/nph | 2 +- waku/api/api_conf.nim | 7 +- waku/common/broker/event_broker.nim | 15 +- waku/common/databases/db_postgres/dbconn.nim | 9 +- waku/common/rate_limit/timed_map.nim | 12 +- waku/factory/builder.nim | 27 +- waku/factory/networks_config.nim | 28 +- waku/factory/node_factory.nim | 9 +- waku/node/peer_manager/peer_manager.nim | 8 +- waku/node/waku_node.nim | 11 +- waku/rest_api/endpoint/builder.nim | 13 +- waku/rest_api/endpoint/server.nim | 33 +- .../postgres_driver/postgres_driver.nim | 36 +- .../driver/sqlite_driver/queries.nim | 44 +- .../driver/sqlite_driver/queries.nim | 55 +- waku/waku_core/peers.nim | 36 +- waku/waku_core/topics/pubsub_topic.nim | 18 +- waku/waku_enr/sharding.nim | 7 +- waku/waku_keystore/protocol_types.nim | 7 +- .../waku_noise/noise_handshake_processing.nim | 17 +- waku/waku_noise/noise_types.nim | 64 +- .../group_manager/on_chain/group_manager.nim | 4 +- waku/waku_rln_relay/rln/wrappers.nim | 13 +- 82 files changed, 4464 insertions(+), 5403 deletions(-) diff --git a/apps/chat2/chat2.nim b/apps/chat2/chat2.nim index 71d8a4e6a..4102cf074 100644 --- a/apps/chat2/chat2.nim +++ b/apps/chat2/chat2.nim @@ -50,8 +50,7 @@ import import libp2p/protocols/pubsub/rpc/messages, libp2p/protocols/pubsub/pubsub import ../../waku/waku_rln_relay -const Help = - """ +const Help = """ Commands: /[?|help|connect|nick|exit] help: Prints this help connect: dials a remote peer @@ -337,16 +336,16 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = builder.withRecord(record) builder - .withNetworkConfigurationDetails( - conf.listenAddress, - Port(uint16(conf.tcpPort) + conf.portsShift), - extIp, - extTcpPort, - wsBindPort = Port(uint16(conf.websocketPort) + conf.portsShift), - wsEnabled = conf.websocketSupport, - wssEnabled = conf.websocketSecureSupport, - ) - .tryGet() + .withNetworkConfigurationDetails( + conf.listenAddress, + Port(uint16(conf.tcpPort) + conf.portsShift), + extIp, + extTcpPort, + wsBindPort = Port(uint16(conf.websocketPort) + conf.portsShift), + wsEnabled = conf.websocketSupport, + wssEnabled = conf.websocketSecureSupport, + ) + .tryGet() builder.build().tryGet() await node.start() diff --git a/apps/chat2bridge/chat2bridge.nim b/apps/chat2bridge/chat2bridge.nim index 77c9e553e..53eb5d04b 100644 --- a/apps/chat2bridge/chat2bridge.nim +++ b/apps/chat2bridge/chat2bridge.nim @@ -127,8 +127,10 @@ proc toMatterbridge( assert chat2Msg.isOk if not cmb.mbClient - .postMessage(text = string.fromBytes(chat2Msg[].payload), username = chat2Msg[].nick) - .containsValue(true): + .postMessage( + text = string.fromBytes(chat2Msg[].payload), username = chat2Msg[].nick + ) + .containsValue(true): chat2_mb_dropped.inc(labelValues = ["duplicate"]) error "Matterbridge host unreachable. Dropping message." @@ -175,10 +177,10 @@ proc new*( builder.withNodeKey(nodev2Key) builder - .withNetworkConfigurationDetails( - nodev2BindIp, nodev2BindPort, nodev2ExtIp, nodev2ExtPort - ) - .tryGet() + .withNetworkConfigurationDetails( + nodev2BindIp, nodev2BindPort, nodev2ExtIp, nodev2ExtPort + ) + .tryGet() builder.build().tryGet() return Chat2MatterBridge( diff --git a/apps/chat2mix/chat2mix.nim b/apps/chat2mix/chat2mix.nim index 558454307..8b786d7b6 100644 --- a/apps/chat2mix/chat2mix.nim +++ b/apps/chat2mix/chat2mix.nim @@ -57,8 +57,7 @@ import ../../waku/waku_rln_relay logScope: topics = "chat2 mix" -const Help = - """ +const Help = """ Commands: /[?|help|connect|nick|exit] help: Prints this help connect: dials a remote peer @@ -429,16 +428,16 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = builder.withRecord(record) builder - .withNetworkConfigurationDetails( - conf.listenAddress, - Port(uint16(conf.tcpPort) + conf.portsShift), - extIp, - extTcpPort, - wsBindPort = Port(uint16(conf.websocketPort) + conf.portsShift), - wsEnabled = conf.websocketSupport, - wssEnabled = conf.websocketSecureSupport, - ) - .tryGet() + .withNetworkConfigurationDetails( + conf.listenAddress, + Port(uint16(conf.tcpPort) + conf.portsShift), + extIp, + extTcpPort, + wsBindPort = Port(uint16(conf.websocketPort) + conf.portsShift), + wsEnabled = conf.websocketSupport, + wssEnabled = conf.websocketSecureSupport, + ) + .tryGet() builder.build().tryGet() node.mountAutoSharding(conf.clusterId, conf.numShardsInNetwork).isOkOr: diff --git a/apps/chat2mix/config_chat2mix.nim b/apps/chat2mix/config_chat2mix.nim index 46cd481d7..4e5a32e6d 100644 --- a/apps/chat2mix/config_chat2mix.nim +++ b/apps/chat2mix/config_chat2mix.nim @@ -113,17 +113,16 @@ type shards* {. desc: "Shards index to subscribe to [0..NUM_SHARDS_IN_NETWORK-1]. Argument may be repeated.", - defaultValue: - @[ - uint16(0), - uint16(1), - uint16(2), - uint16(3), - uint16(4), - uint16(5), - uint16(6), - uint16(7), - ], + defaultValue: @[ + uint16(0), + uint16(1), + uint16(2), + uint16(3), + uint16(4), + uint16(5), + uint16(6), + uint16(7), + ], name: "shard" .}: seq[uint16] diff --git a/apps/wakucanary/wakucanary.nim b/apps/wakucanary/wakucanary.nim index 40bf4db45..bb68f7237 100644 --- a/apps/wakucanary/wakucanary.nim +++ b/apps/wakucanary/wakucanary.nim @@ -161,11 +161,10 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = # create dns resolver let - nameServers = - @[ - initTAddress(parseIpAddress("1.1.1.1"), Port(53)), - initTAddress(parseIpAddress("1.0.0.1"), Port(53)), - ] + nameServers = @[ + initTAddress(parseIpAddress("1.1.1.1"), Port(53)), + initTAddress(parseIpAddress("1.0.0.1"), Port(53)), + ] resolver: DnsResolver = DnsResolver.new(nameServers) if conf.logLevel != LogLevel.NONE: diff --git a/liblogosdelivery/logos_delivery_api/debug_api.nim b/liblogosdelivery/logos_delivery_api/debug_api.nim index 623b3b08f..bb66a0e3f 100644 --- a/liblogosdelivery/logos_delivery_api/debug_api.nim +++ b/liblogosdelivery/logos_delivery_api/debug_api.nim @@ -45,7 +45,9 @@ proc logosdelivery_get_available_configs( for meta in optionMetas: configOptionDetails.add( - %*{meta.fieldName: meta.typeName & "(" & meta.defaultValue & ")", "desc": meta.desc} + %*{ + meta.fieldName: meta.typeName & "(" & meta.defaultValue & ")", "desc": meta.desc + } ) var jsonNode = newJObject() diff --git a/library/events/json_connection_status_change_event.nim b/library/events/json_connection_status_change_event.nim index 347a84c48..86bfda780 100644 --- a/library/events/json_connection_status_change_event.nim +++ b/library/events/json_connection_status_change_event.nim @@ -7,13 +7,9 @@ import ../../waku/api/types type JsonConnectionStatusChangeEvent* = ref object of JsonEvent status*: ConnectionStatus -proc new*( - T: type JsonConnectionStatusChangeEvent, status: ConnectionStatus -): T = - return JsonConnectionStatusChangeEvent( - eventType: "node_health_change", - status: status - ) +proc new*(T: type JsonConnectionStatusChangeEvent, status: ConnectionStatus): T = + return + JsonConnectionStatusChangeEvent(eventType: "node_health_change", status: status) method `$`*(event: JsonConnectionStatusChangeEvent): string = $(%*event) diff --git a/library/libwaku.nim b/library/libwaku.nim index eb3cdff5e..dd1ee9fd9 100644 --- a/library/libwaku.nim +++ b/library/libwaku.nim @@ -72,7 +72,7 @@ proc waku_new( relayHandler: onReceivedMessage(ctx), topicHealthChangeHandler: onTopicHealthChange(ctx), connectionChangeHandler: onConnectionChange(ctx), - connectionStatusChangeHandler: onConnectionStatusChange(ctx) + connectionStatusChangeHandler: onConnectionStatusChange(ctx), ) ffi.sendRequestToFFIThread( diff --git a/migrations/message_store_postgres/content_script_version_1.nim b/migrations/message_store_postgres/content_script_version_1.nim index 18133bdca..37c6bf2ec 100644 --- a/migrations/message_store_postgres/content_script_version_1.nim +++ b/migrations/message_store_postgres/content_script_version_1.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_1* = - """ +const ContentScriptVersion_1* = """ CREATE TABLE IF NOT EXISTS messages ( pubsubTopic VARCHAR NOT NULL, contentTopic VARCHAR NOT NULL, diff --git a/migrations/message_store_postgres/content_script_version_2.nim b/migrations/message_store_postgres/content_script_version_2.nim index 8c3656e64..4065a26c6 100644 --- a/migrations/message_store_postgres/content_script_version_2.nim +++ b/migrations/message_store_postgres/content_script_version_2.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_2* = - """ +const ContentScriptVersion_2* = """ ALTER TABLE IF EXISTS messages_backup RENAME TO messages; ALTER TABLE messages RENAME TO messages_backup; ALTER TABLE messages_backup DROP CONSTRAINT messageIndex; diff --git a/migrations/message_store_postgres/content_script_version_3.nim b/migrations/message_store_postgres/content_script_version_3.nim index 2938087cc..22e7308aa 100644 --- a/migrations/message_store_postgres/content_script_version_3.nim +++ b/migrations/message_store_postgres/content_script_version_3.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_3* = - """ +const ContentScriptVersion_3* = """ CREATE INDEX IF NOT EXISTS i_query ON messages (contentTopic, pubsubTopic, storedAt, id); diff --git a/migrations/message_store_postgres/content_script_version_4.nim b/migrations/message_store_postgres/content_script_version_4.nim index 50ee269f6..6412371e5 100644 --- a/migrations/message_store_postgres/content_script_version_4.nim +++ b/migrations/message_store_postgres/content_script_version_4.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_4* = - """ +const ContentScriptVersion_4* = """ ALTER TABLE messages ADD meta VARCHAR default null; CREATE INDEX IF NOT EXISTS i_query ON messages (contentTopic, pubsubTopic, storedAt, id); diff --git a/migrations/message_store_postgres/content_script_version_5.nim b/migrations/message_store_postgres/content_script_version_5.nim index a59b2da87..8210be4ac 100644 --- a/migrations/message_store_postgres/content_script_version_5.nim +++ b/migrations/message_store_postgres/content_script_version_5.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_5* = - """ +const ContentScriptVersion_5* = """ CREATE INDEX IF NOT EXISTS i_query_storedAt ON messages (storedAt, id); UPDATE version SET version = 5 WHERE version = 4; diff --git a/migrations/message_store_postgres/content_script_version_6.nim b/migrations/message_store_postgres/content_script_version_6.nim index 126ec6da1..88e92ed34 100644 --- a/migrations/message_store_postgres/content_script_version_6.nim +++ b/migrations/message_store_postgres/content_script_version_6.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_6* = - """ +const ContentScriptVersion_6* = """ -- we can drop the timestamp column because this data is also kept in the storedAt column ALTER TABLE messages DROP COLUMN timestamp; diff --git a/migrations/message_store_postgres/content_script_version_7.nim b/migrations/message_store_postgres/content_script_version_7.nim index 01d7ad84e..e2aba854d 100644 --- a/migrations/message_store_postgres/content_script_version_7.nim +++ b/migrations/message_store_postgres/content_script_version_7.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_7* = - """ +const ContentScriptVersion_7* = """ -- Create lookup table CREATE TABLE IF NOT EXISTS messages_lookup ( diff --git a/migrations/message_store_postgres/pg_migration_manager.nim b/migrations/message_store_postgres/pg_migration_manager.nim index 051ac9e79..89a443609 100644 --- a/migrations/message_store_postgres/pg_migration_manager.nim +++ b/migrations/message_store_postgres/pg_migration_manager.nim @@ -10,16 +10,15 @@ type MigrationScript* = object proc init*(T: type MigrationScript, targetVersion: int, scriptContent: string): T = return MigrationScript(targetVersion: targetVersion, scriptContent: scriptContent) -const PgMigrationScripts* = - @[ - MigrationScript(version: 1, scriptContent: ContentScriptVersion_1), - MigrationScript(version: 2, scriptContent: ContentScriptVersion_2), - MigrationScript(version: 3, scriptContent: ContentScriptVersion_3), - MigrationScript(version: 4, scriptContent: ContentScriptVersion_4), - MigrationScript(version: 5, scriptContent: ContentScriptVersion_5), - MigrationScript(version: 6, scriptContent: ContentScriptVersion_6), - MigrationScript(version: 7, scriptContent: ContentScriptVersion_7), - ] +const PgMigrationScripts* = @[ + MigrationScript(version: 1, scriptContent: ContentScriptVersion_1), + MigrationScript(version: 2, scriptContent: ContentScriptVersion_2), + MigrationScript(version: 3, scriptContent: ContentScriptVersion_3), + MigrationScript(version: 4, scriptContent: ContentScriptVersion_4), + MigrationScript(version: 5, scriptContent: ContentScriptVersion_5), + MigrationScript(version: 6, scriptContent: ContentScriptVersion_6), + MigrationScript(version: 7, scriptContent: ContentScriptVersion_7), +] proc getMigrationScripts*(currentVersion: int64, targetVersion: int64): seq[string] = var ret = newSeq[string]() diff --git a/tests/api/test_api_send.nim b/tests/api/test_api_send.nim index 30a176119..28f0ca2ff 100644 --- a/tests/api/test_api_send.nim +++ b/tests/api/test_api_send.nim @@ -88,7 +88,7 @@ proc waitForEvents( return await allFutures( manager.sentFuture, manager.propagatedFuture, manager.errorFuture ) - .withTimeout(timeout) + .withTimeout(timeout) proc outcomes(manager: SendEventListenerManager): set[SendEventOutcome] = if manager.sentFuture.completed(): diff --git a/tests/api/test_entry_nodes.nim b/tests/api/test_entry_nodes.nim index 38dc38ba4..a6faf58c8 100644 --- a/tests/api/test_entry_nodes.nim +++ b/tests/api/test_entry_nodes.nim @@ -126,12 +126,11 @@ suite "Entry Nodes Classification": suite "Entry Nodes Processing": test "Process mixed entry nodes": - let entryNodes = - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g", - ] + let entryNodes = @[ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g", + ] let result = processEntryNodes(entryNodes) check: @@ -147,11 +146,10 @@ suite "Entry Nodes Processing": staticNodes[0] == entryNodes[1] # multiaddr added to static test "Process only ENRTree nodes": - let entryNodes = - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "enrtree://ANOTHER_TREE@example.com", - ] + let entryNodes = @[ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + "enrtree://ANOTHER_TREE@example.com", + ] let result = processEntryNodes(entryNodes) check: @@ -165,11 +163,10 @@ suite "Entry Nodes Processing": enrTreeUrls == entryNodes test "Process only multiaddresses": - let entryNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "/ip4/192.168.1.1/tcp/60001/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYd", - ] + let entryNodes = @[ + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + "/ip4/192.168.1.1/tcp/60001/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYd", + ] let result = processEntryNodes(entryNodes) check: @@ -183,11 +180,10 @@ suite "Entry Nodes Processing": staticNodes == entryNodes test "Process only ENR nodes": - let entryNodes = - @[ - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g", - "enr:-QEkuECnZ3IbVAgkOzv-QLnKC4dRKAPRY80m1-R7G8jZ7yfT3ipEfBrhKN7ARcQgQ-vg-h40AQzyvAkPYlHPaFKk6u9MBgmlkgnY0iXNlY3AyNTZrMaEDk49D8JjMSns4p1XVNBvJquOUzT4PENSJknkROspfAFGg3RjcIJ2X4N1ZHCCd2g", - ] + let entryNodes = @[ + "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g", + "enr:-QEkuECnZ3IbVAgkOzv-QLnKC4dRKAPRY80m1-R7G8jZ7yfT3ipEfBrhKN7ARcQgQ-vg-h40AQzyvAkPYlHPaFKk6u9MBgmlkgnY0iXNlY3AyNTZrMaEDk49D8JjMSns4p1XVNBvJquOUzT4PENSJknkROspfAFGg3RjcIJ2X4N1ZHCCd2g", + ] let result = processEntryNodes(entryNodes) check: @@ -224,13 +220,12 @@ suite "Entry Nodes Processing": "Entry node error: Unrecognized entry node format. Must start with 'enrtree:', 'enr:', or '/'" test "Process different multiaddr formats": - let entryNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "/ip6/::1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYd", - "/dns4/example.com/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYe", - "/dns/node.example.org/tcp/443/wss/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYf", - ] + let entryNodes = @[ + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + "/ip6/::1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYd", + "/dns4/example.com/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYe", + "/dns/node.example.org/tcp/443/wss/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYf", + ] let result = processEntryNodes(entryNodes) check: @@ -244,13 +239,12 @@ suite "Entry Nodes Processing": staticNodes == entryNodes test "Process with duplicate entries": - let entryNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - ] + let entryNodes = @[ + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + ] let result = processEntryNodes(entryNodes) check: diff --git a/tests/common/test_base64_codec.nim b/tests/common/test_base64_codec.nim index 1c2d04c45..b9ac9e464 100644 --- a/tests/common/test_base64_codec.nim +++ b/tests/common/test_base64_codec.nim @@ -4,23 +4,22 @@ import std/strutils, results, stew/byteutils, testutils/unittests import waku/common/base64 suite "Waku Common - stew base64 wrapper": - const TestData = - @[ - # Test vectors from RFC 4648 - # See: https://datatracker.ietf.org/doc/html/rfc4648#section-10 - ("", Base64String("")), - ("f", Base64String("Zg==")), - ("fo", Base64String("Zm8=")), - ("foo", Base64String("Zm9v")), - ("foob", Base64String("Zm9vYg==")), - ("fooba", Base64String("Zm9vYmE=")), - ("foobar", Base64String("Zm9vYmFy")), + const TestData = @[ + # Test vectors from RFC 4648 + # See: https://datatracker.ietf.org/doc/html/rfc4648#section-10 + ("", Base64String("")), + ("f", Base64String("Zg==")), + ("fo", Base64String("Zm8=")), + ("foo", Base64String("Zm9v")), + ("foob", Base64String("Zm9vYg==")), + ("fooba", Base64String("Zm9vYmE=")), + ("foobar", Base64String("Zm9vYmFy")), - # Custom test vectors - ("\x01", Base64String("AQ==")), - ("\x13", Base64String("Ew==")), - ("\x01\x02\x03\x04", Base64String("AQIDBA==")), - ] + # Custom test vectors + ("\x01", Base64String("AQ==")), + ("\x13", Base64String("Ew==")), + ("\x01\x02\x03\x04", Base64String("AQIDBA==")), + ] for (plaintext, encoded) in TestData: test "encode into base64 (" & escape(plaintext) & " -> \"" & string(encoded) & "\")": diff --git a/tests/common/test_request_broker.nim b/tests/common/test_request_broker.nim index 87065a916..b1e16979b 100644 --- a/tests/common/test_request_broker.nim +++ b/tests/common/test_request_broker.nim @@ -45,11 +45,11 @@ static: suite "RequestBroker macro (async mode)": test "serves zero-argument providers": check SimpleResponse - .setProvider( - proc(): Future[Result[SimpleResponse, string]] {.async.} = - ok(SimpleResponse(value: "hi")) - ) - .isOk() + .setProvider( + proc(): Future[Result[SimpleResponse, string]] {.async.} = + ok(SimpleResponse(value: "hi")) + ) + .isOk() let res = waitFor SimpleResponse.request() check res.isOk() @@ -65,12 +65,14 @@ suite "RequestBroker macro (async mode)": test "serves input-based providers": var seen: seq[string] = @[] check KeyedResponse - .setProvider( - proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = - seen.add(key) - ok(KeyedResponse(key: key, payload: key & "-payload+" & $subKey)) - ) - .isOk() + .setProvider( + proc( + key: string, subKey: int + ): Future[Result[KeyedResponse, string]] {.async.} = + seen.add(key) + ok(KeyedResponse(key: key, payload: key & "-payload+" & $subKey)) + ) + .isOk() let res = waitFor KeyedResponse.request("topic", 1) check res.isOk() @@ -82,11 +84,13 @@ suite "RequestBroker macro (async mode)": test "catches provider exception": check KeyedResponse - .setProvider( - proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = - raise newException(ValueError, "simulated failure") - ) - .isOk() + .setProvider( + proc( + key: string, subKey: int + ): Future[Result[KeyedResponse, string]] {.async.} = + raise newException(ValueError, "simulated failure") + ) + .isOk() let res = waitFor KeyedResponse.request("neglected", 11) check res.isErr() @@ -101,18 +105,18 @@ suite "RequestBroker macro (async mode)": test "supports both provider types simultaneously": check DualResponse - .setProvider( - proc(): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "base", count: 1)) - ) - .isOk() + .setProvider( + proc(): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "base", count: 1)) + ) + .isOk() check DualResponse - .setProvider( - proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "base" & suffix, count: suffix.len)) - ) - .isOk() + .setProvider( + proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "base" & suffix, count: suffix.len)) + ) + .isOk() let noInput = waitFor DualResponse.request() check noInput.isOk() @@ -127,11 +131,11 @@ suite "RequestBroker macro (async mode)": test "clearProvider resets both entries": check DualResponse - .setProvider( - proc(): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "temp", count: 0)) - ) - .isOk() + .setProvider( + proc(): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "temp", count: 0)) + ) + .isOk() DualResponse.clearProvider() let res = waitFor DualResponse.request() @@ -139,11 +143,11 @@ suite "RequestBroker macro (async mode)": test "implicit zero-argument provider works by default": check ImplicitResponse - .setProvider( - proc(): Future[Result[ImplicitResponse, string]] {.async.} = - ok(ImplicitResponse(note: "auto")) - ) - .isOk() + .setProvider( + proc(): Future[Result[ImplicitResponse, string]] {.async.} = + ok(ImplicitResponse(note: "auto")) + ) + .isOk() let res = waitFor ImplicitResponse.request() check res.isOk() @@ -158,18 +162,18 @@ suite "RequestBroker macro (async mode)": test "no provider override": check DualResponse - .setProvider( - proc(): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "base", count: 1)) - ) - .isOk() + .setProvider( + proc(): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "base", count: 1)) + ) + .isOk() check DualResponse - .setProvider( - proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "base" & suffix, count: suffix.len)) - ) - .isOk() + .setProvider( + proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = + ok(DualResponse(note: "base" & suffix, count: suffix.len)) + ) + .isOk() let overrideProc = proc(): Future[Result[DualResponse, string]] {.async.} = ok(DualResponse(note: "something else", count: 1)) @@ -207,27 +211,27 @@ suite "RequestBroker macro (async mode)": SimpleResponse.clearProvider() check SimpleResponse - .setProvider( - proc(): Future[Result[SimpleResponse, string]] {.async.} = - ok(SimpleResponse(value: "default")) - ) - .isOk() + .setProvider( + proc(): Future[Result[SimpleResponse, string]] {.async.} = + ok(SimpleResponse(value: "default")) + ) + .isOk() check SimpleResponse - .setProvider( - BrokerContext(0x11111111'u32), - proc(): Future[Result[SimpleResponse, string]] {.async.} = - ok(SimpleResponse(value: "one")), - ) - .isOk() + .setProvider( + BrokerContext(0x11111111'u32), + proc(): Future[Result[SimpleResponse, string]] {.async.} = + ok(SimpleResponse(value: "one")), + ) + .isOk() check SimpleResponse - .setProvider( - BrokerContext(0x22222222'u32), - proc(): Future[Result[SimpleResponse, string]] {.async.} = - ok(SimpleResponse(value: "two")), - ) - .isOk() + .setProvider( + BrokerContext(0x22222222'u32), + proc(): Future[Result[SimpleResponse, string]] {.async.} = + ok(SimpleResponse(value: "two")), + ) + .isOk() let defaultRes = waitFor SimpleResponse.request() check defaultRes.isOk() @@ -246,12 +250,12 @@ suite "RequestBroker macro (async mode)": check missing.error.contains("no provider registered for broker context") check SimpleResponse - .setProvider( - BrokerContext(0x11111111'u32), - proc(): Future[Result[SimpleResponse, string]] {.async.} = - ok(SimpleResponse(value: "dup")), - ) - .isErr() + .setProvider( + BrokerContext(0x11111111'u32), + proc(): Future[Result[SimpleResponse, string]] {.async.} = + ok(SimpleResponse(value: "dup")), + ) + .isErr() SimpleResponse.clearProvider() @@ -259,27 +263,33 @@ suite "RequestBroker macro (async mode)": KeyedResponse.clearProvider() check KeyedResponse - .setProvider( - proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = - ok(KeyedResponse(key: "default-" & key, payload: $subKey)) - ) - .isOk() + .setProvider( + proc( + key: string, subKey: int + ): Future[Result[KeyedResponse, string]] {.async.} = + ok(KeyedResponse(key: "default-" & key, payload: $subKey)) + ) + .isOk() check KeyedResponse - .setProvider( - BrokerContext(0xABCDEF01'u32), - proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = - ok(KeyedResponse(key: "k1-" & key, payload: "p" & $subKey)), - ) - .isOk() + .setProvider( + BrokerContext(0xABCDEF01'u32), + proc( + key: string, subKey: int + ): Future[Result[KeyedResponse, string]] {.async.} = + ok(KeyedResponse(key: "k1-" & key, payload: "p" & $subKey)), + ) + .isOk() check KeyedResponse - .setProvider( - BrokerContext(0xABCDEF02'u32), - proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} = - ok(KeyedResponse(key: "k2-" & key, payload: "q" & $subKey)), - ) - .isOk() + .setProvider( + BrokerContext(0xABCDEF02'u32), + proc( + key: string, subKey: int + ): Future[Result[KeyedResponse, string]] {.async.} = + ok(KeyedResponse(key: "k2-" & key, payload: "q" & $subKey)), + ) + .isOk() let d = waitFor KeyedResponse.request("topic", 7) check d.isOk() @@ -343,11 +353,11 @@ static: suite "RequestBroker macro (sync mode)": test "serves zero-argument providers (sync)": check SimpleResponseSync - .setProvider( - proc(): Result[SimpleResponseSync, string] = - ok(SimpleResponseSync(value: "hi")) - ) - .isOk() + .setProvider( + proc(): Result[SimpleResponseSync, string] = + ok(SimpleResponseSync(value: "hi")) + ) + .isOk() let res = SimpleResponseSync.request() check res.isOk() @@ -363,12 +373,12 @@ suite "RequestBroker macro (sync mode)": test "serves input-based providers (sync)": var seen: seq[string] = @[] check KeyedResponseSync - .setProvider( - proc(key: string, subKey: int): Result[KeyedResponseSync, string] = - seen.add(key) - ok(KeyedResponseSync(key: key, payload: key & "-payload+" & $subKey)) - ) - .isOk() + .setProvider( + proc(key: string, subKey: int): Result[KeyedResponseSync, string] = + seen.add(key) + ok(KeyedResponseSync(key: key, payload: key & "-payload+" & $subKey)) + ) + .isOk() let res = KeyedResponseSync.request("topic", 1) check res.isOk() @@ -380,11 +390,11 @@ suite "RequestBroker macro (sync mode)": test "catches provider exception (sync)": check KeyedResponseSync - .setProvider( - proc(key: string, subKey: int): Result[KeyedResponseSync, string] = - raise newException(ValueError, "simulated failure") - ) - .isOk() + .setProvider( + proc(key: string, subKey: int): Result[KeyedResponseSync, string] = + raise newException(ValueError, "simulated failure") + ) + .isOk() let res = KeyedResponseSync.request("neglected", 11) check res.isErr() @@ -399,18 +409,18 @@ suite "RequestBroker macro (sync mode)": test "supports both provider types simultaneously (sync)": check DualResponseSync - .setProvider( - proc(): Result[DualResponseSync, string] = - ok(DualResponseSync(note: "base", count: 1)) - ) - .isOk() + .setProvider( + proc(): Result[DualResponseSync, string] = + ok(DualResponseSync(note: "base", count: 1)) + ) + .isOk() check DualResponseSync - .setProvider( - proc(suffix: string): Result[DualResponseSync, string] = - ok(DualResponseSync(note: "base" & suffix, count: suffix.len)) - ) - .isOk() + .setProvider( + proc(suffix: string): Result[DualResponseSync, string] = + ok(DualResponseSync(note: "base" & suffix, count: suffix.len)) + ) + .isOk() let noInput = DualResponseSync.request() check noInput.isOk() @@ -425,11 +435,11 @@ suite "RequestBroker macro (sync mode)": test "clearProvider resets both entries (sync)": check DualResponseSync - .setProvider( - proc(): Result[DualResponseSync, string] = - ok(DualResponseSync(note: "temp", count: 0)) - ) - .isOk() + .setProvider( + proc(): Result[DualResponseSync, string] = + ok(DualResponseSync(note: "temp", count: 0)) + ) + .isOk() DualResponseSync.clearProvider() let res = DualResponseSync.request() @@ -437,11 +447,11 @@ suite "RequestBroker macro (sync mode)": test "implicit zero-argument provider works by default (sync)": check ImplicitResponseSync - .setProvider( - proc(): Result[ImplicitResponseSync, string] = - ok(ImplicitResponseSync(note: "auto")) - ) - .isOk() + .setProvider( + proc(): Result[ImplicitResponseSync, string] = + ok(ImplicitResponseSync(note: "auto")) + ) + .isOk() let res = ImplicitResponseSync.request() check res.isOk() @@ -456,11 +466,11 @@ suite "RequestBroker macro (sync mode)": test "implicit zero-argument provider raises error (sync)": check ImplicitResponseSync - .setProvider( - proc(): Result[ImplicitResponseSync, string] = - raise newException(ValueError, "simulated failure") - ) - .isOk() + .setProvider( + proc(): Result[ImplicitResponseSync, string] = + raise newException(ValueError, "simulated failure") + ) + .isOk() let res = ImplicitResponseSync.request() check res.isErr() @@ -472,19 +482,19 @@ suite "RequestBroker macro (sync mode)": SimpleResponseSync.clearProvider() check SimpleResponseSync - .setProvider( - proc(): Result[SimpleResponseSync, string] = - ok(SimpleResponseSync(value: "default")) - ) - .isOk() + .setProvider( + proc(): Result[SimpleResponseSync, string] = + ok(SimpleResponseSync(value: "default")) + ) + .isOk() check SimpleResponseSync - .setProvider( - BrokerContext(0x10101010'u32), - proc(): Result[SimpleResponseSync, string] = - ok(SimpleResponseSync(value: "ten")), - ) - .isOk() + .setProvider( + BrokerContext(0x10101010'u32), + proc(): Result[SimpleResponseSync, string] = + ok(SimpleResponseSync(value: "ten")), + ) + .isOk() let defaultRes = SimpleResponseSync.request() check defaultRes.isOk() @@ -504,19 +514,19 @@ suite "RequestBroker macro (sync mode)": KeyedResponseSync.clearProvider() check KeyedResponseSync - .setProvider( - proc(key: string, subKey: int): Result[KeyedResponseSync, string] = - ok(KeyedResponseSync(key: "default-" & key, payload: $subKey)) - ) - .isOk() + .setProvider( + proc(key: string, subKey: int): Result[KeyedResponseSync, string] = + ok(KeyedResponseSync(key: "default-" & key, payload: $subKey)) + ) + .isOk() check KeyedResponseSync - .setProvider( - BrokerContext(0xA0A0A0A0'u32), - proc(key: string, subKey: int): Result[KeyedResponseSync, string] = - ok(KeyedResponseSync(key: "k-" & key, payload: "p" & $subKey)), - ) - .isOk() + .setProvider( + BrokerContext(0xA0A0A0A0'u32), + proc(key: string, subKey: int): Result[KeyedResponseSync, string] = + ok(KeyedResponseSync(key: "k-" & key, payload: "p" & $subKey)), + ) + .isOk() let d = KeyedResponseSync.request("topic", 2) check d.isOk() @@ -576,11 +586,11 @@ RequestBroker(sync): suite "RequestBroker macro (POD/external types)": test "supports non-object response types (async)": check PodResponse - .setProvider( - proc(): Future[Result[PodResponse, string]] {.async.} = - ok(PodResponse(123)) - ) - .isOk() + .setProvider( + proc(): Future[Result[PodResponse, string]] {.async.} = + ok(PodResponse(123)) + ) + .isOk() let res = waitFor PodResponse.request() check res.isOk() @@ -590,11 +600,11 @@ suite "RequestBroker macro (POD/external types)": test "supports aliased external types (async)": check ExternalAliasedResponse - .setProvider( - proc(): Future[Result[ExternalAliasedResponse, string]] {.async.} = - ok(ExternalAliasedResponse(ExternalDefinedTypeAsync(label: "ext"))) - ) - .isOk() + .setProvider( + proc(): Future[Result[ExternalAliasedResponse, string]] {.async.} = + ok(ExternalAliasedResponse(ExternalDefinedTypeAsync(label: "ext"))) + ) + .isOk() let res = waitFor ExternalAliasedResponse.request() check res.isOk() @@ -604,11 +614,11 @@ suite "RequestBroker macro (POD/external types)": test "supports aliased external types (sync)": check ExternalAliasedResponseSync - .setProvider( - proc(): Result[ExternalAliasedResponseSync, string] = - ok(ExternalAliasedResponseSync(ExternalDefinedTypeSync(label: "ext"))) - ) - .isOk() + .setProvider( + proc(): Result[ExternalAliasedResponseSync, string] = + ok(ExternalAliasedResponseSync(ExternalDefinedTypeSync(label: "ext"))) + ) + .isOk() let res = ExternalAliasedResponseSync.request() check res.isOk() @@ -618,32 +628,32 @@ suite "RequestBroker macro (POD/external types)": test "distinct response types avoid overload ambiguity (sync)": check DistinctStringResponseA - .setProvider( - proc(): Result[DistinctStringResponseA, string] = - ok(DistinctStringResponseA("a")) - ) - .isOk() + .setProvider( + proc(): Result[DistinctStringResponseA, string] = + ok(DistinctStringResponseA("a")) + ) + .isOk() check DistinctStringResponseB - .setProvider( - proc(): Result[DistinctStringResponseB, string] = - ok(DistinctStringResponseB("b")) - ) - .isOk() + .setProvider( + proc(): Result[DistinctStringResponseB, string] = + ok(DistinctStringResponseB("b")) + ) + .isOk() check ExternalDistinctResponseA - .setProvider( - proc(): Result[ExternalDistinctResponseA, string] = - ok(ExternalDistinctResponseA(ExternalDefinedTypeShared(label: "ea"))) - ) - .isOk() + .setProvider( + proc(): Result[ExternalDistinctResponseA, string] = + ok(ExternalDistinctResponseA(ExternalDefinedTypeShared(label: "ea"))) + ) + .isOk() check ExternalDistinctResponseB - .setProvider( - proc(): Result[ExternalDistinctResponseB, string] = - ok(ExternalDistinctResponseB(ExternalDefinedTypeShared(label: "eb"))) - ) - .isOk() + .setProvider( + proc(): Result[ExternalDistinctResponseB, string] = + ok(ExternalDistinctResponseB(ExternalDefinedTypeShared(label: "eb"))) + ) + .isOk() let resA = DistinctStringResponseA.request() let resB = DistinctStringResponseB.request() diff --git a/tests/common/test_sqlite_migrations.nim b/tests/common/test_sqlite_migrations.nim index 9e67fb9c8..2a9cae609 100644 --- a/tests/common/test_sqlite_migrations.nim +++ b/tests/common/test_sqlite_migrations.nim @@ -29,17 +29,16 @@ suite "SQLite - migrations": test "filter and order migration script file paths": ## Given - let paths = - @[ - sourceDir / "00001_valid.up.sql", - sourceDir / "00002_alsoValidWithUpperCaseExtension.UP.SQL", - sourceDir / "00007_unorderedValid.up.sql", - sourceDir / "00003_validRepeated.up.sql", - sourceDir / "00003_validRepeated.up.sql", - sourceDir / "00666_noMigrationScript.bmp", - sourceDir / "00X00_invalidVersion.down.sql", - sourceDir / "00008_notWithinVersionRange.up.sql", - ] + let paths = @[ + sourceDir / "00001_valid.up.sql", + sourceDir / "00002_alsoValidWithUpperCaseExtension.UP.SQL", + sourceDir / "00007_unorderedValid.up.sql", + sourceDir / "00003_validRepeated.up.sql", + sourceDir / "00003_validRepeated.up.sql", + sourceDir / "00666_noMigrationScript.bmp", + sourceDir / "00X00_invalidVersion.down.sql", + sourceDir / "00008_notWithinVersionRange.up.sql", + ] let lowerVersion = 0 @@ -64,16 +63,14 @@ suite "SQLite - migrations": test "break migration scripts into queries": ## Given - let statement1 = - """CREATE TABLE contacts1 ( + let statement1 = """CREATE TABLE contacts1 ( contact_id INTEGER PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, phone TEXT NOT NULL UNIQUE );""" - let statement2 = - """CREATE TABLE contacts2 ( + let statement2 = """CREATE TABLE contacts2 ( contact_id INTEGER PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT NOT NULL, @@ -91,16 +88,14 @@ suite "SQLite - migrations": test "break statements script into queries - empty statements": ## Given - let statement1 = - """CREATE TABLE contacts1 ( + let statement1 = """CREATE TABLE contacts1 ( contact_id INTEGER PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, phone TEXT NOT NULL UNIQUE );""" - let statement2 = - """CREATE TABLE contacts2 ( + let statement2 = """CREATE TABLE contacts2 ( contact_id INTEGER PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT NOT NULL, diff --git a/tests/node/peer_manager/peer_store/test_waku_peer_storage.nim b/tests/node/peer_manager/peer_store/test_waku_peer_storage.nim index c0e25ec6a..bf052205b 100644 --- a/tests/node/peer_manager/peer_store/test_waku_peer_storage.nim +++ b/tests/node/peer_manager/peer_store/test_waku_peer_storage.nim @@ -10,22 +10,21 @@ import import waku/waku_core/peers, waku/node/peer_manager/peer_store/waku_peer_storage proc `==`(a, b: RemotePeerInfo): bool = - let comparisons = - @[ - a.peerId == b.peerId, - a.addrs == b.addrs, - a.enr == b.enr, - a.protocols == b.protocols, - a.agent == b.agent, - a.protoVersion == b.protoVersion, - a.publicKey == b.publicKey, - a.connectedness == b.connectedness, - a.disconnectTime == b.disconnectTime, - a.origin == b.origin, - a.direction == b.direction, - a.lastFailedConn == b.lastFailedConn, - a.numberFailedConn == b.numberFailedConn, - ] + let comparisons = @[ + a.peerId == b.peerId, + a.addrs == b.addrs, + a.enr == b.enr, + a.protocols == b.protocols, + a.agent == b.agent, + a.protoVersion == b.protoVersion, + a.publicKey == b.publicKey, + a.connectedness == b.connectedness, + a.disconnectTime == b.disconnectTime, + a.origin == b.origin, + a.direction == b.direction, + a.lastFailedConn == b.lastFailedConn, + a.numberFailedConn == b.numberFailedConn, + ] allIt(comparisons, it == true) @@ -61,18 +60,17 @@ suite "Protobuf Serialisation": suite "encode": test "simple": # Given the expected bytes representation of a valid RemotePeerInfo - let expectedBuffer: seq[byte] = - @[ - 10, 39, 0, 37, 8, 2, 18, 33, 3, 43, 246, 238, 219, 109, 147, 79, 129, 40, 145, - 217, 209, 109, 105, 185, 186, 200, 180, 203, 72, 166, 220, 196, 232, 170, 74, - 141, 125, 255, 112, 238, 204, 18, 8, 4, 192, 168, 0, 1, 6, 31, 144, 34, 95, 8, - 3, 18, 91, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, - 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 222, 61, 48, 15, 163, 106, 224, 232, 245, - 213, 48, 137, 157, 131, 171, 171, 68, 171, 243, 22, 31, 22, 42, 75, 201, 1, - 216, 230, 236, 218, 2, 14, 139, 109, 95, 141, 163, 5, 37, 231, 29, 104, 81, - 81, 12, 9, 142, 92, 71, 198, 70, 165, 151, 251, 77, 206, 192, 52, 233, 247, - 124, 64, 158, 98, 40, 0, 48, 0, - ] + let expectedBuffer: seq[byte] = @[ + 10, 39, 0, 37, 8, 2, 18, 33, 3, 43, 246, 238, 219, 109, 147, 79, 129, 40, 145, + 217, 209, 109, 105, 185, 186, 200, 180, 203, 72, 166, 220, 196, 232, 170, 74, + 141, 125, 255, 112, 238, 204, 18, 8, 4, 192, 168, 0, 1, 6, 31, 144, 34, 95, 8, + 3, 18, 91, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, + 206, 61, 3, 1, 7, 3, 66, 0, 4, 222, 61, 48, 15, 163, 106, 224, 232, 245, 213, + 48, 137, 157, 131, 171, 171, 68, 171, 243, 22, 31, 22, 42, 75, 201, 1, 216, 230, + 236, 218, 2, 14, 139, 109, 95, 141, 163, 5, 37, 231, 29, 104, 81, 81, 12, 9, + 142, 92, 71, 198, 70, 165, 151, 251, 77, 206, 192, 52, 233, 247, 124, 64, 158, + 98, 40, 0, 48, 0, + ] # When converting a valid RemotePeerInfo to a ProtoBuffer let encodedRemotePeerInfo = encode(remotePeerInfo).get() @@ -87,18 +85,17 @@ suite "Protobuf Serialisation": suite "decode": test "simple": # Given the bytes representation of a valid RemotePeerInfo - let buffer: seq[byte] = - @[ - 10, 39, 0, 37, 8, 2, 18, 33, 3, 43, 246, 238, 219, 109, 147, 79, 129, 40, 145, - 217, 209, 109, 105, 185, 186, 200, 180, 203, 72, 166, 220, 196, 232, 170, 74, - 141, 125, 255, 112, 238, 204, 18, 8, 4, 192, 168, 0, 1, 6, 31, 144, 34, 95, 8, - 3, 18, 91, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, - 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 222, 61, 48, 15, 163, 106, 224, 232, 245, - 213, 48, 137, 157, 131, 171, 171, 68, 171, 243, 22, 31, 22, 42, 75, 201, 1, - 216, 230, 236, 218, 2, 14, 139, 109, 95, 141, 163, 5, 37, 231, 29, 104, 81, - 81, 12, 9, 142, 92, 71, 198, 70, 165, 151, 251, 77, 206, 192, 52, 233, 247, - 124, 64, 158, 98, 40, 0, 48, 0, - ] + let buffer: seq[byte] = @[ + 10, 39, 0, 37, 8, 2, 18, 33, 3, 43, 246, 238, 219, 109, 147, 79, 129, 40, 145, + 217, 209, 109, 105, 185, 186, 200, 180, 203, 72, 166, 220, 196, 232, 170, 74, + 141, 125, 255, 112, 238, 204, 18, 8, 4, 192, 168, 0, 1, 6, 31, 144, 34, 95, 8, + 3, 18, 91, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, + 206, 61, 3, 1, 7, 3, 66, 0, 4, 222, 61, 48, 15, 163, 106, 224, 232, 245, 213, + 48, 137, 157, 131, 171, 171, 68, 171, 243, 22, 31, 22, 42, 75, 201, 1, 216, 230, + 236, 218, 2, 14, 139, 109, 95, 141, 163, 5, 37, 231, 29, 104, 81, 81, 12, 9, + 142, 92, 71, 198, 70, 165, 151, 251, 77, 206, 192, 52, 233, 247, 124, 64, 158, + 98, 40, 0, 48, 0, + ] # When converting a valid buffer to RemotePeerInfo let decodedRemotePeerInfo = RemotePeerInfo.decode(buffer).get() diff --git a/tests/node/test_wakunode_health_monitor.nim b/tests/node/test_wakunode_health_monitor.nim index 8be9c444d..416dc9dda 100644 --- a/tests/node/test_wakunode_health_monitor.nim +++ b/tests/node/test_wakunode_health_monitor.nim @@ -35,13 +35,12 @@ proc protoHealthMock(kind: WakuProtocol, health: HealthStatus): ProtocolHealth = suite "Health Monitor - health state calculation": test "Disconnected, zero peers": - let protocols = - @[ - protoHealthMock(RelayProtocol, HealthStatus.NOT_READY), - protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), - protoHealthMock(FilterClientProtocol, HealthStatus.NOT_READY), - protoHealthMock(LightpushClientProtocol, HealthStatus.NOT_READY), - ] + let protocols = @[ + protoHealthMock(RelayProtocol, HealthStatus.NOT_READY), + protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), + protoHealthMock(FilterClientProtocol, HealthStatus.NOT_READY), + protoHealthMock(LightpushClientProtocol, HealthStatus.NOT_READY), + ] let strength = initTable[WakuProtocol, int]() let state = calculateConnectionState(protocols, strength, some(MockDLow)) check state == ConnectionStatus.Disconnected @@ -64,13 +63,12 @@ suite "Health Monitor - health state calculation": check state == ConnectionStatus.Connected test "Connected, robust edge": - let protocols = - @[ - protoHealthMock(RelayProtocol, HealthStatus.NOT_MOUNTED), - protoHealthMock(LightpushClientProtocol, HealthStatus.READY), - protoHealthMock(FilterClientProtocol, HealthStatus.READY), - protoHealthMock(StoreClientProtocol, HealthStatus.READY), - ] + let protocols = @[ + protoHealthMock(RelayProtocol, HealthStatus.NOT_MOUNTED), + protoHealthMock(LightpushClientProtocol, HealthStatus.READY), + protoHealthMock(FilterClientProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.READY), + ] var strength = initTable[WakuProtocol, int]() strength[LightpushClientProtocol] = HealthyThreshold strength[FilterClientProtocol] = HealthyThreshold @@ -79,12 +77,11 @@ suite "Health Monitor - health state calculation": check state == ConnectionStatus.Connected test "Disconnected, edge missing store": - let protocols = - @[ - protoHealthMock(LightpushClientProtocol, HealthStatus.READY), - protoHealthMock(FilterClientProtocol, HealthStatus.READY), - protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), - ] + let protocols = @[ + protoHealthMock(LightpushClientProtocol, HealthStatus.READY), + protoHealthMock(FilterClientProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), + ] var strength = initTable[WakuProtocol, int]() strength[LightpushClientProtocol] = HealthyThreshold strength[FilterClientProtocol] = HealthyThreshold @@ -94,12 +91,11 @@ suite "Health Monitor - health state calculation": test "PartiallyConnected, edge meets minimum failover requirement": let weakCount = max(1, HealthyThreshold - 1) - let protocols = - @[ - protoHealthMock(LightpushClientProtocol, HealthStatus.READY), - protoHealthMock(FilterClientProtocol, HealthStatus.READY), - protoHealthMock(StoreClientProtocol, HealthStatus.READY), - ] + let protocols = @[ + protoHealthMock(LightpushClientProtocol, HealthStatus.READY), + protoHealthMock(FilterClientProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.READY), + ] var strength = initTable[WakuProtocol, int]() strength[LightpushClientProtocol] = weakCount strength[FilterClientProtocol] = weakCount @@ -108,11 +104,10 @@ suite "Health Monitor - health state calculation": check state == ConnectionStatus.PartiallyConnected test "Connected, robust relay ignores store server": - let protocols = - @[ - protoHealthMock(RelayProtocol, HealthStatus.READY), - protoHealthMock(StoreProtocol, HealthStatus.READY), - ] + let protocols = @[ + protoHealthMock(RelayProtocol, HealthStatus.READY), + protoHealthMock(StoreProtocol, HealthStatus.READY), + ] var strength = initTable[WakuProtocol, int]() strength[RelayProtocol] = MockDLow strength[StoreProtocol] = 0 @@ -120,12 +115,11 @@ suite "Health Monitor - health state calculation": check state == ConnectionStatus.Connected test "Connected, robust relay ignores store client": - let protocols = - @[ - protoHealthMock(RelayProtocol, HealthStatus.READY), - protoHealthMock(StoreProtocol, HealthStatus.READY), - protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), - ] + let protocols = @[ + protoHealthMock(RelayProtocol, HealthStatus.READY), + protoHealthMock(StoreProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), + ] var strength = initTable[WakuProtocol, int]() strength[RelayProtocol] = MockDLow strength[StoreProtocol] = 0 diff --git a/tests/node/test_wakunode_legacy_store.nim b/tests/node/test_wakunode_legacy_store.nim index bf8003743..e9b0c9170 100644 --- a/tests/node/test_wakunode_legacy_store.nim +++ b/tests/node/test_wakunode_legacy_store.nim @@ -37,19 +37,18 @@ suite "Waku Store - End to End - Sorted Archive": contentTopicSeq = @[contentTopic] let timeOrigin = now() - archiveMessages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] + archiveMessages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), + ] historyQuery = HistoryQuery( pubsubTopic: some(pubsubTopic), @@ -500,19 +499,18 @@ suite "Waku Store - End to End - Unsorted Archive": ) let timeOrigin = now() - unsortedArchiveMessages = - @[ # SortIndex (by timestamp and digest) - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), # 1 - fakeWakuMessage(@[byte 03], ts = ts(00, timeOrigin)), # 2 - fakeWakuMessage(@[byte 08], ts = ts(00, timeOrigin)), # 0 - fakeWakuMessage(@[byte 07], ts = ts(10, timeOrigin)), # 4 - fakeWakuMessage(@[byte 02], ts = ts(10, timeOrigin)), # 3 - fakeWakuMessage(@[byte 09], ts = ts(10, timeOrigin)), # 5 - fakeWakuMessage(@[byte 06], ts = ts(20, timeOrigin)), # 6 - fakeWakuMessage(@[byte 01], ts = ts(20, timeOrigin)), # 9 - fakeWakuMessage(@[byte 04], ts = ts(20, timeOrigin)), # 7 - fakeWakuMessage(@[byte 05], ts = ts(20, timeOrigin)), # 8 - ] + unsortedArchiveMessages = @[ # SortIndex (by timestamp and digest) + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), # 1 + fakeWakuMessage(@[byte 03], ts = ts(00, timeOrigin)), # 2 + fakeWakuMessage(@[byte 08], ts = ts(00, timeOrigin)), # 0 + fakeWakuMessage(@[byte 07], ts = ts(10, timeOrigin)), # 4 + fakeWakuMessage(@[byte 02], ts = ts(10, timeOrigin)), # 3 + fakeWakuMessage(@[byte 09], ts = ts(10, timeOrigin)), # 5 + fakeWakuMessage(@[byte 06], ts = ts(20, timeOrigin)), # 6 + fakeWakuMessage(@[byte 01], ts = ts(20, timeOrigin)), # 9 + fakeWakuMessage(@[byte 04], ts = ts(20, timeOrigin)), # 7 + fakeWakuMessage(@[byte 05], ts = ts(20, timeOrigin)), # 8 + ] let serverKey = generateSecp256k1Key() @@ -654,21 +652,20 @@ suite "Waku Store - End to End - Archive with Multiple Topics": originTs = proc(offset = 0): Timestamp {.gcsafe, raises: [].} = ts(offset, timeOrigin) - archiveMessages = - @[ - fakeWakuMessage(@[byte 00], ts = originTs(00), contentTopic = contentTopic), - fakeWakuMessage(@[byte 01], ts = originTs(10), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 02], ts = originTs(20), contentTopic = contentTopicC), - fakeWakuMessage(@[byte 03], ts = originTs(30), contentTopic = contentTopic), - fakeWakuMessage(@[byte 04], ts = originTs(40), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 05], ts = originTs(50), contentTopic = contentTopicC), - fakeWakuMessage(@[byte 06], ts = originTs(60), contentTopic = contentTopic), - fakeWakuMessage(@[byte 07], ts = originTs(70), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 08], ts = originTs(80), contentTopic = contentTopicC), - fakeWakuMessage( - @[byte 09], ts = originTs(90), contentTopic = contentTopicSpecials - ), - ] + archiveMessages = @[ + fakeWakuMessage(@[byte 00], ts = originTs(00), contentTopic = contentTopic), + fakeWakuMessage(@[byte 01], ts = originTs(10), contentTopic = contentTopicB), + fakeWakuMessage(@[byte 02], ts = originTs(20), contentTopic = contentTopicC), + fakeWakuMessage(@[byte 03], ts = originTs(30), contentTopic = contentTopic), + fakeWakuMessage(@[byte 04], ts = originTs(40), contentTopic = contentTopicB), + fakeWakuMessage(@[byte 05], ts = originTs(50), contentTopic = contentTopicC), + fakeWakuMessage(@[byte 06], ts = originTs(60), contentTopic = contentTopic), + fakeWakuMessage(@[byte 07], ts = originTs(70), contentTopic = contentTopicB), + fakeWakuMessage(@[byte 08], ts = originTs(80), contentTopic = contentTopicC), + fakeWakuMessage( + @[byte 09], ts = originTs(90), contentTopic = contentTopicSpecials + ), + ] let serverKey = generateSecp256k1Key() @@ -910,12 +907,11 @@ suite "Waku Store - End to End - Archive with Multiple Topics": xasyncTest "Only ephemeral Messages:": # Given an archive with only ephemeral messages let - ephemeralMessages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), - fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), - fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), - ] + ephemeralMessages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), + fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), + fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), + ] ephemeralArchiveDriver = newSqliteArchiveDriver().put(pubsubTopic, ephemeralMessages) @@ -946,18 +942,16 @@ suite "Waku Store - End to End - Archive with Multiple Topics": xasyncTest "Mixed messages": # Given an archive with both ephemeral and non-ephemeral messages let - ephemeralMessages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), - fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), - fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), - ] - nonEphemeralMessages = - @[ - fakeWakuMessage(@[byte 03], ts = ts(30), ephemeral = false), - fakeWakuMessage(@[byte 04], ts = ts(40), ephemeral = false), - fakeWakuMessage(@[byte 05], ts = ts(50), ephemeral = false), - ] + ephemeralMessages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), + fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), + fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), + ] + nonEphemeralMessages = @[ + fakeWakuMessage(@[byte 03], ts = ts(30), ephemeral = false), + fakeWakuMessage(@[byte 04], ts = ts(40), ephemeral = false), + fakeWakuMessage(@[byte 05], ts = ts(50), ephemeral = false), + ] mixedArchiveDriver = newSqliteArchiveDriver() .put(pubsubTopic, ephemeralMessages) .put(pubsubTopic, nonEphemeralMessages) diff --git a/tests/node/test_wakunode_relay_rln.nim b/tests/node/test_wakunode_relay_rln.nim index 9c5c928f0..c8ca9b43d 100644 --- a/tests/node/test_wakunode_relay_rln.nim +++ b/tests/node/test_wakunode_relay_rln.nim @@ -283,31 +283,31 @@ suite "Waku RlnRelay - End to End - Static": doAssert( client.wakuRlnRelay - .appendRLNProof( - message1b, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 0) - ) - .isOk() + .appendRLNProof( + message1b, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 0) + ) + .isOk() ) doAssert( client.wakuRlnRelay - .appendRLNProof( - message1kib, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 1) - ) - .isOk() + .appendRLNProof( + message1kib, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 1) + ) + .isOk() ) doAssert( client.wakuRlnRelay - .appendRLNProof( - message150kib, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 2) - ) - .isOk() + .appendRLNProof( + message150kib, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 2) + ) + .isOk() ) doAssert( client.wakuRlnRelay - .appendRLNProof( - message151kibPlus, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 3) - ) - .isOk() + .appendRLNProof( + message151kibPlus, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 3) + ) + .isOk() ) # When sending the 1B message @@ -372,10 +372,10 @@ suite "Waku RlnRelay - End to End - Static": doAssert( client.wakuRlnRelay - .appendRLNProof( - message151kibPlus, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 3) - ) - .isOk() + .appendRLNProof( + message151kibPlus, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 3) + ) + .isOk() ) # When sending the 150KiB plus message @@ -496,11 +496,11 @@ suite "Waku RlnRelay - End to End - OnChain": # However, it doesn't reduce the retries against the blockchain that the mounting rln process attempts (until it accepts failure). # Note: These retries might be an unintended library issue. discard await server - .setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig1) - .withTimeout(FUTURE_TIMEOUT) + .setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig1) + .withTimeout(FUTURE_TIMEOUT) discard await client - .setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig2) - .withTimeout(FUTURE_TIMEOUT) + .setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig2) + .withTimeout(FUTURE_TIMEOUT) check: (await serverErrorFuture.waitForResult()).get() == diff --git a/tests/node/test_wakunode_sharding.nim b/tests/node/test_wakunode_sharding.nim index 261077e36..236510fe1 100644 --- a/tests/node/test_wakunode_sharding.nim +++ b/tests/node/test_wakunode_sharding.nim @@ -434,18 +434,16 @@ suite "Sharding": contentTopicShort = "/toychat/2/huilong/proto" contentTopicFull = "/0/toychat/2/huilong/proto" pubsubTopic = "/waku/2/rs/0/58355" - archiveMessages1 = - @[ - fakeWakuMessage( - @[byte 00], ts = ts(00, timeOrigin), contentTopic = contentTopicShort - ) - ] - archiveMessages2 = - @[ - fakeWakuMessage( - @[byte 01], ts = ts(10, timeOrigin), contentTopic = contentTopicFull - ) - ] + archiveMessages1 = @[ + fakeWakuMessage( + @[byte 00], ts = ts(00, timeOrigin), contentTopic = contentTopicShort + ) + ] + archiveMessages2 = @[ + fakeWakuMessage( + @[byte 01], ts = ts(10, timeOrigin), contentTopic = contentTopicFull + ) + ] archiveDriver = newArchiveDriverWithMessages(pubsubTopic, archiveMessages1) discard archiveDriver.put(pubsubTopic, archiveMessages2) let mountArchiveResult = server.mountArchive(archiveDriver) @@ -597,18 +595,16 @@ suite "Sharding": contentTopic2 = "/0/toychat2/2/huilong/proto" pubsubTopic2 = "/waku/2/rs/0/23286" # Automatically generated from the contentTopic above - archiveMessages1 = - @[ - fakeWakuMessage( - @[byte 00], ts = ts(00, timeOrigin), contentTopic = contentTopic1 - ) - ] - archiveMessages2 = - @[ - fakeWakuMessage( - @[byte 01], ts = ts(10, timeOrigin), contentTopic = contentTopic2 - ) - ] + archiveMessages1 = @[ + fakeWakuMessage( + @[byte 00], ts = ts(00, timeOrigin), contentTopic = contentTopic1 + ) + ] + archiveMessages2 = @[ + fakeWakuMessage( + @[byte 01], ts = ts(10, timeOrigin), contentTopic = contentTopic2 + ) + ] archiveDriver = newArchiveDriverWithMessages(pubsubTopic1, archiveMessages1) discard archiveDriver.put(pubsubTopic2, archiveMessages2) let mountArchiveResult = server.mountArchive(archiveDriver) diff --git a/tests/node/test_wakunode_store.nim b/tests/node/test_wakunode_store.nim index 9f312afd5..01deb2903 100644 --- a/tests/node/test_wakunode_store.nim +++ b/tests/node/test_wakunode_store.nim @@ -38,19 +38,18 @@ suite "Waku Store - End to End - Sorted Archive": contentTopicSeq = @[contentTopic] let timeOrigin = now() - let messages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] + let messages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), + ] archiveMessages = messages.mapIt( WakuMessageKeyValue( messageHash: computeMessageHash(pubsubTopic, it), @@ -542,19 +541,18 @@ suite "Waku Store - End to End - Unsorted Archive": ) let timeOrigin = now() - let messages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(20, timeOrigin)), - ] + let messages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(20, timeOrigin)), + ] unsortedArchiveMessages = messages.mapIt( WakuMessageKeyValue( messageHash: computeMessageHash(pubsubTopic, it), @@ -759,19 +757,19 @@ suite "Waku Store - End to End - Unsorted Archive without provided Timestamp": paginationLimit: some(uint64(5)), ) - let messages = - @[ # Not providing explicit timestamp means it will be set in "arrive" order - fakeWakuMessage(@[byte 09]), - fakeWakuMessage(@[byte 07]), - fakeWakuMessage(@[byte 05]), - fakeWakuMessage(@[byte 03]), - fakeWakuMessage(@[byte 01]), - fakeWakuMessage(@[byte 00]), - fakeWakuMessage(@[byte 02]), - fakeWakuMessage(@[byte 04]), - fakeWakuMessage(@[byte 06]), - fakeWakuMessage(@[byte 08]), - ] + let messages = @[ + # Not providing explicit timestamp means it will be set in "arrive" order + fakeWakuMessage(@[byte 09]), + fakeWakuMessage(@[byte 07]), + fakeWakuMessage(@[byte 05]), + fakeWakuMessage(@[byte 03]), + fakeWakuMessage(@[byte 01]), + fakeWakuMessage(@[byte 00]), + fakeWakuMessage(@[byte 02]), + fakeWakuMessage(@[byte 04]), + fakeWakuMessage(@[byte 06]), + fakeWakuMessage(@[byte 08]), + ] unsortedArchiveMessages = messages.mapIt( WakuMessageKeyValue( messageHash: computeMessageHash(pubsubTopic, it), @@ -900,21 +898,20 @@ suite "Waku Store - End to End - Archive with Multiple Topics": originTs = proc(offset = 0): Timestamp {.gcsafe, raises: [].} = ts(offset, timeOrigin) - let messages = - @[ - fakeWakuMessage(@[byte 00], ts = originTs(00), contentTopic = contentTopic), - fakeWakuMessage(@[byte 01], ts = originTs(10), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 02], ts = originTs(20), contentTopic = contentTopicC), - fakeWakuMessage(@[byte 03], ts = originTs(30), contentTopic = contentTopic), - fakeWakuMessage(@[byte 04], ts = originTs(40), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 05], ts = originTs(50), contentTopic = contentTopicC), - fakeWakuMessage(@[byte 06], ts = originTs(60), contentTopic = contentTopic), - fakeWakuMessage(@[byte 07], ts = originTs(70), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 08], ts = originTs(80), contentTopic = contentTopicC), - fakeWakuMessage( - @[byte 09], ts = originTs(90), contentTopic = contentTopicSpecials - ), - ] + let messages = @[ + fakeWakuMessage(@[byte 00], ts = originTs(00), contentTopic = contentTopic), + fakeWakuMessage(@[byte 01], ts = originTs(10), contentTopic = contentTopicB), + fakeWakuMessage(@[byte 02], ts = originTs(20), contentTopic = contentTopicC), + fakeWakuMessage(@[byte 03], ts = originTs(30), contentTopic = contentTopic), + fakeWakuMessage(@[byte 04], ts = originTs(40), contentTopic = contentTopicB), + fakeWakuMessage(@[byte 05], ts = originTs(50), contentTopic = contentTopicC), + fakeWakuMessage(@[byte 06], ts = originTs(60), contentTopic = contentTopic), + fakeWakuMessage(@[byte 07], ts = originTs(70), contentTopic = contentTopicB), + fakeWakuMessage(@[byte 08], ts = originTs(80), contentTopic = contentTopicC), + fakeWakuMessage( + @[byte 09], ts = originTs(90), contentTopic = contentTopicSpecials + ), + ] archiveMessages = messages.mapIt( WakuMessageKeyValue( @@ -1172,12 +1169,11 @@ suite "Waku Store - End to End - Archive with Multiple Topics": xasyncTest "Only ephemeral Messages:": # Given an archive with only ephemeral messages let - ephemeralMessages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), - fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), - fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), - ] + ephemeralMessages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), + fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), + fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), + ] ephemeralArchiveDriver = newSqliteArchiveDriver().put(pubsubTopic, ephemeralMessages) @@ -1207,18 +1203,16 @@ suite "Waku Store - End to End - Archive with Multiple Topics": xasyncTest "Mixed messages": # Given an archive with both ephemeral and non-ephemeral messages let - ephemeralMessages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), - fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), - fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), - ] - nonEphemeralMessages = - @[ - fakeWakuMessage(@[byte 03], ts = ts(30), ephemeral = false), - fakeWakuMessage(@[byte 04], ts = ts(40), ephemeral = false), - fakeWakuMessage(@[byte 05], ts = ts(50), ephemeral = false), - ] + ephemeralMessages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), + fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), + fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), + ] + nonEphemeralMessages = @[ + fakeWakuMessage(@[byte 03], ts = ts(30), ephemeral = false), + fakeWakuMessage(@[byte 04], ts = ts(40), ephemeral = false), + fakeWakuMessage(@[byte 05], ts = ts(50), ephemeral = false), + ] mixedArchiveDriver = newSqliteArchiveDriver() .put(pubsubTopic, ephemeralMessages) .put(pubsubTopic, nonEphemeralMessages) diff --git a/tests/resources/payloads.nim b/tests/resources/payloads.nim index 723bf788c..0fc1eaebb 100644 --- a/tests/resources/payloads.nim +++ b/tests/resources/payloads.nim @@ -8,8 +8,7 @@ const EMOJI* = "😀 😃 😄 😁 😆 😅 🤣 😂 🙂 🙃 😉 😊 😇 🥰 😍 🤩 😘 😗 😚 😙" CODE* = "def main():\n\tprint('Hello, world!')" - QUERY* = - """ + QUERY* = """ SELECT u.id, u.name, @@ -30,8 +29,7 @@ const u.id = 1 """ TEXT_SMALL* = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." - TEXT_LARGE* = - """ + TEXT_LARGE* = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras gravida vulputate semper. Proin eleifend varius cursus. Morbi lacinia posuere quam sit amet pretium. Sed non metus fermentum, venenatis nisl id, vestibulum eros. Quisque non lorem sit amet lectus faucibus elementum eu diff --git a/tests/test_peer_manager.nim b/tests/test_peer_manager.nim index c96f21b6e..f78c3831f 100644 --- a/tests/test_peer_manager.nim +++ b/tests/test_peer_manager.nim @@ -1216,30 +1216,29 @@ procSuite "Peer Manager": shardId1 = 1.uint16 # Create 3 nodes with different shards - let nodes = - @[ - newTestWakuNode( - generateSecp256k1Key(), - parseIpAddress("0.0.0.0"), - Port(0), - clusterId = clusterId, - subscribeShards = @[shardId0], - ), - newTestWakuNode( - generateSecp256k1Key(), - parseIpAddress("0.0.0.0"), - Port(0), - clusterId = clusterId, - subscribeShards = @[shardId1], - ), - newTestWakuNode( - generateSecp256k1Key(), - parseIpAddress("0.0.0.0"), - Port(0), - clusterId = clusterId, - subscribeShards = @[shardId0], - ), - ] + let nodes = @[ + newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId0], + ), + newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId1], + ), + newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId0], + ), + ] await allFutures(nodes.mapIt(it.start())) for node in nodes: @@ -1364,13 +1363,12 @@ procSuite "Peer Manager": node.peerManager.switch.peerStore[ProtoBook][peerInfo.peerId] = @[WakuRelayCodec] ## When: selectPeer is called with malformed pubsub topic - let invalidTopics = - @[ - some(PubsubTopic("invalid-topic")), - some(PubsubTopic("/waku/2/invalid")), - some(PubsubTopic("/waku/2/rs/abc/0")), # non-numeric cluster - some(PubsubTopic("")), # empty topic - ] + let invalidTopics = @[ + some(PubsubTopic("invalid-topic")), + some(PubsubTopic("/waku/2/invalid")), + some(PubsubTopic("/waku/2/rs/abc/0")), # non-numeric cluster + some(PubsubTopic("")), # empty topic + ] ## Then: Returns none(RemotePeerInfo) without crashing for invalidTopic in invalidTopics: diff --git a/tests/test_waku.nim b/tests/test_waku.nim index dabd65af7..cf5675716 100644 --- a/tests/test_waku.nim +++ b/tests/test_waku.nim @@ -35,14 +35,12 @@ suite "Waku API - Create node": nodeConf.rest = false nodeConf.numShardsInNetwork = 16 nodeConf.maxMessageSize = "1024 KiB" - nodeConf.entryNodes = - @[ - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g" - ] - nodeConf.staticnodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" - ] + nodeConf.entryNodes = @[ + "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g" + ] + nodeConf.staticnodes = @[ + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" + ] ## When let node = (await createNode(nodeConf)).valueOr: @@ -67,11 +65,10 @@ suite "Waku API - Create node": nodeConf.mode = Core nodeConf.clusterId = 42'u16 nodeConf.rest = false - nodeConf.entryNodes = - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - ] + nodeConf.entryNodes = @[ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + ] ## When let node = (await createNode(nodeConf)).valueOr: diff --git a/tests/test_waku_keystore_keyfile.nim b/tests/test_waku_keystore_keyfile.nim index 5f4c74591..afdb7e44b 100644 --- a/tests/test_waku_keystore_keyfile.nim +++ b/tests/test_waku_keystore_keyfile.nim @@ -307,30 +307,29 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)": # but the last byte of mac is changed to 00. # While ciphertext is the correct encryption of priv under password, # mac verfication should fail and nothing will be decrypted - let keyfileWrongMac = - %*{ - "keyfile": { - "crypto": { - "cipher": "aes-128-ctr", - "cipherparams": {"iv": "6087dab2f9fdbbfaddc31a909735c1e6"}, - "ciphertext": - "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", - "kdf": "pbkdf2", - "kdfparams": { - "c": 262144, - "dklen": 32, - "prf": "hmac-sha256", - "salt": "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd", - }, - "mac": "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e900", + let keyfileWrongMac = %*{ + "keyfile": { + "crypto": { + "cipher": "aes-128-ctr", + "cipherparams": {"iv": "6087dab2f9fdbbfaddc31a909735c1e6"}, + "ciphertext": + "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", + "kdf": "pbkdf2", + "kdfparams": { + "c": 262144, + "dklen": 32, + "prf": "hmac-sha256", + "salt": "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd", }, - "id": "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version": 3, + "mac": "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e900", }, - "name": "test1", - "password": "testpassword", - "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d", - } + "id": "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version": 3, + }, + "name": "test1", + "password": "testpassword", + "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d", + } # Decryption with correct password let expectedSecret = decodeHex(keyfileWrongMac.getOrDefault("priv").getStr()) diff --git a/tests/test_waku_noise.nim b/tests/test_waku_noise.nim index 980e752f5..6566f9eed 100644 --- a/tests/test_waku_noise.nim +++ b/tests/test_waku_noise.nim @@ -669,11 +669,10 @@ procSuite "Waku Noise": # <- s # ... # So we define accordingly the sequence of the pre-message public keys - let preMessagePKs: seq[NoisePublicKey] = - @[ - toNoisePublicKey(getPublicKey(aliceStaticKey)), - toNoisePublicKey(getPublicKey(bobStaticKey)), - ] + let preMessagePKs: seq[NoisePublicKey] = @[ + toNoisePublicKey(getPublicKey(aliceStaticKey)), + toNoisePublicKey(getPublicKey(bobStaticKey)), + ] var aliceHS = initialize( hsPattern = hsPattern, diff --git a/tests/test_waku_rendezvous.nim b/tests/test_waku_rendezvous.nim index 07113ca4a..88845dc25 100644 --- a/tests/test_waku_rendezvous.nim +++ b/tests/test_waku_rendezvous.nim @@ -117,11 +117,10 @@ procSuite "Waku Rendezvous": ## Given: A light client node with no relay protocol let clusterId = 10.uint16 - configuredShards = - @[ - RelayShard(clusterId: clusterId, shardId: 0), - RelayShard(clusterId: clusterId, shardId: 1), - ] + configuredShards = @[ + RelayShard(clusterId: clusterId, shardId: 0), + RelayShard(clusterId: clusterId, shardId: 1), + ] let lightClient = newTestWakuNode( generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0), clusterId = clusterId diff --git a/tests/test_waku_switch.nim b/tests/test_waku_switch.nim index 3e6fd08eb..9f11a41a1 100644 --- a/tests/test_waku_switch.nim +++ b/tests/test_waku_switch.nim @@ -12,14 +12,14 @@ import waku/node/waku_switch, ./testlib/common, ./testlib/wakucore proc newCircuitRelayClientSwitch(relayClient: RelayClient): Switch = SwitchBuilder - .new() - .withRng(rng()) - .withAddresses(@[MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet()]) - .withTcpTransport() - .withMplex() - .withNoise() - .withCircuitRelay(relayClient) - .build() + .new() + .withRng(rng()) + .withAddresses(@[MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet()]) + .withTcpTransport() + .withMplex() + .withNoise() + .withCircuitRelay(relayClient) + .build() suite "Waku Switch": asyncTest "Waku Switch works with AutoNat": diff --git a/tests/waku_archive/test_driver_postgres_query.nim b/tests/waku_archive/test_driver_postgres_query.nim index 8bbdc52c0..240ac28dd 100644 --- a/tests/waku_archive/test_driver_postgres_query.nim +++ b/tests/waku_archive/test_driver_postgres_query.nim @@ -49,17 +49,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -85,17 +84,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -124,29 +122,28 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), - fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" - ), - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" - ), - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" - ), - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" - ), - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" - ), - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" - ), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), + fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), + fakeWakuMessage( + @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" + ), + fakeWakuMessage( + @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" + ), + fakeWakuMessage( + @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" + ), + fakeWakuMessage( + @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" + ), + fakeWakuMessage( + @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" + ), + fakeWakuMessage( + @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" + ), + ] var messages = expected shuffle(messages) @@ -175,17 +172,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -216,17 +212,16 @@ suite "Postgres driver - queries": const contentTopic2 = "test-content-topic-2" const contentTopic3 = "test-content-topic-3" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -275,14 +270,13 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), + ] var messages = expected shuffle(messages) @@ -338,35 +332,34 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" const pubsubTopic = "test-pubsub-topic" - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -395,35 +388,34 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" const pubsubTopic = "test-pubsub-topic" - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -450,35 +442,34 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" const pubsubTopic = "test-pubsub-topic" - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -509,18 +500,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -551,18 +540,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -593,17 +580,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + var messages = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] shuffle(messages) info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) @@ -640,18 +626,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -685,18 +669,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -732,59 +714,42 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -819,59 +784,42 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -903,19 +851,18 @@ suite "Postgres driver - queries": asyncTest "only hashes - descending order": ## Given let timeOrigin = now() - var expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + var expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -942,17 +889,16 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -982,17 +928,16 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1023,61 +968,44 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # start_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1109,18 +1037,17 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1153,17 +1080,16 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1195,20 +1121,19 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1240,21 +1165,20 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1289,21 +1213,20 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1339,52 +1262,39 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + # start_time + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1421,51 +1331,38 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1502,52 +1399,39 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1585,52 +1469,39 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1667,16 +1538,15 @@ suite "Postgres driver - queries": let timeOrigin = now() let oldestTime = ts(00, timeOrigin) let newestTime = ts(100, timeOrigin) - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = oldestTime), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = newestTime), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = oldestTime), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = newestTime), + ] var messages = expected shuffle(messages) @@ -1714,16 +1584,15 @@ suite "Postgres driver - queries": let timeOrigin = now() let targetTime = ts(40, timeOrigin) - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = targetTime), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = targetTime), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1751,16 +1620,15 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1793,12 +1661,11 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ] var messages = expected var hashes = newSeq[WakuMessageHash](0) diff --git a/tests/waku_archive/test_driver_queue_query.nim b/tests/waku_archive/test_driver_queue_query.nim index 11b39a3f8..b93e79bf6 100644 --- a/tests/waku_archive/test_driver_queue_query.nim +++ b/tests/waku_archive/test_driver_queue_query.nim @@ -3,13 +3,9 @@ import std/[options, sequtils, random, algorithm], testutils/unittests, chronos, chronicles import - waku/ - [ - waku_archive, - waku_archive/driver/queue_driver, - waku_core, - waku_core/message/digest, - ], + waku/[ + waku_archive, waku_archive/driver/queue_driver, waku_core, waku_core/message/digest + ], ../testlib/common, ../testlib/wakucore @@ -29,17 +25,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -71,17 +66,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -115,17 +109,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -161,17 +154,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -207,14 +199,13 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), + ] var messages = expected shuffle(messages) @@ -281,35 +272,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newTestSqliteDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -345,35 +335,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newTestSqliteDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -407,35 +396,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newTestSqliteDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -474,18 +462,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -521,18 +507,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -568,17 +552,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + var messages = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] shuffle(messages) info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) @@ -618,18 +601,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -668,18 +649,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -720,59 +699,42 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -814,59 +776,42 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -908,17 +853,16 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -953,17 +897,16 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -999,61 +942,44 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # start_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1092,18 +1018,17 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1141,17 +1066,16 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1188,20 +1112,19 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1238,21 +1161,20 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1292,21 +1214,20 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1347,52 +1268,39 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + # start_time + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1436,51 +1344,38 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1524,52 +1419,39 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1614,52 +1496,39 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) diff --git a/tests/waku_archive/test_driver_sqlite_query.nim b/tests/waku_archive/test_driver_sqlite_query.nim index 9812a50f3..6ae7c5b9d 100644 --- a/tests/waku_archive/test_driver_sqlite_query.nim +++ b/tests/waku_archive/test_driver_sqlite_query.nim @@ -22,17 +22,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -65,17 +64,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -110,29 +108,28 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), - fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" - ), - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" - ), - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" - ), - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" - ), - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" - ), - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" - ), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), + fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), + fakeWakuMessage( + @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" + ), + fakeWakuMessage( + @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" + ), + fakeWakuMessage( + @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" + ), + fakeWakuMessage( + @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" + ), + fakeWakuMessage( + @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" + ), + fakeWakuMessage( + @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" + ), + ] var messages = expected shuffle(messages) @@ -167,17 +164,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -214,17 +210,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -261,14 +256,13 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), + ] var messages = expected shuffle(messages) @@ -337,35 +331,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -400,35 +393,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -461,35 +453,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -527,18 +518,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -575,18 +564,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -623,17 +610,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + var messages = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] shuffle(messages) info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) @@ -674,18 +660,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -725,18 +709,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -778,59 +760,42 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -871,59 +836,42 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -964,17 +912,16 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1010,17 +957,16 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1057,61 +1003,44 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # start_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1149,18 +1078,17 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1199,17 +1127,16 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1247,20 +1174,19 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1298,21 +1224,20 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1353,21 +1278,20 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1409,52 +1333,39 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + # start_time + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1497,51 +1408,38 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1584,52 +1482,39 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1673,52 +1558,39 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) diff --git a/tests/waku_archive/test_retention_policy.nim b/tests/waku_archive/test_retention_policy.nim index ea86e1d69..394d5711d 100644 --- a/tests/waku_archive/test_retention_policy.nim +++ b/tests/waku_archive/test_retention_policy.nim @@ -121,16 +121,15 @@ suite "Waku Archive - Retention policy": retentionPolicy: RetentionPolicy = CapacityRetentionPolicy.new(capacity = capacity) - let messages = - @[ - fakeWakuMessage(contentTopic = DefaultContentTopic, ts = ts(0)), - fakeWakuMessage(contentTopic = DefaultContentTopic, ts = ts(1)), - fakeWakuMessage(contentTopic = contentTopic, ts = ts(2)), - fakeWakuMessage(contentTopic = contentTopic, ts = ts(3)), - fakeWakuMessage(contentTopic = contentTopic, ts = ts(4)), - fakeWakuMessage(contentTopic = contentTopic, ts = ts(5)), - fakeWakuMessage(contentTopic = contentTopic, ts = ts(6)), - ] + let messages = @[ + fakeWakuMessage(contentTopic = DefaultContentTopic, ts = ts(0)), + fakeWakuMessage(contentTopic = DefaultContentTopic, ts = ts(1)), + fakeWakuMessage(contentTopic = contentTopic, ts = ts(2)), + fakeWakuMessage(contentTopic = contentTopic, ts = ts(3)), + fakeWakuMessage(contentTopic = contentTopic, ts = ts(4)), + fakeWakuMessage(contentTopic = contentTopic, ts = ts(5)), + fakeWakuMessage(contentTopic = contentTopic, ts = ts(6)), + ] ## When for msg in messages: diff --git a/tests/waku_archive/test_waku_archive.nim b/tests/waku_archive/test_waku_archive.nim index 802473d64..9b235dd57 100644 --- a/tests/waku_archive/test_waku_archive.nim +++ b/tests/waku_archive/test_waku_archive.nim @@ -36,14 +36,13 @@ suite "Waku Archive - message handling": let archive = newWakuArchive(driver) ## Given - let msgList = - @[ - fakeWakuMessage(ephemeral = false, payload = "1"), - fakeWakuMessage(ephemeral = true, payload = "2"), - fakeWakuMessage(ephemeral = true, payload = "3"), - fakeWakuMessage(ephemeral = true, payload = "4"), - fakeWakuMessage(ephemeral = false, payload = "5"), - ] + let msgList = @[ + fakeWakuMessage(ephemeral = false, payload = "1"), + fakeWakuMessage(ephemeral = true, payload = "2"), + fakeWakuMessage(ephemeral = true, payload = "3"), + fakeWakuMessage(ephemeral = true, payload = "4"), + fakeWakuMessage(ephemeral = false, payload = "5"), + ] ## When for msg in msgList: @@ -127,39 +126,38 @@ suite "Waku Archive - message handling": procSuite "Waku Archive - find messages": ## Fixtures let timeOrigin = now() - let msgListA = - @[ - fakeWakuMessage( - @[byte 00], contentTopic = ContentTopic("2"), ts = ts(00, timeOrigin) - ), - fakeWakuMessage( - @[byte 01], contentTopic = ContentTopic("1"), ts = ts(10, timeOrigin) - ), - fakeWakuMessage( - @[byte 02], contentTopic = ContentTopic("2"), ts = ts(20, timeOrigin) - ), - fakeWakuMessage( - @[byte 03], contentTopic = ContentTopic("1"), ts = ts(30, timeOrigin) - ), - fakeWakuMessage( - @[byte 04], contentTopic = ContentTopic("2"), ts = ts(40, timeOrigin) - ), - fakeWakuMessage( - @[byte 05], contentTopic = ContentTopic("1"), ts = ts(50, timeOrigin) - ), - fakeWakuMessage( - @[byte 06], contentTopic = ContentTopic("2"), ts = ts(60, timeOrigin) - ), - fakeWakuMessage( - @[byte 07], contentTopic = ContentTopic("1"), ts = ts(70, timeOrigin) - ), - fakeWakuMessage( - @[byte 08], contentTopic = ContentTopic("2"), ts = ts(80, timeOrigin) - ), - fakeWakuMessage( - @[byte 09], contentTopic = ContentTopic("1"), ts = ts(90, timeOrigin) - ), - ] + let msgListA = @[ + fakeWakuMessage( + @[byte 00], contentTopic = ContentTopic("2"), ts = ts(00, timeOrigin) + ), + fakeWakuMessage( + @[byte 01], contentTopic = ContentTopic("1"), ts = ts(10, timeOrigin) + ), + fakeWakuMessage( + @[byte 02], contentTopic = ContentTopic("2"), ts = ts(20, timeOrigin) + ), + fakeWakuMessage( + @[byte 03], contentTopic = ContentTopic("1"), ts = ts(30, timeOrigin) + ), + fakeWakuMessage( + @[byte 04], contentTopic = ContentTopic("2"), ts = ts(40, timeOrigin) + ), + fakeWakuMessage( + @[byte 05], contentTopic = ContentTopic("1"), ts = ts(50, timeOrigin) + ), + fakeWakuMessage( + @[byte 06], contentTopic = ContentTopic("2"), ts = ts(60, timeOrigin) + ), + fakeWakuMessage( + @[byte 07], contentTopic = ContentTopic("1"), ts = ts(70, timeOrigin) + ), + fakeWakuMessage( + @[byte 08], contentTopic = ContentTopic("2"), ts = ts(80, timeOrigin) + ), + fakeWakuMessage( + @[byte 09], contentTopic = ContentTopic("1"), ts = ts(90, timeOrigin) + ), + ] let archiveA = block: let @@ -446,19 +444,18 @@ procSuite "Waku Archive - find messages": driver = newSqliteArchiveDriver() archive = newWakuArchive(driver) - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2")), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 5], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 6], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 7], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 8], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2")), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2")), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 5], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 6], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 7], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 8], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2")), + ] for msg in msgList: require ( diff --git a/tests/waku_archive_legacy/test_driver_postgres_query.nim b/tests/waku_archive_legacy/test_driver_postgres_query.nim index e164a63a8..ff513de76 100644 --- a/tests/waku_archive_legacy/test_driver_postgres_query.nim +++ b/tests/waku_archive_legacy/test_driver_postgres_query.nim @@ -76,17 +76,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -117,17 +116,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -160,29 +158,28 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), - fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" - ), - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" - ), - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" - ), - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" - ), - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" - ), - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" - ), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), + fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), + fakeWakuMessage( + @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" + ), + fakeWakuMessage( + @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" + ), + fakeWakuMessage( + @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" + ), + fakeWakuMessage( + @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" + ), + fakeWakuMessage( + @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" + ), + fakeWakuMessage( + @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" + ), + ] var messages = expected shuffle(messages) @@ -215,17 +212,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -260,17 +256,16 @@ suite "Postgres driver - queries": const contentTopic2 = "test-content-topic-2" const contentTopic3 = "test-content-topic-3" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -323,14 +318,13 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), + ] var messages = expected shuffle(messages) @@ -394,35 +388,34 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" const pubsubTopic = "test-pubsub-topic" - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -455,35 +448,34 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" const pubsubTopic = "test-pubsub-topic" - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -514,35 +506,34 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" const pubsubTopic = "test-pubsub-topic" - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -577,18 +568,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -623,18 +612,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -669,17 +656,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + var messages = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] shuffle(messages) info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) @@ -721,18 +707,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -770,18 +754,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -821,59 +803,42 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -912,59 +877,42 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1000,19 +948,18 @@ suite "Postgres driver - queries": asyncTest "only hashes - descending order": ## Given let timeOrigin = now() - var expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + var expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1043,17 +990,16 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1087,17 +1033,16 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1132,61 +1077,44 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # start_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1222,18 +1150,17 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1270,17 +1197,16 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1316,20 +1242,19 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1365,21 +1290,20 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1418,21 +1342,20 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1472,52 +1395,39 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + # start_time + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1558,51 +1468,38 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1643,52 +1540,39 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1730,52 +1614,39 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1817,16 +1688,15 @@ suite "Postgres driver - queries": let timeOrigin = now() let oldestTime = ts(00, timeOrigin) let newestTime = ts(100, timeOrigin) - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = oldestTime), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = newestTime), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = oldestTime), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = newestTime), + ] var messages = expected shuffle(messages) @@ -1869,16 +1739,15 @@ suite "Postgres driver - queries": let timeOrigin = now() let targetTime = ts(40, timeOrigin) - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = targetTime), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = targetTime), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1911,16 +1780,15 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1957,12 +1825,11 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ] var messages = expected var hashes = newSeq[WakuMessageHash](0) diff --git a/tests/waku_archive_legacy/test_driver_queue_query.nim b/tests/waku_archive_legacy/test_driver_queue_query.nim index 6ebe5963a..0726d1931 100644 --- a/tests/waku_archive_legacy/test_driver_queue_query.nim +++ b/tests/waku_archive_legacy/test_driver_queue_query.nim @@ -35,17 +35,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -81,17 +80,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -129,17 +127,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -179,17 +176,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -229,14 +225,13 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), + ] var messages = expected shuffle(messages) @@ -311,35 +306,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newTestSqliteDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -377,35 +371,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newTestSqliteDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -441,35 +434,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newTestSqliteDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -510,18 +502,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -561,18 +551,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -612,17 +600,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + var messages = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] shuffle(messages) info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) @@ -667,18 +654,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -721,18 +706,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -777,59 +760,42 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -873,59 +839,42 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -969,17 +918,16 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1018,17 +966,16 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1068,61 +1015,44 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # start_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1163,18 +1093,17 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1216,17 +1145,16 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1267,20 +1195,19 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1321,21 +1248,20 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1379,21 +1305,20 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1438,52 +1363,39 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + # start_time + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1529,51 +1441,38 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1619,52 +1518,39 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1711,52 +1597,39 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) diff --git a/tests/waku_archive_legacy/test_driver_sqlite_query.nim b/tests/waku_archive_legacy/test_driver_sqlite_query.nim index 4143decf6..3c3b55232 100644 --- a/tests/waku_archive_legacy/test_driver_sqlite_query.nim +++ b/tests/waku_archive_legacy/test_driver_sqlite_query.nim @@ -24,17 +24,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -71,17 +70,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -120,29 +118,28 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), - fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" - ), - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" - ), - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" - ), - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" - ), - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" - ), - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" - ), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), + fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), + fakeWakuMessage( + @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" + ), + fakeWakuMessage( + @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" + ), + fakeWakuMessage( + @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" + ), + fakeWakuMessage( + @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" + ), + fakeWakuMessage( + @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" + ), + fakeWakuMessage( + @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" + ), + ] var messages = expected shuffle(messages) @@ -181,17 +178,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -232,17 +228,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -283,14 +278,13 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), + ] var messages = expected shuffle(messages) @@ -367,35 +361,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -434,35 +427,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -499,35 +491,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -569,18 +560,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -621,18 +610,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -673,17 +660,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + var messages = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] shuffle(messages) info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) @@ -729,18 +715,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -784,18 +768,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -841,59 +823,42 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -938,59 +903,42 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1035,17 +983,16 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1085,17 +1032,16 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1136,61 +1082,44 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # start_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1232,18 +1161,17 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1286,17 +1214,16 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1338,20 +1265,19 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1393,21 +1319,20 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1452,21 +1377,20 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1512,52 +1436,39 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + # start_time + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1604,51 +1515,38 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1695,52 +1593,39 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1788,52 +1673,39 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) diff --git a/tests/waku_archive_legacy/test_waku_archive.nim b/tests/waku_archive_legacy/test_waku_archive.nim index e58b2cfc9..f6373608d 100644 --- a/tests/waku_archive_legacy/test_waku_archive.nim +++ b/tests/waku_archive_legacy/test_waku_archive.nim @@ -33,14 +33,13 @@ suite "Waku Archive - message handling": let archive = newWakuArchive(driver) ## Given - let msgList = - @[ - fakeWakuMessage(ephemeral = false, payload = "1"), - fakeWakuMessage(ephemeral = true, payload = "2"), - fakeWakuMessage(ephemeral = true, payload = "3"), - fakeWakuMessage(ephemeral = true, payload = "4"), - fakeWakuMessage(ephemeral = false, payload = "5"), - ] + let msgList = @[ + fakeWakuMessage(ephemeral = false, payload = "1"), + fakeWakuMessage(ephemeral = true, payload = "2"), + fakeWakuMessage(ephemeral = true, payload = "3"), + fakeWakuMessage(ephemeral = true, payload = "4"), + fakeWakuMessage(ephemeral = false, payload = "5"), + ] ## When for msg in msgList: @@ -108,39 +107,38 @@ suite "Waku Archive - message handling": procSuite "Waku Archive - find messages": ## Fixtures let timeOrigin = now() - let msgListA = - @[ - fakeWakuMessage( - @[byte 00], contentTopic = ContentTopic("2"), ts = ts(00, timeOrigin) - ), - fakeWakuMessage( - @[byte 01], contentTopic = ContentTopic("1"), ts = ts(10, timeOrigin) - ), - fakeWakuMessage( - @[byte 02], contentTopic = ContentTopic("2"), ts = ts(20, timeOrigin) - ), - fakeWakuMessage( - @[byte 03], contentTopic = ContentTopic("1"), ts = ts(30, timeOrigin) - ), - fakeWakuMessage( - @[byte 04], contentTopic = ContentTopic("2"), ts = ts(40, timeOrigin) - ), - fakeWakuMessage( - @[byte 05], contentTopic = ContentTopic("1"), ts = ts(50, timeOrigin) - ), - fakeWakuMessage( - @[byte 06], contentTopic = ContentTopic("2"), ts = ts(60, timeOrigin) - ), - fakeWakuMessage( - @[byte 07], contentTopic = ContentTopic("1"), ts = ts(70, timeOrigin) - ), - fakeWakuMessage( - @[byte 08], contentTopic = ContentTopic("2"), ts = ts(80, timeOrigin) - ), - fakeWakuMessage( - @[byte 09], contentTopic = ContentTopic("1"), ts = ts(90, timeOrigin) - ), - ] + let msgListA = @[ + fakeWakuMessage( + @[byte 00], contentTopic = ContentTopic("2"), ts = ts(00, timeOrigin) + ), + fakeWakuMessage( + @[byte 01], contentTopic = ContentTopic("1"), ts = ts(10, timeOrigin) + ), + fakeWakuMessage( + @[byte 02], contentTopic = ContentTopic("2"), ts = ts(20, timeOrigin) + ), + fakeWakuMessage( + @[byte 03], contentTopic = ContentTopic("1"), ts = ts(30, timeOrigin) + ), + fakeWakuMessage( + @[byte 04], contentTopic = ContentTopic("2"), ts = ts(40, timeOrigin) + ), + fakeWakuMessage( + @[byte 05], contentTopic = ContentTopic("1"), ts = ts(50, timeOrigin) + ), + fakeWakuMessage( + @[byte 06], contentTopic = ContentTopic("2"), ts = ts(60, timeOrigin) + ), + fakeWakuMessage( + @[byte 07], contentTopic = ContentTopic("1"), ts = ts(70, timeOrigin) + ), + fakeWakuMessage( + @[byte 08], contentTopic = ContentTopic("2"), ts = ts(80, timeOrigin) + ), + fakeWakuMessage( + @[byte 09], contentTopic = ContentTopic("1"), ts = ts(90, timeOrigin) + ), + ] let archiveA = block: let @@ -433,19 +431,18 @@ procSuite "Waku Archive - find messages": driver = newSqliteArchiveDriver() archive = newWakuArchive(driver) - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2")), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 5], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 6], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 7], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 8], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2")), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2")), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 5], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 6], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 7], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 8], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2")), + ] for msg in msgList: require ( diff --git a/tests/waku_enr/test_sharding.nim b/tests/waku_enr/test_sharding.nim index 0984b7d8d..344436d0e 100644 --- a/tests/waku_enr/test_sharding.nim +++ b/tests/waku_enr/test_sharding.nim @@ -140,14 +140,13 @@ suite "Discovery Mechanisms for Shards": test "Bit Vector Representation": # Given a valid bit vector and its representation let - bitVector: seq[byte] = - @[ - 0, 73, 2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ] + bitVector: seq[byte] = @[ + 0, 73, 2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ] clusterId: uint16 = 73 # bitVector's clusterId shardIds: seq[uint16] = @[1u16, 10u16] # bitVector's shardIds diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index e41b79608..08d3daedb 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -79,11 +79,10 @@ suite "Waku rln relay": let rln = rlnInstance.get() # prepare the input - let msg = - @[ - "126f4c026cd731979365f79bd345a46d673c5a3f6f588bdc718e6356d02b6fdc".toBytes(), - "1f0e5db2b69d599166ab16219a97b82b662085c93220382b39f9f911d3b943b1".toBytes(), - ] + let msg = @[ + "126f4c026cd731979365f79bd345a46d673c5a3f6f588bdc718e6356d02b6fdc".toBytes(), + "1f0e5db2b69d599166ab16219a97b82b662085c93220382b39f9f911d3b943b1".toBytes(), + ] let hashRes = poseidon(msg) @@ -348,8 +347,7 @@ suite "Waku rln relay": let idCredentials1 = generateCredentials() (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: - assert false, - "error returned when calling register: " & error + assert false, "error returned when calling register: " & error let index2 = MembershipIndex(6) let rlnConf2 = getWakuRlnConfig(manager = manager, index = index2) @@ -362,8 +360,7 @@ suite "Waku rln relay": let idCredentials2 = generateCredentials() (waitFor manager2.register(idCredentials2, UserMessageLimit(20))).isOkOr: - assert false, - "error returned when calling register: " & error + assert false, "error returned when calling register: " & error # get the current epoch time let epoch = wakuRlnRelay1.getCurrentEpoch() @@ -447,7 +444,7 @@ suite "Waku rln relay": password = password, appInfo = RLNAppInfo, ) - .isOk() + .isOk() let readKeystoreRes = getMembershipCredentials( path = filepath, diff --git a/tests/waku_rln_relay/test_wakunode_rln_relay.nim b/tests/waku_rln_relay/test_wakunode_rln_relay.nim index 79a4d6711..19a47e1aa 100644 --- a/tests/waku_rln_relay/test_wakunode_rln_relay.nim +++ b/tests/waku_rln_relay/test_wakunode_rln_relay.nim @@ -135,8 +135,10 @@ procSuite "WakuNode - RLN relay": WakuMessage(payload: @payload, contentTopic: contentTopic, timestamp: now()) doAssert( node1.wakuRlnRelay - .unsafeAppendRLNProof(message, node1.wakuRlnRelay.getCurrentEpoch(), MessageId(0)) - .isOk() + .unsafeAppendRLNProof( + message, node1.wakuRlnRelay.getCurrentEpoch(), MessageId(0) + ) + .isOk() ) info " Nodes participating in the test", @@ -211,11 +213,10 @@ procSuite "WakuNode - RLN relay": let shards = @[RelayShard(clusterId: 0, shardId: 0), RelayShard(clusterId: 0, shardId: 1)] - let contentTopics = - @[ - ContentTopic("/waku/2/content-topic-a/proto"), - ContentTopic("/waku/2/content-topic-b/proto"), - ] + let contentTopics = @[ + ContentTopic("/waku/2/content-topic-a/proto"), + ContentTopic("/waku/2/content-topic-b/proto"), + ] # connect them together await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) diff --git a/tests/waku_rln_relay/utils_onchain.nim b/tests/waku_rln_relay/utils_onchain.nim index 9f1048097..db07d3cd6 100644 --- a/tests/waku_rln_relay/utils_onchain.nim +++ b/tests/waku_rln_relay/utils_onchain.nim @@ -535,22 +535,21 @@ proc runAnvil*( let anvilPath = getAnvilPath() info "Anvil path", anvilPath - var args = - @[ - "--port", - $port, - "--gas-limit", - "30000000", - "--gas-price", - "7", - "--base-fee", - "7", - "--balance", - "10000000000", - "--chain-id", - $chainId, - "--disable-min-priority-fee", - ] + var args = @[ + "--port", + $port, + "--gas-limit", + "30000000", + "--gas-price", + "7", + "--base-fee", + "7", + "--balance", + "10000000000", + "--chain-id", + $chainId, + "--disable-min-priority-fee", + ] # Add state file argument if provided if stateFile.isSome(): diff --git a/tests/waku_store/test_client.nim b/tests/waku_store/test_client.nim index 38b07bdf4..ec462eb56 100644 --- a/tests/waku_store/test_client.nim +++ b/tests/waku_store/test_client.nim @@ -35,24 +35,23 @@ suite "Store Client": hash1 = computeMessageHash(DefaultPubsubTopic, message1) hash2 = computeMessageHash(DefaultPubsubTopic, message2) hash3 = computeMessageHash(DefaultPubsubTopic, message3) - messageSeq = - @[ - WakuMessageKeyValue( - messageHash: hash1, - message: some(message1), - pubsubTopic: some(DefaultPubsubTopic), - ), - WakuMessageKeyValue( - messageHash: hash2, - message: some(message2), - pubsubTopic: some(DefaultPubsubTopic), - ), - WakuMessageKeyValue( - messageHash: hash3, - message: some(message3), - pubsubTopic: some(DefaultPubsubTopic), - ), - ] + messageSeq = @[ + WakuMessageKeyValue( + messageHash: hash1, + message: some(message1), + pubsubTopic: some(DefaultPubsubTopic), + ), + WakuMessageKeyValue( + messageHash: hash2, + message: some(message2), + pubsubTopic: some(DefaultPubsubTopic), + ), + WakuMessageKeyValue( + messageHash: hash3, + message: some(message3), + pubsubTopic: some(DefaultPubsubTopic), + ), + ] handlerFuture = newHistoryFuture() handler = proc(req: StoreQueryRequest): Future[StoreQueryResult] {.async, gcsafe.} = var request = req diff --git a/tests/waku_store/test_resume.nim b/tests/waku_store/test_resume.nim index 93e07ec0e..eb11b8f8e 100644 --- a/tests/waku_store/test_resume.nim +++ b/tests/waku_store/test_resume.nim @@ -50,19 +50,18 @@ suite "Store Resume - End to End": var clientDriver {.threadvar.}: ArchiveDriver asyncSetup: - let messages = - @[ - fakeWakuMessage(@[byte 00]), - fakeWakuMessage(@[byte 01]), - fakeWakuMessage(@[byte 02]), - fakeWakuMessage(@[byte 03]), - fakeWakuMessage(@[byte 04]), - fakeWakuMessage(@[byte 05]), - fakeWakuMessage(@[byte 06]), - fakeWakuMessage(@[byte 07]), - fakeWakuMessage(@[byte 08]), - fakeWakuMessage(@[byte 09]), - ] + let messages = @[ + fakeWakuMessage(@[byte 00]), + fakeWakuMessage(@[byte 01]), + fakeWakuMessage(@[byte 02]), + fakeWakuMessage(@[byte 03]), + fakeWakuMessage(@[byte 04]), + fakeWakuMessage(@[byte 05]), + fakeWakuMessage(@[byte 06]), + fakeWakuMessage(@[byte 07]), + fakeWakuMessage(@[byte 08]), + fakeWakuMessage(@[byte 09]), + ] let serverKey = generateSecp256k1Key() diff --git a/tests/waku_store/test_wakunode_store.nim b/tests/waku_store/test_wakunode_store.nim index 9239435af..fa73cd16d 100644 --- a/tests/waku_store/test_wakunode_store.nim +++ b/tests/waku_store/test_wakunode_store.nim @@ -32,19 +32,18 @@ import procSuite "WakuNode - Store": ## Fixtures let timeOrigin = now() - let msgListA = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] + let msgListA = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), + ] let hashes = msgListA.mapIt(computeMessageHash(DefaultPubsubTopic, it)) diff --git a/tests/waku_store_legacy/test_resume.nim b/tests/waku_store_legacy/test_resume.nim index 53e48834e..0f8132f08 100644 --- a/tests/waku_store_legacy/test_resume.nim +++ b/tests/waku_store_legacy/test_resume.nim @@ -29,93 +29,91 @@ when defined(waku_exp_store_resume): ## Fixtures let storeA = block: let store = newTestMessageStore() - let msgList = - @[ - fakeWakuMessage( - payload = @[byte 0], contentTopic = ContentTopic("2"), ts = ts(0) - ), - fakeWakuMessage( - payload = @[byte 1], contentTopic = ContentTopic("1"), ts = ts(1) - ), - fakeWakuMessage( - payload = @[byte 2], contentTopic = ContentTopic("2"), ts = ts(2) - ), - fakeWakuMessage( - payload = @[byte 3], contentTopic = ContentTopic("1"), ts = ts(3) - ), - fakeWakuMessage( - payload = @[byte 4], contentTopic = ContentTopic("2"), ts = ts(4) - ), - fakeWakuMessage( - payload = @[byte 5], contentTopic = ContentTopic("1"), ts = ts(5) - ), - fakeWakuMessage( - payload = @[byte 6], contentTopic = ContentTopic("2"), ts = ts(6) - ), - fakeWakuMessage( - payload = @[byte 7], contentTopic = ContentTopic("1"), ts = ts(7) - ), - fakeWakuMessage( - payload = @[byte 8], contentTopic = ContentTopic("2"), ts = ts(8) - ), - fakeWakuMessage( - payload = @[byte 9], contentTopic = ContentTopic("1"), ts = ts(9) - ), - ] + let msgList = @[ + fakeWakuMessage( + payload = @[byte 0], contentTopic = ContentTopic("2"), ts = ts(0) + ), + fakeWakuMessage( + payload = @[byte 1], contentTopic = ContentTopic("1"), ts = ts(1) + ), + fakeWakuMessage( + payload = @[byte 2], contentTopic = ContentTopic("2"), ts = ts(2) + ), + fakeWakuMessage( + payload = @[byte 3], contentTopic = ContentTopic("1"), ts = ts(3) + ), + fakeWakuMessage( + payload = @[byte 4], contentTopic = ContentTopic("2"), ts = ts(4) + ), + fakeWakuMessage( + payload = @[byte 5], contentTopic = ContentTopic("1"), ts = ts(5) + ), + fakeWakuMessage( + payload = @[byte 6], contentTopic = ContentTopic("2"), ts = ts(6) + ), + fakeWakuMessage( + payload = @[byte 7], contentTopic = ContentTopic("1"), ts = ts(7) + ), + fakeWakuMessage( + payload = @[byte 8], contentTopic = ContentTopic("2"), ts = ts(8) + ), + fakeWakuMessage( + payload = @[byte 9], contentTopic = ContentTopic("1"), ts = ts(9) + ), + ] for msg in msgList: require store - .put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - .isOk() + .put( + DefaultPubsubTopic, + msg, + computeDigest(msg), + computeMessageHash(DefaultPubsubTopic, msg), + msg.timestamp, + ) + .isOk() store let storeB = block: let store = newTestMessageStore() - let msgList2 = - @[ - fakeWakuMessage( - payload = @[byte 0], contentTopic = ContentTopic("2"), ts = ts(0) - ), - fakeWakuMessage( - payload = @[byte 11], contentTopic = ContentTopic("1"), ts = ts(1) - ), - fakeWakuMessage( - payload = @[byte 12], contentTopic = ContentTopic("2"), ts = ts(2) - ), - fakeWakuMessage( - payload = @[byte 3], contentTopic = ContentTopic("1"), ts = ts(3) - ), - fakeWakuMessage( - payload = @[byte 4], contentTopic = ContentTopic("2"), ts = ts(4) - ), - fakeWakuMessage( - payload = @[byte 5], contentTopic = ContentTopic("1"), ts = ts(5) - ), - fakeWakuMessage( - payload = @[byte 13], contentTopic = ContentTopic("2"), ts = ts(6) - ), - fakeWakuMessage( - payload = @[byte 14], contentTopic = ContentTopic("1"), ts = ts(7) - ), - ] + let msgList2 = @[ + fakeWakuMessage( + payload = @[byte 0], contentTopic = ContentTopic("2"), ts = ts(0) + ), + fakeWakuMessage( + payload = @[byte 11], contentTopic = ContentTopic("1"), ts = ts(1) + ), + fakeWakuMessage( + payload = @[byte 12], contentTopic = ContentTopic("2"), ts = ts(2) + ), + fakeWakuMessage( + payload = @[byte 3], contentTopic = ContentTopic("1"), ts = ts(3) + ), + fakeWakuMessage( + payload = @[byte 4], contentTopic = ContentTopic("2"), ts = ts(4) + ), + fakeWakuMessage( + payload = @[byte 5], contentTopic = ContentTopic("1"), ts = ts(5) + ), + fakeWakuMessage( + payload = @[byte 13], contentTopic = ContentTopic("2"), ts = ts(6) + ), + fakeWakuMessage( + payload = @[byte 14], contentTopic = ContentTopic("1"), ts = ts(7) + ), + ] for msg in msgList2: require store - .put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - .isOk() + .put( + DefaultPubsubTopic, + msg, + computeDigest(msg), + computeMessageHash(DefaultPubsubTopic, msg), + msg.timestamp, + ) + .isOk() store @@ -136,11 +134,10 @@ when defined(waku_exp_store_resume): client = newTestWakuStoreClient(clientSwitch) ## Given - let peers = - @[ - serverSwitchA.peerInfo.toRemotePeerInfo(), - serverSwitchB.peerInfo.toRemotePeerInfo(), - ] + let peers = @[ + serverSwitchA.peerInfo.toRemotePeerInfo(), + serverSwitchB.peerInfo.toRemotePeerInfo(), + ] let req = HistoryQuery(contentTopics: @[DefaultContentTopic], pageSize: 5) ## When @@ -226,12 +223,11 @@ when defined(waku_exp_store_resume): client = await newTestWakuStore(clientSwitch) ## Given - let peers = - @[ - offlineSwitch.peerInfo.toRemotePeerInfo(), - serverASwitch.peerInfo.toRemotePeerInfo(), - serverBSwitch.peerInfo.toRemotePeerInfo(), - ] + let peers = @[ + offlineSwitch.peerInfo.toRemotePeerInfo(), + serverASwitch.peerInfo.toRemotePeerInfo(), + serverBSwitch.peerInfo.toRemotePeerInfo(), + ] ## When let res = await client.resume(some(peers)) @@ -323,11 +319,11 @@ when defined(waku_exp_store_resume): receivedTime3 = now() + getNanosecondTime(10) digest3 = computeDigest(msg3) require server.wakuStore.store - .put(DefaultPubsubTopic, msg3, digest3, receivedTime3) - .isOk() + .put(DefaultPubsubTopic, msg3, digest3, receivedTime3) + .isOk() require client.wakuStore.store - .put(DefaultPubsubTopic, msg3, digest3, receivedTime3) - .isOk() + .put(DefaultPubsubTopic, msg3, digest3, receivedTime3) + .isOk() let serverPeer = server.peerInfo.toRemotePeerInfo() diff --git a/tests/waku_store_legacy/test_rpc_codec.nim b/tests/waku_store_legacy/test_rpc_codec.nim index 6897bab41..2801cc9a8 100644 --- a/tests/waku_store_legacy/test_rpc_codec.nim +++ b/tests/waku_store_legacy/test_rpc_codec.nim @@ -100,11 +100,10 @@ procSuite "Waku Store - RPC codec": direction: some(PagingDirection.BACKWARD), ) query = HistoryQueryRPC( - contentFilters: - @[ - HistoryContentFilterRPC(contentTopic: DefaultContentTopic), - HistoryContentFilterRPC(contentTopic: DefaultContentTopic), - ], + contentFilters: @[ + HistoryContentFilterRPC(contentTopic: DefaultContentTopic), + HistoryContentFilterRPC(contentTopic: DefaultContentTopic), + ], pagingInfo: some(pagingInfo), startTime: some(Timestamp(10)), endTime: some(Timestamp(11)), diff --git a/tests/waku_store_legacy/test_wakunode_store.nim b/tests/waku_store_legacy/test_wakunode_store.nim index 549033e98..58e3ca9e0 100644 --- a/tests/waku_store_legacy/test_wakunode_store.nim +++ b/tests/waku_store_legacy/test_wakunode_store.nim @@ -30,19 +30,18 @@ import procSuite "WakuNode - Store Legacy": ## Fixtures let timeOrigin = now() - let msgListA = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] + let msgListA = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), + ] let archiveA = block: let driver = newSqliteArchiveDriver() diff --git a/tests/waku_store_sync/test_range_split.nim b/tests/waku_store_sync/test_range_split.nim index 546f2cfa5..fe5252416 100644 --- a/tests/waku_store_sync/test_range_split.nim +++ b/tests/waku_store_sync/test_range_split.nim @@ -119,12 +119,11 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(wholeRange, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - wholeRange, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint( + wholeRange, @[DefaultPubsubTopic], @[DefaultContentTopic] + ) + ], itemSets: @[], ) @@ -180,12 +179,11 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(sliceWhole, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - sliceWhole, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint( + sliceWhole, @[DefaultPubsubTopic], @[DefaultContentTopic] + ) + ], itemSets: @[], ) @@ -207,12 +205,11 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(subSlice, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - subSlice, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint( + subSlice, @[DefaultPubsubTopic], @[DefaultContentTopic] + ) + ], itemSets: @[], ) @@ -272,12 +269,9 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(slice, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - slice, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint(slice, @[DefaultPubsubTopic], @[DefaultContentTopic]) + ], itemSets: @[], ) diff --git a/tests/waku_store_sync/test_state_transition.nim b/tests/waku_store_sync/test_state_transition.nim index d94d6bed2..2e6bb30c3 100644 --- a/tests/waku_store_sync/test_state_transition.nim +++ b/tests/waku_store_sync/test_state_transition.nim @@ -44,12 +44,9 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(whole, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - whole, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint(whole, @[DefaultPubsubTopic], @[DefaultContentTopic]) + ], itemSets: @[], ) let rep1 = local.processPayload(p1, s1, r1) @@ -131,15 +128,10 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(sliceA, RangeType.Fingerprint), (sliceB, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - sliceA, @[DefaultPubsubTopic], @[DefaultContentTopic] - ), - remote.computeFingerprint( - sliceB, @[DefaultPubsubTopic], @[DefaultContentTopic] - ), - ], + fingerprints: @[ + remote.computeFingerprint(sliceA, @[DefaultPubsubTopic], @[DefaultContentTopic]), + remote.computeFingerprint(sliceB, @[DefaultPubsubTopic], @[DefaultContentTopic]), + ], itemSets: @[], ) let reply = local.processPayload(payload, s, r) @@ -180,12 +172,9 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(slice, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - slice, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint(slice, @[DefaultPubsubTopic], @[DefaultContentTopic]) + ], itemSets: @[], ) let reply = local.processPayload(p, toS, toR) @@ -236,12 +225,9 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(s, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - s, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint(s, @[DefaultPubsubTopic], @[DefaultContentTopic]) + ], itemSets: @[], ), sendQ, diff --git a/tests/wakunode_rest/test_rest_filter.nim b/tests/wakunode_rest/test_rest_filter.nim index 3d4d741a5..1a4731d6a 100644 --- a/tests/wakunode_rest/test_rest_filter.nim +++ b/tests/wakunode_rest/test_rest_filter.nim @@ -176,12 +176,11 @@ suite "Waku v2 Rest API - Filter V2": ) discard await restFilterTest.client.filterPostSubscriptions(requestBody) - let contentFilters = - @[ - ContentTopic("1"), - ContentTopic("2"), - ContentTopic("3"), # ,ContentTopic("4") # Keep this subscription for check - ] + let contentFilters = @[ + ContentTopic("1"), + ContentTopic("2"), + ContentTopic("3"), # ,ContentTopic("4") # Keep this subscription for check + ] let requestBodyUnsub = FilterUnsubscribeRequest( requestId: "4321", diff --git a/tests/wakunode_rest/test_rest_relay.nim b/tests/wakunode_rest/test_rest_relay.nim index efdd597ba..b791da29f 100644 --- a/tests/wakunode_rest/test_rest_relay.nim +++ b/tests/wakunode_rest/test_rest_relay.nim @@ -193,15 +193,14 @@ suite "Waku v2 Rest API - Relay": let pubSubTopic = "/waku/2/rs/0/0" - var messages = - @[ - fakeWakuMessage( - contentTopic = "content-topic-x", - payload = toBytes("TEST-1"), - meta = toBytes("test-meta"), - ephemeral = true, - ) - ] + var messages = @[ + fakeWakuMessage( + contentTopic = "content-topic-x", + payload = toBytes("TEST-1"), + meta = toBytes("test-meta"), + ephemeral = true, + ) + ] # Prevent duplicate messages for i in 0 ..< 2: @@ -345,12 +344,11 @@ suite "Waku v2 Rest API - Relay": installRelayApiHandlers(restServer.router, node, cache) restServer.start() - let contentTopics = - @[ - ContentTopic("/app-1/2/default-content/proto"), - ContentTopic("/app-2/2/default-content/proto"), - ContentTopic("/app-3/2/default-content/proto"), - ] + let contentTopics = @[ + ContentTopic("/app-1/2/default-content/proto"), + ContentTopic("/app-2/2/default-content/proto"), + ContentTopic("/app-3/2/default-content/proto"), + ] # When let client = newRestHttpClient(initTAddress(restAddress, restPort)) @@ -391,13 +389,12 @@ suite "Waku v2 Rest API - Relay": restPort = restServer.httpServer.address.port # update with bound port for client use - let contentTopics = - @[ - ContentTopic("/waku/2/default-content1/proto"), - ContentTopic("/waku/2/default-content2/proto"), - ContentTopic("/waku/2/default-content3/proto"), - ContentTopic("/waku/2/default-contentX/proto"), - ] + let contentTopics = @[ + ContentTopic("/waku/2/default-content1/proto"), + ContentTopic("/waku/2/default-content2/proto"), + ContentTopic("/waku/2/default-content3/proto"), + ContentTopic("/waku/2/default-contentX/proto"), + ] let cache = MessageCache.init() cache.contentSubscribe(contentTopics[0]) @@ -451,10 +448,9 @@ suite "Waku v2 Rest API - Relay": let contentTopic = DefaultContentTopic - var messages = - @[ - fakeWakuMessage(contentTopic = DefaultContentTopic, payload = toBytes("TEST-1")) - ] + var messages = @[ + fakeWakuMessage(contentTopic = DefaultContentTopic, payload = toBytes("TEST-1")) + ] # Prevent duplicate messages for i in 0 ..< 2: diff --git a/tests/wakunode_rest/test_rest_store.nim b/tests/wakunode_rest/test_rest_store.nim index 70a3c137a..01ccea9dd 100644 --- a/tests/wakunode_rest/test_rest_store.nim +++ b/tests/wakunode_rest/test_rest_store.nim @@ -115,17 +115,16 @@ procSuite "Waku Rest API - Store v3": await sleepAsync(1.seconds()) # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 1, byte 2], ts = 2), - fakeWakuMessage(@[byte 1], ts = 3), - fakeWakuMessage(@[byte 1], ts = 4), - fakeWakuMessage(@[byte 1], ts = 5), - fakeWakuMessage(@[byte 1], ts = 6), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("c2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 1, byte 2], ts = 2), + fakeWakuMessage(@[byte 1], ts = 3), + fakeWakuMessage(@[byte 1], ts = 4), + fakeWakuMessage(@[byte 1], ts = 5), + fakeWakuMessage(@[byte 1], ts = 6), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("c2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -191,17 +190,16 @@ procSuite "Waku Rest API - Store v3": peerSwitch.mount(node.wakuStore) # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 1, byte 2], ts = 2), - fakeWakuMessage(@[byte 1], ts = 3), - fakeWakuMessage(@[byte 1], ts = 4), - fakeWakuMessage(@[byte 1], ts = 5), - fakeWakuMessage(@[byte 1], ts = 6), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("c2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 1, byte 2], ts = 2), + fakeWakuMessage(@[byte 1], ts = 3), + fakeWakuMessage(@[byte 1], ts = 4), + fakeWakuMessage(@[byte 1], ts = 5), + fakeWakuMessage(@[byte 1], ts = 6), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("c2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -262,19 +260,18 @@ procSuite "Waku Rest API - Store v3": # Now prime it with some history before tests let timeOrigin = wakucore.now() - let msgList = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] + let msgList = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -357,12 +354,11 @@ procSuite "Waku Rest API - Store v3": peerSwitch.mount(node.wakuStore) # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -431,12 +427,11 @@ procSuite "Waku Rest API - Store v3": peerSwitch.mount(node.wakuStore) # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -521,12 +516,11 @@ procSuite "Waku Rest API - Store v3": peerSwitch.mount(node.wakuStore) # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -594,12 +588,11 @@ procSuite "Waku Rest API - Store v3": await node.mountStore() # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -640,14 +633,13 @@ procSuite "Waku Rest API - Store v3": await node.mountStore() # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage( - @[byte 0], contentTopic = ContentTopic("ct1"), ts = 0, meta = (@[byte 8]) - ), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage( + @[byte 0], contentTopic = ContentTopic("ct1"), ts = 0, meta = (@[byte 8]) + ), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -757,19 +749,18 @@ procSuite "Waku Rest API - Store v3": # Now prime it with some history before tests let timeOrigin = wakucore.now() - let msgList = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] + let msgList = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 4a6e8c618..9854828ff 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -263,8 +263,7 @@ type WakuNodeConf* = object ## Circuit-relay config isRelayClient* {. - desc: - """Set the node as a relay-client. + desc: """Set the node as a relay-client. Set it to true for nodes that run behind a NAT or firewall and hence would have reachability issues.""", defaultValue: false, diff --git a/vendor/nph b/vendor/nph index c6e03162d..2cacf6cc2 160000 --- a/vendor/nph +++ b/vendor/nph @@ -1 +1 @@ -Subproject commit c6e03162dc2820d3088660f644818d7040e95791 +Subproject commit 2cacf6cc28116e4046e0b67a13545af5c4e756bd diff --git a/waku/api/api_conf.nim b/waku/api/api_conf.nim index 70bb02af3..30dfd1b2c 100644 --- a/waku/api/api_conf.nim +++ b/waku/api/api_conf.nim @@ -62,10 +62,9 @@ proc init*( ) const TheWakuNetworkPreset* = ProtocolsConfig( - entryNodes: - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im" - ], + entryNodes: @[ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im" + ], staticStoreNodes: @[], clusterId: 1, autoShardingConfig: AutoShardingConfig(numShardsInCluster: 8), diff --git a/waku/common/broker/event_broker.nim b/waku/common/broker/event_broker.nim index 779689f88..3fd10cea2 100644 --- a/waku/common/broker/event_broker.nim +++ b/waku/common/broker/event_broker.nim @@ -151,14 +151,13 @@ macro EventBroker*(body: untyped): untyped = proc `accessProcIdent`(): `brokerTypeIdent` = if `globalVarIdent`.isNil(): new(`globalVarIdent`) - `globalVarIdent`.buckets = - @[ - `bucketTypeIdent`( - brokerCtx: DefaultBrokerContext, - listeners: initTable[uint64, `handlerProcIdent`](), - nextId: 1'u64, - ) - ] + `globalVarIdent`.buckets = @[ + `bucketTypeIdent`( + brokerCtx: DefaultBrokerContext, + listeners: initTable[uint64, `handlerProcIdent`](), + nextId: 1'u64, + ) + ] `globalVarIdent` ) diff --git a/waku/common/databases/db_postgres/dbconn.nim b/waku/common/databases/db_postgres/dbconn.nim index 7ccf32099..f24f3d4dd 100644 --- a/waku/common/databases/db_postgres/dbconn.nim +++ b/waku/common/databases/db_postgres/dbconn.nim @@ -211,11 +211,10 @@ proc waitQueryToFinish( pqclear(pqResult) proc containsRiskyPatterns(input: string): bool = - let riskyPatterns = - @[ - " OR ", " AND ", " UNION ", " SELECT ", "INSERT ", "DELETE ", "UPDATE ", "DROP ", - "EXEC ", "--", "/*", "*/", - ] + let riskyPatterns = @[ + " OR ", " AND ", " UNION ", " SELECT ", "INSERT ", "DELETE ", "UPDATE ", "DROP ", + "EXEC ", "--", "/*", "*/", + ] for pattern in riskyPatterns: if pattern.toLowerAscii() in input.toLowerAscii(): diff --git a/waku/common/rate_limit/timed_map.nim b/waku/common/rate_limit/timed_map.nim index b05dfb0fb..b9a5c4cbf 100644 --- a/waku/common/rate_limit/timed_map.nim +++ b/waku/common/rate_limit/timed_map.nim @@ -106,16 +106,8 @@ proc mgetOrPut*[K, V](t: var TimedMap[K, V], k: K, v: V, now = Moment.now()): va let previous = t.del(k) # Refresh existing item - addedAt = - if previous.isSome(): - previous[].addedAt - else: - now - value = - if previous.isSome(): - previous[].value - else: - v + addedAt = if previous.isSome(): previous[].addedAt else: now + value = if previous.isSome(): previous[].value else: v let node = TimedEntry[K, V](key: k, value: value, addedAt: addedAt, expiresAt: now + t.timeout) diff --git a/waku/factory/builder.nim b/waku/factory/builder.nim index e0b643fc0..87b0db492 100644 --- a/waku/factory/builder.nim +++ b/waku/factory/builder.nim @@ -84,20 +84,19 @@ proc withNetworkConfigurationDetails*( ): WakuNodeBuilderResult {. deprecated: "use 'builder.withNetworkConfiguration()' instead" .} = - let netConfig = - ?NetConfig.init( - bindIp = bindIp, - bindPort = bindPort, - extIp = extIp, - extPort = extPort, - extMultiAddrs = extMultiAddrs, - wsBindPort = some(wsBindPort), - wsEnabled = wsEnabled, - wssEnabled = wssEnabled, - wakuFlags = wakuFlags, - dns4DomainName = dns4DomainName, - dnsNameServers = dnsNameServers, - ) + let netConfig = ?NetConfig.init( + bindIp = bindIp, + bindPort = bindPort, + extIp = extIp, + extPort = extPort, + extMultiAddrs = extMultiAddrs, + wsBindPort = some(wsBindPort), + wsEnabled = wsEnabled, + wssEnabled = wssEnabled, + wakuFlags = wakuFlags, + dns4DomainName = dns4DomainName, + dnsNameServers = dnsNameServers, + ) builder.withNetworkConfiguration(netConfig) ok() diff --git a/waku/factory/networks_config.nim b/waku/factory/networks_config.nim index 94856fb21..d9c0cf879 100644 --- a/waku/factory/networks_config.nim +++ b/waku/factory/networks_config.nim @@ -56,12 +56,11 @@ proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf = mix: false, p2pReliability: false, discv5Discovery: true, - discv5BootstrapNodes: - @[ - "enr:-QESuED0qW1BCmF-oH_ARGPr97Nv767bl_43uoy70vrbah3EaCAdK3Q0iRQ6wkSTTpdrg_dU_NC2ydO8leSlRpBX4pxiAYJpZIJ2NIJpcIRA4VDAim11bHRpYWRkcnO4XAArNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwAtNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOTd-h5owwj-cx7xrmbvQKU8CV3Fomfdvcv1MBc-67T5oN0Y3CCdl-DdWRwgiMohXdha3UyDw", - "enr:-QEkuED9X80QF_jcN9gA2ZRhhmwVEeJnsg_Hyg7IFCTYnZD0BDI7a8HArE61NhJZFwygpHCWkgwSt2vqiABXkBxzIqZBAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQPFAS8zz2cg1QQhxMaK8CzkGQ5wdHvPJcrgLzJGOiHpwYN0Y3CCdl-DdWRwgiMohXdha3UyDw", - "enr:-QEkuEBfEzJm_kigJ2HoSS_RBFJYhKHocGdkhhBr6jSUAWjLdFPp6Pj1l4yiTQp7TGHyu1kC6FyaU573VN8klLsEm-XuAYJpZIJ2NIJpcIQI2SVcim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOwsS69tgD7u1K50r5-qG5hweuTwa0W26aYPnvivpNlrYN0Y3CCdl-DdWRwgiMohXdha3UyDw", - ], + discv5BootstrapNodes: @[ + "enr:-QESuED0qW1BCmF-oH_ARGPr97Nv767bl_43uoy70vrbah3EaCAdK3Q0iRQ6wkSTTpdrg_dU_NC2ydO8leSlRpBX4pxiAYJpZIJ2NIJpcIRA4VDAim11bHRpYWRkcnO4XAArNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwAtNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOTd-h5owwj-cx7xrmbvQKU8CV3Fomfdvcv1MBc-67T5oN0Y3CCdl-DdWRwgiMohXdha3UyDw", + "enr:-QEkuED9X80QF_jcN9gA2ZRhhmwVEeJnsg_Hyg7IFCTYnZD0BDI7a8HArE61NhJZFwygpHCWkgwSt2vqiABXkBxzIqZBAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQPFAS8zz2cg1QQhxMaK8CzkGQ5wdHvPJcrgLzJGOiHpwYN0Y3CCdl-DdWRwgiMohXdha3UyDw", + "enr:-QEkuEBfEzJm_kigJ2HoSS_RBFJYhKHocGdkhhBr6jSUAWjLdFPp6Pj1l4yiTQp7TGHyu1kC6FyaU573VN8klLsEm-XuAYJpZIJ2NIJpcIQI2SVcim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOwsS69tgD7u1K50r5-qG5hweuTwa0W26aYPnvivpNlrYN0Y3CCdl-DdWRwgiMohXdha3UyDw", + ], ) # cluster-id=2 (Logos Dev Network) @@ -83,15 +82,14 @@ proc LogosDevConf*(T: type NetworkConf): NetworkConf = p2pReliability: true, discv5Discovery: true, discv5BootstrapNodes: @[], - entryNodes: - @[ - "/dns4/delivery-01.do-ams3.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby", - "/dns4/delivery-02.do-ams3.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAmMK7PYygBtKUQ8EHp7EfaD3bCEsJrkFooK8RQ2PVpJprH", - "/dns4/delivery-01.gc-us-central1-a.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm4S1JYkuzDKLKQvwgAhZKs9otxXqt8SCGtB4hoJP1S397", - "/dns4/delivery-02.gc-us-central1-a.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm8Y9kgBNtjxvCnf1X6gnZJW5EGE4UwwCL3CCm55TwqBiH", - "/dns4/delivery-01.ac-cn-hongkong-c.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm8YokiNun9BkeA1ZRmhLbtNUvcwRr64F69tYj9fkGyuEP", - "/dns4/delivery-02.ac-cn-hongkong-c.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAkvwhGHKNry6LACrB8TmEFoCJKEX29XR5dDUzk3UT3UNSE", - ], + entryNodes: @[ + "/dns4/delivery-01.do-ams3.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby", + "/dns4/delivery-02.do-ams3.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAmMK7PYygBtKUQ8EHp7EfaD3bCEsJrkFooK8RQ2PVpJprH", + "/dns4/delivery-01.gc-us-central1-a.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm4S1JYkuzDKLKQvwgAhZKs9otxXqt8SCGtB4hoJP1S397", + "/dns4/delivery-02.gc-us-central1-a.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm8Y9kgBNtjxvCnf1X6gnZJW5EGE4UwwCL3CCm55TwqBiH", + "/dns4/delivery-01.ac-cn-hongkong-c.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm8YokiNun9BkeA1ZRmhLbtNUvcwRr64F69tYj9fkGyuEP", + "/dns4/delivery-02.ac-cn-hongkong-c.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAkvwhGHKNry6LACrB8TmEFoCJKEX29XR5dDUzk3UT3UNSE", + ], ) proc validateShards*( diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index 2f82440f6..57e18e3c0 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -126,11 +126,10 @@ proc initNode( builder.withRateLimit(conf.rateLimit) builder.withCircuitRelay(relay) - let node = - ?builder.build().mapErr( - proc(err: string): string = - "failed to create waku node instance: " & err - ) + let node = ?builder.build().mapErr( + proc(err: string): string = + "failed to create waku node instance: " & err + ) ok(node) diff --git a/waku/node/peer_manager/peer_manager.nim b/waku/node/peer_manager/peer_manager.nim index 0c435468f..dc0da9624 100644 --- a/waku/node/peer_manager/peer_manager.nim +++ b/waku/node/peer_manager/peer_manager.nim @@ -1,11 +1,9 @@ {.push raises: [].} import - std/ - [ - options, sets, sequtils, times, strformat, strutils, math, random, tables, - algorithm, - ], + std/[ + options, sets, sequtils, times, strformat, strutils, math, random, tables, algorithm + ], chronos, chronicles, metrics, diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 0cef4cc5d..0c6cb7ac4 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -356,12 +356,11 @@ proc mountStoreSync*( let pubsubTopics = shards.mapIt($RelayShard(clusterId: cluster, shardId: it)) - let recon = - ?await SyncReconciliation.new( - pubsubTopics, contentTopics, node.peerManager, node.wakuArchive, - storeSyncRange.seconds, storeSyncInterval.seconds, storeSyncRelayJitter.seconds, - idsChannel, wantsChannel, needsChannel, - ) + let recon = ?await SyncReconciliation.new( + pubsubTopics, contentTopics, node.peerManager, node.wakuArchive, + storeSyncRange.seconds, storeSyncInterval.seconds, storeSyncRelayJitter.seconds, + idsChannel, wantsChannel, needsChannel, + ) node.wakuStoreReconciliation = recon diff --git a/waku/rest_api/endpoint/builder.nim b/waku/rest_api/endpoint/builder.nim index 41ab7e06b..cb23bc284 100644 --- a/waku/rest_api/endpoint/builder.nim +++ b/waku/rest_api/endpoint/builder.nim @@ -86,13 +86,12 @@ proc startRestServerEssentials*( let address = conf.listenAddress let port = Port(conf.port.uint16 + portsShift) - let server = - ?newRestHttpServer( - address, - port, - allowedOrigin = allowedOrigin, - requestErrorHandler = requestErrorHandler, - ) + let server = ?newRestHttpServer( + address, + port, + allowedOrigin = allowedOrigin, + requestErrorHandler = requestErrorHandler, + ) ## Health REST API installHealthApiHandler(server.router, nodeHealthMonitor) diff --git a/waku/rest_api/endpoint/server.nim b/waku/rest_api/endpoint/server.nim index 1b61425c8..44a02ccb2 100644 --- a/waku/rest_api/endpoint/server.nim +++ b/waku/rest_api/endpoint/server.nim @@ -91,23 +91,22 @@ proc new*( ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = discard - server.httpServer = - ?HttpServerRef.new( - address, - defaultProcessCallback, - serverFlags, - socketFlags, - serverUri, - serverIdent, - maxConnections, - bufferSize, - backlogSize, - httpHeadersTimeout, - maxHeadersSize, - maxRequestBodySize, - dualstack = dualstack, - middlewares = middlewares, - ) + server.httpServer = ?HttpServerRef.new( + address, + defaultProcessCallback, + serverFlags, + socketFlags, + serverUri, + serverIdent, + maxConnections, + bufferSize, + backlogSize, + httpHeadersTimeout, + maxHeadersSize, + maxRequestBodySize, + dualstack = dualstack, + middlewares = middlewares, + ) return ok(server) proc getRouter(): RestRouter = diff --git a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim index 2f495ba5d..6a322cf77 100644 --- a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim @@ -45,8 +45,7 @@ const SelectClause = const SelectNoCursorAscStmtName = "SelectWithoutCursorAsc" const SelectNoCursorAscStmtDef = - SelectClause & - """WHERE contentTopic IN ($1) AND + SelectClause & """WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND timestamp >= $4 AND @@ -54,8 +53,7 @@ const SelectNoCursorAscStmtDef = ORDER BY timestamp ASC, messageHash ASC LIMIT $6;""" const SelectNoCursorNoDataAscStmtName = "SelectWithoutCursorAndDataAsc" -const SelectNoCursorNoDataAscStmtDef = - """SELECT messageHash FROM messages +const SelectNoCursorNoDataAscStmtDef = """SELECT messageHash FROM messages WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND @@ -65,8 +63,7 @@ const SelectNoCursorNoDataAscStmtDef = const SelectNoCursorDescStmtName = "SelectWithoutCursorDesc" const SelectNoCursorDescStmtDef = - SelectClause & - """WHERE contentTopic IN ($1) AND + SelectClause & """WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND timestamp >= $4 AND @@ -74,8 +71,7 @@ const SelectNoCursorDescStmtDef = ORDER BY timestamp DESC, messageHash DESC LIMIT $6;""" const SelectNoCursorNoDataDescStmtName = "SelectWithoutCursorAndDataDesc" -const SelectNoCursorNoDataDescStmtDef = - """SELECT messageHash FROM messages +const SelectNoCursorNoDataDescStmtDef = """SELECT messageHash FROM messages WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND @@ -85,8 +81,7 @@ const SelectNoCursorNoDataDescStmtDef = const SelectWithCursorDescStmtName = "SelectWithCursorDesc" const SelectWithCursorDescStmtDef = - SelectClause & - """WHERE contentTopic IN ($1) AND + SelectClause & """WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND (timestamp, messageHash) < ($4,$5) AND @@ -95,8 +90,7 @@ const SelectWithCursorDescStmtDef = ORDER BY timestamp DESC, messageHash DESC LIMIT $8;""" const SelectWithCursorNoDataDescStmtName = "SelectWithCursorNoDataDesc" -const SelectWithCursorNoDataDescStmtDef = - """SELECT messageHash FROM messages +const SelectWithCursorNoDataDescStmtDef = """SELECT messageHash FROM messages WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND @@ -107,8 +101,7 @@ const SelectWithCursorNoDataDescStmtDef = const SelectWithCursorAscStmtName = "SelectWithCursorAsc" const SelectWithCursorAscStmtDef = - SelectClause & - """WHERE contentTopic IN ($1) AND + SelectClause & """WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND (timestamp, messageHash) > ($4,$5) AND @@ -117,8 +110,7 @@ const SelectWithCursorAscStmtDef = ORDER BY timestamp ASC, messageHash ASC LIMIT $8;""" const SelectWithCursorNoDataAscStmtName = "SelectWithCursorNoDataAsc" -const SelectWithCursorNoDataAscStmtDef = - """SELECT messageHash FROM messages +const SelectWithCursorNoDataAscStmtDef = """SELECT messageHash FROM messages WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND @@ -128,8 +120,7 @@ const SelectWithCursorNoDataAscStmtDef = ORDER BY timestamp ASC, messageHash ASC LIMIT $8;""" const SelectCursorByHashName = "SelectMessageByHashInMessagesLookup" -const SelectCursorByHashDef = - """SELECT timestamp FROM messages_lookup +const SelectCursorByHashDef = """SELECT timestamp FROM messages_lookup WHERE messageHash = $1""" const @@ -896,11 +887,10 @@ method getMessages*( let splittedHashes = hashes[i ..< stop] - let subRows = - ?await s.getMessagesWithinLimits( - includeData, contentTopics, pubsubTopic, cursor, startTime, endTime, - splittedHashes, maxPageSize, ascendingOrder, requestId, - ) + let subRows = ?await s.getMessagesWithinLimits( + includeData, contentTopics, pubsubTopic, cursor, startTime, endTime, + splittedHashes, maxPageSize, ascendingOrder, requestId, + ) for row in subRows: row diff --git a/waku/waku_archive/driver/sqlite_driver/queries.nim b/waku/waku_archive/driver/sqlite_driver/queries.nim index e7e31dbe0..9ef6591c2 100644 --- a/waku/waku_archive/driver/sqlite_driver/queries.nim +++ b/waku/waku_archive/driver/sqlite_driver/queries.nim @@ -78,12 +78,11 @@ proc createTableQuery(table: string): SqlQueryStr = proc createTable*(db: SqliteDatabase): DatabaseResult[void] = let query = createTableQuery(DbTable) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Create indices @@ -93,12 +92,11 @@ proc createOldestMessageTimestampIndexQuery(table: string): SqlQueryStr = proc createOldestMessageTimestampIndex*(db: SqliteDatabase): DatabaseResult[void] = let query = createOldestMessageTimestampIndexQuery(DbTable) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Insert message @@ -175,12 +173,11 @@ proc deleteMessagesOlderThanTimestamp*( db: SqliteDatabase, ts: int64 ): DatabaseResult[void] = let query = deleteMessagesOlderThanTimestampQuery(DbTable, ts) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Delete oldest messages not within limit @@ -196,12 +193,11 @@ proc deleteOldestMessagesNotWithinLimit*( ): DatabaseResult[void] = # NOTE: The word `limit` here refers the store capacity/maximum number-of-messages allowed limit let query = deleteOldestMessagesNotWithinLimitQuery(DbTable, limit = limit) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Select all messages diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim b/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim index 0cb2bf64d..4590a8df1 100644 --- a/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim +++ b/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim @@ -92,12 +92,11 @@ proc createTableQuery(table: string): SqlQueryStr = proc createTable*(db: SqliteDatabase): DatabaseResult[void] = let query = createTableQuery(DbTable) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Create indices @@ -107,12 +106,11 @@ proc createOldestMessageTimestampIndexQuery(table: string): SqlQueryStr = proc createOldestMessageTimestampIndex*(db: SqliteDatabase): DatabaseResult[void] = let query = createOldestMessageTimestampIndexQuery(DbTable) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() proc createHistoryQueryIndexQuery(table: string): SqlQueryStr = @@ -121,12 +119,11 @@ proc createHistoryQueryIndexQuery(table: string): SqlQueryStr = proc createHistoryQueryIndex*(db: SqliteDatabase): DatabaseResult[void] = let query = createHistoryQueryIndexQuery(DbTable) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Insert message @@ -216,12 +213,11 @@ proc deleteMessagesOlderThanTimestamp*( db: SqliteDatabase, ts: int64 ): DatabaseResult[void] = let query = deleteMessagesOlderThanTimestampQuery(DbTable, ts) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Delete oldest messages not within limit @@ -237,12 +233,11 @@ proc deleteOldestMessagesNotWithinLimit*( ): DatabaseResult[void] = # NOTE: The word `limit` here refers the store capacity/maximum number-of-messages allowed limit let query = deleteOldestMessagesNotWithinLimitQuery(DbTable, limit = limit) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Select all messages diff --git a/waku/waku_core/peers.nim b/waku/waku_core/peers.nim index 51a8e1157..c4b8b593e 100644 --- a/waku/waku_core/peers.nim +++ b/waku/waku_core/peers.nim @@ -176,17 +176,15 @@ proc parsePeerInfoFromRegularAddr(peer: MultiAddress): Result[RemotePeerInfo, st case addrPart[].protoName()[] # All protocols listed here: https://github.com/multiformats/multiaddr/blob/b746a7d014e825221cc3aea6e57a92d78419990f/protocols.csv of "p2p": - p2pPart = - ?addrPart.mapErr( - proc(err: string): string = - "Error getting p2pPart [" & err & "]" - ) + p2pPart = ?addrPart.mapErr( + proc(err: string): string = + "Error getting p2pPart [" & err & "]" + ) of "ip4", "ip6", "dns", "dnsaddr", "dns4", "dns6", "tcp", "ws", "wss": - let val = - ?addrPart.mapErr( - proc(err: string): string = - "Error getting addrPart [" & err & "]" - ) + let val = ?addrPart.mapErr( + proc(err: string): string = + "Error getting addrPart [" & err & "]" + ) ?wireAddr.append(val).mapErr( proc(err: string): string = "Error appending addrPart [" & err & "]" @@ -199,11 +197,10 @@ proc parsePeerInfoFromRegularAddr(peer: MultiAddress): Result[RemotePeerInfo, st "] [peer:" & $peer & "]" return err(msg) - let peerId = - ?PeerID.init(p2pPartStr.split("/")[^1]).mapErr( - proc(e: cstring): string = - $e - ) + let peerId = ?PeerID.init(p2pPartStr.split("/")[^1]).mapErr( + proc(e: cstring): string = + $e + ) if not wireAddr.validWireAddr(): return err("invalid multiaddress: no supported transport found") @@ -233,11 +230,10 @@ proc parsePeerInfo*(maddrs: varargs[string]): Result[RemotePeerInfo, string] = ## format `(ip4|ip6)/tcp/p2p`, into dialable PeerInfo var multiAddresses = newSeq[MultiAddress]() for maddr in maddrs: - let multiAddr = - ?MultiAddress.init(maddr).mapErr( - proc(err: string): string = - "MultiAddress.init [" & err & "]" - ) + let multiAddr = ?MultiAddress.init(maddr).mapErr( + proc(err: string): string = + "MultiAddress.init [" & err & "]" + ) multiAddresses.add(multiAddr) parsePeerInfo(multiAddresses) diff --git a/waku/waku_core/topics/pubsub_topic.nim b/waku/waku_core/topics/pubsub_topic.nim index 27ea27180..1e921d06d 100644 --- a/waku/waku_core/topics/pubsub_topic.nim +++ b/waku/waku_core/topics/pubsub_topic.nim @@ -54,20 +54,18 @@ proc parseStaticSharding*( let clusterPart = parts[0] if clusterPart.len == 0: return err(ParsingError.missingPart("cluster_id")) - let clusterId = - ?Base10.decode(uint16, clusterPart).mapErr( - proc(err: auto): auto = - ParsingError.invalidFormat($err) - ) + let clusterId = ?Base10.decode(uint16, clusterPart).mapErr( + proc(err: auto): auto = + ParsingError.invalidFormat($err) + ) let shardPart = parts[1] if shardPart.len == 0: return err(ParsingError.missingPart("shard_number")) - let shardId = - ?Base10.decode(uint16, shardPart).mapErr( - proc(err: auto): auto = - ParsingError.invalidFormat($err) - ) + let shardId = ?Base10.decode(uint16, shardPart).mapErr( + proc(err: auto): auto = + ParsingError.invalidFormat($err) + ) ok(RelayShard(clusterId: clusterId, shardId: shardId)) diff --git a/waku/waku_enr/sharding.nim b/waku/waku_enr/sharding.nim index 392900cdb..2aeb96a9d 100644 --- a/waku/waku_enr/sharding.nim +++ b/waku/waku_enr/sharding.nim @@ -70,10 +70,9 @@ func topicsToRelayShards*(topics: seq[string]): Result[Option[RelayShards], stri if parsedTopicsRes.anyIt(it.get().clusterId != parsedTopicsRes[0].get().clusterId): return err("use shards with the same cluster Id.") - let relayShard = - ?RelayShards.init( - parsedTopicsRes[0].get().clusterId, parsedTopicsRes.mapIt(it.get().shardId) - ) + let relayShard = ?RelayShards.init( + parsedTopicsRes[0].get().clusterId, parsedTopicsRes.mapIt(it.get().shardId) + ) return ok(some(relayShard)) diff --git a/waku/waku_keystore/protocol_types.nim b/waku/waku_keystore/protocol_types.nim index 6cfc2f183..0f50c66ee 100644 --- a/waku/waku_keystore/protocol_types.nim +++ b/waku/waku_keystore/protocol_types.nim @@ -119,10 +119,9 @@ proc `==`*(x, y: KeystoreMembership): bool = proc hash*(m: KeystoreMembership): string = # hash together the chainId, address and treeIndex - return - $sha256.digest( - m.membershipContract.chainId & m.membershipContract.address & $m.treeIndex - ) + return $sha256.digest( + m.membershipContract.chainId & m.membershipContract.address & $m.treeIndex + ) type MembershipTable* = Table[string, KeystoreMembership] diff --git a/waku/waku_noise/noise_handshake_processing.nim b/waku/waku_noise/noise_handshake_processing.nim index 7688f0a80..8b84bf958 100644 --- a/waku/waku_noise/noise_handshake_processing.nim +++ b/waku/waku_noise/noise_handshake_processing.nim @@ -59,15 +59,14 @@ proc isValid(msg: seq[PreMessagePattern]): bool = var isValid: bool = true # Non-empty pre-messages can only have patterns "e", "s", "e,s" in each direction - let allowedPatterns: seq[PreMessagePattern] = - @[ - PreMessagePattern(direction: D_r, tokens: @[T_s]), - PreMessagePattern(direction: D_r, tokens: @[T_e]), - PreMessagePattern(direction: D_r, tokens: @[T_e, T_s]), - PreMessagePattern(direction: D_l, tokens: @[T_s]), - PreMessagePattern(direction: D_l, tokens: @[T_e]), - PreMessagePattern(direction: D_l, tokens: @[T_e, T_s]), - ] + let allowedPatterns: seq[PreMessagePattern] = @[ + PreMessagePattern(direction: D_r, tokens: @[T_s]), + PreMessagePattern(direction: D_r, tokens: @[T_e]), + PreMessagePattern(direction: D_r, tokens: @[T_e, T_s]), + PreMessagePattern(direction: D_l, tokens: @[T_s]), + PreMessagePattern(direction: D_l, tokens: @[T_e]), + PreMessagePattern(direction: D_l, tokens: @[T_e, T_s]), + ] # We check if pre message patterns are allowed for pattern in msg: diff --git a/waku/waku_noise/noise_types.nim b/waku/waku_noise/noise_types.nim index 3b88c43e8..543bd4329 100644 --- a/waku/waku_noise/noise_types.nim +++ b/waku/waku_noise/noise_types.nim @@ -223,57 +223,51 @@ const NoiseHandshakePatterns* = { "K1K1": HandshakePattern( name: "Noise_K1K1_25519_ChaChaPoly_SHA256", - preMessagePatterns: - @[ - PreMessagePattern(direction: D_r, tokens: @[T_s]), - PreMessagePattern(direction: D_l, tokens: @[T_s]), - ], - messagePatterns: - @[ - MessagePattern(direction: D_r, tokens: @[T_e]), - MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]), - MessagePattern(direction: D_r, tokens: @[T_se]), - ], + preMessagePatterns: @[ + PreMessagePattern(direction: D_r, tokens: @[T_s]), + PreMessagePattern(direction: D_l, tokens: @[T_s]), + ], + messagePatterns: @[ + MessagePattern(direction: D_r, tokens: @[T_e]), + MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]), + MessagePattern(direction: D_r, tokens: @[T_se]), + ], ), "XK1": HandshakePattern( name: "Noise_XK1_25519_ChaChaPoly_SHA256", preMessagePatterns: @[PreMessagePattern(direction: D_l, tokens: @[T_s])], - messagePatterns: - @[ - MessagePattern(direction: D_r, tokens: @[T_e]), - MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]), - MessagePattern(direction: D_r, tokens: @[T_s, T_se]), - ], + messagePatterns: @[ + MessagePattern(direction: D_r, tokens: @[T_e]), + MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]), + MessagePattern(direction: D_r, tokens: @[T_s, T_se]), + ], ), "XX": HandshakePattern( name: "Noise_XX_25519_ChaChaPoly_SHA256", preMessagePatterns: EmptyPreMessage, - messagePatterns: - @[ - MessagePattern(direction: D_r, tokens: @[T_e]), - MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]), - MessagePattern(direction: D_r, tokens: @[T_s, T_se]), - ], + messagePatterns: @[ + MessagePattern(direction: D_r, tokens: @[T_e]), + MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]), + MessagePattern(direction: D_r, tokens: @[T_s, T_se]), + ], ), "XXpsk0": HandshakePattern( name: "Noise_XXpsk0_25519_ChaChaPoly_SHA256", preMessagePatterns: EmptyPreMessage, - messagePatterns: - @[ - MessagePattern(direction: D_r, tokens: @[T_psk, T_e]), - MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]), - MessagePattern(direction: D_r, tokens: @[T_s, T_se]), - ], + messagePatterns: @[ + MessagePattern(direction: D_r, tokens: @[T_psk, T_e]), + MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]), + MessagePattern(direction: D_r, tokens: @[T_s, T_se]), + ], ), "WakuPairing": HandshakePattern( name: "Noise_WakuPairing_25519_ChaChaPoly_SHA256", preMessagePatterns: @[PreMessagePattern(direction: D_l, tokens: @[T_e])], - messagePatterns: - @[ - MessagePattern(direction: D_r, tokens: @[T_e, T_ee]), - MessagePattern(direction: D_l, tokens: @[T_s, T_es]), - MessagePattern(direction: D_r, tokens: @[T_s, T_se, T_ss]), - ], + messagePatterns: @[ + MessagePattern(direction: D_r, tokens: @[T_e, T_ee]), + MessagePattern(direction: D_l, tokens: @[T_s, T_es]), + MessagePattern(direction: D_r, tokens: @[T_s, T_se, T_ss]), + ], ), }.toTable() diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 2e4882891..38c533029 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 @@ -235,8 +235,8 @@ method register*( "Failed to register the member", proc(): Future[TxHash] {.async.} = return await wakuRlnContract - .register(idCommitment, userMessageLimit.stuint(32), idCommitmentsToErase) - .send(gasPrice = gasPrice), + .register(idCommitment, userMessageLimit.stuint(32), idCommitmentsToErase) + .send(gasPrice = gasPrice), ) ).valueOr: return err("Failed to register member: " & error) diff --git a/waku/waku_rln_relay/rln/wrappers.nim b/waku/waku_rln_relay/rln/wrappers.nim index 1b2b0270f..f6f001d70 100644 --- a/waku/waku_rln_relay/rln/wrappers.nim +++ b/waku/waku_rln_relay/rln/wrappers.nim @@ -72,13 +72,12 @@ type RlnConfig = ref object of RootObj proc `%`(c: RlnConfig): JsonNode = ## wrapper around the generic JObject constructor. ## We don't need to have a separate proc for the tree_config field - let tree_config = - %{ - "cache_capacity": %c.tree_config.cache_capacity, - "mode": %c.tree_config.mode, - "compression": %c.tree_config.compression, - "flush_every_ms": %c.tree_config.flush_every_ms, - } + let tree_config = %{ + "cache_capacity": %c.tree_config.cache_capacity, + "mode": %c.tree_config.mode, + "compression": %c.tree_config.compression, + "flush_every_ms": %c.tree_config.flush_every_ms, + } return %[("resources_folder", %c.resources_folder), ("tree_config", %tree_config)] proc createRLNInstanceLocal(): RLNResult = From 85a7bf3322bb9087a3983288117eabbb5e322da2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:39:00 +0100 Subject: [PATCH 093/155] chore: add .nph.toml to exclude vendor and nimbledeps from nph formatting (#3762) Co-authored-by: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> --- .nph.toml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .nph.toml diff --git a/.nph.toml b/.nph.toml new file mode 100644 index 000000000..f0398059c --- /dev/null +++ b/.nph.toml @@ -0,0 +1,4 @@ +extend-exclude = [ + "vendor", + "nimbledeps", +] From 11461aed44e777ffde5655405c8fbce9c0c2e9c3 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:37:04 +0100 Subject: [PATCH 094/155] allow union of several retention policies (#3766) * refactor retention policy to allow union of several retention policies * bug fix time retention policy * add removal of orphan partitions if any * use nim-http-utils 0.4.1 --- tests/api/test_node_conf.nim | 65 ++++++++++++- tests/wakunode2/test_cli_args.nim | 81 +++++++++++++++- tools/confutils/cli_args.nim | 4 +- vendor/nim-http-utils | 2 +- .../store_service_conf_builder.nim | 50 +++++++++- waku/factory/node_factory.nim | 4 +- waku/factory/waku_conf.nim | 2 +- waku/node/kernel_api/store.nim | 4 +- waku/waku_archive/archive.nim | 33 ++++--- .../postgres_driver/postgres_driver.nim | 95 ++++++++++++++++--- waku/waku_archive/retention_policy.nim | 3 + .../waku_archive/retention_policy/builder.nim | 13 ++- .../retention_policy_capacity.nim | 3 + .../retention_policy_size.nim | 3 + .../retention_policy_time.nim | 16 +--- 15 files changed, 323 insertions(+), 55 deletions(-) diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index 232ffc7d2..255dec8b2 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -1,7 +1,11 @@ {.used.} -import std/options, results, stint, testutils/unittests -import waku/api/api_conf, waku/factory/waku_conf, waku/factory/networks_config +import std/[options, strutils], results, stint, testutils/unittests, chronos +import + waku/api/api_conf, + waku/factory/waku_conf, + waku/factory/networks_config, + waku/factory/conf_builder/conf_builder suite "LibWaku Conf - toWakuConf": test "Minimal configuration": @@ -277,3 +281,60 @@ suite "LibWaku Conf - toWakuConf": check: wakuConf.staticNodes.len == 1 wakuConf.staticNodes[0] == entryNodes[1] + +suite "WakuConfBuilder - store retention policies": + test "Multiple retention policies": + ## Given + var b = WakuConfBuilder.init() + b.storeServiceConf.withEnabled(true) + b.storeServiceConf.withDbUrl("sqlite://test.db") + b.storeServiceConf.withRetentionPolicies(@["time:86400", "capacity:10000"]) + + ## When + let wakuConf = b.build().valueOr: + raiseAssert error + + ## Then + require wakuConf.storeServiceConf.isSome() + let storeConf = wakuConf.storeServiceConf.get() + check storeConf.retentionPolicies == @["time:86400", "capacity:10000"] + + test "Duplicated retention policies returns error": + ## Given + var b = WakuConfBuilder.init() + b.storeServiceConf.withEnabled(true) + b.storeServiceConf.withDbUrl("sqlite://test.db") + b.storeServiceConf.withRetentionPolicies(@["time:86400", "time:800", "capacity:10000"]) + + ## When + let wakuConfRes = b.build() + + ## Then + check wakuConfRes.isErr() + check wakuConfRes.error.contains("duplicated retention policy type") + + test "Incorrect retention policy type returns error": + ## Given + var b = WakuConfBuilder.init() + b.storeServiceConf.withEnabled(true) + b.storeServiceConf.withDbUrl("sqlite://test.db") + b.storeServiceConf.withRetentionPolicies(@["capaity:10000"]) + + ## When + let wakuConfRes = b.build() + + ## Then + check wakuConfRes.isErr() + check wakuConfRes.error.contains("unknown retention policy type") + + test "Store disabled - no retention policy applied": + ## Given + var b = WakuConfBuilder.init() + # storeServiceConf not enabled + + ## When + let wakuConf = b.build().valueOr: + raiseAssert error + + ## Then + check wakuConf.storeServiceConf.isNone() diff --git a/tests/wakunode2/test_cli_args.nim b/tests/wakunode2/test_cli_args.nim index dabc78083..5108b4a9d 100644 --- a/tests/wakunode2/test_cli_args.nim +++ b/tests/wakunode2/test_cli_args.nim @@ -1,7 +1,7 @@ {.used.} import - std/options, + std/[options, strutils], testutils/unittests, chronos, libp2p/crypto/[crypto, secp], @@ -261,6 +261,85 @@ suite "Waku external config - Shards": ## Then assert res.isErr(), "Invalid shard was accepted" +suite "Waku external config - store retention policy": + test "Default retention policy": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + # storeMessageRetentionPolicy keeps its default: "time:<2 days in seconds>" + + ## When + let res = conf.toWakuConf() + + ## Then + assert res.isOk(), $res.error + let wakuConf = res.get() + require wakuConf.storeServiceConf.isSome() + check wakuConf.storeServiceConf.get().retentionPolicies == + @["time:" & $2.days.seconds] + + test "Single custom retention policy": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "capacity:50000" + + ## When + let res = conf.toWakuConf() + + ## Then + assert res.isOk(), $res.error + let wakuConf = res.get() + require wakuConf.storeServiceConf.isSome() + check wakuConf.storeServiceConf.get().retentionPolicies == @["capacity:50000"] + + test "Retention policies with whitespace around semicolons and colons": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "time:3600 ; capacity:10000 ; size : 30GB" + + ## When + let res = conf.toWakuConf() + + ## Then + assert res.isOk(), $res.error + let wakuConf = res.get() + require wakuConf.storeServiceConf.isSome() + check wakuConf.storeServiceConf.get().retentionPolicies == + @["time:3600", "capacity:10000", "size:30GB"] + + test "Invalid retention policy type returns error": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "foo:1234" + + ## When + let res = conf.toWakuConf() + + ## Then + check res.isErr() + check res.error.contains("unknown retention policy type") + + test "Duplicated retention policy type returns error": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "time:3600;time:7200;capacity:10000" + + ## When + let res = conf.toWakuConf() + + ## Then + check res.isErr() + check res.error.contains("duplicated retention policy type") + suite "Waku external config - http url parsing": test "Basic HTTP URLs without authentication": check string(parseCmdArg(EthRpcUrl, "https://example.com/path")) == diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index e6b3fc97d..994bc6a2d 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -347,7 +347,7 @@ hence would have reachability issues.""", storeMessageRetentionPolicy* {. desc: - "Message store retention policy. Time retention policy: 'time:'. Capacity retention policy: 'capacity:'. Size retention policy: 'size:'. Set to 'none' to disable.", + "Message store retention policy. Multiple policies may be provided as a semicolon-separated string and are applied as a union. Time retention policy: 'time:'. Capacity retention policy: 'capacity:'. Size retention policy: 'size:'. Set to 'none' to disable. Example: 'time:3600;size:1GB;capacity:100'.", defaultValue: "time:" & $2.days.seconds, name: "store-message-retention-policy" .}: string @@ -991,7 +991,7 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.storeServiceConf.withEnabled(n.store) b.storeServiceConf.withSupportV2(n.legacyStore) - b.storeServiceConf.withRetentionPolicy(n.storeMessageRetentionPolicy) + b.storeServiceConf.withRetentionPolicies(n.storeMessageRetentionPolicy) b.storeServiceConf.withDbUrl(n.storeMessageDbUrl) b.storeServiceConf.withDbVacuum(n.storeMessageDbVacuum) b.storeServiceConf.withDbMigration(n.storeMessageDbMigration) diff --git a/vendor/nim-http-utils b/vendor/nim-http-utils index 08db60946..f142cb2e8 160000 --- a/vendor/nim-http-utils +++ b/vendor/nim-http-utils @@ -1 +1 @@ -Subproject commit 08db609467a0e2b5a6e8ce118bb83dba7a8a9375 +Subproject commit f142cb2e8bd812dd002a6493b6082827bb248592 diff --git a/waku/factory/conf_builder/store_service_conf_builder.nim b/waku/factory/conf_builder/store_service_conf_builder.nim index d5d48c34d..30c743e01 100644 --- a/waku/factory/conf_builder/store_service_conf_builder.nim +++ b/waku/factory/conf_builder/store_service_conf_builder.nim @@ -1,4 +1,5 @@ -import chronicles, std/options, results, chronos +import std/[options, strutils, sequtils] +import chronicles, results, chronos import ../waku_conf, ./store_sync_conf_builder logScope: @@ -15,7 +16,7 @@ type StoreServiceConfBuilder* = object dbVacuum*: Option[bool] supportV2*: Option[bool] maxNumDbConnections*: Option[int] - retentionPolicy*: Option[string] + retentionPolicies*: seq[string] resume*: Option[bool] storeSyncConf*: StoreSyncConfBuilder @@ -42,12 +43,43 @@ proc withMaxNumDbConnections*( ) = b.maxNumDbConnections = some(maxNumDbConnections) -proc withRetentionPolicy*(b: var StoreServiceConfBuilder, retentionPolicy: string) = - b.retentionPolicy = some(retentionPolicy) +proc withRetentionPolicies*(b: var StoreServiceConfBuilder, retentionPolicies: string) = + b.retentionPolicies = retentionPolicies + .multiReplace((" ", ""), ("\t", "")) + .split(";") + .mapIt(it.strip()) + .filterIt(it.len > 0) proc withResume*(b: var StoreServiceConfBuilder, resume: bool) = b.resume = some(resume) +const ValidRetentionPolicyTypes = ["time", "capacity", "size"] + +proc validateRetentionPolicies(policies: seq[string]): Result[void, string] = + var seen: seq[string] + + for p in policies: + let policy = p.multiReplace((" ", ""), ("\t", "")) + let parts = policy.split(":", 1) + if parts.len != 2 or parts[1] == "": + return err( + "invalid retention policy format: '" & policy & "', expected ':'" + ) + + let policyType = parts[0].toLowerAscii() + if policyType notin ValidRetentionPolicyTypes: + return err( + "unknown retention policy type: '" & policyType & + "', valid types are: time, capacity, size" + ) + + if policyType in seen: + return err("duplicated retention policy type: '" & policyType & "'") + + seen.add(policyType) + + return ok() + proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string] = if not b.enabled.get(false): return ok(none(StoreServiceConf)) @@ -58,6 +90,14 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string let storeSyncConf = b.storeSyncConf.build().valueOr: return err("Store Sync Conf failed to build") + let retentionPolicies = + if b.retentionPolicies.len == 0: + @["time:" & $2.days.seconds] + else: + validateRetentionPolicies(b.retentionPolicies).isOkOr: + return err("invalid retention policies: " & error) + b.retentionPolicies + return ok( some( StoreServiceConf( @@ -66,7 +106,7 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string dbVacuum: b.dbVacuum.get(false), supportV2: b.supportV2.get(false), maxNumDbConnections: b.maxNumDbConnections.get(50), - retentionPolicy: b.retentionPolicy.get("time:" & $2.days.seconds), + retentionPolicies: retentionPolicies, resume: b.resume.get(false), storeSyncConf: storeSyncConf, ) diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index 488d07c06..86ab75d77 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -206,10 +206,10 @@ proc setupProtocols( ).valueOr: return err("failed to setup archive driver: " & error) - let retPolicy = policy.RetentionPolicy.new(storeServiceConf.retentionPolicy).valueOr: + let retPolicies = policy.RetentionPolicy.new(storeServiceConf.retentionPolicies).valueOr: return err("failed to create retention policy: " & error) - node.mountArchive(archiveDriver, retPolicy).isOkOr: + node.mountArchive(archiveDriver, retPolicies).isOkOr: return err("failed to mount waku archive protocol: " & error) if storeServiceConf.supportV2: diff --git a/waku/factory/waku_conf.nim b/waku/factory/waku_conf.nim index 89ffb366c..3b99d1070 100644 --- a/waku/factory/waku_conf.nim +++ b/waku/factory/waku_conf.nim @@ -57,7 +57,7 @@ type StoreServiceConf* {.requiresInit.} = object dbVacuum*: bool supportV2*: bool maxNumDbConnections*: int - retentionPolicy*: string + retentionPolicies*: seq[string] resume*: bool storeSyncConf*: Option[StoreSyncConf] diff --git a/waku/node/kernel_api/store.nim b/waku/node/kernel_api/store.nim index 7edae7966..ca9917163 100644 --- a/waku/node/kernel_api/store.nim +++ b/waku/node/kernel_api/store.nim @@ -39,10 +39,10 @@ logScope: proc mountArchive*( node: WakuNode, driver: waku_archive.ArchiveDriver, - retentionPolicy = none(waku_archive.RetentionPolicy), + retentionPolicies = newSeq[waku_archive.RetentionPolicy](), ): Result[void, string] = node.wakuArchive = waku_archive.WakuArchive.new( - driver = driver, retentionPolicy = retentionPolicy + driver = driver, retentionPolicies = retentionPolicies ).valueOr: return err("error in mountArchive: " & error) diff --git a/waku/waku_archive/archive.nim b/waku/waku_archive/archive.nim index 707c757a3..4c4ac4bba 100644 --- a/waku/waku_archive/archive.nim +++ b/waku/waku_archive/archive.nim @@ -45,7 +45,7 @@ type WakuArchive* = ref object validator: MessageValidator - retentionPolicy: Option[RetentionPolicy] + retentionPolicies: seq[RetentionPolicy] retentionPolicyHandle: Future[void] metricsHandle: Future[void] @@ -72,13 +72,17 @@ proc new*( T: type WakuArchive, driver: ArchiveDriver, validator: MessageValidator = validate, - retentionPolicy = none(RetentionPolicy), + retentionPolicies = newSeq[RetentionPolicy](0), ): Result[T, string] = if driver.isNil(): return err("archive driver is Nil") - let archive = - WakuArchive(driver: driver, validator: validator, retentionPolicy: retentionPolicy) + if retentionPolicies.len == 0: + return err("at least one retention policy must be provided") + + let archive = WakuArchive( + driver: driver, validator: validator, retentionPolicies: retentionPolicies + ) return ok(archive) @@ -253,16 +257,15 @@ proc findMessages*( ) proc periodicRetentionPolicy(self: WakuArchive) {.async.} = - let policy = self.retentionPolicy.get() - while true: - info "executing message retention policy" - (await policy.execute(self.driver)).isOkOr: - waku_archive_errors.inc(labelValues = [retPolicyFailure]) - error "failed execution of retention policy", error = error - await sleepAsync(WakuArchiveDefaultRetentionPolicyIntervalWhenError) - ## in case of error, let's try again faster - continue + for policy in self.retentionPolicies: + info "executing message retention policy", policy = $policy + (await policy.execute(self.driver)).isOkOr: + waku_archive_errors.inc(labelValues = [retPolicyFailure]) + error "failed execution of retention policy", policy = $policy, error = error + await sleepAsync(WakuArchiveDefaultRetentionPolicyIntervalWhenError) + ## in case of error, let's try again faster + continue await sleepAsync(WakuArchiveDefaultRetentionPolicyInterval) @@ -279,7 +282,7 @@ proc periodicMetricReport(self: WakuArchive) {.async.} = await sleepAsync(WakuArchiveDefaultMetricsReportInterval) proc start*(self: WakuArchive) = - if self.retentionPolicy.isSome(): + if self.retentionPolicies.len > 0: self.retentionPolicyHandle = self.periodicRetentionPolicy() self.metricsHandle = self.periodicMetricReport() @@ -287,7 +290,7 @@ proc start*(self: WakuArchive) = proc stopWait*(self: WakuArchive) {.async.} = var futures: seq[Future[void]] - if self.retentionPolicy.isSome() and not self.retentionPolicyHandle.isNil(): + if not self.retentionPolicyHandle.isNil(): futures.add(self.retentionPolicyHandle.cancelAndWait()) if not self.metricsHandle.isNil: diff --git a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim index 842d7cbc2..c6e50d0dd 100644 --- a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim @@ -16,6 +16,9 @@ import ./postgres_healthcheck, ./partitions_manager +logScope: + topics = "postgres driver" + type PostgresDriver* = ref object of ArchiveDriver ## Establish a separate pools for read/write operations writeConnPool: PgAsyncPool @@ -367,6 +370,7 @@ proc getPartitionsList( ): Future[ArchiveDriverResult[seq[string]]] {.async.} = ## Retrieves the seq of partition table names. ## e.g: @["messages_1708534333_1708534393", "messages_1708534273_1708534333"] + ## This returns the partitions that are attached to the main messages table. var partitions: seq[string] proc rowCallback(pqResult: ptr PGresult) = for iRow in 0 ..< pqResult.pqNtuples(): @@ -393,6 +397,49 @@ proc getPartitionsList( return ok(partitions) +## fwd declaration. The implementation is below. +proc dropPartition( + self: PostgresDriver, partitionName: string +): Future[ArchiveDriverResult[void]] {.async.} + +proc dropOrphanPartitions( + s: PostgresDriver +): Future[ArchiveDriverResult[void]] {.async.} = + ## Tries to remove partitions that weren't correctly removed during retention policy execution. + ## Orphan partition is a partition that is not attached to the main messages table. + ## Therefore, it is not used for queries and can be safely removed. + var partitions: seq[string] + proc rowCallback(pqResult: ptr PGresult) = + for iRow in 0 ..< pqResult.pqNtuples(): + let partitionName = $(pqgetvalue(pqResult, iRow, 0)) + partitions.add(partitionName) + + ( + await s.readConnPool.pgQuery( + """ + SELECT c.relname AS partition_name + FROM pg_class c + LEFT JOIN pg_inherits i ON i.inhrelid = c.oid + WHERE c.relname LIKE 'messages_%' + AND c.relname != 'messages_lookup' + AND c.relkind = 'r' -- only regular tables + AND i.inhrelid IS NULL -- detached partition + ORDER BY partition_name + """, + newSeq[string](0), + rowCallback, + ) + ).isOkOr: + return err("dropOrphanPartitions failed in query: " & $error) + + for partition in partitions: + info "orphan partition found", partitionName = partition + (await s.dropPartition(partition)).isOkOr: + error "failed to drop orphan partition", partitionName = partition, error = $error + continue + + return ok() + proc getTimeCursor( s: PostgresDriver, hashHex: string ): Future[ArchiveDriverResult[Option[Timestamp]]] {.async.} = @@ -1259,11 +1306,18 @@ proc loopPartitionFactory( self: PostgresDriver, onFatalError: OnFatalErrorHandler ) {.async.} = ## Loop proc that continuously checks whether we need to create a new partition. - ## Notice that the deletion of partitions is handled by the retention policy modules. + ## Notice that the deletion of partitions is mostly handled by the retention policy modules. + ## This loop only removes orphan partitions which were detached but not properly removed by the + ## retention policy module due to some error. However, the main task of this loop is to create + ## new partitions when needed. info "starting loopPartitionFactory" while true: + trace "loopPartitionFactory iteration started" + (await self.dropOrphanPartitions()).isOkOr: + onFatalError("error when dropping orphan partitions: " & $error) + trace "Check if a new partition is needed" ## Let's make the 'partition_manager' aware of the current partitions @@ -1321,14 +1375,24 @@ proc getTableSize*( return ok(tableSize) -proc removePartition( +proc dropPartition( + self: PostgresDriver, partitionName: string +): Future[ArchiveDriverResult[void]] {.async.} = + let dropPartitionQuery = "DROP TABLE " & partitionName + info "drop partition", query = dropPartitionQuery + (await self.performWriteQuery(dropPartitionQuery)).isOkOr: + return err(fmt"error in dropPartition: {dropPartitionQuery}: " & $error) + + return ok() + +proc detachAndDropPartition( self: PostgresDriver, partition: Partition ): Future[ArchiveDriverResult[void]] {.async.} = - ## Removes the desired partition and also removes the rows from messages_lookup table + ## Detaches and drops the desired partition and also removes the rows from messages_lookup table ## whose rows belong to the partition time range let partitionName = partition.getName() - info "beginning of removePartition", partitionName + info "beginning of detachAndDropPartition", partitionName let partSize = (await self.getTableSize(partitionName)).valueOr("") @@ -1351,11 +1415,8 @@ proc removePartition( else: return err(fmt"error in {detachPartitionQuery}: " & $error) - ## Drop the partition - let dropPartitionQuery = "DROP TABLE " & partitionName - info "removeOldestPartition drop partition", query = dropPartitionQuery - (await self.performWriteQuery(dropPartitionQuery)).isOkOr: - return err(fmt"error in {dropPartitionQuery}: " & $error) + ## Drop partition + ?(await self.dropPartition(partitionName)) info "removed partition", partition_name = partitionName, partition_size = partSize self.partitionMngr.removeOldestPartitionName() @@ -1380,8 +1441,18 @@ proc removePartitionsOlderThan( var oldestPartition = self.partitionMngr.getOldestPartition().valueOr: return err("could not get oldest partition in removePartitionOlderThan: " & $error) - while not oldestPartition.containsMoment(tsInSec): - (await self.removePartition(oldestPartition)).isOkOr: + debug "oldest partition info", + partitionName = oldestPartition.getName(), + partitionLastMoment = oldestPartition.getLastMoment(), + tsInSec + + while oldestPartition.getLastMoment() < tsInSec: + info "start removing partition whose first record is older than the specified timestamp", + partitionName = oldestPartition.getName(), + partitionFirstMoment = oldestPartition.getLastMoment(), + tsInSec + + (await self.detachAndDropPartition(oldestPartition)).isOkOr: return err("issue in removePartitionsOlderThan: " & $error) oldestPartition = self.partitionMngr.getOldestPartition().valueOr: @@ -1409,7 +1480,7 @@ proc removeOldestPartition( info "Skipping to remove the current partition" return ok() - return await self.removePartition(oldestPartition) + return await self.detachAndDropPartition(oldestPartition) proc containsAnyPartition*(self: PostgresDriver): bool = return not self.partitionMngr.isEmpty() diff --git a/waku/waku_archive/retention_policy.nim b/waku/waku_archive/retention_policy.nim index d4b75ee1f..c2663fb66 100644 --- a/waku/waku_archive/retention_policy.nim +++ b/waku/waku_archive/retention_policy.nim @@ -11,3 +11,6 @@ method execute*( p: RetentionPolicy, store: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.base, async.} = discard + +method `$`*(p: RetentionPolicy): string {.base, gcsafe.} = + "unknown retention policy" diff --git a/waku/waku_archive/retention_policy/builder.nim b/waku/waku_archive/retention_policy/builder.nim index 6cb131bbc..7e777f4a0 100644 --- a/waku/waku_archive/retention_policy/builder.nim +++ b/waku/waku_archive/retention_policy/builder.nim @@ -7,7 +7,7 @@ import ./retention_policy_capacity, ./retention_policy_size -proc new*( +proc new( T: type RetentionPolicy, retPolicy: string ): RetentionPolicyResult[Option[RetentionPolicy]] = let retPolicy = retPolicy.toLower @@ -83,3 +83,14 @@ proc new*( return ok(some(retPolicy)) else: return err("unknown retention policy") + +proc new*( + T: typedesc[RetentionPolicy], retPolicies: seq[string] +): RetentionPolicyResult[seq[RetentionPolicy]] = + var policies: seq[RetentionPolicy] + for retPolicy in retPolicies: + let policy = RetentionPolicy.new(retPolicy).valueOr: + return err(error) + if policy.isSome(): + policies.add(policy.get()) + return ok(policies) diff --git a/waku/waku_archive/retention_policy/retention_policy_capacity.nim b/waku/waku_archive/retention_policy/retention_policy_capacity.nim index ed4dd2339..ff4da6861 100644 --- a/waku/waku_archive/retention_policy/retention_policy_capacity.nim +++ b/waku/waku_archive/retention_policy/retention_policy_capacity.nim @@ -50,6 +50,9 @@ proc new*(T: type CapacityRetentionPolicy, capacity = DefaultCapacity): T = capacity: capacity, totalCapacity: totalCapacity, deleteWindow: deleteWindow ) +method `$`*(p: CapacityRetentionPolicy): string = + "capacity:" & $p.capacity + method execute*( p: CapacityRetentionPolicy, driver: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.async.} = diff --git a/waku/waku_archive/retention_policy/retention_policy_size.nim b/waku/waku_archive/retention_policy/retention_policy_size.nim index e60aba303..416d95ec0 100644 --- a/waku/waku_archive/retention_policy/retention_policy_size.nim +++ b/waku/waku_archive/retention_policy/retention_policy_size.nim @@ -15,6 +15,9 @@ type SizeRetentionPolicy* = ref object of RetentionPolicy proc new*(T: type SizeRetentionPolicy, size = DefaultRetentionSize): T = SizeRetentionPolicy(sizeLimit: size) +method `$`*(p: SizeRetentionPolicy): string = + "size:" & $p.sizeLimit & "b" + method execute*( p: SizeRetentionPolicy, driver: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.async.} = diff --git a/waku/waku_archive/retention_policy/retention_policy_time.nim b/waku/waku_archive/retention_policy/retention_policy_time.nim index 6d4c0815a..12f056c7b 100644 --- a/waku/waku_archive/retention_policy/retention_policy_time.nim +++ b/waku/waku_archive/retention_policy/retention_policy_time.nim @@ -6,29 +6,23 @@ import ../../waku_core, ../driver, ../retention_policy logScope: topics = "waku archive retention_policy" -const DefaultRetentionTime*: int64 = 30.days.seconds - type TimeRetentionPolicy* = ref object of RetentionPolicy retentionTime: chronos.Duration -proc new*(T: type TimeRetentionPolicy, retentionTime = DefaultRetentionTime): T = +proc new*(T: type TimeRetentionPolicy, retentionTime: int64): T = TimeRetentionPolicy(retentionTime: retentionTime.seconds) +method `$`*(p: TimeRetentionPolicy): string = + "time:" & $p.retentionTime.seconds + method execute*( p: TimeRetentionPolicy, driver: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.async.} = - ## Delete messages that exceed the retention time by 10% and more (batch delete for efficiency) + ## Delete messages that exceed the retention time info "beginning of executing message retention policy - time" - let omt = (await driver.getOldestMessageTimestamp()).valueOr: - return err("failed to get oldest message timestamp: " & error) - let now = getNanosecondTime(getTime().toUnixFloat()) let retentionTimestamp = now - p.retentionTime.nanoseconds - let thresholdTimestamp = retentionTimestamp - p.retentionTime.nanoseconds div 10 - - if thresholdTimestamp <= omt: - return ok() (await driver.deleteMessagesOlderThanTimestamp(ts = retentionTimestamp)).isOkOr: return err("failed to delete oldest messages: " & error) From 84e0af8dc0ff9bb505889c24c93cba131eb4fb41 Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Thu, 19 Mar 2026 23:09:22 +0100 Subject: [PATCH 095/155] update changelog for v0.37.2 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index edc4a705c..a5fefbc3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## v0.37.2 (2026-03-19) + +### Features + +- Allow union of several retention policies ([#3766](https://github.com/logos-messaging/logos-delivery/pull/3766)) + +### Bug Fixes + +- Bump nim-http-utils to v0.4.1 to allow accepting <:><(> as a valid header and tests to validate html rfc7230 ([#43](https://github.com/status-im/nim-http-utils/pull/43)) + ## v0.37.1 (2026-03-12) ### Bug Fixes From 3e7aa18a42067a7bb3aa3f9724f02295712c132f Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:18:46 +0100 Subject: [PATCH 096/155] force FINALIZE partition detach after detecting shorter error (#3728) --- .../waku_archive/driver/postgres_driver/postgres_driver.nim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim index c6e50d0dd..64ede6cf5 100644 --- a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim @@ -1403,8 +1403,10 @@ proc detachAndDropPartition( (await self.performWriteQuery(detachPartitionQuery)).isOkOr: info "detected error when trying to detach partition", error - if ($error).contains("FINALIZE") or - ($error).contains("already pending detach in part"): + if ($error).contains("FINALIZE") or ($error).contains("already pending"): + ## We assume "already pending detach in partitioned table ..." as possible error + debug "enforce detach with FINALIZE because of detected error", error + ## We assume the database is suggesting to use FINALIZE when detaching a partition let detachPartitionFinalizeQuery = "ALTER TABLE messages DETACH PARTITION " & partitionName & " FINALIZE;" From 4e17776f71caf12917549988cfe5fe2bdf448250 Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Fri, 20 Mar 2026 00:18:26 +0100 Subject: [PATCH 097/155] update change log for v0.37.2 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5fefbc3d..45f9e2a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Bug Fixes - Bump nim-http-utils to v0.4.1 to allow accepting <:><(> as a valid header and tests to validate html rfc7230 ([#43](https://github.com/status-im/nim-http-utils/pull/43)) +- Force FINALIZE partition detach after detecting shorter error ([#3728](https://github.com/logos-messaging/logos-delivery/pull/3766)) ## v0.37.1 (2026-03-12) From d9aa46e22f958def8df1a538331552bdd5911bd7 Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Fri, 20 Mar 2026 16:54:42 +0100 Subject: [PATCH 098/155] fix compilation issue in test_node_conf.nim --- tests/api/test_node_conf.nim | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index 342fe809d..b19739393 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -1,17 +1,14 @@ {.used.} -import std/[options, strutils], results, stint, testutils/unittests, chronos +import std/[options, json, strutils], results, stint, testutils/unittests +import json_serialization, confutils, confutils/std/net import + tools/confutils/cli_args, waku/api/api_conf, waku/factory/waku_conf, waku/factory/networks_config, - waku/factory/conf_builder/conf_builder -import std/[options, json, strutils], results, stint, testutils/unittests -import json_serialization -import confutils, confutils/std/net -import tools/confutils/cli_args -import waku/factory/waku_conf, waku/factory/networks_config -import waku/common/logging + waku/factory/conf_builder/conf_builder, + waku/common/logging # Helper: parse JSON into WakuNodeConf using fieldPairs (same as liblogosdelivery) proc parseWakuNodeConfFromJson(jsonStr: string): Result[WakuNodeConf, string] = @@ -81,7 +78,7 @@ suite "WakuNodeConf - mode-driven toWakuConf": ## Given var conf = defaultWakuNodeConf().valueOr: raiseAssert error - conf.mode = WakuMode.noMode + conf.mode = cli_args.WakuMode.noMode conf.relay = true conf.lightpush = false conf.clusterId = 5 @@ -124,7 +121,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs": require confRes.isOk() let conf = confRes.get() check: - conf.mode == WakuMode.noMode + conf.mode == cli_args.WakuMode.noMode conf.clusterId == 0 conf.logLevel == logging.LogLevel.INFO @@ -381,7 +378,9 @@ suite "WakuConfBuilder - store retention policies": var b = WakuConfBuilder.init() b.storeServiceConf.withEnabled(true) b.storeServiceConf.withDbUrl("sqlite://test.db") - b.storeServiceConf.withRetentionPolicies(@["time:86400", "capacity:10000"]) + b.storeServiceConf.withRetentionPolicies( + "time:86400 ; capacity:10000; size : 50GB" + ) ## When let wakuConf = b.build().valueOr: @@ -390,14 +389,14 @@ suite "WakuConfBuilder - store retention policies": ## Then require wakuConf.storeServiceConf.isSome() let storeConf = wakuConf.storeServiceConf.get() - check storeConf.retentionPolicies == @["time:86400", "capacity:10000"] + check storeConf.retentionPolicies == @["time:86400", "capacity:10000", "size:50GB"] test "Duplicated retention policies returns error": ## Given var b = WakuConfBuilder.init() b.storeServiceConf.withEnabled(true) b.storeServiceConf.withDbUrl("sqlite://test.db") - b.storeServiceConf.withRetentionPolicies(@["time:86400", "time:800", "capacity:10000"]) + b.storeServiceConf.withRetentionPolicies("time:86400;time:800;capacity:10000") ## When let wakuConfRes = b.build() @@ -409,7 +408,7 @@ suite "WakuConfBuilder - store retention policies": var b = WakuConfBuilder.init() b.storeServiceConf.withEnabled(true) b.storeServiceConf.withDbUrl("sqlite://test.db") - b.storeServiceConf.withRetentionPolicies(@["capaity:10000"]) + b.storeServiceConf.withRetentionPolicies("capaity:10000") ## When let wakuConfRes = b.build() From de3143e3515a093eaaf04614e1d2eff0db87032d Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Fri, 20 Mar 2026 21:05:42 +0100 Subject: [PATCH 099/155] set default retention policy in archive.nim --- waku/waku_archive/archive.nim | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/waku/waku_archive/archive.nim b/waku/waku_archive/archive.nim index 95c1a905d..976d7d035 100644 --- a/waku/waku_archive/archive.nim +++ b/waku/waku_archive/archive.nim @@ -14,7 +14,8 @@ import ../waku_core, ../waku_core/message/digest, ./common, - ./archive_metrics + ./archive_metrics, + waku/waku_archive/retention_policy/retention_policy_time logScope: topics = "waku archive" @@ -82,14 +83,11 @@ proc new*( T: type WakuArchive, driver: ArchiveDriver, validator: MessageValidator = validate, - retentionPolicies = newSeq[RetentionPolicy](0), + retentionPolicies = @[RetentionPolicy(TimeRetentionPolicy.new(2.days.seconds))], ): Result[T, string] = if driver.isNil(): return err("archive driver is Nil") - if retentionPolicies.len == 0: - return err("at least one retention policy must be provided") - let archive = WakuArchive( driver: driver, validator: validator, retentionPolicies: retentionPolicies ) From d2fdd6ff36b198a51fd1f084f6c394c905569ec9 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:37:04 +0100 Subject: [PATCH 100/155] allow union of several retention policies (#3766) * refactor retention policy to allow union of several retention policies * bug fix time retention policy * add removal of orphan partitions if any * use nim-http-utils 0.4.1 --- tests/wakunode2/test_cli_args.nim | 81 +++++++++++++++- tools/confutils/cli_args.nim | 4 +- vendor/nim-http-utils | 2 +- .../store_service_conf_builder.nim | 50 +++++++++- waku/factory/node_factory.nim | 4 +- waku/factory/waku_conf.nim | 2 +- waku/node/kernel_api/store.nim | 4 +- waku/waku_archive/archive.nim | 33 ++++--- .../postgres_driver/postgres_driver.nim | 95 ++++++++++++++++--- waku/waku_archive/retention_policy.nim | 3 + .../waku_archive/retention_policy/builder.nim | 13 ++- .../retention_policy_capacity.nim | 3 + .../retention_policy_size.nim | 3 + .../retention_policy_time.nim | 16 +--- 14 files changed, 260 insertions(+), 53 deletions(-) diff --git a/tests/wakunode2/test_cli_args.nim b/tests/wakunode2/test_cli_args.nim index dabc78083..5108b4a9d 100644 --- a/tests/wakunode2/test_cli_args.nim +++ b/tests/wakunode2/test_cli_args.nim @@ -1,7 +1,7 @@ {.used.} import - std/options, + std/[options, strutils], testutils/unittests, chronos, libp2p/crypto/[crypto, secp], @@ -261,6 +261,85 @@ suite "Waku external config - Shards": ## Then assert res.isErr(), "Invalid shard was accepted" +suite "Waku external config - store retention policy": + test "Default retention policy": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + # storeMessageRetentionPolicy keeps its default: "time:<2 days in seconds>" + + ## When + let res = conf.toWakuConf() + + ## Then + assert res.isOk(), $res.error + let wakuConf = res.get() + require wakuConf.storeServiceConf.isSome() + check wakuConf.storeServiceConf.get().retentionPolicies == + @["time:" & $2.days.seconds] + + test "Single custom retention policy": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "capacity:50000" + + ## When + let res = conf.toWakuConf() + + ## Then + assert res.isOk(), $res.error + let wakuConf = res.get() + require wakuConf.storeServiceConf.isSome() + check wakuConf.storeServiceConf.get().retentionPolicies == @["capacity:50000"] + + test "Retention policies with whitespace around semicolons and colons": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "time:3600 ; capacity:10000 ; size : 30GB" + + ## When + let res = conf.toWakuConf() + + ## Then + assert res.isOk(), $res.error + let wakuConf = res.get() + require wakuConf.storeServiceConf.isSome() + check wakuConf.storeServiceConf.get().retentionPolicies == + @["time:3600", "capacity:10000", "size:30GB"] + + test "Invalid retention policy type returns error": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "foo:1234" + + ## When + let res = conf.toWakuConf() + + ## Then + check res.isErr() + check res.error.contains("unknown retention policy type") + + test "Duplicated retention policy type returns error": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "time:3600;time:7200;capacity:10000" + + ## When + let res = conf.toWakuConf() + + ## Then + check res.isErr() + check res.error.contains("duplicated retention policy type") + suite "Waku external config - http url parsing": test "Basic HTTP URLs without authentication": check string(parseCmdArg(EthRpcUrl, "https://example.com/path")) == diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 4a6e8c618..74e3c66bd 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -363,7 +363,7 @@ hence would have reachability issues.""", storeMessageRetentionPolicy* {. desc: - "Message store retention policy. Time retention policy: 'time:'. Capacity retention policy: 'capacity:'. Size retention policy: 'size:'. Set to 'none' to disable.", + "Message store retention policy. Multiple policies may be provided as a semicolon-separated string and are applied as a union. Time retention policy: 'time:'. Capacity retention policy: 'capacity:'. Size retention policy: 'size:'. Set to 'none' to disable. Example: 'time:3600;size:1GB;capacity:100'.", defaultValue: "time:" & $2.days.seconds, name: "store-message-retention-policy" .}: string @@ -1047,7 +1047,7 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.storeServiceConf.withEnabled(n.store) b.storeServiceConf.withSupportV2(n.legacyStore) - b.storeServiceConf.withRetentionPolicy(n.storeMessageRetentionPolicy) + b.storeServiceConf.withRetentionPolicies(n.storeMessageRetentionPolicy) b.storeServiceConf.withDbUrl(n.storeMessageDbUrl) b.storeServiceConf.withDbVacuum(n.storeMessageDbVacuum) b.storeServiceConf.withDbMigration(n.storeMessageDbMigration) diff --git a/vendor/nim-http-utils b/vendor/nim-http-utils index c53852d9e..f142cb2e8 160000 --- a/vendor/nim-http-utils +++ b/vendor/nim-http-utils @@ -1 +1 @@ -Subproject commit c53852d9e24205b6363bba517fa8ee7bde823691 +Subproject commit f142cb2e8bd812dd002a6493b6082827bb248592 diff --git a/waku/factory/conf_builder/store_service_conf_builder.nim b/waku/factory/conf_builder/store_service_conf_builder.nim index d5d48c34d..30c743e01 100644 --- a/waku/factory/conf_builder/store_service_conf_builder.nim +++ b/waku/factory/conf_builder/store_service_conf_builder.nim @@ -1,4 +1,5 @@ -import chronicles, std/options, results, chronos +import std/[options, strutils, sequtils] +import chronicles, results, chronos import ../waku_conf, ./store_sync_conf_builder logScope: @@ -15,7 +16,7 @@ type StoreServiceConfBuilder* = object dbVacuum*: Option[bool] supportV2*: Option[bool] maxNumDbConnections*: Option[int] - retentionPolicy*: Option[string] + retentionPolicies*: seq[string] resume*: Option[bool] storeSyncConf*: StoreSyncConfBuilder @@ -42,12 +43,43 @@ proc withMaxNumDbConnections*( ) = b.maxNumDbConnections = some(maxNumDbConnections) -proc withRetentionPolicy*(b: var StoreServiceConfBuilder, retentionPolicy: string) = - b.retentionPolicy = some(retentionPolicy) +proc withRetentionPolicies*(b: var StoreServiceConfBuilder, retentionPolicies: string) = + b.retentionPolicies = retentionPolicies + .multiReplace((" ", ""), ("\t", "")) + .split(";") + .mapIt(it.strip()) + .filterIt(it.len > 0) proc withResume*(b: var StoreServiceConfBuilder, resume: bool) = b.resume = some(resume) +const ValidRetentionPolicyTypes = ["time", "capacity", "size"] + +proc validateRetentionPolicies(policies: seq[string]): Result[void, string] = + var seen: seq[string] + + for p in policies: + let policy = p.multiReplace((" ", ""), ("\t", "")) + let parts = policy.split(":", 1) + if parts.len != 2 or parts[1] == "": + return err( + "invalid retention policy format: '" & policy & "', expected ':'" + ) + + let policyType = parts[0].toLowerAscii() + if policyType notin ValidRetentionPolicyTypes: + return err( + "unknown retention policy type: '" & policyType & + "', valid types are: time, capacity, size" + ) + + if policyType in seen: + return err("duplicated retention policy type: '" & policyType & "'") + + seen.add(policyType) + + return ok() + proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string] = if not b.enabled.get(false): return ok(none(StoreServiceConf)) @@ -58,6 +90,14 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string let storeSyncConf = b.storeSyncConf.build().valueOr: return err("Store Sync Conf failed to build") + let retentionPolicies = + if b.retentionPolicies.len == 0: + @["time:" & $2.days.seconds] + else: + validateRetentionPolicies(b.retentionPolicies).isOkOr: + return err("invalid retention policies: " & error) + b.retentionPolicies + return ok( some( StoreServiceConf( @@ -66,7 +106,7 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string dbVacuum: b.dbVacuum.get(false), supportV2: b.supportV2.get(false), maxNumDbConnections: b.maxNumDbConnections.get(50), - retentionPolicy: b.retentionPolicy.get("time:" & $2.days.seconds), + retentionPolicies: retentionPolicies, resume: b.resume.get(false), storeSyncConf: storeSyncConf, ) diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index 2f82440f6..f6b39b93f 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -240,10 +240,10 @@ proc setupProtocols( ).valueOr: return err("failed to setup archive driver: " & error) - let retPolicy = policy.RetentionPolicy.new(storeServiceConf.retentionPolicy).valueOr: + let retPolicies = policy.RetentionPolicy.new(storeServiceConf.retentionPolicies).valueOr: return err("failed to create retention policy: " & error) - node.mountArchive(archiveDriver, retPolicy).isOkOr: + node.mountArchive(archiveDriver, retPolicies).isOkOr: return err("failed to mount waku archive protocol: " & error) if storeServiceConf.supportV2: diff --git a/waku/factory/waku_conf.nim b/waku/factory/waku_conf.nim index 01574d067..6ed34e131 100644 --- a/waku/factory/waku_conf.nim +++ b/waku/factory/waku_conf.nim @@ -62,7 +62,7 @@ type StoreServiceConf* {.requiresInit.} = object dbVacuum*: bool supportV2*: bool maxNumDbConnections*: int - retentionPolicy*: string + retentionPolicies*: seq[string] resume*: bool storeSyncConf*: Option[StoreSyncConf] diff --git a/waku/node/kernel_api/store.nim b/waku/node/kernel_api/store.nim index 7edae7966..ca9917163 100644 --- a/waku/node/kernel_api/store.nim +++ b/waku/node/kernel_api/store.nim @@ -39,10 +39,10 @@ logScope: proc mountArchive*( node: WakuNode, driver: waku_archive.ArchiveDriver, - retentionPolicy = none(waku_archive.RetentionPolicy), + retentionPolicies = newSeq[waku_archive.RetentionPolicy](), ): Result[void, string] = node.wakuArchive = waku_archive.WakuArchive.new( - driver = driver, retentionPolicy = retentionPolicy + driver = driver, retentionPolicies = retentionPolicies ).valueOr: return err("error in mountArchive: " & error) diff --git a/waku/waku_archive/archive.nim b/waku/waku_archive/archive.nim index 8eb1fc051..95c1a905d 100644 --- a/waku/waku_archive/archive.nim +++ b/waku/waku_archive/archive.nim @@ -45,7 +45,7 @@ type WakuArchive* = ref object validator: MessageValidator - retentionPolicy: Option[RetentionPolicy] + retentionPolicies: seq[RetentionPolicy] retentionPolicyHandle: Future[void] metricsHandle: Future[void] @@ -82,13 +82,17 @@ proc new*( T: type WakuArchive, driver: ArchiveDriver, validator: MessageValidator = validate, - retentionPolicy = none(RetentionPolicy), + retentionPolicies = newSeq[RetentionPolicy](0), ): Result[T, string] = if driver.isNil(): return err("archive driver is Nil") - let archive = - WakuArchive(driver: driver, validator: validator, retentionPolicy: retentionPolicy) + if retentionPolicies.len == 0: + return err("at least one retention policy must be provided") + + let archive = WakuArchive( + driver: driver, validator: validator, retentionPolicies: retentionPolicies + ) return ok(archive) @@ -263,16 +267,15 @@ proc findMessages*( ) proc periodicRetentionPolicy(self: WakuArchive) {.async.} = - let policy = self.retentionPolicy.get() - while true: - info "executing message retention policy" - (await policy.execute(self.driver)).isOkOr: - waku_archive_errors.inc(labelValues = [retPolicyFailure]) - error "failed execution of retention policy", error = error - await sleepAsync(WakuArchiveDefaultRetentionPolicyIntervalWhenError) - ## in case of error, let's try again faster - continue + for policy in self.retentionPolicies: + info "executing message retention policy", policy = $policy + (await policy.execute(self.driver)).isOkOr: + waku_archive_errors.inc(labelValues = [retPolicyFailure]) + error "failed execution of retention policy", policy = $policy, error = error + await sleepAsync(WakuArchiveDefaultRetentionPolicyIntervalWhenError) + ## in case of error, let's try again faster + continue await sleepAsync(WakuArchiveDefaultRetentionPolicyInterval) @@ -289,7 +292,7 @@ proc periodicMetricReport(self: WakuArchive) {.async.} = await sleepAsync(WakuArchiveDefaultMetricsReportInterval) proc start*(self: WakuArchive) = - if self.retentionPolicy.isSome(): + if self.retentionPolicies.len > 0: self.retentionPolicyHandle = self.periodicRetentionPolicy() self.metricsHandle = self.periodicMetricReport() @@ -297,7 +300,7 @@ proc start*(self: WakuArchive) = proc stopWait*(self: WakuArchive) {.async.} = var futures: seq[Future[void]] - if self.retentionPolicy.isSome() and not self.retentionPolicyHandle.isNil(): + if not self.retentionPolicyHandle.isNil(): futures.add(self.retentionPolicyHandle.cancelAndWait()) if not self.metricsHandle.isNil: diff --git a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim index 2f495ba5d..f632513bc 100644 --- a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim @@ -20,6 +20,9 @@ import declarePublicGauge postgres_payload_size_bytes, "Payload size in bytes of correctly stored messages" +logScope: + topics = "postgres driver" + type PostgresDriver* = ref object of ArchiveDriver ## Establish a separate pools for read/write operations writeConnPool: PgAsyncPool @@ -375,6 +378,7 @@ proc getPartitionsList( ): Future[ArchiveDriverResult[seq[string]]] {.async.} = ## Retrieves the seq of partition table names. ## e.g: @["messages_1708534333_1708534393", "messages_1708534273_1708534333"] + ## This returns the partitions that are attached to the main messages table. var partitions: seq[string] proc rowCallback(pqResult: ptr PGresult) = for iRow in 0 ..< pqResult.pqNtuples(): @@ -401,6 +405,49 @@ proc getPartitionsList( return ok(partitions) +## fwd declaration. The implementation is below. +proc dropPartition( + self: PostgresDriver, partitionName: string +): Future[ArchiveDriverResult[void]] {.async.} + +proc dropOrphanPartitions( + s: PostgresDriver +): Future[ArchiveDriverResult[void]] {.async.} = + ## Tries to remove partitions that weren't correctly removed during retention policy execution. + ## Orphan partition is a partition that is not attached to the main messages table. + ## Therefore, it is not used for queries and can be safely removed. + var partitions: seq[string] + proc rowCallback(pqResult: ptr PGresult) = + for iRow in 0 ..< pqResult.pqNtuples(): + let partitionName = $(pqgetvalue(pqResult, iRow, 0)) + partitions.add(partitionName) + + ( + await s.readConnPool.pgQuery( + """ + SELECT c.relname AS partition_name + FROM pg_class c + LEFT JOIN pg_inherits i ON i.inhrelid = c.oid + WHERE c.relname LIKE 'messages_%' + AND c.relname != 'messages_lookup' + AND c.relkind = 'r' -- only regular tables + AND i.inhrelid IS NULL -- detached partition + ORDER BY partition_name + """, + newSeq[string](0), + rowCallback, + ) + ).isOkOr: + return err("dropOrphanPartitions failed in query: " & $error) + + for partition in partitions: + info "orphan partition found", partitionName = partition + (await s.dropPartition(partition)).isOkOr: + error "failed to drop orphan partition", partitionName = partition, error = $error + continue + + return ok() + proc getTimeCursor( s: PostgresDriver, hashHex: string ): Future[ArchiveDriverResult[Option[Timestamp]]] {.async.} = @@ -1267,11 +1314,18 @@ proc loopPartitionFactory( self: PostgresDriver, onFatalError: OnFatalErrorHandler ) {.async.} = ## Loop proc that continuously checks whether we need to create a new partition. - ## Notice that the deletion of partitions is handled by the retention policy modules. + ## Notice that the deletion of partitions is mostly handled by the retention policy modules. + ## This loop only removes orphan partitions which were detached but not properly removed by the + ## retention policy module due to some error. However, the main task of this loop is to create + ## new partitions when needed. info "starting loopPartitionFactory" while true: + trace "loopPartitionFactory iteration started" + (await self.dropOrphanPartitions()).isOkOr: + onFatalError("error when dropping orphan partitions: " & $error) + trace "Check if a new partition is needed" ## Let's make the 'partition_manager' aware of the current partitions @@ -1329,14 +1383,24 @@ proc getTableSize*( return ok(tableSize) -proc removePartition( +proc dropPartition( + self: PostgresDriver, partitionName: string +): Future[ArchiveDriverResult[void]] {.async.} = + let dropPartitionQuery = "DROP TABLE " & partitionName + info "drop partition", query = dropPartitionQuery + (await self.performWriteQuery(dropPartitionQuery)).isOkOr: + return err(fmt"error in dropPartition: {dropPartitionQuery}: " & $error) + + return ok() + +proc detachAndDropPartition( self: PostgresDriver, partition: Partition ): Future[ArchiveDriverResult[void]] {.async.} = - ## Removes the desired partition and also removes the rows from messages_lookup table + ## Detaches and drops the desired partition and also removes the rows from messages_lookup table ## whose rows belong to the partition time range let partitionName = partition.getName() - info "beginning of removePartition", partitionName + info "beginning of detachAndDropPartition", partitionName let partSize = (await self.getTableSize(partitionName)).valueOr("") @@ -1361,11 +1425,8 @@ proc removePartition( else: return err(fmt"error in {detachPartitionQuery}: " & $error) - ## Drop the partition - let dropPartitionQuery = "DROP TABLE " & partitionName - info "removeOldestPartition drop partition", query = dropPartitionQuery - (await self.performWriteQuery(dropPartitionQuery)).isOkOr: - return err(fmt"error in {dropPartitionQuery}: " & $error) + ## Drop partition + ?(await self.dropPartition(partitionName)) info "removed partition", partition_name = partitionName, partition_size = partSize self.partitionMngr.removeOldestPartitionName() @@ -1390,8 +1451,18 @@ proc removePartitionsOlderThan( var oldestPartition = self.partitionMngr.getOldestPartition().valueOr: return err("could not get oldest partition in removePartitionOlderThan: " & $error) - while not oldestPartition.containsMoment(tsInSec): - (await self.removePartition(oldestPartition)).isOkOr: + debug "oldest partition info", + partitionName = oldestPartition.getName(), + partitionLastMoment = oldestPartition.getLastMoment(), + tsInSec + + while oldestPartition.getLastMoment() < tsInSec: + info "start removing partition whose first record is older than the specified timestamp", + partitionName = oldestPartition.getName(), + partitionFirstMoment = oldestPartition.getLastMoment(), + tsInSec + + (await self.detachAndDropPartition(oldestPartition)).isOkOr: return err("issue in removePartitionsOlderThan: " & $error) oldestPartition = self.partitionMngr.getOldestPartition().valueOr: @@ -1419,7 +1490,7 @@ proc removeOldestPartition( info "Skipping to remove the current partition" return ok() - return await self.removePartition(oldestPartition) + return await self.detachAndDropPartition(oldestPartition) proc containsAnyPartition*(self: PostgresDriver): bool = return not self.partitionMngr.isEmpty() diff --git a/waku/waku_archive/retention_policy.nim b/waku/waku_archive/retention_policy.nim index d4b75ee1f..c2663fb66 100644 --- a/waku/waku_archive/retention_policy.nim +++ b/waku/waku_archive/retention_policy.nim @@ -11,3 +11,6 @@ method execute*( p: RetentionPolicy, store: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.base, async.} = discard + +method `$`*(p: RetentionPolicy): string {.base, gcsafe.} = + "unknown retention policy" diff --git a/waku/waku_archive/retention_policy/builder.nim b/waku/waku_archive/retention_policy/builder.nim index 6cb131bbc..7e777f4a0 100644 --- a/waku/waku_archive/retention_policy/builder.nim +++ b/waku/waku_archive/retention_policy/builder.nim @@ -7,7 +7,7 @@ import ./retention_policy_capacity, ./retention_policy_size -proc new*( +proc new( T: type RetentionPolicy, retPolicy: string ): RetentionPolicyResult[Option[RetentionPolicy]] = let retPolicy = retPolicy.toLower @@ -83,3 +83,14 @@ proc new*( return ok(some(retPolicy)) else: return err("unknown retention policy") + +proc new*( + T: typedesc[RetentionPolicy], retPolicies: seq[string] +): RetentionPolicyResult[seq[RetentionPolicy]] = + var policies: seq[RetentionPolicy] + for retPolicy in retPolicies: + let policy = RetentionPolicy.new(retPolicy).valueOr: + return err(error) + if policy.isSome(): + policies.add(policy.get()) + return ok(policies) diff --git a/waku/waku_archive/retention_policy/retention_policy_capacity.nim b/waku/waku_archive/retention_policy/retention_policy_capacity.nim index ed4dd2339..ff4da6861 100644 --- a/waku/waku_archive/retention_policy/retention_policy_capacity.nim +++ b/waku/waku_archive/retention_policy/retention_policy_capacity.nim @@ -50,6 +50,9 @@ proc new*(T: type CapacityRetentionPolicy, capacity = DefaultCapacity): T = capacity: capacity, totalCapacity: totalCapacity, deleteWindow: deleteWindow ) +method `$`*(p: CapacityRetentionPolicy): string = + "capacity:" & $p.capacity + method execute*( p: CapacityRetentionPolicy, driver: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.async.} = diff --git a/waku/waku_archive/retention_policy/retention_policy_size.nim b/waku/waku_archive/retention_policy/retention_policy_size.nim index e60aba303..416d95ec0 100644 --- a/waku/waku_archive/retention_policy/retention_policy_size.nim +++ b/waku/waku_archive/retention_policy/retention_policy_size.nim @@ -15,6 +15,9 @@ type SizeRetentionPolicy* = ref object of RetentionPolicy proc new*(T: type SizeRetentionPolicy, size = DefaultRetentionSize): T = SizeRetentionPolicy(sizeLimit: size) +method `$`*(p: SizeRetentionPolicy): string = + "size:" & $p.sizeLimit & "b" + method execute*( p: SizeRetentionPolicy, driver: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.async.} = diff --git a/waku/waku_archive/retention_policy/retention_policy_time.nim b/waku/waku_archive/retention_policy/retention_policy_time.nim index 6d4c0815a..12f056c7b 100644 --- a/waku/waku_archive/retention_policy/retention_policy_time.nim +++ b/waku/waku_archive/retention_policy/retention_policy_time.nim @@ -6,29 +6,23 @@ import ../../waku_core, ../driver, ../retention_policy logScope: topics = "waku archive retention_policy" -const DefaultRetentionTime*: int64 = 30.days.seconds - type TimeRetentionPolicy* = ref object of RetentionPolicy retentionTime: chronos.Duration -proc new*(T: type TimeRetentionPolicy, retentionTime = DefaultRetentionTime): T = +proc new*(T: type TimeRetentionPolicy, retentionTime: int64): T = TimeRetentionPolicy(retentionTime: retentionTime.seconds) +method `$`*(p: TimeRetentionPolicy): string = + "time:" & $p.retentionTime.seconds + method execute*( p: TimeRetentionPolicy, driver: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.async.} = - ## Delete messages that exceed the retention time by 10% and more (batch delete for efficiency) + ## Delete messages that exceed the retention time info "beginning of executing message retention policy - time" - let omt = (await driver.getOldestMessageTimestamp()).valueOr: - return err("failed to get oldest message timestamp: " & error) - let now = getNanosecondTime(getTime().toUnixFloat()) let retentionTimestamp = now - p.retentionTime.nanoseconds - let thresholdTimestamp = retentionTimestamp - p.retentionTime.nanoseconds div 10 - - if thresholdTimestamp <= omt: - return ok() (await driver.deleteMessagesOlderThanTimestamp(ts = retentionTimestamp)).isOkOr: return err("failed to delete oldest messages: " & error) From a0f134aadb9fcb61801afb9e6235ac18808a1109 Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Thu, 19 Mar 2026 23:09:22 +0100 Subject: [PATCH 101/155] update changelog for v0.37.2 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc5155b6e..e2de7307f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,16 @@ | [`66/WAKU2-METADATA`](https://github.com/waku-org/specs/blob/master/standards/core/metadata.md) | `raw` | `/vac/waku/metadata/1.0.0` | | [`WAKU-SYNC`](https://github.com/waku-org/specs/blob/master/standards/core/sync.md) | `draft` | `/vac/waku/sync/1.0.0` | +## v0.37.2 (2026-03-19) + +### Features + +- Allow union of several retention policies ([#3766](https://github.com/logos-messaging/logos-delivery/pull/3766)) + +### Bug Fixes + +- Bump nim-http-utils to v0.4.1 to allow accepting <:><(> as a valid header and tests to validate html rfc7230 ([#43](https://github.com/status-im/nim-http-utils/pull/43)) + ## v0.37.1 (2026-03-12) ### Bug Fixes From 4b5f91c0ce76e34394481513adfa9f2895ff89cd Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Fri, 20 Mar 2026 16:54:42 +0100 Subject: [PATCH 102/155] fix compilation issue in test_node_conf.nim --- tests/api/test_node_conf.nim | 74 ++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index d0b3d433c..b19739393 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -1,11 +1,14 @@ {.used.} import std/[options, json, strutils], results, stint, testutils/unittests -import json_serialization -import confutils, confutils/std/net -import tools/confutils/cli_args -import waku/factory/waku_conf, waku/factory/networks_config -import waku/common/logging +import json_serialization, confutils, confutils/std/net +import + tools/confutils/cli_args, + waku/api/api_conf, + waku/factory/waku_conf, + waku/factory/networks_config, + waku/factory/conf_builder/conf_builder, + waku/common/logging # Helper: parse JSON into WakuNodeConf using fieldPairs (same as liblogosdelivery) proc parseWakuNodeConfFromJson(jsonStr: string): Result[WakuNodeConf, string] = @@ -75,7 +78,7 @@ suite "WakuNodeConf - mode-driven toWakuConf": ## Given var conf = defaultWakuNodeConf().valueOr: raiseAssert error - conf.mode = WakuMode.noMode + conf.mode = cli_args.WakuMode.noMode conf.relay = true conf.lightpush = false conf.clusterId = 5 @@ -118,7 +121,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs": require confRes.isOk() let conf = confRes.get() check: - conf.mode == WakuMode.noMode + conf.mode == cli_args.WakuMode.noMode conf.clusterId == 0 conf.logLevel == logging.LogLevel.INFO @@ -368,3 +371,60 @@ suite "NodeConfig (deprecated) - toWakuConf": wakuConf.peerExchangeService == true {.pop.} + +suite "WakuConfBuilder - store retention policies": + test "Multiple retention policies": + ## Given + var b = WakuConfBuilder.init() + b.storeServiceConf.withEnabled(true) + b.storeServiceConf.withDbUrl("sqlite://test.db") + b.storeServiceConf.withRetentionPolicies( + "time:86400 ; capacity:10000; size : 50GB" + ) + + ## When + let wakuConf = b.build().valueOr: + raiseAssert error + + ## Then + require wakuConf.storeServiceConf.isSome() + let storeConf = wakuConf.storeServiceConf.get() + check storeConf.retentionPolicies == @["time:86400", "capacity:10000", "size:50GB"] + + test "Duplicated retention policies returns error": + ## Given + var b = WakuConfBuilder.init() + b.storeServiceConf.withEnabled(true) + b.storeServiceConf.withDbUrl("sqlite://test.db") + b.storeServiceConf.withRetentionPolicies("time:86400;time:800;capacity:10000") + + ## When + let wakuConfRes = b.build() + check wakuConfRes.isErr() + check wakuConfRes.error.contains("duplicated retention policy type") + + test "Incorrect retention policy type returns error": + ## Given + var b = WakuConfBuilder.init() + b.storeServiceConf.withEnabled(true) + b.storeServiceConf.withDbUrl("sqlite://test.db") + b.storeServiceConf.withRetentionPolicies("capaity:10000") + + ## When + let wakuConfRes = b.build() + + ## Then + check wakuConfRes.isErr() + check wakuConfRes.error.contains("unknown retention policy type") + + test "Store disabled - no retention policy applied": + ## Given + var b = WakuConfBuilder.init() + # storeServiceConf not enabled + + ## When + let wakuConf = b.build().valueOr: + raiseAssert error + + ## Then + check wakuConf.storeServiceConf.isNone() From 37f587f057cb3f94a73db2ba4fc687175b46686d Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Fri, 20 Mar 2026 21:05:42 +0100 Subject: [PATCH 103/155] set default retention policy in archive.nim --- waku/waku_archive/archive.nim | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/waku/waku_archive/archive.nim b/waku/waku_archive/archive.nim index 95c1a905d..976d7d035 100644 --- a/waku/waku_archive/archive.nim +++ b/waku/waku_archive/archive.nim @@ -14,7 +14,8 @@ import ../waku_core, ../waku_core/message/digest, ./common, - ./archive_metrics + ./archive_metrics, + waku/waku_archive/retention_policy/retention_policy_time logScope: topics = "waku archive" @@ -82,14 +83,11 @@ proc new*( T: type WakuArchive, driver: ArchiveDriver, validator: MessageValidator = validate, - retentionPolicies = newSeq[RetentionPolicy](0), + retentionPolicies = @[RetentionPolicy(TimeRetentionPolicy.new(2.days.seconds))], ): Result[T, string] = if driver.isNil(): return err("archive driver is Nil") - if retentionPolicies.len == 0: - return err("at least one retention policy must be provided") - let archive = WakuArchive( driver: driver, validator: validator, retentionPolicies: retentionPolicies ) From 67491447396fb9408f0d9f5fcb88c71f5c5dca07 Mon Sep 17 00:00:00 2001 From: Ivan Folgueira Bande Date: Fri, 20 Mar 2026 00:18:26 +0100 Subject: [PATCH 104/155] update change log for v0.37.2 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2de7307f..48ead0a63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ ### Bug Fixes - Bump nim-http-utils to v0.4.1 to allow accepting <:><(> as a valid header and tests to validate html rfc7230 ([#43](https://github.com/status-im/nim-http-utils/pull/43)) +- Force FINALIZE partition detach after detecting shorter error ([#3728](https://github.com/logos-messaging/logos-delivery/pull/3766)) ## v0.37.1 (2026-03-12) From 0b86093247da92060c503544d39e5d0a23922c15 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:23:20 +0100 Subject: [PATCH 105/155] allow override user-message-rate-limit (#3778) --- tests/factory/test_waku_conf.nim | 10 +++-- tests/wakunode2/test_cli_args.nim | 37 ------------------- tools/confutils/cli_args.nim | 12 +++--- .../conf_builder/waku_conf_builder.nim | 7 +++- 4 files changed, 18 insertions(+), 48 deletions(-) diff --git a/tests/factory/test_waku_conf.nim b/tests/factory/test_waku_conf.nim index 3d3fec20e..9d05f7fb5 100644 --- a/tests/factory/test_waku_conf.nim +++ b/tests/factory/test_waku_conf.nim @@ -4,7 +4,7 @@ import libp2p/crypto/[crypto, secp], libp2p/multiaddress, nimcrypto/utils, - std/[options, sequtils], + std/[options, random, sequtils], results, testutils/unittests import @@ -22,11 +22,13 @@ suite "Waku Conf - build with cluster conf": builder.withRelayServiceRatio("50:50") # Mount all shards in network let expectedShards = toSeq[0.uint16 .. 7.uint16] + let userMessageLimit = rand(1 .. 1000).uint64 ## Given builder.rlnRelayConf.withEthClientUrls(@["https://my_eth_rpc_url/"]) builder.withNetworkConf(networkConf) builder.withRelay(true) + builder.rlnRelayConf.withUserMessageLimit(userMessageLimit) ## When let resConf = builder.build() @@ -54,7 +56,7 @@ suite "Waku Conf - build with cluster conf": check rlnRelayConf.dynamic == networkConf.rlnRelayDynamic check rlnRelayConf.chainId == networkConf.rlnRelayChainId check rlnRelayConf.epochSizeSec == networkConf.rlnEpochSizeSec - check rlnRelayConf.userMessageLimit == networkConf.rlnRelayUserMessageLimit + check rlnRelayConf.userMessageLimit == userMessageLimit.uint test "Cluster Conf is passed, but relay is disabled": ## Setup @@ -174,11 +176,13 @@ suite "Waku Conf - build with cluster conf": # Mount all shards in network let expectedShards = toSeq[0.uint16 .. 7.uint16] let contractAddress = "0x0123456789ABCDEF" + let userMessageLimit = rand(1 .. 1000).uint64 ## Given builder.rlnRelayConf.withEthContractAddress(contractAddress) builder.withNetworkConf(networkConf) builder.withRelay(true) + builder.rlnRelayConf.withUserMessageLimit(userMessageLimit) ## When let resConf = builder.build() @@ -207,7 +211,7 @@ suite "Waku Conf - build with cluster conf": check rlnRelayConf.dynamic == networkConf.rlnRelayDynamic check rlnRelayConf.chainId == networkConf.rlnRelayChainId check rlnRelayConf.epochSizeSec == networkConf.rlnEpochSizeSec - check rlnRelayConf.userMessageLimit == networkConf.rlnRelayUserMessageLimit + check rlnRelayConf.userMessageLimit == userMessageLimit.uint suite "Waku Conf - node key": test "Node key is generated": diff --git a/tests/wakunode2/test_cli_args.nim b/tests/wakunode2/test_cli_args.nim index 5108b4a9d..9197afe02 100644 --- a/tests/wakunode2/test_cli_args.nim +++ b/tests/wakunode2/test_cli_args.nim @@ -141,43 +141,6 @@ suite "Waku external config - apply preset": ## Then assert res.isErr(), "Invalid shard was accepted" - test "Apply TWN preset when cluster id = 1": - ## Setup - let expectedConf = NetworkConf.TheWakuNetworkConf() - - ## Given - let preConfig = WakuNodeConf( - cmd: noCommand, - clusterId: 1.uint16, - relay: true, - ethClientUrls: @["http://someaddress".EthRpcUrl], - ) - - ## When - let res = preConfig.toWakuConf() - assert res.isOk(), $res.error - - ## Then - let conf = res.get() - check conf.maxMessageSizeBytes == - uint64(parseCorrectMsgSize(expectedConf.maxMessageSize)) - check conf.clusterId == expectedConf.clusterId - check conf.rlnRelayConf.isSome() == expectedConf.rlnRelay - if conf.rlnRelayConf.isSome(): - let rlnRelayConf = conf.rlnRelayConf.get() - check rlnRelayConf.ethContractAddress == expectedConf.rlnRelayEthContractAddress - check rlnRelayConf.dynamic == expectedConf.rlnRelayDynamic - check rlnRelayConf.chainId == expectedConf.rlnRelayChainId - check rlnRelayConf.epochSizeSec == expectedConf.rlnEpochSizeSec - check rlnRelayConf.userMessageLimit == expectedConf.rlnRelayUserMessageLimit - check conf.shardingConf.kind == expectedConf.shardingConf.kind - check conf.shardingConf.numShardsInCluster == - expectedConf.shardingConf.numShardsInCluster - check conf.discv5Conf.isSome() == expectedConf.discv5Discovery - if conf.discv5Conf.isSome(): - let discv5Conf = conf.discv5Conf.get() - check discv5Conf.bootstrapNodes == expectedConf.discv5BootstrapNodes - suite "Waku external config - node key": test "Passed node key is used": ## Setup diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 17b77816e..541215f76 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -948,6 +948,12 @@ proc toNetworkConf( proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = var b = WakuConfBuilder.init() + let networkConf = toNetworkConf(n.preset, some(n.clusterId)).valueOr: + return err("Error determining cluster from preset: " & $error) + + if networkConf.isSome(): + b.withNetworkConf(networkConf.get()) + b.withLogLevel(n.logLevel) b.withLogFormat(n.logFormat) @@ -976,12 +982,6 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.withProtectedShards(n.protectedShards) b.withClusterId(n.clusterId) - let networkConf = toNetworkConf(n.preset, some(n.clusterId)).valueOr: - return err("Error determining cluster from preset: " & $error) - - if networkConf.isSome(): - b.withNetworkConf(networkConf.get()) - b.withAgentString(n.agentString) if n.nodeKey.isSome(): diff --git a/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index 2c427918d..956d733d3 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -353,10 +353,13 @@ proc applyNetworkConf(builder: var WakuConfBuilder) = builder.rlnRelayConf.withEpochSizeSec(networkConf.rlnEpochSizeSec) if builder.rlnRelayConf.userMessageLimit.isSome(): - warn "RLN Relay Dynamic was provided alongside a network conf", + warn "RLN Relay User Message Limit was provided alongside a network conf", used = networkConf.rlnRelayUserMessageLimit, discarded = builder.rlnRelayConf.userMessageLimit - builder.rlnRelayConf.withUserMessageLimit(networkConf.rlnRelayUserMessageLimit) + if builder.rlnRelayConf.userMessageLimit.get(0) == 0: + ## only override with the "preset" value if there was not explicit set value + builder.rlnRelayConf.withUserMessageLimit(networkConf.rlnRelayUserMessageLimit) + # End Apply relay parameters case builder.maxMessageSize.kind From b1e1c875343df05352a2c97ce6901dacd00162b8 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:54:06 +0100 Subject: [PATCH 106/155] update changelog for v0.37.3 (#3783) Co-authored-by: darshankabariya --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f9e2a1e..be2b795f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v0.37.3 (2026-03-25) + +### Features + +- Allow override user-message-rate-limit ([#3778](https://github.com/logos-messaging/logos-delivery/pull/3778)) + ## v0.37.2 (2026-03-19) ### Features From 5c335c20022704e9230f487727adb8f3c51d998c Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:55:27 +0100 Subject: [PATCH 107/155] address leftover comments (#3782) --- .github/ISSUE_TEMPLATE/deploy_release.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/deploy_release.md b/.github/ISSUE_TEMPLATE/deploy_release.md index 68557bf46..9b9a4f32c 100644 --- a/.github/ISSUE_TEMPLATE/deploy_release.md +++ b/.github/ISSUE_TEMPLATE/deploy_release.md @@ -8,7 +8,7 @@ assignees: '' --- ### Link to the Release PR @@ -20,17 +20,22 @@ Kindly add a link to the release PR where we have a sign-off from QA. At this ti ### Items to complete, in order - [ ] Receive sign-off from DST. - [ ] Inform DST team about what are the expectations for this release. For example, if we expect higher, same or lower bandwidth consumption. Or a new protocol appears, etc. - [ ] Ask DST to add a comment approving this deployment and add a link to the analysis report. -- [ ] Update waku.sandbox with [this deployment job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox/). +- [ ] Deploy to waku.sandbox + - [ ] Coordinate with Infra Team about possible changes in CI behavior + - [ ] Update waku.sandbox with [this deployment job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox/). - [ ] Deploy to status.prod + - [ ] Coordinate with Infra Team about possible changes in CI behavior - [ ] Ask Status admin to add a comment approving that this deployment to happen now. - [ ] Update status.prod with [this deployment job](https://ci.infra.status.im/job/nim-waku/job/deploy-status-prod/). @@ -42,7 +47,7 @@ For status.prod it is crucial to coordinate such deployment with status friends. - [Release process](https://github.com/logos-messaging/logos-delivery/blob/master/docs/contributors/release-process.md) - [Release notes](https://github.com/logos-messaging/logos-delivery/blob/master/CHANGELOG.md) - [Infra-role-nim-waku](https://github.com/status-im/infra-role-nim-waku) -- [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) +- [Infra-waku](https://github.com/status-im/infra-waku) - [Infra-Status](https://github.com/status-im/infra-status) - [Jenkins](https://ci.infra.status.im/job/nim-waku/) - [Fleets](https://fleets.waku.org/) From 0623c10635e5f00cd73db50e28aafc535b4b59ee Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:08:08 +0200 Subject: [PATCH 108/155] completely remove storev2 (#3781) --- apps/chat2/chat2.nim | 1 - tests/all_tests_waku.nim | 22 +- tests/common/test_ratelimit_setting.nim | 29 +- tests/node/test_all.nim | 1 - tests/node/test_wakunode_legacy_store.nim | 1064 ---------- tests/node/test_wakunode_sharding.nim | 71 +- tests/testlib/futures.nim | 5 +- tests/testlib/postgres_legacy.nim | 27 - tests/testlib/wakunode.nim | 1 - tests/waku_archive_legacy/archive_utils.nim | 55 - tests/waku_archive_legacy/test_all.nim | 13 - .../test_driver_postgres.nim | 220 -- .../test_driver_postgres_query.nim | 1854 ----------------- .../waku_archive_legacy/test_driver_queue.nim | 182 -- .../test_driver_queue_index.nim | 219 -- .../test_driver_queue_pagination.nim | 405 ---- .../test_driver_queue_query.nim | 1668 --------------- .../test_driver_sqlite.nim | 58 - .../test_driver_sqlite_query.nim | 1745 ---------------- .../waku_archive_legacy/test_waku_archive.nim | 532 ----- tests/waku_store_legacy/store_utils.nim | 33 - tests/waku_store_legacy/test_all.nim | 8 - tests/waku_store_legacy/test_client.nim | 214 -- tests/waku_store_legacy/test_resume.nim | 338 --- tests/waku_store_legacy/test_rpc_codec.nim | 184 -- tests/waku_store_legacy/test_waku_store.nim | 113 - .../waku_store_legacy/test_wakunode_store.nim | 315 --- tests/wakunode_rest/test_rest_health.nim | 4 +- tools/confutils/cli_args.nim | 9 +- waku/common/rate_limit/setting.nim | 6 +- waku/common/waku_protocol.nim | 4 +- .../store_service_conf_builder.nim | 5 - waku/factory/node_factory.nim | 55 +- waku/factory/waku_conf.nim | 1 - .../health_monitor/node_health_monitor.nim | 34 - waku/node/kernel_api/relay.nim | 6 - waku/node/kernel_api/store.nim | 157 +- waku/node/waku_node.nim | 7 - waku/rest_api/endpoint/admin/handlers.nim | 5 +- waku/rest_api/endpoint/builder.nim | 2 - .../rest_api/endpoint/legacy_store/client.nim | 75 - .../endpoint/legacy_store/handlers.nim | 246 --- waku/rest_api/endpoint/legacy_store/types.nim | 375 ---- waku/waku_archive_legacy.nim | 6 - waku/waku_archive_legacy/archive.nim | 285 --- waku/waku_archive_legacy/archive_metrics.nim | 22 - waku/waku_archive_legacy/common.nim | 88 - waku/waku_archive_legacy/driver.nim | 121 -- waku/waku_archive_legacy/driver/builder.nim | 89 - .../driver/postgres_driver.nim | 8 - .../postgres_driver/postgres_driver.nim | 976 --------- .../postgres_driver/postgres_healthcheck.nim | 37 - .../driver/queue_driver.nim | 8 - .../driver/queue_driver/index.nim | 91 - .../driver/queue_driver/queue_driver.nim | 363 ---- .../driver/sqlite_driver.nim | 8 - .../driver/sqlite_driver/cursor.nim | 11 - .../driver/sqlite_driver/migrations.nim | 71 - .../driver/sqlite_driver/queries.nim | 729 ------- .../driver/sqlite_driver/sqlite_driver.nim | 220 -- waku/waku_core/codecs.nim | 1 - waku/waku_store_legacy.nim | 3 - waku/waku_store_legacy/README.md | 3 - waku/waku_store_legacy/client.nim | 241 --- waku/waku_store_legacy/common.nim | 108 - waku/waku_store_legacy/protocol.nim | 188 -- waku/waku_store_legacy/protocol_metrics.nim | 21 - waku/waku_store_legacy/rpc.nim | 218 -- waku/waku_store_legacy/rpc_codec.nim | 255 --- waku/waku_store_legacy/self_req_handler.nim | 31 - 70 files changed, 56 insertions(+), 14514 deletions(-) delete mode 100644 tests/node/test_wakunode_legacy_store.nim delete mode 100644 tests/testlib/postgres_legacy.nim delete mode 100644 tests/waku_archive_legacy/archive_utils.nim delete mode 100644 tests/waku_archive_legacy/test_all.nim delete mode 100644 tests/waku_archive_legacy/test_driver_postgres.nim delete mode 100644 tests/waku_archive_legacy/test_driver_postgres_query.nim delete mode 100644 tests/waku_archive_legacy/test_driver_queue.nim delete mode 100644 tests/waku_archive_legacy/test_driver_queue_index.nim delete mode 100644 tests/waku_archive_legacy/test_driver_queue_pagination.nim delete mode 100644 tests/waku_archive_legacy/test_driver_queue_query.nim delete mode 100644 tests/waku_archive_legacy/test_driver_sqlite.nim delete mode 100644 tests/waku_archive_legacy/test_driver_sqlite_query.nim delete mode 100644 tests/waku_archive_legacy/test_waku_archive.nim delete mode 100644 tests/waku_store_legacy/store_utils.nim delete mode 100644 tests/waku_store_legacy/test_all.nim delete mode 100644 tests/waku_store_legacy/test_client.nim delete mode 100644 tests/waku_store_legacy/test_resume.nim delete mode 100644 tests/waku_store_legacy/test_rpc_codec.nim delete mode 100644 tests/waku_store_legacy/test_waku_store.nim delete mode 100644 tests/waku_store_legacy/test_wakunode_store.nim delete mode 100644 waku/rest_api/endpoint/legacy_store/client.nim delete mode 100644 waku/rest_api/endpoint/legacy_store/handlers.nim delete mode 100644 waku/rest_api/endpoint/legacy_store/types.nim delete mode 100644 waku/waku_archive_legacy.nim delete mode 100644 waku/waku_archive_legacy/archive.nim delete mode 100644 waku/waku_archive_legacy/archive_metrics.nim delete mode 100644 waku/waku_archive_legacy/common.nim delete mode 100644 waku/waku_archive_legacy/driver.nim delete mode 100644 waku/waku_archive_legacy/driver/builder.nim delete mode 100644 waku/waku_archive_legacy/driver/postgres_driver.nim delete mode 100644 waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim delete mode 100644 waku/waku_archive_legacy/driver/postgres_driver/postgres_healthcheck.nim delete mode 100644 waku/waku_archive_legacy/driver/queue_driver.nim delete mode 100644 waku/waku_archive_legacy/driver/queue_driver/index.nim delete mode 100644 waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim delete mode 100644 waku/waku_archive_legacy/driver/sqlite_driver.nim delete mode 100644 waku/waku_archive_legacy/driver/sqlite_driver/cursor.nim delete mode 100644 waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim delete mode 100644 waku/waku_archive_legacy/driver/sqlite_driver/queries.nim delete mode 100644 waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim delete mode 100644 waku/waku_store_legacy.nim delete mode 100644 waku/waku_store_legacy/README.md delete mode 100644 waku/waku_store_legacy/client.nim delete mode 100644 waku/waku_store_legacy/common.nim delete mode 100644 waku/waku_store_legacy/protocol.nim delete mode 100644 waku/waku_store_legacy/protocol_metrics.nim delete mode 100644 waku/waku_store_legacy/rpc.nim delete mode 100644 waku/waku_store_legacy/rpc_codec.nim delete mode 100644 waku/waku_store_legacy/self_req_handler.nim diff --git a/apps/chat2/chat2.nim b/apps/chat2/chat2.nim index 4102cf074..e76c7be17 100644 --- a/apps/chat2/chat2.nim +++ b/apps/chat2/chat2.nim @@ -36,7 +36,6 @@ import waku_lightpush_legacy/rpc, waku_enr, discovery/waku_dnsdisc, - waku_store_legacy, waku_node, node/waku_metrics, node/peer_manager, diff --git a/tests/all_tests_waku.nim b/tests/all_tests_waku.nim index 4d4225f9f..879b1a55a 100644 --- a/tests/all_tests_waku.nim +++ b/tests/all_tests_waku.nim @@ -20,14 +20,7 @@ import ./waku_archive/test_driver_sqlite, ./waku_archive/test_retention_policy, ./waku_archive/test_waku_archive, - ./waku_archive/test_partition_manager, - ./waku_archive_legacy/test_driver_queue_index, - ./waku_archive_legacy/test_driver_queue_pagination, - ./waku_archive_legacy/test_driver_queue_query, - ./waku_archive_legacy/test_driver_queue, - ./waku_archive_legacy/test_driver_sqlite_query, - ./waku_archive_legacy/test_driver_sqlite, - ./waku_archive_legacy/test_waku_archive + ./waku_archive/test_partition_manager const os* {.strdefine.} = "" when os == "Linux" and @@ -37,8 +30,6 @@ when os == "Linux" and import ./waku_archive/test_driver_postgres_query, ./waku_archive/test_driver_postgres, - #./waku_archive_legacy/test_driver_postgres_query, - #./waku_archive_legacy/test_driver_postgres, ./factory/test_node_factory, ./wakunode_rest/test_rest_store, ./wakunode_rest/test_all @@ -50,20 +41,9 @@ import ./waku_store/test_waku_store, ./waku_store/test_wakunode_store -# Waku legacy store test suite -import - ./waku_store_legacy/test_client, - ./waku_store_legacy/test_rpc_codec, - ./waku_store_legacy/test_waku_store, - ./waku_store_legacy/test_wakunode_store - # Waku store sync suite import ./waku_store_sync/test_all -when defined(waku_exp_store_resume): - # TODO: Review store resume test cases (#1282) - import ./waku_store_legacy/test_resume - import ./node/test_all, ./waku_filter_v2/test_all, diff --git a/tests/common/test_ratelimit_setting.nim b/tests/common/test_ratelimit_setting.nim index 97d69e06a..2bc95fbfb 100644 --- a/tests/common/test_ratelimit_setting.nim +++ b/tests/common/test_ratelimit_setting.nim @@ -25,7 +25,6 @@ suite "RateLimitSetting": test "Parse rate limit setting - ok": let test1 = "10/2m" let test2 = " store : 10 /1h" - let test2a = "storev2 : 10 /1h" let test2b = "storeV3: 12 /1s" let test3 = "LIGHTPUSH: 10/ 1m" let test4 = "px:10/2 s " @@ -34,7 +33,6 @@ suite "RateLimitSetting": let expU = UnlimitedRateLimit let exp1: RateLimitSetting = (10, 2.minutes) let exp2: RateLimitSetting = (10, 1.hours) - let exp2a: RateLimitSetting = (10, 1.hours) let exp2b: RateLimitSetting = (12, 1.seconds) let exp3: RateLimitSetting = (10, 1.minutes) let exp4: RateLimitSetting = (10, 2.seconds) @@ -42,7 +40,6 @@ suite "RateLimitSetting": let res1 = ProtocolRateLimitSettings.parse(@[test1]) let res2 = ProtocolRateLimitSettings.parse(@[test2]) - let res2a = ProtocolRateLimitSettings.parse(@[test2a]) let res2b = ProtocolRateLimitSettings.parse(@[test2b]) let res3 = ProtocolRateLimitSettings.parse(@[test3]) let res4 = ProtocolRateLimitSettings.parse(@[test4]) @@ -53,15 +50,7 @@ suite "RateLimitSetting": res1.get() == {GLOBAL: exp1, FILTER: FilterDefaultPerPeerRateLimit}.toTable() res2.isOk() res2.get() == - { - GLOBAL: expU, - FILTER: FilterDefaultPerPeerRateLimit, - STOREV2: exp2, - STOREV3: exp2, - }.toTable() - res2a.isOk() - res2a.get() == - {GLOBAL: expU, FILTER: FilterDefaultPerPeerRateLimit, STOREV2: exp2a}.toTable() + {GLOBAL: expU, FILTER: FilterDefaultPerPeerRateLimit, STOREV3: exp2}.toTable() res2b.isOk() res2b.get() == {GLOBAL: expU, FILTER: FilterDefaultPerPeerRateLimit, STOREV3: exp2b}.toTable() @@ -77,7 +66,6 @@ suite "RateLimitSetting": test "Parse rate limit setting - err": let test1 = "10/2d" let test2 = " stre : 10 /1h" - let test2a = "storev2 10 /1h" let test2b = "storev3: 12 1s" let test3 = "somethingelse: 10/ 1m" let test4 = ":px:10/2 s " @@ -85,7 +73,6 @@ suite "RateLimitSetting": let res1 = ProtocolRateLimitSettings.parse(@[test1]) let res2 = ProtocolRateLimitSettings.parse(@[test2]) - let res2a = ProtocolRateLimitSettings.parse(@[test2a]) let res2b = ProtocolRateLimitSettings.parse(@[test2b]) let res3 = ProtocolRateLimitSettings.parse(@[test3]) let res4 = ProtocolRateLimitSettings.parse(@[test4]) @@ -94,7 +81,6 @@ suite "RateLimitSetting": check: res1.isErr() res2.isErr() - res2a.isErr() res2b.isErr() res3.isErr() res4.isErr() @@ -103,13 +89,12 @@ suite "RateLimitSetting": test "Parse rate limit setting - complex": let expU = UnlimitedRateLimit - let test1 = @["lightpush:2/2ms", "10/2m", " store: 3/3s", " storev2:12/12s"] + let test1 = @["lightpush:2/2ms", "10/2m", " store: 3/3s"] let exp1 = { GLOBAL: (10, 2.minutes), FILTER: FilterDefaultPerPeerRateLimit, LIGHTPUSH: (2, 2.milliseconds), STOREV3: (3, 3.seconds), - STOREV2: (12, 12.seconds), }.toTable() let res1 = ProtocolRateLimitSettings.parse(test1) @@ -118,7 +103,6 @@ suite "RateLimitSetting": res1.isOk() res1.get() == exp1 res1.get().getSetting(PEEREXCHG) == (10, 2.minutes) - res1.get().getSetting(STOREV2) == (12, 12.seconds) res1.get().getSetting(STOREV3) == (3, 3.seconds) res1.get().getSetting(LIGHTPUSH) == (2, 2.milliseconds) @@ -127,7 +111,6 @@ suite "RateLimitSetting": GLOBAL: expU, LIGHTPUSH: (2, 2.milliseconds), STOREV3: (3, 3.seconds), - STOREV2: (3, 3.seconds), FILTER: (4, 42.milliseconds), PEEREXCHG: (10, 10.hours), }.toTable() @@ -138,13 +121,9 @@ suite "RateLimitSetting": res2.isOk() res2.get() == exp2 - let test3 = - @["storev2:1/1s", "store:3/3s", "storev3:4/42ms", "storev3:5/5s", "storev3:6/6s"] + let test3 = @["store:3/3s", "storev3:4/42ms", "storev3:5/5s", "storev3:6/6s"] let exp3 = { - GLOBAL: expU, - FILTER: FilterDefaultPerPeerRateLimit, - STOREV3: (6, 6.seconds), - STOREV2: (1, 1.seconds), + GLOBAL: expU, FILTER: FilterDefaultPerPeerRateLimit, STOREV3: (6, 6.seconds) }.toTable() let res3 = ProtocolRateLimitSettings.parse(test3) diff --git a/tests/node/test_all.nim b/tests/node/test_all.nim index fe785dee2..2846fdb7f 100644 --- a/tests/node/test_all.nim +++ b/tests/node/test_all.nim @@ -6,6 +6,5 @@ import ./test_wakunode_lightpush, ./test_wakunode_peer_exchange, ./test_wakunode_store, - ./test_wakunode_legacy_store, ./test_wakunode_peer_manager, ./test_wakunode_health_monitor diff --git a/tests/node/test_wakunode_legacy_store.nim b/tests/node/test_wakunode_legacy_store.nim deleted file mode 100644 index e9b0c9170..000000000 --- a/tests/node/test_wakunode_legacy_store.nim +++ /dev/null @@ -1,1064 +0,0 @@ -{.used.} - -import std/options, testutils/unittests, chronos, libp2p/crypto/crypto - -import - waku/[ - common/paging, - node/waku_node, - node/kernel_api, - node/peer_manager, - waku_core, - waku_store_legacy, - waku_archive_legacy, - ], - ../waku_store_legacy/store_utils, - ../waku_archive_legacy/archive_utils, - ../testlib/[wakucore, wakunode, testasync, testutils] - -suite "Waku Store - End to End - Sorted Archive": - var pubsubTopic {.threadvar.}: PubsubTopic - var contentTopic {.threadvar.}: ContentTopic - var contentTopicSeq {.threadvar.}: seq[ContentTopic] - - var archiveMessages {.threadvar.}: seq[WakuMessage] - var historyQuery {.threadvar.}: HistoryQuery - - var server {.threadvar.}: WakuNode - var client {.threadvar.}: WakuNode - - var archiveDriver {.threadvar.}: ArchiveDriver - var serverRemotePeerInfo {.threadvar.}: RemotePeerInfo - var clientPeerId {.threadvar.}: PeerId - - asyncSetup: - pubsubTopic = DefaultPubsubTopic - contentTopic = DefaultContentTopic - contentTopicSeq = @[contentTopic] - - let timeOrigin = now() - archiveMessages = @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] - - historyQuery = HistoryQuery( - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.Forward, - pageSize: 5, - ) - - let - serverKey = generateSecp256k1Key() - clientKey = generateSecp256k1Key() - - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - archiveDriver = newArchiveDriverWithMessages(pubsubTopic, archiveMessages) - let mountArchiveResult = server.mountLegacyArchive(archiveDriver) - assert mountArchiveResult.isOk() - - await server.mountLegacyStore() - client.mountLegacyStoreClient() - - await allFutures(server.start(), client.start()) - - serverRemotePeerInfo = server.peerInfo.toRemotePeerInfo() - clientPeerId = client.peerInfo.toRemotePeerInfo().peerId - - asyncTeardown: - await allFutures(client.stop(), server.stop()) - - suite "Message Pagination": - asyncTest "Forward Pagination": - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == archiveMessages[0 ..< 5] - - # Given the next query - var otherHistoryQuery = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - # When making the next history query - let otherQueryResponse = - await client.query(otherHistoryQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - otherQueryResponse.get().messages == archiveMessages[5 ..< 10] - - asyncTest "Backward Pagination": - # Given the history query is backward - historyQuery.direction = PagingDirection.BACKWARD - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == archiveMessages[5 ..< 10] - - # Given the next query - var nextHistoryQuery = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.BACKWARD, - pageSize: 5, - ) - - # When making the next history query - let otherQueryResponse = - await client.query(nextHistoryQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - otherQueryResponse.get().messages == archiveMessages[0 ..< 5] - - suite "Pagination with Differente Page Sizes": - asyncTest "Pagination with Small Page Size": - # Given the first query (1/5) - historyQuery.pageSize = 2 - - # When making a history query - let queryResponse1 = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse1.get().messages == archiveMessages[0 ..< 2] - - # Given the next query (2/5) - let historyQuery2 = HistoryQuery( - cursor: queryResponse1.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 2, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == archiveMessages[2 ..< 4] - - # Given the next query (3/5) - let historyQuery3 = HistoryQuery( - cursor: queryResponse2.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 2, - ) - - # When making the next history query - let queryResponse3 = await client.query(historyQuery3, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse3.get().messages == archiveMessages[4 ..< 6] - - # Given the next query (4/5) - let historyQuery4 = HistoryQuery( - cursor: queryResponse3.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 2, - ) - - # When making the next history query - let queryResponse4 = await client.query(historyQuery4, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse4.get().messages == archiveMessages[6 ..< 8] - - # Given the next query (5/5) - let historyQuery5 = HistoryQuery( - cursor: queryResponse4.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 2, - ) - - # When making the next history query - let queryResponse5 = await client.query(historyQuery5, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse5.get().messages == archiveMessages[8 ..< 10] - - asyncTest "Pagination with Large Page Size": - # Given the first query (1/2) - historyQuery.pageSize = 8 - - # When making a history query - let queryResponse1 = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse1.get().messages == archiveMessages[0 ..< 8] - - # Given the next query (2/2) - let historyQuery2 = HistoryQuery( - cursor: queryResponse1.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 8, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == archiveMessages[8 ..< 10] - - asyncTest "Pagination with Excessive Page Size": - # Given the first query (1/1) - historyQuery.pageSize = 100 - - # When making a history query - let queryResponse1 = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse1.get().messages == archiveMessages[0 ..< 10] - - asyncTest "Pagination with Mixed Page Size": - # Given the first query (1/3) - historyQuery.pageSize = 2 - - # When making a history query - let queryResponse1 = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse1.get().messages == archiveMessages[0 ..< 2] - - # Given the next query (2/3) - let historyQuery2 = HistoryQuery( - cursor: queryResponse1.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 4, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == archiveMessages[2 ..< 6] - - # Given the next query (3/3) - let historyQuery3 = HistoryQuery( - cursor: queryResponse2.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 6, - ) - - # When making the next history query - let queryResponse3 = await client.query(historyQuery3, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse3.get().messages == archiveMessages[6 ..< 10] - - asyncTest "Pagination with Zero Page Size (Behaves as DefaultPageSize)": - # Given a message list of size higher than the default page size - let currentStoreLen = uint((await archiveDriver.getMessagesCount()).get()) - assert archive.DefaultPageSize > currentStoreLen, - "This test requires a store with more than (DefaultPageSize) messages" - let missingMessagesAmount = archive.DefaultPageSize - currentStoreLen + 5 - - let lastMessageTimestamp = archiveMessages[archiveMessages.len - 1].timestamp - var extraMessages: seq[WakuMessage] = @[] - for i in 0 ..< missingMessagesAmount: - let - timestampOffset = 10 * int(i + 1) - # + 1 to avoid collision with existing messages - message: WakuMessage = - fakeWakuMessage(@[byte i], ts = ts(timestampOffset, lastMessageTimestamp)) - extraMessages.add(message) - discard archiveDriver.put(pubsubTopic, extraMessages) - - let totalMessages = archiveMessages & extraMessages - - # Given the a query with zero page size (1/2) - historyQuery.pageSize = 0 - - # When making a history query - let queryResponse1 = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the archive.DefaultPageSize messages - check: - queryResponse1.get().messages == totalMessages[0 ..< archive.DefaultPageSize] - - # Given the next query (2/2) - let historyQuery2 = HistoryQuery( - cursor: queryResponse1.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 0, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the remaining messages - check: - queryResponse2.get().messages == - totalMessages[archive.DefaultPageSize ..< archive.DefaultPageSize + 5] - - asyncTest "Pagination with Default Page Size": - # Given a message list of size higher than the default page size - let currentStoreLen = uint((await archiveDriver.getMessagesCount()).get()) - assert archive.DefaultPageSize > currentStoreLen, - "This test requires a store with more than (DefaultPageSize) messages" - let missingMessagesAmount = archive.DefaultPageSize - currentStoreLen + 5 - - let lastMessageTimestamp = archiveMessages[archiveMessages.len - 1].timestamp - var extraMessages: seq[WakuMessage] = @[] - for i in 0 ..< missingMessagesAmount: - let - timestampOffset = 10 * int(i + 1) - # + 1 to avoid collision with existing messages - message: WakuMessage = - fakeWakuMessage(@[byte i], ts = ts(timestampOffset, lastMessageTimestamp)) - extraMessages.add(message) - discard archiveDriver.put(pubsubTopic, extraMessages) - - let totalMessages = archiveMessages & extraMessages - - # Given a query with default page size (1/2) - historyQuery = HistoryQuery( - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - ) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == totalMessages[0 ..< archive.DefaultPageSize] - - # Given the next query (2/2) - let historyQuery2 = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == - totalMessages[archive.DefaultPageSize ..< archive.DefaultPageSize + 5] - - suite "Pagination with Different Cursors": - asyncTest "Starting Cursor": - # Given a cursor pointing to the first message - let cursor = computeHistoryCursor(pubsubTopic, archiveMessages[0]) - historyQuery.cursor = some(cursor) - historyQuery.pageSize = 1 - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the message - check: - queryResponse.get().messages == archiveMessages[1 ..< 2] - - asyncTest "Middle Cursor": - # Given a cursor pointing to the middle message1 - let cursor = computeHistoryCursor(pubsubTopic, archiveMessages[5]) - historyQuery.cursor = some(cursor) - historyQuery.pageSize = 1 - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the message - check: - queryResponse.get().messages == archiveMessages[6 ..< 7] - - asyncTest "Ending Cursor": - # Given a cursor pointing to the last message - let cursor = computeHistoryCursor(pubsubTopic, archiveMessages[9]) - historyQuery.cursor = some(cursor) - historyQuery.pageSize = 1 - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - suite "Message Sorting": - asyncTest "Cursor Reusability Across Nodes": - # Given a different server node with the same archive - let - otherArchiveDriverWithMessages = - newArchiveDriverWithMessages(pubsubTopic, archiveMessages) - otherServerKey = generateSecp256k1Key() - otherServer = - newTestWakuNode(otherServerKey, parseIpAddress("0.0.0.0"), Port(0)) - mountOtherArchiveResult = - otherServer.mountLegacyArchive(otherArchiveDriverWithMessages) - assert mountOtherArchiveResult.isOk() - - await otherServer.mountLegacyStore() - - await otherServer.start() - let otherServerRemotePeerInfo = otherServer.peerInfo.toRemotePeerInfo() - - # When making a history query to the first server node - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == archiveMessages[0 ..< 5] - - # Given the cursor from the first query - let cursor = queryResponse.get().cursor - - # When making a history query to the second server node - let otherHistoryQuery = HistoryQuery( - cursor: cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - let otherQueryResponse = - await client.query(otherHistoryQuery, otherServerRemotePeerInfo) - - # Then the response contains the remaining messages - check: - otherQueryResponse.get().messages == archiveMessages[5 ..< 10] - - # Cleanup - await otherServer.stop() - -suite "Waku Store - End to End - Unsorted Archive": - var pubsubTopic {.threadvar.}: PubsubTopic - var contentTopic {.threadvar.}: ContentTopic - var contentTopicSeq {.threadvar.}: seq[ContentTopic] - - var historyQuery {.threadvar.}: HistoryQuery - var unsortedArchiveMessages {.threadvar.}: seq[WakuMessage] - - var server {.threadvar.}: WakuNode - var client {.threadvar.}: WakuNode - - var serverRemotePeerInfo {.threadvar.}: RemotePeerInfo - - asyncSetup: - pubsubTopic = DefaultPubsubTopic - contentTopic = DefaultContentTopic - contentTopicSeq = @[contentTopic] - - historyQuery = HistoryQuery( - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - let timeOrigin = now() - unsortedArchiveMessages = @[ # SortIndex (by timestamp and digest) - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), # 1 - fakeWakuMessage(@[byte 03], ts = ts(00, timeOrigin)), # 2 - fakeWakuMessage(@[byte 08], ts = ts(00, timeOrigin)), # 0 - fakeWakuMessage(@[byte 07], ts = ts(10, timeOrigin)), # 4 - fakeWakuMessage(@[byte 02], ts = ts(10, timeOrigin)), # 3 - fakeWakuMessage(@[byte 09], ts = ts(10, timeOrigin)), # 5 - fakeWakuMessage(@[byte 06], ts = ts(20, timeOrigin)), # 6 - fakeWakuMessage(@[byte 01], ts = ts(20, timeOrigin)), # 9 - fakeWakuMessage(@[byte 04], ts = ts(20, timeOrigin)), # 7 - fakeWakuMessage(@[byte 05], ts = ts(20, timeOrigin)), # 8 - ] - - let - serverKey = generateSecp256k1Key() - clientKey = generateSecp256k1Key() - - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - let - unsortedArchiveDriverWithMessages = - newArchiveDriverWithMessages(pubsubTopic, unsortedArchiveMessages) - mountUnsortedArchiveResult = - server.mountLegacyArchive(unsortedArchiveDriverWithMessages) - - assert mountUnsortedArchiveResult.isOk() - - await server.mountLegacyStore() - client.mountLegacyStoreClient() - - await allFutures(server.start(), client.start()) - - serverRemotePeerInfo = server.peerInfo.toRemotePeerInfo() - - asyncTeardown: - await allFutures(client.stop(), server.stop()) - - asyncTest "Basic (Timestamp and Digest) Sorting Validation": - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - unsortedArchiveMessages[2], - unsortedArchiveMessages[0], - unsortedArchiveMessages[1], - unsortedArchiveMessages[4], - unsortedArchiveMessages[3], - ] - - # Given the next query - var historyQuery2 = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == - @[ - unsortedArchiveMessages[5], - unsortedArchiveMessages[6], - unsortedArchiveMessages[8], - unsortedArchiveMessages[9], - unsortedArchiveMessages[7], - ] - - asyncTest "Backward pagination with Ascending Sorting": - # Given a history query with backward pagination - let cursor = computeHistoryCursor(pubsubTopic, unsortedArchiveMessages[4]) - historyQuery.direction = PagingDirection.BACKWARD - historyQuery.cursor = some(cursor) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - unsortedArchiveMessages[2], - unsortedArchiveMessages[0], - unsortedArchiveMessages[1], - ] - - asyncTest "Forward Pagination with Ascending Sorting": - # Given a history query with forward pagination - let cursor = computeHistoryCursor(pubsubTopic, unsortedArchiveMessages[4]) - historyQuery.direction = PagingDirection.FORWARD - historyQuery.cursor = some(cursor) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - unsortedArchiveMessages[3], - unsortedArchiveMessages[5], - unsortedArchiveMessages[6], - unsortedArchiveMessages[8], - unsortedArchiveMessages[9], - ] - -suite "Waku Store - End to End - Archive with Multiple Topics": - var pubsubTopic {.threadvar.}: PubsubTopic - var pubsubTopicB {.threadvar.}: PubsubTopic - var contentTopic {.threadvar.}: ContentTopic - var contentTopicB {.threadvar.}: ContentTopic - var contentTopicC {.threadvar.}: ContentTopic - var contentTopicSpecials {.threadvar.}: ContentTopic - var contentTopicSeq {.threadvar.}: seq[ContentTopic] - - var historyQuery {.threadvar.}: HistoryQuery - var originTs {.threadvar.}: proc(offset: int): Timestamp {.gcsafe, raises: [].} - var archiveMessages {.threadvar.}: seq[WakuMessage] - - var server {.threadvar.}: WakuNode - var client {.threadvar.}: WakuNode - - var serverRemotePeerInfo {.threadvar.}: RemotePeerInfo - - asyncSetup: - pubsubTopic = DefaultPubsubTopic - pubsubTopicB = "topicB" - contentTopic = DefaultContentTopic - contentTopicB = "topicB" - contentTopicC = "topicC" - contentTopicSpecials = "!@#$%^&*()_+" - contentTopicSeq = - @[contentTopic, contentTopicB, contentTopicC, contentTopicSpecials] - - historyQuery = HistoryQuery( - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - let timeOrigin = now() - originTs = proc(offset = 0): Timestamp {.gcsafe, raises: [].} = - ts(offset, timeOrigin) - - archiveMessages = @[ - fakeWakuMessage(@[byte 00], ts = originTs(00), contentTopic = contentTopic), - fakeWakuMessage(@[byte 01], ts = originTs(10), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 02], ts = originTs(20), contentTopic = contentTopicC), - fakeWakuMessage(@[byte 03], ts = originTs(30), contentTopic = contentTopic), - fakeWakuMessage(@[byte 04], ts = originTs(40), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 05], ts = originTs(50), contentTopic = contentTopicC), - fakeWakuMessage(@[byte 06], ts = originTs(60), contentTopic = contentTopic), - fakeWakuMessage(@[byte 07], ts = originTs(70), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 08], ts = originTs(80), contentTopic = contentTopicC), - fakeWakuMessage( - @[byte 09], ts = originTs(90), contentTopic = contentTopicSpecials - ), - ] - - let - serverKey = generateSecp256k1Key() - clientKey = generateSecp256k1Key() - - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - let archiveDriver = newSqliteArchiveDriver() - .put(pubsubTopic, archiveMessages[0 ..< 6]) - .put(pubsubTopicB, archiveMessages[6 ..< 10]) - let mountSortedArchiveResult = server.mountLegacyArchive(archiveDriver) - - assert mountSortedArchiveResult.isOk() - - await server.mountLegacyStore() - client.mountLegacyStoreClient() - - await allFutures(server.start(), client.start()) - - serverRemotePeerInfo = server.peerInfo.toRemotePeerInfo() - - asyncTeardown: - await allFutures(client.stop(), server.stop()) - - suite "Validation of Content Filtering": - asyncTest "Basic Content Filtering": - # Given a history query with content filtering - historyQuery.contentTopics = @[contentTopic] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == @[archiveMessages[0], archiveMessages[3]] - - asyncTest "Multiple Content Filters": - # Given a history query with multiple content filtering - historyQuery.contentTopics = @[contentTopic, contentTopicB] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - archiveMessages[0], - archiveMessages[1], - archiveMessages[3], - archiveMessages[4], - ] - - asyncTest "Empty Content Filtering": - # Given a history query with empty content filtering - historyQuery.contentTopics = @[] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == archiveMessages[0 ..< 5] - - # Given the next query - let historyQuery2 = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: none(PubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == archiveMessages[5 ..< 10] - - asyncTest "Non-Existent Content Topic": - # Given a history query with non-existent content filtering - historyQuery.contentTopics = @["non-existent-topic"] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - asyncTest "Special Characters in Content Filtering": - # Given a history query with special characters in content filtering - historyQuery.pubsubTopic = some(pubsubTopicB) - historyQuery.contentTopics = @["!@#$%^&*()_+"] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages == @[archiveMessages[9]] - - asyncTest "PubsubTopic Specified": - # Given a history query with pubsub topic specified - historyQuery.pubsubTopic = some(pubsubTopicB) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - archiveMessages[6], - archiveMessages[7], - archiveMessages[8], - archiveMessages[9], - ] - - asyncTest "PubsubTopic Left Empty": - # Given a history query with pubsub topic left empty - historyQuery.pubsubTopic = none(PubsubTopic) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == archiveMessages[0 ..< 5] - - # Given the next query - let historyQuery2 = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: none(PubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == archiveMessages[5 ..< 10] - - suite "Validation of Time-based Filtering": - asyncTest "Basic Time Filtering": - # Given a history query with start and end time - historyQuery.startTime = some(originTs(20)) - historyQuery.endTime = some(originTs(40)) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[archiveMessages[2], archiveMessages[3], archiveMessages[4]] - - asyncTest "Only Start Time Specified": - # Given a history query with only start time - historyQuery.startTime = some(originTs(20)) - historyQuery.endTime = none(Timestamp) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - archiveMessages[2], - archiveMessages[3], - archiveMessages[4], - archiveMessages[5], - ] - - asyncTest "Only End Time Specified": - # Given a history query with only end time - historyQuery.startTime = none(Timestamp) - historyQuery.endTime = some(originTs(40)) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages == - @[ - archiveMessages[0], - archiveMessages[1], - archiveMessages[2], - archiveMessages[3], - archiveMessages[4], - ] - - asyncTest "Invalid Time Range": - # Given a history query with invalid time range - historyQuery.startTime = some(originTs(60)) - historyQuery.endTime = some(originTs(40)) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - asyncTest "Time Filtering with Content Filtering": - # Given a history query with time and content filtering - historyQuery.startTime = some(originTs(20)) - historyQuery.endTime = some(originTs(60)) - historyQuery.contentTopics = @[contentTopicC] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == @[archiveMessages[2], archiveMessages[5]] - - asyncTest "Messages Outside of Time Range": - # Given a history query with a valid time range which does not contain any messages - historyQuery.startTime = some(originTs(100)) - historyQuery.endTime = some(originTs(200)) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - suite "Ephemeral": - # TODO: Ephemeral value is not properly set for Sqlite - xasyncTest "Only ephemeral Messages:": - # Given an archive with only ephemeral messages - let - ephemeralMessages = @[ - fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), - fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), - fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), - ] - ephemeralArchiveDriver = - newSqliteArchiveDriver().put(pubsubTopic, ephemeralMessages) - - # And a server node with the ephemeral archive - let - ephemeralServerKey = generateSecp256k1Key() - ephemeralServer = - newTestWakuNode(ephemeralServerKey, parseIpAddress("0.0.0.0"), Port(0)) - mountEphemeralArchiveResult = - ephemeralServer.mountLegacyArchive(ephemeralArchiveDriver) - assert mountEphemeralArchiveResult.isOk() - - await ephemeralServer.mountLegacyStore() - await ephemeralServer.start() - let ephemeralServerRemotePeerInfo = ephemeralServer.peerInfo.toRemotePeerInfo() - - # When making a history query to the server with only ephemeral messages - let queryResponse = - await client.query(historyQuery, ephemeralServerRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - # Cleanup - await ephemeralServer.stop() - - xasyncTest "Mixed messages": - # Given an archive with both ephemeral and non-ephemeral messages - let - ephemeralMessages = @[ - fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), - fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), - fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), - ] - nonEphemeralMessages = @[ - fakeWakuMessage(@[byte 03], ts = ts(30), ephemeral = false), - fakeWakuMessage(@[byte 04], ts = ts(40), ephemeral = false), - fakeWakuMessage(@[byte 05], ts = ts(50), ephemeral = false), - ] - mixedArchiveDriver = newSqliteArchiveDriver() - .put(pubsubTopic, ephemeralMessages) - .put(pubsubTopic, nonEphemeralMessages) - - # And a server node with the mixed archive - let - mixedServerKey = generateSecp256k1Key() - mixedServer = - newTestWakuNode(mixedServerKey, parseIpAddress("0.0.0.0"), Port(0)) - mountMixedArchiveResult = mixedServer.mountLegacyArchive(mixedArchiveDriver) - assert mountMixedArchiveResult.isOk() - - await mixedServer.mountLegacyStore() - await mixedServer.start() - let mixedServerRemotePeerInfo = mixedServer.peerInfo.toRemotePeerInfo() - - # When making a history query to the server with mixed messages - let queryResponse = await client.query(historyQuery, mixedServerRemotePeerInfo) - - # Then the response contains the non-ephemeral messages - check: - queryResponse.get().messages == nonEphemeralMessages - - # Cleanup - await mixedServer.stop() - - suite "Edge Case Scenarios": - asyncTest "Empty Message Store": - # Given an empty archive - let emptyArchiveDriver = newSqliteArchiveDriver() - - # And a server node with the empty archive - let - emptyServerKey = generateSecp256k1Key() - emptyServer = - newTestWakuNode(emptyServerKey, parseIpAddress("0.0.0.0"), Port(0)) - mountEmptyArchiveResult = emptyServer.mountLegacyArchive(emptyArchiveDriver) - assert mountEmptyArchiveResult.isOk() - - await emptyServer.mountLegacyStore() - await emptyServer.start() - let emptyServerRemotePeerInfo = emptyServer.peerInfo.toRemotePeerInfo() - - # When making a history query to the server with an empty archive - let queryResponse = await client.query(historyQuery, emptyServerRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - # Cleanup - await emptyServer.stop() - - asyncTest "Voluminous Message Store": - # Given a voluminous archive (1M+ messages) - var voluminousArchiveMessages: seq[WakuMessage] = @[] - for i in 0 ..< 100000: - let topic = "topic" & $i - voluminousArchiveMessages.add(fakeWakuMessage(@[byte i], contentTopic = topic)) - let voluminousArchiveDriverWithMessages = - newArchiveDriverWithMessages(pubsubTopic, voluminousArchiveMessages) - - # And a server node with the voluminous archive - let - voluminousServerKey = generateSecp256k1Key() - voluminousServer = - newTestWakuNode(voluminousServerKey, parseIpAddress("0.0.0.0"), Port(0)) - mountVoluminousArchiveResult = - voluminousServer.mountLegacyArchive(voluminousArchiveDriverWithMessages) - assert mountVoluminousArchiveResult.isOk() - - await voluminousServer.mountLegacyStore() - await voluminousServer.start() - let voluminousServerRemotePeerInfo = voluminousServer.peerInfo.toRemotePeerInfo() - - # Given the following history query - historyQuery.contentTopics = - @["topic10000", "topic30000", "topic50000", "topic70000", "topic90000"] - - # When making a history query to the server with a voluminous archive - let queryResponse = - await client.query(historyQuery, voluminousServerRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - voluminousArchiveMessages[10000], - voluminousArchiveMessages[30000], - voluminousArchiveMessages[50000], - voluminousArchiveMessages[70000], - voluminousArchiveMessages[90000], - ] - - # Cleanup - await voluminousServer.stop() - - asyncTest "Large contentFilters Array": - # Given a history query with the max contentFilters len, 10 - historyQuery.contentTopics = @[contentTopic] - for i in 0 ..< 9: - let topic = "topic" & $i - historyQuery.contentTopics.add(topic) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response should trigger no errors - check: - queryResponse.get().messages == @[archiveMessages[0], archiveMessages[3]] diff --git a/tests/node/test_wakunode_sharding.nim b/tests/node/test_wakunode_sharding.nim index 236510fe1..f482b6abc 100644 --- a/tests/node/test_wakunode_sharding.nim +++ b/tests/node/test_wakunode_sharding.nim @@ -14,7 +14,6 @@ import waku/[ waku_core/topics/pubsub_topic, waku_core/topics/sharding, - waku_store_legacy/common, node/waku_node, node/kernel_api, common/paging, @@ -454,29 +453,33 @@ suite "Sharding": # Given one query for each content topic format let - historyQuery1 = HistoryQuery( + storeQuery1 = StoreQueryRequest( contentTopics: @[contentTopicShort], - direction: PagingDirection.Forward, - pageSize: 3, + paginationForward: PagingDirection.Forward, + paginationLimit: some(3'u64), + includeData: true, ) - historyQuery2 = HistoryQuery( + storeQuery2 = StoreQueryRequest( contentTopics: @[contentTopicFull], - direction: PagingDirection.Forward, - pageSize: 3, + paginationForward: PagingDirection.Forward, + paginationLimit: some(3'u64), + includeData: true, ) # When the client queries the server for the messages let serverRemotePeerInfo = server.switch.peerInfo.toRemotePeerInfo() - queryResponse1 = await client.query(historyQuery1, serverRemotePeerInfo) - queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) + queryResponse1 = await client.query(storeQuery1, serverRemotePeerInfo) + queryResponse2 = await client.query(storeQuery2, serverRemotePeerInfo) assertResultOk(queryResponse1) assertResultOk(queryResponse2) # Then the responses of both queries should contain all the messages check: - queryResponse1.get().messages == archiveMessages1 & archiveMessages2 - queryResponse2.get().messages == archiveMessages1 & archiveMessages2 + queryResponse1.get().messages.mapIt(it.message.get()) == + archiveMessages1 & archiveMessages2 + queryResponse2.get().messages.mapIt(it.message.get()) == + archiveMessages1 & archiveMessages2 asyncTest "relay - exclusion (automatic sharding filtering)": # Given a connected server and client subscribed to different content topics @@ -615,29 +618,31 @@ suite "Sharding": # Given one query for each content topic let - historyQuery1 = HistoryQuery( + storeQuery1 = StoreQueryRequest( contentTopics: @[contentTopic1], - direction: PagingDirection.Forward, - pageSize: 2, + paginationForward: PagingDirection.Forward, + paginationLimit: some(2'u64), + includeData: true, ) - historyQuery2 = HistoryQuery( + storeQuery2 = StoreQueryRequest( contentTopics: @[contentTopic2], - direction: PagingDirection.Forward, - pageSize: 2, + paginationForward: PagingDirection.Forward, + paginationLimit: some(2'u64), + includeData: true, ) # When the client queries the server for the messages let serverRemotePeerInfo = server.switch.peerInfo.toRemotePeerInfo() - queryResponse1 = await client.query(historyQuery1, serverRemotePeerInfo) - queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) + queryResponse1 = await client.query(storeQuery1, serverRemotePeerInfo) + queryResponse2 = await client.query(storeQuery2, serverRemotePeerInfo) assertResultOk(queryResponse1) assertResultOk(queryResponse2) # Then each response should contain only the messages of the corresponding content topic check: - queryResponse1.get().messages == archiveMessages1 - queryResponse2.get().messages == archiveMessages2 + queryResponse1.get().messages.mapIt(it.message.get()) == archiveMessages1 + queryResponse2.get().messages.mapIt(it.message.get()) == archiveMessages2 suite "Specific Tests": asyncTest "Configure Node with Multiple PubSub Topics": @@ -1003,22 +1008,30 @@ suite "Sharding": # Given one query for each pubsub topic let - historyQuery1 = HistoryQuery( - pubsubTopic: some(topic1), direction: PagingDirection.Forward, pageSize: 2 + storeQuery1 = StoreQueryRequest( + pubsubTopic: some(topic1), + paginationForward: PagingDirection.Forward, + paginationLimit: some(2'u64), + includeData: true, ) - historyQuery2 = HistoryQuery( - pubsubTopic: some(topic2), direction: PagingDirection.Forward, pageSize: 2 + storeQuery2 = StoreQueryRequest( + pubsubTopic: some(topic2), + paginationForward: PagingDirection.Forward, + paginationLimit: some(2'u64), + includeData: true, ) # When the client queries the server for the messages let serverRemotePeerInfo = server.switch.peerInfo.toRemotePeerInfo() - queryResponse1 = await client.query(historyQuery1, serverRemotePeerInfo) - queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) + queryResponse1 = await client.query(storeQuery1, serverRemotePeerInfo) + queryResponse2 = await client.query(storeQuery2, serverRemotePeerInfo) assertResultOk(queryResponse1) assertResultOk(queryResponse2) # Then each response should contain only the messages of the corresponding pubsub topic check: - queryResponse1.get().messages == archiveMessages1[0 ..< 1] - queryResponse2.get().messages == archiveMessages2[0 ..< 1] + queryResponse1.get().messages.mapIt(it.message.get()) == + archiveMessages1[0 ..< 1] + queryResponse2.get().messages.mapIt(it.message.get()) == + archiveMessages2[0 ..< 1] diff --git a/tests/testlib/futures.nim b/tests/testlib/futures.nim index e9a793388..dad1baec8 100644 --- a/tests/testlib/futures.nim +++ b/tests/testlib/futures.nim @@ -1,6 +1,6 @@ import chronos -import waku/[waku_core/message, waku_store, waku_store_legacy] +import waku/[waku_core/message, waku_store] const FUTURE_TIMEOUT* = 1.seconds @@ -18,9 +18,6 @@ proc newBoolFuture*(): Future[bool] = proc newHistoryFuture*(): Future[StoreQueryRequest] = newFuture[StoreQueryRequest]() -proc newLegacyHistoryFuture*(): Future[waku_store_legacy.HistoryQuery] = - newFuture[waku_store_legacy.HistoryQuery]() - proc toResult*[T](future: Future[T]): Result[T, string] = if future.cancelled(): return chronos.err("Future timeouted before completing.") diff --git a/tests/testlib/postgres_legacy.nim b/tests/testlib/postgres_legacy.nim deleted file mode 100644 index 50988c6c8..000000000 --- a/tests/testlib/postgres_legacy.nim +++ /dev/null @@ -1,27 +0,0 @@ -import chronicles, chronos -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver as driver_module, - waku/waku_archive_legacy/driver/builder, - waku/waku_archive_legacy/driver/postgres_driver - -const storeMessageDbUrl = "postgres://postgres:test123@localhost:5432/postgres" - -proc newTestPostgresDriver*(): Future[Result[ArchiveDriver, string]] {. - async, deprecated -.} = - proc onErr(errMsg: string) {.gcsafe, closure.} = - error "error creating ArchiveDriver", error = errMsg - quit(QuitFailure) - - let - vacuum = false - migrate = true - maxNumConn = 50 - - let driverRes = - await ArchiveDriver.new(storeMessageDbUrl, vacuum, migrate, maxNumConn, onErr) - if driverRes.isErr(): - onErr("could not create archive driver: " & driverRes.error) - - return ok(driverRes.get()) diff --git a/tests/testlib/wakunode.nim b/tests/testlib/wakunode.nim index f59546ec8..e904604ab 100644 --- a/tests/testlib/wakunode.nim +++ b/tests/testlib/wakunode.nim @@ -42,7 +42,6 @@ proc defaultTestWakuConfBuilder*(): WakuConfBuilder = builder.withRelay(true) builder.withRendezvous(true) builder.storeServiceConf.withDbMigration(false) - builder.storeServiceConf.withSupportV2(false) return builder proc defaultTestWakuConf*(): WakuConf = diff --git a/tests/waku_archive_legacy/archive_utils.nim b/tests/waku_archive_legacy/archive_utils.nim deleted file mode 100644 index 8df0f5d7f..000000000 --- a/tests/waku_archive_legacy/archive_utils.nim +++ /dev/null @@ -1,55 +0,0 @@ -{.used.} - -import std/options, results, chronos, libp2p/crypto/crypto - -import - waku/[ - node/peer_manager, - waku_core, - waku_archive_legacy, - waku_archive_legacy/common, - waku_archive_legacy/driver/sqlite_driver, - waku_archive_legacy/driver/sqlite_driver/migrations, - common/databases/db_sqlite, - ], - ../testlib/[wakucore] - -proc newSqliteDatabase*(path: Option[string] = string.none()): SqliteDatabase = - SqliteDatabase.new(path.get(":memory:")).tryGet() - -proc newSqliteArchiveDriver*(): ArchiveDriver = - let database = newSqliteDatabase() - migrate(database).tryGet() - return SqliteDriver.new(database).tryGet() - -proc newWakuArchive*(driver: ArchiveDriver): WakuArchive = - WakuArchive.new(driver).get() - -proc computeArchiveCursor*( - pubsubTopic: PubsubTopic, message: WakuMessage -): ArchiveCursor = - ArchiveCursor( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - storeTime: message.timestamp, - digest: computeDigest(message), - hash: computeMessageHash(pubsubTopic, message), - ) - -proc put*( - driver: ArchiveDriver, pubsubTopic: PubSubTopic, msgList: seq[WakuMessage] -): ArchiveDriver = - for msg in msgList: - let - msgDigest = computeDigest(msg) - msgHash = computeMessageHash(pubsubTopic, msg) - _ = waitFor driver.put(pubsubTopic, msg, msgDigest, msgHash, msg.timestamp) - # discard crashes - return driver - -proc newArchiveDriverWithMessages*( - pubsubTopic: PubSubTopic, msgList: seq[WakuMessage] -): ArchiveDriver = - var driver = newSqliteArchiveDriver() - driver = driver.put(pubsubTopic, msgList) - return driver diff --git a/tests/waku_archive_legacy/test_all.nim b/tests/waku_archive_legacy/test_all.nim deleted file mode 100644 index 9d45d99a1..000000000 --- a/tests/waku_archive_legacy/test_all.nim +++ /dev/null @@ -1,13 +0,0 @@ -{.used.} - -import - ./test_driver_postgres_query, - ./test_driver_postgres, - ./test_driver_queue_index, - ./test_driver_queue_pagination, - ./test_driver_queue_query, - ./test_driver_queue, - ./test_driver_sqlite_query, - ./test_driver_sqlite, - ./test_retention_policy, - ./test_waku_archive diff --git a/tests/waku_archive_legacy/test_driver_postgres.nim b/tests/waku_archive_legacy/test_driver_postgres.nim deleted file mode 100644 index 7657b6e1f..000000000 --- a/tests/waku_archive_legacy/test_driver_postgres.nim +++ /dev/null @@ -1,220 +0,0 @@ -{.used.} - -import std/[sequtils, options], testutils/unittests, chronos -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver/postgres_driver, - waku/waku_archive/driver/postgres_driver as new_postgres_driver, - waku/waku_core, - waku/waku_core/message/digest, - ../testlib/wakucore, - ../testlib/testasync, - ../testlib/postgres_legacy, - ../testlib/postgres as new_postgres - -proc computeTestCursor(pubsubTopic: PubsubTopic, message: WakuMessage): ArchiveCursor = - ArchiveCursor( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - storeTime: message.timestamp, - digest: computeDigest(message), - hash: computeMessageHash(pubsubTopic, message), - ) - -suite "Postgres driver": - ## Unique driver instance - var driver {.threadvar.}: postgres_driver.PostgresDriver - - ## We need to artificially create an instance of the "newDriver" - ## because this is the only one in charge of creating partitions - ## We will clean legacy store soon and this file will get removed. - var newDriver {.threadvar.}: new_postgres_driver.PostgresDriver - - asyncSetup: - let driverRes = await postgres_legacy.newTestPostgresDriver() - if driverRes.isErr(): - assert false, driverRes.error - - driver = postgres_driver.PostgresDriver(driverRes.get()) - - let newDriverRes = await new_postgres.newTestPostgresDriver() - if driverRes.isErr(): - assert false, driverRes.error - - newDriver = new_postgres_driver.PostgresDriver(newDriverRes.get()) - - asyncTeardown: - var resetRes = await driver.reset() - if resetRes.isErr(): - assert false, resetRes.error - - (await driver.close()).expect("driver to close") - - resetRes = await newDriver.reset() - if resetRes.isErr(): - assert false, resetRes.error - - (await newDriver.close()).expect("driver to close") - - asyncTest "Asynchronous queries": - var futures = newSeq[Future[ArchiveDriverResult[void]]](0) - - let beforeSleep = now() - for _ in 1 .. 100: - futures.add(driver.sleep(1)) - - await allFutures(futures) - - let diff = now() - beforeSleep - # Actually, the diff randomly goes between 1 and 2 seconds. - # although in theory it should spend 1s because we establish 100 - # connections and we spawn 100 tasks that spend ~1s each. - assert diff < 20_000_000_000 - - asyncTest "Insert a message": - const contentTopic = "test-content-topic" - const meta = "test meta" - - let msg = fakeWakuMessage(contentTopic = contentTopic, meta = meta) - - let computedDigest = computeDigest(msg) - let computedHash = computeMessageHash(DefaultPubsubTopic, msg) - - let putRes = await driver.put( - DefaultPubsubTopic, msg, computedDigest, computedHash, msg.timestamp - ) - assert putRes.isOk(), putRes.error - - let storedMsg = (await driver.getAllMessages()).tryGet() - - assert storedMsg.len == 1 - - let (pubsubTopic, actualMsg, digest, _, hash) = storedMsg[0] - assert actualMsg.contentTopic == contentTopic - assert pubsubTopic == DefaultPubsubTopic - assert toHex(computedDigest.data) == toHex(digest) - assert toHex(actualMsg.payload) == toHex(msg.payload) - assert toHex(computedHash) == toHex(hash) - assert toHex(actualMsg.meta) == toHex(msg.meta) - - asyncTest "Insert and query message": - const contentTopic1 = "test-content-topic-1" - const contentTopic2 = "test-content-topic-2" - const pubsubTopic1 = "pubsubtopic-1" - const pubsubTopic2 = "pubsubtopic-2" - - let msg1 = fakeWakuMessage(contentTopic = contentTopic1) - - var putRes = await driver.put( - pubsubTopic1, - msg1, - computeDigest(msg1), - computeMessageHash(pubsubTopic1, msg1), - msg1.timestamp, - ) - assert putRes.isOk(), putRes.error - - let msg2 = fakeWakuMessage(contentTopic = contentTopic2) - - putRes = await driver.put( - pubsubTopic2, - msg2, - computeDigest(msg2), - computeMessageHash(pubsubTopic2, msg2), - msg2.timestamp, - ) - assert putRes.isOk(), putRes.error - - let countMessagesRes = await driver.getMessagesCount() - - assert countMessagesRes.isOk(), $countMessagesRes.error - assert countMessagesRes.get() == 2 - - var messagesRes = await driver.getMessages(contentTopic = @[contentTopic1]) - - assert messagesRes.isOk(), $messagesRes.error - assert messagesRes.get().len == 1 - - # Get both content topics, check ordering - messagesRes = - await driver.getMessages(contentTopic = @[contentTopic1, contentTopic2]) - assert messagesRes.isOk(), messagesRes.error - - assert messagesRes.get().len == 2 - assert messagesRes.get()[0][1].contentTopic == contentTopic1 - - # Descending order - messagesRes = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], ascendingOrder = false - ) - assert messagesRes.isOk(), messagesRes.error - - assert messagesRes.get().len == 2 - assert messagesRes.get()[0][1].contentTopic == contentTopic2 - - # cursor - # Get both content topics - messagesRes = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], - cursor = some(computeTestCursor(pubsubTopic1, messagesRes.get()[1][1])), - ) - assert messagesRes.isOk() - assert messagesRes.get().len == 1 - - # Get both content topics but one pubsub topic - messagesRes = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], pubsubTopic = some(pubsubTopic1) - ) - assert messagesRes.isOk(), messagesRes.error - - assert messagesRes.get().len == 1 - assert messagesRes.get()[0][1].contentTopic == contentTopic1 - - # Limit - messagesRes = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], maxPageSize = 1 - ) - assert messagesRes.isOk(), messagesRes.error - assert messagesRes.get().len == 1 - - asyncTest "Insert true duplicated messages": - # Validates that two completely equal messages can not be stored. - - let now = now() - - let msg1 = fakeWakuMessage(ts = now) - let msg2 = fakeWakuMessage(ts = now) - - let initialNumMsgs = (await driver.getMessagesCount()).valueOr: - raiseAssert "could not get num mgs correctly: " & $error - - var putRes = await driver.put( - DefaultPubsubTopic, - msg1, - computeDigest(msg1), - computeMessageHash(DefaultPubsubTopic, msg1), - msg1.timestamp, - ) - assert putRes.isOk(), putRes.error - - var newNumMsgs = (await driver.getMessagesCount()).valueOr: - raiseAssert "could not get num mgs correctly: " & $error - - assert newNumMsgs == (initialNumMsgs + 1.int64), - "wrong number of messages: " & $newNumMsgs - - putRes = await driver.put( - DefaultPubsubTopic, - msg2, - computeDigest(msg2), - computeMessageHash(DefaultPubsubTopic, msg2), - msg2.timestamp, - ) - - assert putRes.isOk() - - newNumMsgs = (await driver.getMessagesCount()).valueOr: - raiseAssert "could not get num mgs correctly: " & $error - - assert newNumMsgs == (initialNumMsgs + 1.int64), - "wrong number of messages: " & $newNumMsgs diff --git a/tests/waku_archive_legacy/test_driver_postgres_query.nim b/tests/waku_archive_legacy/test_driver_postgres_query.nim deleted file mode 100644 index ff513de76..000000000 --- a/tests/waku_archive_legacy/test_driver_postgres_query.nim +++ /dev/null @@ -1,1854 +0,0 @@ -{.used.} - -import - std/[options, sequtils, strformat, random, algorithm], - testutils/unittests, - chronos, - chronicles -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver as driver_module, - waku/waku_archive_legacy/driver/postgres_driver, - waku/waku_archive/driver/postgres_driver as new_postgres_driver, - waku/waku_core, - waku/waku_core/message/digest, - ../testlib/common, - ../testlib/wakucore, - ../testlib/testasync, - ../testlib/postgres_legacy, - ../testlib/postgres as new_postgres, - ../testlib/testutils - -logScope: - topics = "test archive postgres driver" - -## This whole file is copied from the 'test_driver_sqlite_query.nim' file -## and it tests the same use cases but using the postgres driver. - -# Initialize the random number generator -common.randomize() - -proc computeTestCursor(pubsubTopic: PubsubTopic, message: WakuMessage): ArchiveCursor = - ArchiveCursor( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - storeTime: message.timestamp, - digest: computeDigest(message), - hash: computeMessageHash(pubsubTopic, message), - ) - -suite "Postgres driver - queries": - ## Unique driver instance - var driver {.threadvar.}: postgres_driver.PostgresDriver - - ## We need to artificially create an instance of the "newDriver" - ## because this is the only one in charge of creating partitions - ## We will clean legacy store soon and this file will get removed. - var newDriver {.threadvar.}: new_postgres_driver.PostgresDriver - - asyncSetup: - let driverRes = await postgres_legacy.newTestPostgresDriver() - if driverRes.isErr(): - assert false, driverRes.error - - driver = postgres_driver.PostgresDriver(driverRes.get()) - - let newDriverRes = await new_postgres.newTestPostgresDriver() - if driverRes.isErr(): - assert false, driverRes.error - - newDriver = new_postgres_driver.PostgresDriver(newDriverRes.get()) - - asyncTeardown: - var resetRes = await driver.reset() - if resetRes.isErr(): - assert false, resetRes.error - - (await driver.close()).expect("driver to close") - - resetRes = await newDriver.reset() - if resetRes.isErr(): - assert false, resetRes.error - - (await newDriver.close()).expect("driver to close") - - asyncTest "no content topic": - ## Given - const contentTopic = "test-content-topic" - - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages(maxPageSize = 5, ascendingOrder = true) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - asyncTest "single content topic": - ## Given - const contentTopic = "test-content-topic" - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - asyncTest "single content topic with meta field": - ## Given - const contentTopic = "test-content-topic" - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), - fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" - ), - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" - ), - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" - ), - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" - ), - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" - ), - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - asyncTest "single content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = false - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[6 .. 7].reversed() - - asyncTest "multiple content topic": - ## Given - const contentTopic1 = "test-content-topic-1" - const contentTopic2 = "test-content-topic-2" - const contentTopic3 = "test-content-topic-3" - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - var res = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], - pubsubTopic = some(DefaultPubsubTopic), - maxPageSize = 2, - ascendingOrder = true, - startTime = some(ts(00)), - endTime = some(ts(40)), - ) - - ## Then - assert res.isOk(), res.error - var filteredMessages = res.tryGet().mapIt(it[1]) - check filteredMessages == expected[2 .. 3] - - ## When - ## This is very similar to the previous one but we enforce to use the prepared - ## statement by querying one single content topic - res = await driver.getMessages( - contentTopic = @[contentTopic1], - pubsubTopic = some(DefaultPubsubTopic), - maxPageSize = 2, - ascendingOrder = true, - startTime = some(ts(00)), - endTime = some(ts(40)), - ) - - ## Then - assert res.isOk(), res.error - filteredMessages = res.tryGet().mapIt(it[1]) - check filteredMessages == @[expected[2]] - - asyncTest "single content topic - no results": - ## Given - const contentTopic = "test-content-topic" - - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - asyncTest "content topic and max page size - not enough messages stored": - ## Given - const pageSize: uint = 50 - - for t in 0 ..< 40: - let msg = fakeWakuMessage(@[byte t], DefaultContentTopic, ts = ts(t)) - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[DefaultContentTopic], - maxPageSize = pageSize, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 40 - - asyncTest "pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - asyncTest "no pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages(maxPageSize = 2, ascendingOrder = true) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[0 .. 1] - - asyncTest "content topic and pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - maxPageSize = 2, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - asyncTest "only cursor": - ## Given - const contentTopic = "test-content-topic" - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - asyncTest "only cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = false - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3].reversed() - - asyncTest "only cursor - invalid": - ## Given - const contentTopic = "test-content-topic" - - var messages = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let fakeCursor = computeMessageHash(DefaultPubsubTopic, fakeWakuMessage()) - let cursor = ArchiveCursor(hash: fakeCursor) - - ## When - let res = await driver.getMessages( - includeData = true, - contentTopicSeq = @[DefaultContentTopic], - pubsubTopic = none(PubsubTopic), - cursor = some(cursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = @[], - maxPageSize = 5, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - check: - res.value.len == 0 - - asyncTest "content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - asyncTest "content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 5].reversed() - - asyncTest "pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(expected[5][0], expected[5][1]) - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[6 .. 7] - - asyncTest "pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(expected[6][0], expected[6][1]) - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - asyncTest "only hashes - descending order": - ## Given - let timeOrigin = now() - var expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - let hashes = messages.mapIt(computeMessageHash(DefaultPubsubTopic, it)) - - for (msg, hash) in messages.zip(hashes): - require ( - await driver.put( - DefaultPubsubTopic, msg, computeDigest(msg), hash, msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages(hashes = hashes, ascendingOrder = false) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.reversed() - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages - - asyncTest "start time only": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - startTime = some(ts(15, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - asyncTest "end time only": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - endTime = some(ts(45, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - asyncTest "start time and end time": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - startTime = some(ts(15, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[2 .. 4] - - asyncTest "invalid time range - no results": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(45, timeOrigin)), - endTime = some(ts(15, timeOrigin)), - maxPageSize = 2, - ascendingOrder = true, - ) - - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - asyncTest "time range start and content topic": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - asyncTest "time range start and content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6].reversed() - - asyncTest "time range start, single content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[3]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[4 .. 9] - - asyncTest "time range start, single content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[3 .. 4].reversed() - - asyncTest "time range, content topic, pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(0, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[3 .. 4] - - asyncTest "time range, content topic, pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(expected[7][0], expected[7][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - asyncTest "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(expected[1][0], expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - asyncTest "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range, descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(expected[1][0], expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - xasyncTest "Get oldest and newest message timestamp": - ## This test no longer makes sense because that will always be controlled by the newDriver - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let oldestTime = ts(00, timeOrigin) - let newestTime = ts(100, timeOrigin) - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = oldestTime), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = newestTime), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## just keep the second resolution. - ## Notice that the oldest timestamps considers the minimum partition timestamp, which - ## is expressed in seconds. - let oldestPartitionTimestamp = - Timestamp(float(oldestTime) / 1_000_000_000) * 1_000_000_000 - - var res = await driver.getOldestMessageTimestamp() - assert res.isOk(), res.error - - ## We give certain margin of error. The oldest timestamp is obtained from - ## the oldest partition timestamp and there might be at most one second of difference - ## between the time created in the test and the oldest-partition-timestamp created within - ## the driver logic. - assert abs(res.get() - oldestPartitionTimestamp) < (2 * 1_000_000_000), - fmt"Failed to retrieve the latest timestamp {res.get()} != {oldestPartitionTimestamp}" - - res = await driver.getNewestMessageTimestamp() - assert res.isOk(), res.error - assert res.get() == newestTime, "Failed to retrieve the newest timestamp" - - xasyncTest "Delete messages older than certain timestamp": - ## This test no longer makes sense because that will always be controlled by the newDriver - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let targetTime = ts(40, timeOrigin) - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = targetTime), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - var res = await driver.getMessagesCount() - assert res.isOk(), res.error - assert res.get() == 7, "Failed to retrieve the initial number of messages" - - let deleteRes = await driver.deleteMessagesOlderThanTimestamp(targetTime) - assert deleteRes.isOk(), deleteRes.error - - res = await driver.getMessagesCount() - assert res.isOk(), res.error - assert res.get() == 3, "Failed to retrieve the # of messages after deletion" - - xasyncTest "Keep last n messages": - ## This test no longer makes sense because that will always be controlled by the newDriver - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - var res = await driver.getMessagesCount() - assert res.isOk(), res.error - assert res.get() == 7, "Failed to retrieve the initial number of messages" - - let deleteRes = await driver.deleteOldestMessagesNotWithinLimit(2) - assert deleteRes.isOk(), deleteRes.error - - res = await driver.getMessagesCount() - assert res.isOk(), res.error - assert res.get() == 2, "Failed to retrieve the # of messages after deletion" - - asyncTest "Exists table": - var existsRes = await driver.existsTable("version") - assert existsRes.isOk(), existsRes.error - check existsRes.get() == true - - asyncTest "Query by message hash only - legacy": - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ] - var messages = expected - - var hashes = newSeq[WakuMessageHash](0) - for msg in messages: - let hash = computeMessageHash(DefaultPubsubTopic, msg) - hashes.add(hash) - require ( - await driver.put( - DefaultPubsubTopic, msg, computeDigest(msg), hash, msg.timestamp - ) - ).isOk() - - let ret = (await driver.getMessages(hashes = hashes)).valueOr: - assert false, $error - return - - check: - ret.len == 3 - ## (PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash) - ret[2][4] == hashes[0] - ret[1][4] == hashes[1] - ret[0][4] == hashes[2] diff --git a/tests/waku_archive_legacy/test_driver_queue.nim b/tests/waku_archive_legacy/test_driver_queue.nim deleted file mode 100644 index aec9ad65d..000000000 --- a/tests/waku_archive_legacy/test_driver_queue.nim +++ /dev/null @@ -1,182 +0,0 @@ -{.used.} - -import std/options, results, testutils/unittests -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver/queue_driver/queue_driver {.all.}, - waku/waku_archive_legacy/driver/queue_driver/index, - waku/waku_core - -# Helper functions - -proc genIndexedWakuMessage(i: int8): (Index, WakuMessage) = - ## Use i to generate an Index WakuMessage - var data {.noinit.}: array[32, byte] - for x in data.mitems: - x = i.byte - - let - message = WakuMessage(payload: @[byte i], timestamp: Timestamp(i)) - topic = "test-pubsub-topic" - cursor = Index( - receiverTime: Timestamp(i), - senderTime: Timestamp(i), - digest: MessageDigest(data: data), - pubsubTopic: topic, - hash: computeMessageHash(topic, message), - ) - - (cursor, message) - -proc getPrepopulatedTestQueue(unsortedSet: auto, capacity: int): QueueDriver = - let driver = QueueDriver.new(capacity) - - for i in unsortedSet: - let (index, message) = genIndexedWakuMessage(i.int8) - discard driver.add(index, message) - - driver - -procSuite "Sorted driver queue": - test "queue capacity - add a message over the limit": - ## Given - let capacity = 5 - let driver = QueueDriver.new(capacity) - - ## When - # Fill up the queue - for i in 1 .. capacity: - let (index, message) = genIndexedWakuMessage(i.int8) - require(driver.add(index, message).isOk()) - - # Add one more. Capacity should not be exceeded - let (index, message) = genIndexedWakuMessage(capacity.int8 + 1) - require(driver.add(index, message).isOk()) - - ## Then - check: - driver.len == capacity - - test "queue capacity - add message older than oldest in the queue": - ## Given - let capacity = 5 - let driver = QueueDriver.new(capacity) - - ## When - # Fill up the queue - for i in 1 .. capacity: - let (index, message) = genIndexedWakuMessage(i.int8) - require(driver.add(index, message).isOk()) - - # Attempt to add message with older value than oldest in queue should fail - let - oldestTimestamp = driver.first().get().senderTime - (index, message) = genIndexedWakuMessage(oldestTimestamp.int8 - 1) - addRes = driver.add(index, message) - - ## Then - check: - addRes.isErr() - addRes.error() == "too_old" - - check: - driver.len == capacity - - test "queue sort-on-insert": - ## Given - let - capacity = 5 - unsortedSet = [5, 1, 3, 2, 4] - let driver = getPrepopulatedTestQueue(unsortedSet, capacity) - - # Walk forward through the set and verify ascending order - var (prevSmaller, _) = genIndexedWakuMessage(min(unsortedSet).int8 - 1) - for i in driver.fwdIterator: - let (index, _) = i - check cmp(index, prevSmaller) > 0 - prevSmaller = index - - # Walk backward through the set and verify descending order - var (prevLarger, _) = genIndexedWakuMessage(max(unsortedSet).int8 + 1) - for i in driver.bwdIterator: - let (index, _) = i - check cmp(index, prevLarger) < 0 - prevLarger = index - - test "access first item from queue": - ## Given - let - capacity = 5 - unsortedSet = [5, 1, 3, 2, 4] - let driver = getPrepopulatedTestQueue(unsortedSet, capacity) - - ## When - let firstRes = driver.first() - - ## Then - check: - firstRes.isOk() - - let first = firstRes.tryGet() - check: - first.senderTime == Timestamp(1) - - test "get first item from empty queue should fail": - ## Given - let capacity = 5 - let driver = QueueDriver.new(capacity) - - ## When - let firstRes = driver.first() - - ## Then - check: - firstRes.isErr() - firstRes.error() == "Not found" - - test "access last item from queue": - ## Given - let - capacity = 5 - unsortedSet = [5, 1, 3, 2, 4] - let driver = getPrepopulatedTestQueue(unsortedSet, capacity) - - ## When - let lastRes = driver.last() - - ## Then - check: - lastRes.isOk() - - let last = lastRes.tryGet() - check: - last.senderTime == Timestamp(5) - - test "get last item from empty queue should fail": - ## Given - let capacity = 5 - let driver = QueueDriver.new(capacity) - - ## When - let lastRes = driver.last() - - ## Then - check: - lastRes.isErr() - lastRes.error() == "Not found" - - test "verify if queue contains an index": - ## Given - let - capacity = 5 - unsortedSet = [5, 1, 3, 2, 4] - let driver = getPrepopulatedTestQueue(unsortedSet, capacity) - - let - (existingIndex, _) = genIndexedWakuMessage(4) - (nonExistingIndex, _) = genIndexedWakuMessage(99) - - ## Then - check: - driver.contains(existingIndex) == true - driver.contains(nonExistingIndex) == false diff --git a/tests/waku_archive_legacy/test_driver_queue_index.nim b/tests/waku_archive_legacy/test_driver_queue_index.nim deleted file mode 100644 index 404dca8cb..000000000 --- a/tests/waku_archive_legacy/test_driver_queue_index.nim +++ /dev/null @@ -1,219 +0,0 @@ -{.used.} - -import std/[times, random], stew/byteutils, testutils/unittests, nimcrypto -import waku/waku_core, waku/waku_archive_legacy/driver/queue_driver/index - -var rng = initRand() - -## Helpers - -proc getTestTimestamp(offset = 0): Timestamp = - let now = getNanosecondTime(epochTime() + float(offset)) - Timestamp(now) - -proc hashFromStr(input: string): MDigest[256] = - var ctx: sha256 - - ctx.init() - ctx.update(input.toBytes()) - let hashed = ctx.finish() - ctx.clear() - - return hashed - -proc randomHash(): WakuMessageHash = - var hash: WakuMessageHash - - for i in 0 ..< hash.len: - let numb: byte = byte(rng.next()) - hash[i] = numb - - hash - -suite "Queue Driver - index": - ## Test vars - let - smallIndex1 = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(1000), - hash: randomHash(), - ) - smallIndex2 = Index( - digest: hashFromStr("1234567"), # digest is less significant than senderTime - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(1000), - hash: randomHash(), - ) - largeIndex1 = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(9000), - hash: randomHash(), - ) # only senderTime differ from smallIndex1 - largeIndex2 = Index( - digest: hashFromStr("12345"), # only digest differs from smallIndex1 - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(1000), - hash: randomHash(), - ) - eqIndex1 = Index( - digest: hashFromStr("0003"), - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(54321), - hash: randomHash(), - ) - eqIndex2 = Index( - digest: hashFromStr("0003"), - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(54321), - hash: randomHash(), - ) - eqIndex3 = Index( - digest: hashFromStr("0003"), - receiverTime: getNanosecondTime(9999), - # receiverTime difference should have no effect on comparisons - senderTime: getNanosecondTime(54321), - hash: randomHash(), - ) - diffPsTopic = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(1000), - pubsubTopic: "zzzz", - hash: randomHash(), - ) - noSenderTime1 = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(1100), - senderTime: getNanosecondTime(0), - pubsubTopic: "zzzz", - hash: randomHash(), - ) - noSenderTime2 = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(10000), - senderTime: getNanosecondTime(0), - pubsubTopic: "zzzz", - hash: randomHash(), - ) - noSenderTime3 = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(1200), - senderTime: getNanosecondTime(0), - pubsubTopic: "aaaa", - hash: randomHash(), - ) - noSenderTime4 = Index( - digest: hashFromStr("0"), - receiverTime: getNanosecondTime(1200), - senderTime: getNanosecondTime(0), - pubsubTopic: "zzzz", - hash: randomHash(), - ) - - test "Index comparison": - # Index comparison with senderTime diff - check: - cmp(smallIndex1, largeIndex1) < 0 - cmp(smallIndex2, largeIndex1) < 0 - - # Index comparison with digest diff - check: - cmp(smallIndex1, smallIndex2) < 0 - cmp(smallIndex1, largeIndex2) < 0 - cmp(smallIndex2, largeIndex2) > 0 - cmp(largeIndex1, largeIndex2) > 0 - - # Index comparison when equal - check: - cmp(eqIndex1, eqIndex2) == 0 - - # pubsubTopic difference - check: - cmp(smallIndex1, diffPsTopic) < 0 - - # receiverTime diff plays no role when senderTime set - check: - cmp(eqIndex1, eqIndex3) == 0 - - # receiverTime diff plays no role when digest/pubsubTopic equal - check: - cmp(noSenderTime1, noSenderTime2) == 0 - - # sort on receiverTime with no senderTimestamp and unequal pubsubTopic - check: - cmp(noSenderTime1, noSenderTime3) < 0 - - # sort on receiverTime with no senderTimestamp and unequal digest - check: - cmp(noSenderTime1, noSenderTime4) < 0 - - # sort on receiverTime if no senderTimestamp on only one side - check: - cmp(smallIndex1, noSenderTime1) < 0 - cmp(noSenderTime1, smallIndex1) > 0 # Test symmetry - cmp(noSenderTime2, eqIndex3) < 0 - cmp(eqIndex3, noSenderTime2) > 0 # Test symmetry - - test "Index equality": - # Exactly equal - check: - eqIndex1 == eqIndex2 - - # Receiver time plays no role, even without sender time - check: - eqIndex1 == eqIndex3 - noSenderTime1 == noSenderTime2 # only receiver time differs, indices are equal - noSenderTime1 != noSenderTime3 # pubsubTopics differ - noSenderTime1 != noSenderTime4 # digests differ - - # Unequal sender time - check: - smallIndex1 != largeIndex1 - - # Unequal digest - check: - smallIndex1 != smallIndex2 - - # Unequal hash and digest - check: - smallIndex1 != eqIndex1 - - # Unequal pubsubTopic - check: - smallIndex1 != diffPsTopic - - test "Index computation should not be empty": - ## Given - let ts = getTestTimestamp() - let wm = WakuMessage(payload: @[byte 1, 2, 3], timestamp: ts) - - ## When - let ts2 = getTestTimestamp() + 10 - let index = Index.compute(wm, ts2, DefaultContentTopic) - - ## Then - check: - index.digest.data.len != 0 - index.digest.data.len == 32 # sha2 output length in bytes - index.receiverTime == ts2 # the receiver timestamp should be a non-zero value - index.senderTime == ts - index.pubsubTopic == DefaultContentTopic - - test "Index digest of two identical messsage should be the same": - ## Given - let topic = ContentTopic("test-content-topic") - let - wm1 = WakuMessage(payload: @[byte 1, 2, 3], contentTopic: topic) - wm2 = WakuMessage(payload: @[byte 1, 2, 3], contentTopic: topic) - - ## When - let ts = getTestTimestamp() - let - index1 = Index.compute(wm1, ts, DefaultPubsubTopic) - index2 = Index.compute(wm2, ts, DefaultPubsubTopic) - - ## Then - check: - index1.digest == index2.digest diff --git a/tests/waku_archive_legacy/test_driver_queue_pagination.nim b/tests/waku_archive_legacy/test_driver_queue_pagination.nim deleted file mode 100644 index 05d9759a2..000000000 --- a/tests/waku_archive_legacy/test_driver_queue_pagination.nim +++ /dev/null @@ -1,405 +0,0 @@ -{.used.} - -import - std/[options, sequtils, algorithm], testutils/unittests, libp2p/protobuf/minprotobuf -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver/queue_driver/queue_driver {.all.}, - waku/waku_archive_legacy/driver/queue_driver/index, - waku/waku_core, - ../testlib/wakucore - -proc getTestQueueDriver(numMessages: int): QueueDriver = - let testQueueDriver = QueueDriver.new(numMessages) - - var data {.noinit.}: array[32, byte] - for x in data.mitems: - x = 1 - - for i in 0 ..< numMessages: - let msg = WakuMessage(payload: @[byte i], timestamp: Timestamp(i)) - - let index = Index( - receiverTime: Timestamp(i), - senderTime: Timestamp(i), - digest: MessageDigest(data: data), - hash: computeMessageHash(DefaultPubsubTopic, msg), - ) - - discard testQueueDriver.add(index, msg) - - return testQueueDriver - -procSuite "Queue driver - pagination": - let driver = getTestQueueDriver(10) - let - indexList: seq[Index] = toSeq(driver.fwdIterator()).mapIt(it[0]) - msgList: seq[WakuMessage] = toSeq(driver.fwdIterator()).mapIt(it[1]) - - test "Forward pagination - normal pagination": - ## Given - let - pageSize: uint = 2 - cursor: Option[Index] = some(indexList[3]) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 2 - data == msgList[4 .. 5] - - test "Forward pagination - initial pagination request with an empty cursor": - ## Given - let - pageSize: uint = 2 - cursor: Option[Index] = none(Index) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 2 - data == msgList[0 .. 1] - - test "Forward pagination - initial pagination request with an empty cursor to fetch the entire history": - ## Given - let - pageSize: uint = 13 - cursor: Option[Index] = none(Index) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 10 - data == msgList[0 .. 9] - - test "Forward pagination - empty msgList": - ## Given - let driver = getTestQueueDriver(0) - let - pageSize: uint = 2 - cursor: Option[Index] = none(Index) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Forward pagination - page size larger than the remaining messages": - ## Given - let - pageSize: uint = 10 - cursor: Option[Index] = some(indexList[3]) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 6 - data == msgList[4 .. 9] - - test "Forward pagination - page size larger than the maximum allowed page size": - ## Given - let - pageSize: uint = MaxPageSize + 1 - cursor: Option[Index] = some(indexList[3]) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - uint(data.len) <= MaxPageSize - - test "Forward pagination - cursor pointing to the end of the message list": - ## Given - let - pageSize: uint = 10 - cursor: Option[Index] = some(indexList[9]) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Forward pagination - invalid cursor": - ## Given - let msg = fakeWakuMessage(payload = @[byte 10]) - let index = ArchiveCursor( - pubsubTopic: DefaultPubsubTopic, - senderTime: msg.timestamp, - storeTime: msg.timestamp, - digest: computeDigest(msg), - ).toIndex() - - let - pageSize: uint = 10 - cursor: Option[Index] = some(index) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let error = page.tryError() - check: - error == QueueDriverErrorKind.INVALID_CURSOR - - test "Forward pagination - initial paging query over a message list with one message": - ## Given - let driver = getTestQueueDriver(1) - let - pageSize: uint = 10 - cursor: Option[Index] = none(Index) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 1 - - test "Forward pagination - pagination over a message list with one message": - ## Given - let driver = getTestQueueDriver(1) - let - pageSize: uint = 10 - cursor: Option[Index] = some(indexList[0]) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Forward pagination - with pradicate": - ## Given - let - pageSize: uint = 3 - cursor: Option[Index] = none(Index) - forward = true - - proc onlyEvenTimes(index: Index, msg: WakuMessage): bool = - msg.timestamp.int64 mod 2 == 0 - - ## When - let page = driver.getPage( - pageSize = pageSize, forward = forward, cursor = cursor, predicate = onlyEvenTimes - ) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.mapIt(it.timestamp.int) == @[0, 2, 4] - - test "Backward pagination - normal pagination": - ## Given - let - pageSize: uint = 2 - cursor: Option[Index] = some(indexList[3]) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data == msgList[1 .. 2].reversed - - test "Backward pagination - empty msgList": - ## Given - let driver = getTestQueueDriver(0) - let - pageSize: uint = 2 - cursor: Option[Index] = none(Index) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Backward pagination - initial pagination request with an empty cursor": - ## Given - let - pageSize: uint = 2 - cursor: Option[Index] = none(Index) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 2 - data == msgList[8 .. 9].reversed - - test "Backward pagination - initial pagination request with an empty cursor to fetch the entire history": - ## Given - let - pageSize: uint = 13 - cursor: Option[Index] = none(Index) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 10 - data == msgList[0 .. 9].reversed - - test "Backward pagination - page size larger than the remaining messages": - ## Given - let - pageSize: uint = 5 - cursor: Option[Index] = some(indexList[3]) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data == msgList[0 .. 2].reversed - - test "Backward pagination - page size larger than the Maximum allowed page size": - ## Given - let - pageSize: uint = MaxPageSize + 1 - cursor: Option[Index] = some(indexList[3]) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - uint(data.len) <= MaxPageSize - - test "Backward pagination - cursor pointing to the begining of the message list": - ## Given - let - pageSize: uint = 5 - cursor: Option[Index] = some(indexList[0]) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Backward pagination - invalid cursor": - ## Given - let msg = fakeWakuMessage(payload = @[byte 10]) - let index = ArchiveCursor( - pubsubTopic: DefaultPubsubTopic, - senderTime: msg.timestamp, - storeTime: msg.timestamp, - digest: computeDigest(msg), - ).toIndex() - - let - pageSize: uint = 2 - cursor: Option[Index] = some(index) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let error = page.tryError() - check: - error == QueueDriverErrorKind.INVALID_CURSOR - - test "Backward pagination - initial paging query over a message list with one message": - ## Given - let driver = getTestQueueDriver(1) - let - pageSize: uint = 10 - cursor: Option[Index] = none(Index) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 1 - - test "Backward pagination - paging query over a message list with one message": - ## Given - let driver = getTestQueueDriver(1) - let - pageSize: uint = 10 - cursor: Option[Index] = some(indexList[0]) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Backward pagination - with predicate": - ## Given - let - pageSize: uint = 3 - cursor: Option[Index] = none(Index) - forward = false - - proc onlyOddTimes(index: Index, msg: WakuMessage): bool = - msg.timestamp.int64 mod 2 != 0 - - ## When - let page = driver.getPage( - pageSize = pageSize, forward = forward, cursor = cursor, predicate = onlyOddTimes - ) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.mapIt(it.timestamp.int) == @[5, 7, 9].reversed diff --git a/tests/waku_archive_legacy/test_driver_queue_query.nim b/tests/waku_archive_legacy/test_driver_queue_query.nim deleted file mode 100644 index 0726d1931..000000000 --- a/tests/waku_archive_legacy/test_driver_queue_query.nim +++ /dev/null @@ -1,1668 +0,0 @@ -{.used.} - -import - std/[options, sequtils, random, algorithm], testutils/unittests, chronos, chronicles -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver/queue_driver, - waku/waku_core, - waku/waku_core/message/digest, - ../testlib/common, - ../testlib/wakucore - -logScope: - topics = "test archive queue_driver" - -# Initialize the random number generator -common.randomize() - -proc newTestSqliteDriver(): ArchiveDriver = - QueueDriver.new(capacity = 50) - -proc computeTestCursor(pubsubTopic: PubsubTopic, message: WakuMessage): ArchiveCursor = - ArchiveCursor( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - storeTime: message.timestamp, - digest: computeDigest(message), - hash: computeMessageHash(pubsubTopic, message), - ) - -suite "Queue driver - query by content topic": - test "no content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages(maxPageSize = 5, ascendingOrder = true) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "single content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "single content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = false - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[6 .. 7].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "multiple content topic": - ## Given - const contentTopic1 = "test-content-topic-1" - const contentTopic2 = "test-content-topic-2" - const contentTopic3 = "test-content-topic-3" - - let driver = newTestSqliteDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], - maxPageSize = 2, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "single content topic - no results": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "content topic and max page size - not enough messages stored": - ## Given - const pageSize: uint = 50 - - let driver = newTestSqliteDriver() - - for t in 0 ..< 40: - let msg = fakeWakuMessage(@[byte t], DefaultContentTopic, ts = ts(t)) - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[DefaultContentTopic], - maxPageSize = pageSize, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 40 - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - -suite "SQLite driver - query by pubsub topic": - test "pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - pubsubTopic = some(pubsubTopic), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "no pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages(maxPageSize = 2, ascendingOrder = true) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[0 .. 1] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "content topic and pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - maxPageSize = 2, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - -suite "Queue driver - query by cursor": - test "only cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = waitFor driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "only cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = waitFor driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = false - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "only cursor - invalid": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - var messages = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let fakeCursor = computeMessageHash(DefaultPubsubTopic, fakeWakuMessage()) - let cursor = ArchiveCursor(hash: fakeCursor) - - ## When - let res = waitFor driver.getMessages( - includeData = true, - contentTopic = @[DefaultContentTopic], - pubsubTopic = none(PubsubTopic), - cursor = some(cursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = @[], - maxPageSize = 5, - ascendingOrder = true, - ) - - ## Then - check: - res.isErr() - res.error == "invalid_cursor" - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 5].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(expected[5][0], expected[5][1]) - - ## When - let res = waitFor driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[6 .. 7] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(expected[6][0], expected[6][1]) - - ## When - let res = waitFor driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - -suite "Queue driver - query by time range": - test "start time only": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - startTime = some(ts(15, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "end time only": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - endTime = some(ts(45, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "start time and end time": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - startTime = some(ts(15, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[2 .. 4] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "invalid time range - no results": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(45, timeOrigin)), - endTime = some(ts(15, timeOrigin)), - maxPageSize = 2, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - asynctest "time range start and content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - ## Cleanup - (await driver.close()).expect("driver to close") - - test "time range start and content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - asynctest "time range start, single content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[3]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[4 .. 9] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asynctest "time range start, single content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[3 .. 4].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - test "time range, content topic, pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[1][1]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(0, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[3 .. 4] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "time range, content topic, pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(expected[7][0], expected[7][1]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(expected[1][0], expected[1][1]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range, descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(expected[1][0], expected[1][1]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (waitFor driver.close()).expect("driver to close") diff --git a/tests/waku_archive_legacy/test_driver_sqlite.nim b/tests/waku_archive_legacy/test_driver_sqlite.nim deleted file mode 100644 index 9d8c4d14b..000000000 --- a/tests/waku_archive_legacy/test_driver_sqlite.nim +++ /dev/null @@ -1,58 +0,0 @@ -{.used.} - -import std/sequtils, testutils/unittests, chronos -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver/sqlite_driver, - waku/waku_core, - ../waku_archive_legacy/archive_utils, - ../testlib/wakucore - -suite "SQLite driver": - test "init driver and database": - ## Given - let database = newSqliteDatabase() - - ## When - let driverRes = SqliteDriver.new(database) - - ## Then - check: - driverRes.isOk() - - let driver: ArchiveDriver = driverRes.tryGet() - check: - not driver.isNil() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "insert a message": - ## Given - const contentTopic = "test-content-topic" - const meta = "test meta" - - let driver = newSqliteArchiveDriver() - - let msg = fakeWakuMessage(contentTopic = contentTopic, meta = meta) - let msgHash = computeMessageHash(DefaultPubsubTopic, msg) - - ## When - let putRes = waitFor driver.put( - DefaultPubsubTopic, msg, computeDigest(msg), msgHash, msg.timestamp - ) - - ## Then - check: - putRes.isOk() - - let storedMsg = (waitFor driver.getAllMessages()).tryGet() - check: - storedMsg.len == 1 - storedMsg.all do(item: auto) -> bool: - let (pubsubTopic, actualMsg, _, _, hash) = item - actualMsg.contentTopic == contentTopic and pubsubTopic == DefaultPubsubTopic and - hash == msgHash and msg.meta == actualMsg.meta - - ## Cleanup - (waitFor driver.close()).expect("driver to close") diff --git a/tests/waku_archive_legacy/test_driver_sqlite_query.nim b/tests/waku_archive_legacy/test_driver_sqlite_query.nim deleted file mode 100644 index 3c3b55232..000000000 --- a/tests/waku_archive_legacy/test_driver_sqlite_query.nim +++ /dev/null @@ -1,1745 +0,0 @@ -{.used.} - -import - std/[options, sequtils, random, algorithm], testutils/unittests, chronos, chronicles - -import - waku/waku_archive_legacy, - waku/waku_core, - waku/waku_core/message/digest, - ../testlib/common, - ../testlib/wakucore, - ../waku_archive_legacy/archive_utils - -logScope: - topics = "test archive _driver" - -# Initialize the random number generator -common.randomize() - -suite "SQLite driver - query by content topic": - asyncTest "no content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages(maxPageSize = 5, ascendingOrder = true) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "single content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "single content topic with meta field": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), - fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" - ), - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" - ), - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" - ), - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" - ), - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" - ), - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "single content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = false - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[6 .. 7].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "multiple content topic": - ## Given - const contentTopic1 = "test-content-topic-1" - const contentTopic2 = "test-content-topic-2" - const contentTopic3 = "test-content-topic-3" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], - maxPageSize = 2, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "single content topic - no results": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "content topic and max page size - not enough messages stored": - ## Given - const pageSize: uint = 50 - - let driver = newSqliteArchiveDriver() - - for t in 0 ..< 40: - let msg = fakeWakuMessage(@[byte t], DefaultContentTopic, ts = ts(t)) - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[DefaultContentTopic], - maxPageSize = pageSize, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 40 - - ## Cleanup - (await driver.close()).expect("driver to close") - -suite "SQLite driver - query by pubsub topic": - asyncTest "pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "no pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages(maxPageSize = 2, ascendingOrder = true) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[0 .. 1] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "content topic and pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - maxPageSize = 2, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (await driver.close()).expect("driver to close") - -suite "SQLite driver - query by cursor": - asyncTest "only cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "only cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = false - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "only cursor - invalid": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - var messages = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let fakeCursor = computeMessageHash(DefaultPubsubTopic, fakeWakuMessage()) - let cursor = ArchiveCursor(hash: fakeCursor) - - ## When - let res = await driver.getMessages( - includeData = true, - contentTopic = @[DefaultContentTopic], - pubsubTopic = none(PubsubTopic), - cursor = some(cursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = @[], - maxPageSize = 5, - ascendingOrder = true, - ) - - ## Then - check: - res.isErr() - res.error == "cursor not found" - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 5].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(expected[5][0], expected[5][1]) - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[6 .. 7] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(expected[6][0], expected[6][1]) - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - -suite "SQLite driver - query by time range": - asyncTest "start time only": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - startTime = some(ts(15, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "end time only": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - endTime = some(ts(45, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "start time and end time": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - startTime = some(ts(15, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[2 .. 4] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "invalid time range - no results": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(45, timeOrigin)), - endTime = some(ts(15, timeOrigin)), - maxPageSize = 2, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range start and content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range start and content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range start, single content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[3]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[4 .. 9] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range start, single content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[3 .. 4].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range, content topic, pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(0, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[3 .. 4] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range, content topic, pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(expected[7][0], expected[7][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(expected[1][0], expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range, descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(expected[1][0], expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (await driver.close()).expect("driver to close") diff --git a/tests/waku_archive_legacy/test_waku_archive.nim b/tests/waku_archive_legacy/test_waku_archive.nim deleted file mode 100644 index f6373608d..000000000 --- a/tests/waku_archive_legacy/test_waku_archive.nim +++ /dev/null @@ -1,532 +0,0 @@ -{.used.} - -import std/[options, sequtils], testutils/unittests, chronos, libp2p/crypto/crypto - -import - waku/common/paging, - waku/waku_core, - waku/waku_core/message/digest, - waku/waku_archive_legacy, - ../waku_archive_legacy/archive_utils, - ../testlib/wakucore - -suite "Waku Archive - message handling": - test "it should archive a valid and non-ephemeral message": - ## Setup - let driver = newSqliteArchiveDriver() - let archive = newWakuArchive(driver) - - ## Given - let validSenderTime = now() - let message = fakeWakuMessage(ephemeral = false, ts = validSenderTime) - - ## When - waitFor archive.handleMessage(DefaultPubSubTopic, message) - - ## Then - check: - (waitFor driver.getMessagesCount()).tryGet() == 1 - - test "it should not archive ephemeral messages": - ## Setup - let driver = newSqliteArchiveDriver() - let archive = newWakuArchive(driver) - - ## Given - let msgList = @[ - fakeWakuMessage(ephemeral = false, payload = "1"), - fakeWakuMessage(ephemeral = true, payload = "2"), - fakeWakuMessage(ephemeral = true, payload = "3"), - fakeWakuMessage(ephemeral = true, payload = "4"), - fakeWakuMessage(ephemeral = false, payload = "5"), - ] - - ## When - for msg in msgList: - waitFor archive.handleMessage(DefaultPubsubTopic, msg) - - ## Then - check: - (waitFor driver.getMessagesCount()).tryGet() == 2 - - test "it should archive a message with no sender timestamp": - ## Setup - let driver = newSqliteArchiveDriver() - let archive = newWakuArchive(driver) - - ## Given - let invalidSenderTime = 0 - let message = fakeWakuMessage(ts = invalidSenderTime) - - ## When - waitFor archive.handleMessage(DefaultPubSubTopic, message) - - ## Then - check: - (waitFor driver.getMessagesCount()).tryGet() == 1 - - test "it should not archive a message with a sender time variance greater than max time variance (future)": - ## Setup - let driver = newSqliteArchiveDriver() - let archive = newWakuArchive(driver) - - ## Given - let - now = now() - invalidSenderTime = now + MaxMessageTimestampVariance + 1_000_000_000 - # 1 second over the max variance - - let message = fakeWakuMessage(ts = invalidSenderTime) - - ## When - waitFor archive.handleMessage(DefaultPubSubTopic, message) - - ## Then - check: - (waitFor driver.getMessagesCount()).tryGet() == 0 - - test "it should not archive a message with a sender time variance greater than max time variance (past)": - ## Setup - let driver = newSqliteArchiveDriver() - let archive = newWakuArchive(driver) - - ## Given - let - now = now() - invalidSenderTime = now - MaxMessageTimestampVariance - 1 - - let message = fakeWakuMessage(ts = invalidSenderTime) - - ## When - waitFor archive.handleMessage(DefaultPubSubTopic, message) - - ## Then - check: - (waitFor driver.getMessagesCount()).tryGet() == 0 - -procSuite "Waku Archive - find messages": - ## Fixtures - let timeOrigin = now() - let msgListA = @[ - fakeWakuMessage( - @[byte 00], contentTopic = ContentTopic("2"), ts = ts(00, timeOrigin) - ), - fakeWakuMessage( - @[byte 01], contentTopic = ContentTopic("1"), ts = ts(10, timeOrigin) - ), - fakeWakuMessage( - @[byte 02], contentTopic = ContentTopic("2"), ts = ts(20, timeOrigin) - ), - fakeWakuMessage( - @[byte 03], contentTopic = ContentTopic("1"), ts = ts(30, timeOrigin) - ), - fakeWakuMessage( - @[byte 04], contentTopic = ContentTopic("2"), ts = ts(40, timeOrigin) - ), - fakeWakuMessage( - @[byte 05], contentTopic = ContentTopic("1"), ts = ts(50, timeOrigin) - ), - fakeWakuMessage( - @[byte 06], contentTopic = ContentTopic("2"), ts = ts(60, timeOrigin) - ), - fakeWakuMessage( - @[byte 07], contentTopic = ContentTopic("1"), ts = ts(70, timeOrigin) - ), - fakeWakuMessage( - @[byte 08], contentTopic = ContentTopic("2"), ts = ts(80, timeOrigin) - ), - fakeWakuMessage( - @[byte 09], contentTopic = ContentTopic("1"), ts = ts(90, timeOrigin) - ), - ] - - let archiveA = block: - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - for msg in msgListA: - require ( - waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - archive - - test "handle query": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let topic = ContentTopic("1") - let - msg1 = fakeWakuMessage(contentTopic = topic) - msg2 = fakeWakuMessage() - - waitFor archive.handleMessage("foo", msg1) - waitFor archive.handleMessage("foo", msg2) - - ## Given - let req = ArchiveQuery(includeData: true, contentTopics: @[topic]) - - ## When - let queryRes = waitFor archive.findMessages(req) - - ## Then - check: - queryRes.isOk() - - let response = queryRes.tryGet() - check: - response.messages.len == 1 - response.messages == @[msg1] - - test "handle query with multiple content filters": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let - topic1 = ContentTopic("1") - topic2 = ContentTopic("2") - topic3 = ContentTopic("3") - - let - msg1 = fakeWakuMessage(contentTopic = topic1) - msg2 = fakeWakuMessage(contentTopic = topic2) - msg3 = fakeWakuMessage(contentTopic = topic3) - - waitFor archive.handleMessage("foo", msg1) - waitFor archive.handleMessage("foo", msg2) - waitFor archive.handleMessage("foo", msg3) - - ## Given - let req = ArchiveQuery(includeData: true, contentTopics: @[topic1, topic3]) - - ## When - let queryRes = waitFor archive.findMessages(req) - - ## Then - check: - queryRes.isOk() - - let response = queryRes.tryGet() - check: - response.messages.len() == 2 - response.messages.anyIt(it == msg1) - response.messages.anyIt(it == msg3) - - test "handle query with more than 10 content filters": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let queryTopics = toSeq(1 .. 15).mapIt(ContentTopic($it)) - - ## Given - let req = ArchiveQuery(contentTopics: queryTopics) - - ## When - let queryRes = waitFor archive.findMessages(req) - - ## Then - check: - queryRes.isErr() - - let error = queryRes.tryError() - check: - error.kind == ArchiveErrorKind.INVALID_QUERY - error.cause == "too many content topics" - - test "handle query with pubsub topic filter": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let - pubsubTopic1 = "queried-topic" - pubsubTopic2 = "non-queried-topic" - - let - contentTopic1 = ContentTopic("1") - contentTopic2 = ContentTopic("2") - contentTopic3 = ContentTopic("3") - - let - msg1 = fakeWakuMessage(contentTopic = contentTopic1) - msg2 = fakeWakuMessage(contentTopic = contentTopic2) - msg3 = fakeWakuMessage(contentTopic = contentTopic3) - - waitFor archive.handleMessage(pubsubtopic1, msg1) - waitFor archive.handleMessage(pubsubtopic2, msg2) - waitFor archive.handleMessage(pubsubtopic2, msg3) - - ## Given - # This query targets: pubsubtopic1 AND (contentTopic1 OR contentTopic3) - let req = ArchiveQuery( - includeData: true, - pubsubTopic: some(pubsubTopic1), - contentTopics: @[contentTopic1, contentTopic3], - ) - - ## When - let queryRes = waitFor archive.findMessages(req) - - ## Then - check: - queryRes.isOk() - - let response = queryRes.tryGet() - check: - response.messages.len() == 1 - response.messages.anyIt(it == msg1) - - test "handle query with pubsub topic filter - no match": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let - pubsubtopic1 = "queried-topic" - pubsubtopic2 = "non-queried-topic" - - let - msg1 = fakeWakuMessage() - msg2 = fakeWakuMessage() - msg3 = fakeWakuMessage() - - waitFor archive.handleMessage(pubsubtopic2, msg1) - waitFor archive.handleMessage(pubsubtopic2, msg2) - waitFor archive.handleMessage(pubsubtopic2, msg3) - - ## Given - let req = ArchiveQuery(pubsubTopic: some(pubsubTopic1)) - - ## When - let res = waitFor archive.findMessages(req) - - ## Then - check: - res.isOk() - - let response = res.tryGet() - check: - response.messages.len() == 0 - - test "handle query with pubsub topic filter - match the entire stored messages": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let pubsubTopic = "queried-topic" - - let - msg1 = fakeWakuMessage(payload = "TEST-1") - msg2 = fakeWakuMessage(payload = "TEST-2") - msg3 = fakeWakuMessage(payload = "TEST-3") - - waitFor archive.handleMessage(pubsubTopic, msg1) - waitFor archive.handleMessage(pubsubTopic, msg2) - waitFor archive.handleMessage(pubsubTopic, msg3) - - ## Given - let req = ArchiveQuery(includeData: true, pubsubTopic: some(pubsubTopic)) - - ## When - let res = waitFor archive.findMessages(req) - - ## Then - check: - res.isOk() - - let response = res.tryGet() - check: - response.messages.len() == 3 - response.messages.anyIt(it == msg1) - response.messages.anyIt(it == msg2) - response.messages.anyIt(it == msg3) - - test "handle query with forward pagination": - ## Given - let req = - ArchiveQuery(includeData: true, pageSize: 4, direction: PagingDirection.FORWARD) - - ## When - var nextReq = req # copy - - var pages = newSeq[seq[WakuMessage]](3) - var cursors = newSeq[Option[ArchiveCursor]](3) - - for i in 0 ..< 3: - let res = waitFor archiveA.findMessages(nextReq) - require res.isOk() - - # Keep query response content - let response = res.get() - pages[i] = response.messages - cursors[i] = response.cursor - - # Set/update the request cursor - nextReq.cursor = cursors[i] - - ## Then - check: - cursors[0] == some(computeArchiveCursor(DefaultPubsubTopic, msgListA[3])) - cursors[1] == some(computeArchiveCursor(DefaultPubsubTopic, msgListA[7])) - cursors[2] == none(ArchiveCursor) - - check: - pages[0] == msgListA[0 .. 3] - pages[1] == msgListA[4 .. 7] - pages[2] == msgListA[8 .. 9] - - test "handle query with backward pagination": - ## Given - let req = - ArchiveQuery(includeData: true, pageSize: 4, direction: PagingDirection.BACKWARD) - - ## When - var nextReq = req # copy - - var pages = newSeq[seq[WakuMessage]](3) - var cursors = newSeq[Option[ArchiveCursor]](3) - - for i in 0 ..< 3: - let res = waitFor archiveA.findMessages(nextReq) - require res.isOk() - - # Keep query response content - let response = res.get() - pages[i] = response.messages - cursors[i] = response.cursor - - # Set/update the request cursor - nextReq.cursor = cursors[i] - - ## Then - check: - cursors[0] == some(computeArchiveCursor(DefaultPubsubTopic, msgListA[6])) - cursors[1] == some(computeArchiveCursor(DefaultPubsubTopic, msgListA[2])) - cursors[2] == none(ArchiveCursor) - - check: - pages[0] == msgListA[6 .. 9] - pages[1] == msgListA[2 .. 5] - pages[2] == msgListA[0 .. 1] - - test "handle query with no paging info - auto-pagination": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let msgList = @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2")), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 5], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 6], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 7], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 8], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2")), - ] - - for msg in msgList: - require ( - waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## Given - let req = ArchiveQuery(includeData: true, contentTopics: @[DefaultContentTopic]) - - ## When - let res = waitFor archive.findMessages(req) - - ## Then - check: - res.isOk() - - let response = res.tryGet() - check: - ## No pagination specified. Response will be auto-paginated with - ## up to MaxPageSize messages per page. - response.messages.len() == 8 - response.cursor.isNone() - - test "handle temporal history query with a valid time window": - ## Given - let req = ArchiveQuery( - includeData: true, - contentTopics: @[ContentTopic("1")], - startTime: some(ts(15, timeOrigin)), - endTime: some(ts(55, timeOrigin)), - direction: PagingDirection.FORWARD, - ) - - ## When - let res = waitFor archiveA.findMessages(req) - - ## Then - check res.isOk() - - let response = res.tryGet() - check: - response.messages.len() == 2 - response.messages.mapIt(it.timestamp) == @[ts(30, timeOrigin), ts(50, timeOrigin)] - - test "handle temporal history query with a zero-size time window": - ## A zero-size window results in an empty list of history messages - ## Given - let req = ArchiveQuery( - contentTopics: @[ContentTopic("1")], - startTime: some(Timestamp(2)), - endTime: some(Timestamp(2)), - ) - - ## When - let res = waitFor archiveA.findMessages(req) - - ## Then - check res.isOk() - - let response = res.tryGet() - check: - response.messages.len == 0 - - test "handle temporal history query with an invalid time window": - ## A history query with an invalid time range results in an empty list of history messages - ## Given - let req = ArchiveQuery( - contentTopics: @[ContentTopic("1")], - startTime: some(Timestamp(5)), - endTime: some(Timestamp(2)), - ) - - ## When - let res = waitFor archiveA.findMessages(req) - - ## Then - check res.isOk() - - let response = res.tryGet() - check: - response.messages.len == 0 diff --git a/tests/waku_store_legacy/store_utils.nim b/tests/waku_store_legacy/store_utils.nim deleted file mode 100644 index a70ca9376..000000000 --- a/tests/waku_store_legacy/store_utils.nim +++ /dev/null @@ -1,33 +0,0 @@ -{.used.} - -import std/options, chronos - -import - waku/[node/peer_manager, waku_core, waku_store_legacy, waku_store_legacy/client], - ../testlib/[common, wakucore] - -proc newTestWakuStore*( - switch: Switch, handler: HistoryQueryHandler -): Future[WakuStore] {.async.} = - let - peerManager = PeerManager.new(switch) - proto = WakuStore.new(peerManager, rng, handler) - - await proto.start() - switch.mount(proto) - - return proto - -proc newTestWakuStoreClient*(switch: Switch): WakuStoreClient = - let peerManager = PeerManager.new(switch) - WakuStoreClient.new(peerManager, rng) - -proc computeHistoryCursor*( - pubsubTopic: PubsubTopic, message: WakuMessage -): HistoryCursor = - HistoryCursor( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - storeTime: message.timestamp, - digest: computeDigest(message), - ) diff --git a/tests/waku_store_legacy/test_all.nim b/tests/waku_store_legacy/test_all.nim deleted file mode 100644 index b495310f2..000000000 --- a/tests/waku_store_legacy/test_all.nim +++ /dev/null @@ -1,8 +0,0 @@ -{.used.} - -import - ./test_client, - ./test_resume, - ./test_rpc_codec, - ./test_waku_store, - ./test_wakunode_store diff --git a/tests/waku_store_legacy/test_client.nim b/tests/waku_store_legacy/test_client.nim deleted file mode 100644 index 2a8616375..000000000 --- a/tests/waku_store_legacy/test_client.nim +++ /dev/null @@ -1,214 +0,0 @@ -{.used.} - -import std/options, testutils/unittests, chronos, libp2p/crypto/crypto - -import - waku/[ - node/peer_manager, - waku_core, - waku_store_legacy, - waku_store_legacy/client, - common/paging, - ], - ../testlib/[wakucore, testasync, futures], - ./store_utils - -suite "Store Client": - var message1 {.threadvar.}: WakuMessage - var message2 {.threadvar.}: WakuMessage - var message3 {.threadvar.}: WakuMessage - var messageSeq {.threadvar.}: seq[WakuMessage] - var handlerFuture {.threadvar.}: Future[HistoryQuery] - var handler {.threadvar.}: HistoryQueryHandler - var historyQuery {.threadvar.}: HistoryQuery - - var serverSwitch {.threadvar.}: Switch - var clientSwitch {.threadvar.}: Switch - - var server {.threadvar.}: WakuStore - var client {.threadvar.}: WakuStoreClient - - var serverPeerInfo {.threadvar.}: RemotePeerInfo - var clientPeerInfo {.threadvar.}: RemotePeerInfo - - asyncSetup: - message1 = fakeWakuMessage(contentTopic = DefaultContentTopic) - message2 = fakeWakuMessage(contentTopic = DefaultContentTopic) - message3 = fakeWakuMessage(contentTopic = DefaultContentTopic) - messageSeq = @[message1, message2, message3] - handlerFuture = newLegacyHistoryFuture() - handler = proc(req: HistoryQuery): Future[HistoryResult] {.async, gcsafe.} = - handlerFuture.complete(req) - return ok(HistoryResponse(messages: messageSeq)) - historyQuery = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[DefaultContentTopic], - direction: PagingDirection.FORWARD, - requestId: "customRequestId", - ) - - serverSwitch = newTestSwitch() - clientSwitch = newTestSwitch() - - server = await newTestWakuStore(serverSwitch, handler = handler) - client = newTestWakuStoreClient(clientSwitch) - - await allFutures(serverSwitch.start(), clientSwitch.start()) - - ## The following sleep is aimed to prevent macos failures in CI - #[ -2024-05-16T13:24:45.5106200Z INF 2024-05-16 13:24:45.509+00:00 Stopping AutonatService topics="libp2p autonatservice" tid=53712 file=service.nim:203 -2024-05-16T13:24:45.5107960Z WRN 2024-05-16 13:24:45.509+00:00 service is already stopped topics="libp2p switch" tid=53712 file=switch.nim:86 -2024-05-16T13:24:45.5109010Z . (1.68s) -2024-05-16T13:24:45.5109320Z Store Client (0.00s) -2024-05-16T13:24:45.5109870Z SIGSEGV: Illegal storage access. (Attempt to read from nil?) -2024-05-16T13:24:45.5111470Z stack trace: (most recent call last) - ]# - await sleepAsync(500.millis) - - serverPeerInfo = serverSwitch.peerInfo.toRemotePeerInfo() - clientPeerInfo = clientSwitch.peerInfo.toRemotePeerInfo() - - asyncTeardown: - await allFutures(serverSwitch.stop(), clientSwitch.stop()) - - suite "HistoryQuery Creation and Execution": - asyncTest "Valid Queries": - # When a valid query is sent to the server - let queryResponse = await client.query(historyQuery, peer = serverPeerInfo) - - # Then the query is processed successfully - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == historyQuery - queryResponse.get().messages == messageSeq - - asyncTest "Invalid Queries": - # TODO: IMPROVE: We can't test "actual" invalid queries because - # it directly depends on the handler implementation, to achieve - # proper coverage we'd need an example implementation. - - # Given some invalid queries - let - invalidQuery1 = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[], - direction: PagingDirection.FORWARD, - requestId: "reqId1", - ) - invalidQuery2 = HistoryQuery( - pubsubTopic: PubsubTopic.none(), - contentTopics: @[DefaultContentTopic], - direction: PagingDirection.FORWARD, - requestId: "reqId2", - ) - invalidQuery3 = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[DefaultContentTopic], - pageSize: 0, - requestId: "reqId3", - ) - invalidQuery4 = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[DefaultContentTopic], - pageSize: 0, - requestId: "reqId4", - ) - invalidQuery5 = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[DefaultContentTopic], - startTime: some(0.Timestamp), - endTime: some(0.Timestamp), - requestId: "reqId5", - ) - invalidQuery6 = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[DefaultContentTopic], - startTime: some(0.Timestamp), - endTime: some(-1.Timestamp), - requestId: "reqId6", - ) - - # When the query is sent to the server - let queryResponse1 = await client.query(invalidQuery1, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery1 - queryResponse1.get().messages == messageSeq - - # When the query is sent to the server - handlerFuture = newLegacyHistoryFuture() - let queryResponse2 = await client.query(invalidQuery2, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery2 - queryResponse2.get().messages == messageSeq - - # When the query is sent to the server - handlerFuture = newLegacyHistoryFuture() - let queryResponse3 = await client.query(invalidQuery3, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery3 - queryResponse3.get().messages == messageSeq - - # When the query is sent to the server - handlerFuture = newLegacyHistoryFuture() - let queryResponse4 = await client.query(invalidQuery4, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery4 - queryResponse4.get().messages == messageSeq - - # When the query is sent to the server - handlerFuture = newLegacyHistoryFuture() - let queryResponse5 = await client.query(invalidQuery5, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery5 - queryResponse5.get().messages == messageSeq - - # When the query is sent to the server - handlerFuture = newLegacyHistoryFuture() - let queryResponse6 = await client.query(invalidQuery6, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery6 - queryResponse6.get().messages == messageSeq - - suite "Verification of HistoryResponse Payload": - asyncTest "Positive Responses": - # When a valid query is sent to the server - let queryResponse = await client.query(historyQuery, peer = serverPeerInfo) - - # Then the query is processed successfully, and is of the expected type - check: - await handlerFuture.withTimeout(FUTURE_TIMEOUT) - type(queryResponse.get()) is HistoryResponse - - asyncTest "Negative Responses - PeerDialFailure": - # Given a stopped peer - let - otherServerSwitch = newTestSwitch() - otherServerPeerInfo = otherServerSwitch.peerInfo.toRemotePeerInfo() - - # When a query is sent to the stopped peer - let queryResponse = await client.query(historyQuery, peer = otherServerPeerInfo) - - # Then the query is not processed - check: - not await handlerFuture.withTimeout(FUTURE_TIMEOUT) - queryResponse.isErr() - queryResponse.error.kind == HistoryErrorKind.PEER_DIAL_FAILURE diff --git a/tests/waku_store_legacy/test_resume.nim b/tests/waku_store_legacy/test_resume.nim deleted file mode 100644 index 0f8132f08..000000000 --- a/tests/waku_store_legacy/test_resume.nim +++ /dev/null @@ -1,338 +0,0 @@ -{.used.} - -when defined(waku_exp_store_resume): - # TODO: Review store resume test cases (#1282) - # Ongoing changes to test code base had ruin this test meanwhile, need to investigate and fix - - import - std/[options, tables, sets], - testutils/unittests, - chronos, - chronicles, - libp2p/crypto/crypto - import - waku/[ - common/databases/db_sqlite, - waku_archive_legacy/driver, - waku_archive_legacy/driver/sqlite_driver/sqlite_driver, - node/peer_manager, - waku_core, - waku_core/message/digest, - waku_store_legacy, - ], - ../waku_store_legacy/store_utils, - ../waku_archive_legacy/archive_utils, - ./testlib/common, - ./testlib/switch - - procSuite "Waku Store - resume store": - ## Fixtures - let storeA = block: - let store = newTestMessageStore() - let msgList = @[ - fakeWakuMessage( - payload = @[byte 0], contentTopic = ContentTopic("2"), ts = ts(0) - ), - fakeWakuMessage( - payload = @[byte 1], contentTopic = ContentTopic("1"), ts = ts(1) - ), - fakeWakuMessage( - payload = @[byte 2], contentTopic = ContentTopic("2"), ts = ts(2) - ), - fakeWakuMessage( - payload = @[byte 3], contentTopic = ContentTopic("1"), ts = ts(3) - ), - fakeWakuMessage( - payload = @[byte 4], contentTopic = ContentTopic("2"), ts = ts(4) - ), - fakeWakuMessage( - payload = @[byte 5], contentTopic = ContentTopic("1"), ts = ts(5) - ), - fakeWakuMessage( - payload = @[byte 6], contentTopic = ContentTopic("2"), ts = ts(6) - ), - fakeWakuMessage( - payload = @[byte 7], contentTopic = ContentTopic("1"), ts = ts(7) - ), - fakeWakuMessage( - payload = @[byte 8], contentTopic = ContentTopic("2"), ts = ts(8) - ), - fakeWakuMessage( - payload = @[byte 9], contentTopic = ContentTopic("1"), ts = ts(9) - ), - ] - - for msg in msgList: - require store - .put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - .isOk() - - store - - let storeB = block: - let store = newTestMessageStore() - let msgList2 = @[ - fakeWakuMessage( - payload = @[byte 0], contentTopic = ContentTopic("2"), ts = ts(0) - ), - fakeWakuMessage( - payload = @[byte 11], contentTopic = ContentTopic("1"), ts = ts(1) - ), - fakeWakuMessage( - payload = @[byte 12], contentTopic = ContentTopic("2"), ts = ts(2) - ), - fakeWakuMessage( - payload = @[byte 3], contentTopic = ContentTopic("1"), ts = ts(3) - ), - fakeWakuMessage( - payload = @[byte 4], contentTopic = ContentTopic("2"), ts = ts(4) - ), - fakeWakuMessage( - payload = @[byte 5], contentTopic = ContentTopic("1"), ts = ts(5) - ), - fakeWakuMessage( - payload = @[byte 13], contentTopic = ContentTopic("2"), ts = ts(6) - ), - fakeWakuMessage( - payload = @[byte 14], contentTopic = ContentTopic("1"), ts = ts(7) - ), - ] - - for msg in msgList2: - require store - .put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - .isOk() - - store - - asyncTest "multiple query to multiple peers with pagination": - ## Setup - let - serverSwitchA = newTestSwitch() - serverSwitchB = newTestSwitch() - clientSwitch = newTestSwitch() - - await allFutures( - serverSwitchA.start(), serverSwitchB.start(), clientSwitch.start() - ) - - let - serverA = await newTestWakuStoreNode(serverSwitchA, store = testStore) - serverB = await newTestWakuStoreNode(serverSwitchB, store = testStore) - client = newTestWakuStoreClient(clientSwitch) - - ## Given - let peers = @[ - serverSwitchA.peerInfo.toRemotePeerInfo(), - serverSwitchB.peerInfo.toRemotePeerInfo(), - ] - let req = HistoryQuery(contentTopics: @[DefaultContentTopic], pageSize: 5) - - ## When - let res = await client.queryLoop(req, peers) - - ## Then - check: - res.isOk() - - let response = res.tryGet() - check: - response.len == 10 - - ## Cleanup - await allFutures(clientSwitch.stop(), serverSwitchA.stop(), serverSwitchB.stop()) - - asyncTest "resume message history": - ## Setup - let - serverSwitch = newTestSwitch() - clientSwitch = newTestSwitch() - - await allFutures(serverSwitch.start(), clientSwitch.start()) - - let - server = await newTestWakuStore(serverSwitch, store = storeA) - client = await newTestWakuStore(clientSwitch) - - client.setPeer(serverSwitch.peerInfo.toRemotePeerInfo()) - - ## When - let res = await client.resume() - - ## Then - check res.isOk() - - let resumedMessagesCount = res.tryGet() - let storedMessagesCount = client.store.getMessagesCount().tryGet() - check: - resumedMessagesCount == 10 - storedMessagesCount == 10 - - ## Cleanup - await allFutures(clientSwitch.stop(), serverSwitch.stop()) - - asyncTest "resume history from a list of candidates - offline peer": - ## Setup - let - clientSwitch = newTestSwitch() - offlineSwitch = newTestSwitch() - - await clientSwitch.start() - - let client = await newTestWakuStore(clientSwitch) - - ## Given - let peers = @[offlineSwitch.peerInfo.toRemotePeerInfo()] - - ## When - let res = await client.resume(some(peers)) - - ## Then - check res.isErr() - - ## Cleanup - await clientSwitch.stop() - - asyncTest "resume history from a list of candidates - online and offline peers": - ## Setup - let - offlineSwitch = newTestSwitch() - serverASwitch = newTestSwitch() - serverBSwitch = newTestSwitch() - clientSwitch = newTestSwitch() - - await allFutures( - serverASwitch.start(), serverBSwitch.start(), clientSwitch.start() - ) - - let - serverA = await newTestWakuStore(serverASwitch, store = storeA) - serverB = await newTestWakuStore(serverBSwitch, store = storeB) - client = await newTestWakuStore(clientSwitch) - - ## Given - let peers = @[ - offlineSwitch.peerInfo.toRemotePeerInfo(), - serverASwitch.peerInfo.toRemotePeerInfo(), - serverBSwitch.peerInfo.toRemotePeerInfo(), - ] - - ## When - let res = await client.resume(some(peers)) - - ## Then - # `client` is expected to retrieve 14 messages: - # - The store mounted on `serverB` holds 10 messages (see `storeA` fixture) - # - The store mounted on `serverB` holds 7 messages (see `storeB` fixture) - # Both stores share 3 messages, resulting in 14 unique messages in total - check res.isOk() - - let restoredMessagesCount = res.tryGet() - let storedMessagesCount = client.store.getMessagesCount().tryGet() - check: - restoredMessagesCount == 14 - storedMessagesCount == 14 - - ## Cleanup - await allFutures(serverASwitch.stop(), serverBSwitch.stop(), clientSwitch.stop()) - - suite "WakuNode - waku store": - asyncTest "Resume proc fetches the history": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - await allFutures(client.start(), server.start()) - - let driver = newSqliteArchiveDriver() - server.mountArchive(some(driver), none(MessageValidator), none(RetentionPolicy)) - await server.mountStore() - - let clientStore = StoreQueueRef.new() - await client.mountStore(store = clientStore) - client.mountStoreClient(store = clientStore) - - ## Given - let message = fakeWakuMessage() - require server.wakuStore.store.put(DefaultPubsubTopic, message).isOk() - - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - await client.resume(some(@[serverPeer])) - - # Then - check: - client.wakuStore.store.getMessagesCount().tryGet() == 1 - - ## Cleanup - await allFutures(client.stop(), server.stop()) - - asyncTest "Resume proc discards duplicate messages": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - await allFutures(server.start(), client.start()) - await server.mountStore(store = StoreQueueRef.new()) - - let clientStore = StoreQueueRef.new() - await client.mountStore(store = clientStore) - client.mountStoreClient(store = clientStore) - - ## Given - let timeOrigin = now() - let - msg1 = fakeWakuMessage( - payload = "hello world1", ts = (timeOrigin + getNanoSecondTime(1)) - ) - msg2 = fakeWakuMessage( - payload = "hello world2", ts = (timeOrigin + getNanoSecondTime(2)) - ) - msg3 = fakeWakuMessage( - payload = "hello world3", ts = (timeOrigin + getNanoSecondTime(3)) - ) - - require server.wakuStore.store.put(DefaultPubsubTopic, msg1).isOk() - require server.wakuStore.store.put(DefaultPubsubTopic, msg2).isOk() - - # Insert the same message in both node's store - let - receivedTime3 = now() + getNanosecondTime(10) - digest3 = computeDigest(msg3) - require server.wakuStore.store - .put(DefaultPubsubTopic, msg3, digest3, receivedTime3) - .isOk() - require client.wakuStore.store - .put(DefaultPubsubTopic, msg3, digest3, receivedTime3) - .isOk() - - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - await client.resume(some(@[serverPeer])) - - ## Then - check: - # If the duplicates are discarded properly, then the total number of messages after resume should be 3 - client.wakuStore.store.getMessagesCount().tryGet() == 3 - - await allFutures(client.stop(), server.stop()) diff --git a/tests/waku_store_legacy/test_rpc_codec.nim b/tests/waku_store_legacy/test_rpc_codec.nim deleted file mode 100644 index 2801cc9a8..000000000 --- a/tests/waku_store_legacy/test_rpc_codec.nim +++ /dev/null @@ -1,184 +0,0 @@ -{.used.} - -import std/options, testutils/unittests, chronos -import - waku/[ - common/protobuf, - common/paging, - waku_core, - waku_store_legacy/rpc, - waku_store_legacy/rpc_codec, - ], - ../testlib/wakucore - -procSuite "Waku Store - RPC codec": - test "PagingIndexRPC protobuf codec": - ## Given - let index = PagingIndexRPC.compute( - fakeWakuMessage(), receivedTime = ts(), pubsubTopic = DefaultPubsubTopic - ) - - ## When - let encodedIndex = index.encode() - let decodedIndexRes = PagingIndexRPC.decode(encodedIndex.buffer) - - ## Then - check: - decodedIndexRes.isOk() - - let decodedIndex = decodedIndexRes.tryGet() - check: - # The fields of decodedIndex must be the same as the original index - decodedIndex == index - - test "PagingIndexRPC protobuf codec - empty index": - ## Given - let emptyIndex = PagingIndexRPC() - - let encodedIndex = emptyIndex.encode() - let decodedIndexRes = PagingIndexRPC.decode(encodedIndex.buffer) - - ## Then - check: - decodedIndexRes.isOk() - - let decodedIndex = decodedIndexRes.tryGet() - check: - # Check the correctness of init and encode for an empty PagingIndexRPC - decodedIndex == emptyIndex - - test "PagingInfoRPC protobuf codec": - ## Given - let - index = PagingIndexRPC.compute( - fakeWakuMessage(), receivedTime = ts(), pubsubTopic = DefaultPubsubTopic - ) - pagingInfo = PagingInfoRPC( - pageSize: some(1'u64), - cursor: some(index), - direction: some(PagingDirection.FORWARD), - ) - - ## When - let pb = pagingInfo.encode() - let decodedPagingInfo = PagingInfoRPC.decode(pb.buffer) - - ## Then - check: - decodedPagingInfo.isOk() - - check: - # The fields of decodedPagingInfo must be the same as the original pagingInfo - decodedPagingInfo.value == pagingInfo - decodedPagingInfo.value.direction == pagingInfo.direction - - test "PagingInfoRPC protobuf codec - empty paging info": - ## Given - let emptyPagingInfo = PagingInfoRPC() - - ## When - let pb = emptyPagingInfo.encode() - let decodedEmptyPagingInfo = PagingInfoRPC.decode(pb.buffer) - - ## Then - check: - decodedEmptyPagingInfo.isOk() - - check: - # check the correctness of init and encode for an empty PagingInfoRPC - decodedEmptyPagingInfo.value == emptyPagingInfo - - test "HistoryQueryRPC protobuf codec": - ## Given - let - index = PagingIndexRPC.compute( - fakeWakuMessage(), receivedTime = ts(), pubsubTopic = DefaultPubsubTopic - ) - pagingInfo = PagingInfoRPC( - pageSize: some(1'u64), - cursor: some(index), - direction: some(PagingDirection.BACKWARD), - ) - query = HistoryQueryRPC( - contentFilters: @[ - HistoryContentFilterRPC(contentTopic: DefaultContentTopic), - HistoryContentFilterRPC(contentTopic: DefaultContentTopic), - ], - pagingInfo: some(pagingInfo), - startTime: some(Timestamp(10)), - endTime: some(Timestamp(11)), - ) - - ## When - let pb = query.encode() - let decodedQuery = HistoryQueryRPC.decode(pb.buffer) - - ## Then - check: - decodedQuery.isOk() - - check: - # the fields of decoded query decodedQuery must be the same as the original query query - decodedQuery.value == query - - test "HistoryQueryRPC protobuf codec - empty history query": - ## Given - let emptyQuery = HistoryQueryRPC() - - ## When - let pb = emptyQuery.encode() - let decodedEmptyQuery = HistoryQueryRPC.decode(pb.buffer) - - ## Then - check: - decodedEmptyQuery.isOk() - - check: - # check the correctness of init and encode for an empty HistoryQueryRPC - decodedEmptyQuery.value == emptyQuery - - test "HistoryResponseRPC protobuf codec": - ## Given - let - message = fakeWakuMessage() - index = PagingIndexRPC.compute( - message, receivedTime = ts(), pubsubTopic = DefaultPubsubTopic - ) - pagingInfo = PagingInfoRPC( - pageSize: some(1'u64), - cursor: some(index), - direction: some(PagingDirection.BACKWARD), - ) - res = HistoryResponseRPC( - messages: @[message], - pagingInfo: some(pagingInfo), - error: HistoryResponseErrorRPC.INVALID_CURSOR, - ) - - ## When - let pb = res.encode() - let decodedRes = HistoryResponseRPC.decode(pb.buffer) - - ## Then - check: - decodedRes.isOk() - - check: - # the fields of decoded response decodedRes must be the same as the original response res - decodedRes.value == res - - test "HistoryResponseRPC protobuf codec - empty history response": - ## Given - let emptyRes = HistoryResponseRPC() - - ## When - let pb = emptyRes.encode() - let decodedEmptyRes = HistoryResponseRPC.decode(pb.buffer) - - ## Then - check: - decodedEmptyRes.isOk() - - check: - # check the correctness of init and encode for an empty HistoryResponseRPC - decodedEmptyRes.value == emptyRes diff --git a/tests/waku_store_legacy/test_waku_store.nim b/tests/waku_store_legacy/test_waku_store.nim deleted file mode 100644 index b8dc835c8..000000000 --- a/tests/waku_store_legacy/test_waku_store.nim +++ /dev/null @@ -1,113 +0,0 @@ -{.used.} - -import testutils/unittests, chronos, libp2p/crypto/crypto - -import - waku/[ - common/paging, - node/peer_manager, - waku_core, - waku_store_legacy, - waku_store_legacy/client, - ], - ../testlib/wakucore, - ./store_utils - -suite "Waku Store - query handler legacy": - asyncTest "history query handler should be called": - ## Setup - let - serverSwitch = newTestSwitch() - clientSwitch = newTestSwitch() - - await allFutures(serverSwitch.start(), clientSwitch.start()) - - ## Given - let serverPeerInfo = serverSwitch.peerInfo.toRemotePeerInfo() - - let msg = fakeWakuMessage(contentTopic = DefaultContentTopic) - - var queryHandlerFut = newFuture[(HistoryQuery)]() - - let queryHandler = proc( - req: HistoryQuery - ): Future[HistoryResult] {.async, gcsafe.} = - queryHandlerFut.complete(req) - return ok(HistoryResponse(messages: @[msg])) - - let - server = await newTestWakuStore(serverSwitch, handler = queryhandler) - client = newTestWakuStoreClient(clientSwitch) - - let req = HistoryQuery( - contentTopics: @[DefaultContentTopic], - direction: PagingDirection.FORWARD, - requestId: "reqId", - ) - - ## When - let queryRes = await client.query(req, peer = serverPeerInfo) - - ## Then - check: - not queryHandlerFut.failed() - queryRes.isOk() - - let request = queryHandlerFut.read() - check: - request == req - - let response = queryRes.tryGet() - check: - response.messages.len == 1 - response.messages == @[msg] - - ## Cleanup - await allFutures(serverSwitch.stop(), clientSwitch.stop()) - - asyncTest "history query handler should be called and return an error": - ## Setup - let - serverSwitch = newTestSwitch() - clientSwitch = newTestSwitch() - - await allFutures(serverSwitch.start(), clientSwitch.start()) - - ## Given - let serverPeerInfo = serverSwitch.peerInfo.toRemotePeerInfo() - - var queryHandlerFut = newFuture[(HistoryQuery)]() - let queryHandler = proc( - req: HistoryQuery - ): Future[HistoryResult] {.async, gcsafe.} = - queryHandlerFut.complete(req) - return err(HistoryError(kind: HistoryErrorKind.BAD_REQUEST)) - - let - server = await newTestWakuStore(serverSwitch, handler = queryhandler) - client = newTestWakuStoreClient(clientSwitch) - - let req = HistoryQuery( - contentTopics: @[DefaultContentTopic], - direction: PagingDirection.FORWARD, - requestId: "reqId", - ) - - ## When - let queryRes = await client.query(req, peer = serverPeerInfo) - - ## Then - check: - not queryHandlerFut.failed() - queryRes.isErr() - - let request = queryHandlerFut.read() - check: - request == req - - let error = queryRes.tryError() - check: - error.kind == HistoryErrorKind.BAD_REQUEST - - ## Cleanup - await allFutures(serverSwitch.stop(), clientSwitch.stop()) diff --git a/tests/waku_store_legacy/test_wakunode_store.nim b/tests/waku_store_legacy/test_wakunode_store.nim deleted file mode 100644 index 58e3ca9e0..000000000 --- a/tests/waku_store_legacy/test_wakunode_store.nim +++ /dev/null @@ -1,315 +0,0 @@ -{.used.} - -import - std/net, - testutils/unittests, - chronos, - libp2p/crypto/crypto, - libp2p/peerid, - libp2p/multiaddress, - libp2p/switch, - libp2p/protocols/pubsub/pubsub, - libp2p/protocols/pubsub/gossipsub -import - waku/[ - common/paging, - waku_core, - waku_core/message/digest, - node/peer_manager, - waku_archive_legacy, - waku_filter_v2, - waku_filter_v2/client, - waku_store_legacy, - waku_node, - ], - ../waku_store_legacy/store_utils, - ../waku_archive_legacy/archive_utils, - ../testlib/wakucore, - ../testlib/wakunode - -procSuite "WakuNode - Store Legacy": - ## Fixtures - let timeOrigin = now() - let msgListA = @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] - - let archiveA = block: - let driver = newSqliteArchiveDriver() - - for msg in msgListA: - let msg_digest = waku_archive_legacy.computeDigest(msg) - let msg_hash = computeMessageHash(DefaultPubsubTopic, msg) - require ( - waitFor driver.put(DefaultPubsubTopic, msg, msg_digest, msg_hash, msg.timestamp) - ).isOk() - - driver - - test "Store protocol returns expected messages": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - waitFor allFutures(client.start(), server.start()) - - let mountArchiveRes = server.mountLegacyArchive(archiveA) - assert mountArchiveRes.isOk(), mountArchiveRes.error - - waitFor server.mountLegacyStore() - - client.mountLegacyStoreClient() - - ## Given - let req = HistoryQuery(contentTopics: @[DefaultContentTopic]) - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - let queryRes = waitFor client.query(req, peer = serverPeer) - - ## Then - check queryRes.isOk() - - let response = queryRes.get() - check: - response.messages == msgListA - - # Cleanup - waitFor allFutures(client.stop(), server.stop()) - - test "Store node history response - forward pagination": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - waitFor allFutures(client.start(), server.start()) - - let mountArchiveRes = server.mountLegacyArchive(archiveA) - assert mountArchiveRes.isOk(), mountArchiveRes.error - - waitFor server.mountLegacyStore() - - client.mountLegacyStoreClient() - - ## Given - let req = HistoryQuery( - contentTopics: @[DefaultContentTopic], - pageSize: 7, - direction: PagingDirection.FORWARD, - ) - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - var nextReq = req # copy - - var pages = newSeq[seq[WakuMessage]](2) - var cursors = newSeq[Option[HistoryCursor]](2) - - for i in 0 ..< 2: - let res = waitFor client.query(nextReq, peer = serverPeer) - require res.isOk() - - # Keep query response content - let response = res.get() - pages[i] = response.messages - cursors[i] = response.cursor - - # Set/update the request cursor - nextReq.cursor = cursors[i] - - ## Then - check: - cursors[0] == some(computeHistoryCursor(DefaultPubsubTopic, msgListA[6])) - cursors[1] == none(HistoryCursor) - - check: - pages[0] == msgListA[0 .. 6] - pages[1] == msgListA[7 .. 9] - - # Cleanup - waitFor allFutures(client.stop(), server.stop()) - - test "Store node history response - backward pagination": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - waitFor allFutures(client.start(), server.start()) - - let mountArchiveRes = server.mountLegacyArchive(archiveA) - assert mountArchiveRes.isOk(), mountArchiveRes.error - - waitFor server.mountLegacyStore() - - client.mountLegacyStoreClient() - - ## Given - let req = HistoryQuery( - contentTopics: @[DefaultContentTopic], - pageSize: 7, - direction: PagingDirection.BACKWARD, - ) - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - var nextReq = req # copy - - var pages = newSeq[seq[WakuMessage]](2) - var cursors = newSeq[Option[HistoryCursor]](2) - - for i in 0 ..< 2: - let res = waitFor client.query(nextReq, peer = serverPeer) - require res.isOk() - - # Keep query response content - let response = res.get() - pages[i] = response.messages - cursors[i] = response.cursor - - # Set/update the request cursor - nextReq.cursor = cursors[i] - - ## Then - check: - cursors[0] == some(computeHistoryCursor(DefaultPubsubTopic, msgListA[3])) - cursors[1] == none(HistoryCursor) - - check: - pages[0] == msgListA[3 .. 9] - pages[1] == msgListA[0 .. 2] - - # Cleanup - waitFor allFutures(client.stop(), server.stop()) - - test "Store protocol returns expected message when relay is disabled and filter enabled": - ## See nwaku issue #937: 'Store: ability to decouple store from relay' - ## Setup - let - filterSourceKey = generateSecp256k1Key() - filterSource = - newTestWakuNode(filterSourceKey, parseIpAddress("0.0.0.0"), Port(0)) - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - waitFor allFutures(client.start(), server.start(), filterSource.start()) - - waitFor filterSource.mountFilter() - let driver = newSqliteArchiveDriver() - - let mountArchiveRes = server.mountLegacyArchive(driver) - assert mountArchiveRes.isOk(), mountArchiveRes.error - - waitFor server.mountLegacyStore() - waitFor server.mountFilterClient() - client.mountLegacyStoreClient() - - ## Given - let message = fakeWakuMessage() - let - serverPeer = server.peerInfo.toRemotePeerInfo() - filterSourcePeer = filterSource.peerInfo.toRemotePeerInfo() - - ## Then - let filterFut = newFuture[(PubsubTopic, WakuMessage)]() - proc filterHandler( - pubsubTopic: PubsubTopic, msg: WakuMessage - ) {.async, gcsafe, closure.} = - await server.wakuLegacyArchive.handleMessage(pubsubTopic, msg) - filterFut.complete((pubsubTopic, msg)) - - server.wakuFilterClient.registerPushHandler(filterHandler) - let resp = waitFor server.filterSubscribe( - some(DefaultPubsubTopic), DefaultContentTopic, peer = filterSourcePeer - ) - - waitFor sleepAsync(100.millis) - - waitFor filterSource.wakuFilter.handleMessage(DefaultPubsubTopic, message) - - # Wait for the server filter to receive the push message - require waitFor filterFut.withTimeout(5.seconds) - - let res = waitFor client.query( - HistoryQuery(contentTopics: @[DefaultContentTopic]), peer = serverPeer - ) - - ## Then - check res.isOk() - - let response = res.get() - check: - response.messages.len == 1 - response.messages[0] == message - - let (handledPubsubTopic, handledMsg) = filterFut.read() - check: - handledPubsubTopic == DefaultPubsubTopic - handledMsg == message - - ## Cleanup - waitFor allFutures(client.stop(), server.stop(), filterSource.stop()) - - test "history query should return INVALID_CURSOR if the cursor has empty data in the request": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - waitFor allFutures(client.start(), server.start()) - - let mountArchiveRes = server.mountLegacyArchive(archiveA) - assert mountArchiveRes.isOk(), mountArchiveRes.error - - waitFor server.mountLegacyStore() - - client.mountLegacyStoreClient() - - ## Forcing a bad cursor with empty digest data - var data: array[32, byte] = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ] - let cursor = HistoryCursor( - pubsubTopic: "pubsubTopic", - senderTime: now(), - storeTime: now(), - digest: waku_archive_legacy.MessageDigest(data: data), - ) - - ## Given - let req = HistoryQuery(contentTopics: @[DefaultContentTopic], cursor: some(cursor)) - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - let queryRes = waitFor client.query(req, peer = serverPeer) - - ## Then - check not queryRes.isOk() - - check queryRes.error == - "legacy store client query error: BAD_REQUEST: invalid cursor" - - # Cleanup - waitFor allFutures(client.stop(), server.stop()) diff --git a/tests/wakunode_rest/test_rest_health.nim b/tests/wakunode_rest/test_rest_health.nim index 37abaf4f5..0bdb93123 100644 --- a/tests/wakunode_rest/test_rest_health.nim +++ b/tests/wakunode_rest/test_rest_health.nim @@ -86,7 +86,7 @@ suite "Waku v2 REST API - health": response.status == 200 $response.contentType == $MIMETYPE_JSON report.nodeHealth == HealthStatus.READY - report.protocolsHealth.len() == 15 + report.protocolsHealth.len() == 13 report.getHealth(RelayProtocol).health == HealthStatus.NOT_READY report.getHealth(RelayProtocol).desc == some("No connected peers") @@ -97,7 +97,6 @@ suite "Waku v2 REST API - health": report.getHealth(LegacyLightpushProtocol).health == HealthStatus.NOT_MOUNTED report.getHealth(FilterProtocol).health == HealthStatus.NOT_MOUNTED report.getHealth(StoreProtocol).health == HealthStatus.NOT_MOUNTED - report.getHealth(LegacyStoreProtocol).health == HealthStatus.NOT_MOUNTED report.getHealth(PeerExchangeProtocol).health == HealthStatus.NOT_MOUNTED report.getHealth(RendezvousProtocol).health == HealthStatus.NOT_MOUNTED report.getHealth(MixProtocol).health == HealthStatus.NOT_MOUNTED @@ -108,7 +107,6 @@ suite "Waku v2 REST API - health": report.getHealth(LegacyLightpushClientProtocol).health == HealthStatus.NOT_MOUNTED report.getHealth(StoreClientProtocol).health == HealthStatus.NOT_MOUNTED - report.getHealth(LegacyStoreClientProtocol).health == HealthStatus.NOT_MOUNTED report.getHealth(FilterClientProtocol).health == HealthStatus.NOT_READY report.getHealth(FilterClientProtocol).desc == diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 541215f76..a99ba43ee 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -348,12 +348,6 @@ hence would have reachability issues.""", desc: "Enable/disable waku store protocol", defaultValue: false, name: "store" .}: bool - legacyStore* {. - desc: "Enable/disable support of Waku Store v2 as a service", - defaultValue: false, - name: "legacy-store" - .}: bool - storenode* {. desc: "Peer multiaddress to query for storage", defaultValue: "", @@ -691,7 +685,7 @@ with the drawback of consuming some more bandwidth.""", desc: "Rate limit settings for different protocols." & "Format: protocol:volume/period" & - " Where 'protocol' can be one of: if not defined it means a global setting" & + " Where 'protocol' can be one of: if not defined it means a global setting" & " 'volume' and period must be an integer value. " & " 'unit' must be one of - hours, minutes, seconds, milliseconds respectively. " & "Argument may be repeated.", @@ -1045,7 +1039,6 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.withContentTopics(n.contentTopics) b.storeServiceConf.withEnabled(n.store) - b.storeServiceConf.withSupportV2(n.legacyStore) b.storeServiceConf.withRetentionPolicies(n.storeMessageRetentionPolicy) b.storeServiceConf.withDbUrl(n.storeMessageDbUrl) b.storeServiceConf.withDbVacuum(n.storeMessageDbVacuum) diff --git a/waku/common/rate_limit/setting.nim b/waku/common/rate_limit/setting.nim index 70f0ee721..66ff79d17 100644 --- a/waku/common/rate_limit/setting.nim +++ b/waku/common/rate_limit/setting.nim @@ -7,7 +7,6 @@ type RateLimitSetting* = tuple[volume: int, period: Duration] type RateLimitedProtocol* = enum GLOBAL - STOREV2 STOREV3 LIGHTPUSH PEEREXCHG @@ -47,8 +46,6 @@ proc translate(sProtocol: string): RateLimitedProtocol {.raises: [ValueError].} case sProtocol of "global": return GLOBAL - of "storev2": - return STOREV2 of "storev3": return STOREV3 of "lightpush": @@ -65,7 +62,6 @@ proc fillSettingTable( ) {.raises: [ValueError].} = if sProtocol == "store": # generic store will only applies to version which is not listed directly - discard t.hasKeyOrPut(STOREV2, setting) discard t.hasKeyOrPut(STOREV3, setting) else: let protocol = translate(sProtocol) @@ -87,7 +83,7 @@ proc parse*( ## group4: Unit of period - only h:hour, m:minute, s:second, ms:millisecond allowed ## whitespaces are allowed lazily const parseRegex = - """^\s*((store|storev2|storev3|lightpush|px|filter)\s*:)?\s*(\d+)\s*\/\s*(\d+)\s*(s|h|m|ms)\s*$""" + """^\s*((store|storev3|lightpush|px|filter)\s*:)?\s*(\d+)\s*\/\s*(\d+)\s*(s|h|m|ms)\s*$""" const regexParseSize = re2(parseRegex) for settingStr in settings: let aSetting = settingStr.toLower() diff --git a/waku/common/waku_protocol.nim b/waku/common/waku_protocol.nim index 5063f4c98..76a8aded0 100644 --- a/waku/common/waku_protocol.nim +++ b/waku/common/waku_protocol.nim @@ -4,7 +4,6 @@ type WakuProtocol* {.pure.} = enum RelayProtocol = "Relay" RlnRelayProtocol = "Rln Relay" StoreProtocol = "Store" - LegacyStoreProtocol = "Legacy Store" FilterProtocol = "Filter" LightpushProtocol = "Lightpush" LegacyLightpushProtocol = "Legacy Lightpush" @@ -12,13 +11,12 @@ type WakuProtocol* {.pure.} = enum RendezvousProtocol = "Rendezvous" MixProtocol = "Mix" StoreClientProtocol = "Store Client" - LegacyStoreClientProtocol = "Legacy Store Client" FilterClientProtocol = "Filter Client" LightpushClientProtocol = "Lightpush Client" LegacyLightpushClientProtocol = "Legacy Lightpush Client" const RelayProtocols* = {RelayProtocol} - StoreClientProtocols* = {StoreClientProtocol, LegacyStoreClientProtocol} + StoreClientProtocols* = {StoreClientProtocol} LightpushClientProtocols* = {LightpushClientProtocol, LegacyLightpushClientProtocol} FilterClientProtocols* = {FilterClientProtocol} diff --git a/waku/factory/conf_builder/store_service_conf_builder.nim b/waku/factory/conf_builder/store_service_conf_builder.nim index 30c743e01..f1b0b1402 100644 --- a/waku/factory/conf_builder/store_service_conf_builder.nim +++ b/waku/factory/conf_builder/store_service_conf_builder.nim @@ -14,7 +14,6 @@ type StoreServiceConfBuilder* = object dbMigration*: Option[bool] dbURl*: Option[string] dbVacuum*: Option[bool] - supportV2*: Option[bool] maxNumDbConnections*: Option[int] retentionPolicies*: seq[string] resume*: Option[bool] @@ -35,9 +34,6 @@ proc withDbUrl*(b: var StoreServiceConfBuilder, dbUrl: string) = proc withDbVacuum*(b: var StoreServiceConfBuilder, dbVacuum: bool) = b.dbVacuum = some(dbVacuum) -proc withSupportV2*(b: var StoreServiceConfBuilder, supportV2: bool) = - b.supportV2 = some(supportV2) - proc withMaxNumDbConnections*( b: var StoreServiceConfBuilder, maxNumDbConnections: int ) = @@ -104,7 +100,6 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string dbMigration: b.dbMigration.get(true), dbURl: b.dbUrl.get(), dbVacuum: b.dbVacuum.get(false), - supportV2: b.supportV2.get(false), maxNumDbConnections: b.maxNumDbConnections.get(50), retentionPolicies: retentionPolicies, resume: b.resume.get(false), diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index ebf91f415..52b719b8f 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -25,12 +25,8 @@ import ../waku_archive/retention_policy/builder as policy_builder, ../waku_archive/driver as driver, ../waku_archive/driver/builder as driver_builder, - ../waku_archive_legacy/driver as legacy_driver, - ../waku_archive_legacy/driver/builder as legacy_driver_builder, ../waku_store, ../waku_store/common as store_common, - ../waku_store_legacy, - ../waku_store_legacy/common as legacy_common, ../waku_filter_v2, ../waku_peer_exchange, ../discovery/waku_kademlia, @@ -38,8 +34,7 @@ import ../node/peer_manager/peer_store/waku_peer_storage, ../node/peer_manager/peer_store/migrations as peer_store_sqlite_migrations, ../waku_lightpush_legacy/common, - ../common/rate_limit/setting, - ../common/databases/dburl + ../common/rate_limit/setting ## Peer persistence @@ -198,42 +193,10 @@ proc setupProtocols( if conf.storeServiceConf.isSome(): let storeServiceConf = conf.storeServiceConf.get() - if storeServiceConf.supportV2: - let archiveDriver = ( - await legacy_driver.ArchiveDriver.new( - storeServiceConf.dbUrl, storeServiceConf.dbVacuum, - storeServiceConf.dbMigration, storeServiceConf.maxNumDbConnections, - onFatalErrorAction, - ) - ).valueOr: - return err("failed to setup legacy archive driver: " & error) - - node.mountLegacyArchive(archiveDriver).isOkOr: - return err("failed to mount waku legacy archive protocol: " & error) - - ## For now we always mount the future archive driver but if the legacy one is mounted, - ## then the legacy will be in charge of performing the archiving. - ## Regarding storage, the only diff between the current/future archive driver and the legacy - ## one, is that the legacy stores an extra field: the id (message digest.) - - ## TODO: remove this "migrate" variable once legacy store is removed - ## It is now necessary because sqlite's legacy store has an extra field: storedAt - ## This breaks compatibility between store's and legacy store's schemas in sqlite - ## So for now, we need to make sure that when legacy store is enabled and we use sqlite - ## that we migrate our db according to legacy store's schema to have the extra field - - let engine = dburl.getDbEngine(storeServiceConf.dbUrl).valueOr: - return err("error getting db engine in setupProtocols: " & error) - - let migrate = - if engine == "sqlite" and storeServiceConf.supportV2: - false - else: - storeServiceConf.dbMigration let archiveDriver = ( await driver.ArchiveDriver.new( - storeServiceConf.dbUrl, storeServiceConf.dbVacuum, migrate, + storeServiceConf.dbUrl, storeServiceConf.dbVacuum, storeServiceConf.dbMigration, storeServiceConf.maxNumDbConnections, onFatalErrorAction, ) ).valueOr: @@ -245,14 +208,6 @@ proc setupProtocols( node.mountArchive(archiveDriver, retPolicies).isOkOr: return err("failed to mount waku archive protocol: " & error) - if storeServiceConf.supportV2: - # Store legacy setup - try: - await mountLegacyStore(node, node.rateLimitSettings.getSetting(STOREV2)) - except CatchableError: - return - err("failed to mount waku legacy store protocol: " & getCurrentExceptionMsg()) - # Store setup try: await mountStore(node, node.rateLimitSettings.getSetting(STOREV3)) @@ -284,12 +239,6 @@ proc setupProtocols( return err("failed to set node waku store peer: " & error) node.peerManager.addServicePeer(storeNode, WakuStoreCodec) - mountLegacyStoreClient(node) - if conf.remoteStoreNode.isSome(): - let storeNode = parsePeerInfo(conf.remoteStoreNode.get()).valueOr: - return err("failed to set node waku legacy store peer: " & error) - node.peerManager.addServicePeer(storeNode, WakuLegacyStoreCodec) - if conf.storeServiceConf.isSome and conf.storeServiceConf.get().resume: node.setupStoreResume() diff --git a/waku/factory/waku_conf.nim b/waku/factory/waku_conf.nim index 6ed34e131..4934faccc 100644 --- a/waku/factory/waku_conf.nim +++ b/waku/factory/waku_conf.nim @@ -60,7 +60,6 @@ type StoreServiceConf* {.requiresInit.} = object dbMigration*: bool dbURl*: string dbVacuum*: bool - supportV2*: bool maxNumDbConnections*: int retentionPolicies*: seq[string] resume*: bool diff --git a/waku/node/health_monitor/node_health_monitor.nim b/waku/node/health_monitor/node_health_monitor.nim index ddba47ccb..79bf9f92a 100644 --- a/waku/node/health_monitor/node_health_monitor.nim +++ b/waku/node/health_monitor/node_health_monitor.nim @@ -163,17 +163,6 @@ proc getStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = hm.strength[WakuProtocol.StoreProtocol] = peerCount return p.ready() -proc getLegacyStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init(WakuProtocol.LegacyStoreProtocol) - - if isNil(hm.node.wakuLegacyStore): - hm.strength[WakuProtocol.LegacyStoreProtocol] = 0 - return p.notMounted() - - let peerCount = hm.countCapablePeers(WakuLegacyStoreCodec) - hm.strength[WakuProtocol.LegacyStoreProtocol] = peerCount - return p.ready() - proc getLightpushClientHealth(hm: NodeHealthMonitor): ProtocolHealth = var p = ProtocolHealth.init(WakuProtocol.LightpushClientProtocol) @@ -233,23 +222,6 @@ proc getStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = "No Store service peer available yet, neither Store service set up for the node" ) -proc getLegacyStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init(WakuProtocol.LegacyStoreClientProtocol) - - if isNil(hm.node.wakuLegacyStoreClient): - hm.strength[WakuProtocol.LegacyStoreClientProtocol] = 0 - return p.notMounted() - - let peerCount = countCapablePeers(hm, WakuLegacyStoreCodec) - hm.strength[WakuProtocol.LegacyStoreClientProtocol] = peerCount - - if peerCount > 0 or not isNil(hm.node.wakuLegacyStore): - return p.ready() - - return p.notReady( - "No Legacy Store service peers are available yet, neither Store service set up for the node" - ) - proc getPeerExchangeHealth(hm: NodeHealthMonitor): ProtocolHealth = var p = ProtocolHealth.init(WakuProtocol.PeerExchangeProtocol) @@ -294,8 +266,6 @@ proc getSyncProtocolHealthInfo*( return hm.getRelayHealth() of WakuProtocol.StoreProtocol: return hm.getStoreHealth() - of WakuProtocol.LegacyStoreProtocol: - return hm.getLegacyStoreHealth() of WakuProtocol.FilterProtocol: return hm.getFilterHealth(hm.getRelayHealth().health) of WakuProtocol.LightpushProtocol: @@ -310,8 +280,6 @@ proc getSyncProtocolHealthInfo*( return hm.getMixHealth() of WakuProtocol.StoreClientProtocol: return hm.getStoreClientHealth() - of WakuProtocol.LegacyStoreClientProtocol: - return hm.getLegacyStoreClientHealth() of WakuProtocol.FilterClientProtocol: return hm.getFilterClientHealth() of WakuProtocol.LightpushClientProtocol: @@ -349,7 +317,6 @@ proc getSyncAllProtocolHealthInfo(hm: NodeHealthMonitor): seq[ProtocolHealth] = protocols.add(hm.getLegacyLightpushHealth(relayHealth.health)) protocols.add(hm.getFilterHealth(relayHealth.health)) protocols.add(hm.getStoreHealth()) - protocols.add(hm.getLegacyStoreHealth()) protocols.add(hm.getPeerExchangeHealth()) protocols.add(hm.getRendezvousHealth()) protocols.add(hm.getMixHealth()) @@ -357,7 +324,6 @@ proc getSyncAllProtocolHealthInfo(hm: NodeHealthMonitor): seq[ProtocolHealth] = protocols.add(hm.getLightpushClientHealth()) protocols.add(hm.getLegacyLightpushClientHealth()) protocols.add(hm.getStoreClientHealth()) - protocols.add(hm.getLegacyStoreClientHealth()) protocols.add(hm.getFilterClientHealth()) return protocols diff --git a/waku/node/kernel_api/relay.nim b/waku/node/kernel_api/relay.nim index ec4d05ddd..c5a11ff02 100644 --- a/waku/node/kernel_api/relay.nim +++ b/waku/node/kernel_api/relay.nim @@ -24,7 +24,6 @@ import waku_core, waku_core/topics/sharding, waku_filter_v2, - waku_archive_legacy, waku_archive, waku_store_sync, waku_rln_relay, @@ -81,11 +80,6 @@ proc registerRelayHandler( await node.wakuFilter.handleMessage(topic, msg) proc archiveHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = - if not node.wakuLegacyArchive.isNil(): - ## we try to store with legacy archive - await node.wakuLegacyArchive.handleMessage(topic, msg) - return - if node.wakuArchive.isNil(): return diff --git a/waku/node/kernel_api/store.nim b/waku/node/kernel_api/store.nim index ca9917163..fcf0dfc89 100644 --- a/waku/node/kernel_api/store.nim +++ b/waku/node/kernel_api/store.nim @@ -20,17 +20,13 @@ import import ../waku_node, ../../waku_core, - ../../waku_store_legacy/protocol as legacy_store, - ../../waku_store_legacy/client as legacy_store_client, - ../../waku_store_legacy/common as legacy_store_common, ../../waku_store/protocol as store, ../../waku_store/client as store_client, ../../waku_store/common as store_common, ../../waku_store/resume, ../peer_manager, ../../common/rate_limit/setting, - ../../waku_archive, - ../../waku_archive_legacy + ../../waku_archive logScope: topics = "waku node store api" @@ -50,157 +46,6 @@ proc mountArchive*( return ok() -proc mountLegacyArchive*( - node: WakuNode, driver: waku_archive_legacy.ArchiveDriver -): Result[void, string] = - node.wakuLegacyArchive = waku_archive_legacy.WakuArchive.new(driver = driver).valueOr: - return err("error in mountLegacyArchive: " & error) - - return ok() - -## Legacy Waku Store - -# TODO: Review this mapping logic. Maybe, move it to the appplication code -proc toArchiveQuery( - request: legacy_store_common.HistoryQuery -): waku_archive_legacy.ArchiveQuery = - waku_archive_legacy.ArchiveQuery( - pubsubTopic: request.pubsubTopic, - contentTopics: request.contentTopics, - cursor: request.cursor.map( - proc(cursor: HistoryCursor): waku_archive_legacy.ArchiveCursor = - waku_archive_legacy.ArchiveCursor( - pubsubTopic: cursor.pubsubTopic, - senderTime: cursor.senderTime, - storeTime: cursor.storeTime, - digest: cursor.digest, - ) - ), - startTime: request.startTime, - endTime: request.endTime, - pageSize: request.pageSize.uint, - direction: request.direction, - requestId: request.requestId, - ) - -# TODO: Review this mapping logic. Maybe, move it to the appplication code -proc toHistoryResult*( - res: waku_archive_legacy.ArchiveResult -): legacy_store_common.HistoryResult = - let response = res.valueOr: - case error.kind - of waku_archive_legacy.ArchiveErrorKind.DRIVER_ERROR, - waku_archive_legacy.ArchiveErrorKind.INVALID_QUERY: - return err(HistoryError(kind: HistoryErrorKind.BAD_REQUEST, cause: error.cause)) - else: - return err(HistoryError(kind: HistoryErrorKind.UNKNOWN)) - return ok( - HistoryResponse( - messages: response.messages, - cursor: response.cursor.map( - proc(cursor: waku_archive_legacy.ArchiveCursor): HistoryCursor = - HistoryCursor( - pubsubTopic: cursor.pubsubTopic, - senderTime: cursor.senderTime, - storeTime: cursor.storeTime, - digest: cursor.digest, - ) - ), - ) - ) - -proc mountLegacyStore*( - node: WakuNode, rateLimit: RateLimitSetting = DefaultGlobalNonRelayRateLimit -) {.async.} = - info "mounting waku legacy store protocol" - - if node.wakuLegacyArchive.isNil(): - error "failed to mount waku legacy store protocol", error = "waku archive not set" - return - - # TODO: Review this handler logic. Maybe, move it to the appplication code - let queryHandler: HistoryQueryHandler = proc( - request: HistoryQuery - ): Future[legacy_store_common.HistoryResult] {.async.} = - if request.cursor.isSome(): - ?request.cursor.get().checkHistCursor() - - let request = request.toArchiveQuery() - let response = await node.wakuLegacyArchive.findMessagesV2(request) - return response.toHistoryResult() - - node.wakuLegacyStore = legacy_store.WakuStore.new( - node.peerManager, node.rng, queryHandler, some(rateLimit) - ) - - if node.started: - # Node has started already. Let's start store too. - await node.wakuLegacyStore.start() - - node.switch.mount( - node.wakuLegacyStore, protocolMatcher(legacy_store_common.WakuLegacyStoreCodec) - ) - -proc mountLegacyStoreClient*(node: WakuNode) = - info "mounting legacy store client" - - node.wakuLegacyStoreClient = - legacy_store_client.WakuStoreClient.new(node.peerManager, node.rng) - -proc query*( - node: WakuNode, query: legacy_store_common.HistoryQuery, peer: RemotePeerInfo -): Future[legacy_store_common.WakuStoreResult[legacy_store_common.HistoryResponse]] {. - async, gcsafe -.} = - ## Queries known nodes for historical messages - if node.wakuLegacyStoreClient.isNil(): - return err("waku legacy store client is nil") - - let response = (await node.wakuLegacyStoreClient.query(query, peer)).valueOr: - return err("legacy store client query error: " & $error) - - return ok(response) - -# TODO: Move to application module (e.g., wakunode2.nim) -proc query*( - node: WakuNode, query: legacy_store_common.HistoryQuery -): Future[legacy_store_common.WakuStoreResult[legacy_store_common.HistoryResponse]] {. - async, gcsafe, deprecated: "Use 'node.query()' with peer destination instead" -.} = - ## Queries known nodes for historical messages - if node.wakuLegacyStoreClient.isNil(): - return err("waku legacy store client is nil") - - let peerOpt = node.peerManager.selectPeer(legacy_store_common.WakuLegacyStoreCodec) - if peerOpt.isNone(): - error "no suitable remote peers" - return err("peer_not_found_failure") - - return await node.query(query, peerOpt.get()) - -when defined(waku_exp_store_resume): - # TODO: Move to application module (e.g., wakunode2.nim) - proc resume*( - node: WakuNode, peerList: Option[seq[RemotePeerInfo]] = none(seq[RemotePeerInfo]) - ) {.async, gcsafe.} = - ## resume proc retrieves the history of waku messages published on the default waku pubsub topic since the last time the waku node has been online - ## for resume to work properly the waku node must have the store protocol mounted in the full mode (i.e., persisting messages) - ## messages are stored in the wakuStore's messages field and in the message db - ## the offline time window is measured as the difference between the current time and the timestamp of the most recent persisted waku message - ## an offset of 20 second is added to the time window to count for nodes asynchrony - ## peerList indicates the list of peers to query from. The history is fetched from the first available peer in this list. Such candidates should be found through a discovery method (to be developed). - ## if no peerList is passed, one of the peers in the underlying peer manager unit of the store protocol is picked randomly to fetch the history from. - ## The history gets fetched successfully if the dialed peer has been online during the queried time window. - if node.wakuLegacyStoreClient.isNil(): - return - - let retrievedMessages = (await node.wakuLegacyStoreClient.resume(peerList)).valueOr: - error "failed to resume store", error = error - return - - info "the number of retrieved messages since the last online time: ", - number = retrievedMessages.value - ## Waku Store proc toArchiveQuery(request: StoreQueryRequest): waku_archive.ArchiveQuery = diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 0c6cb7ac4..92528c7b9 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -32,10 +32,6 @@ import waku_core/topics/sharding, waku_relay, waku_archive, - waku_archive_legacy, - waku_store_legacy/protocol as legacy_store, - waku_store_legacy/client as legacy_store_client, - waku_store_legacy/common as legacy_store_common, waku_store/protocol as store, waku_store/client as store_client, waku_store/common as store_common, @@ -116,9 +112,6 @@ type switch*: Switch wakuRelay*: WakuRelay wakuArchive*: waku_archive.WakuArchive - wakuLegacyArchive*: waku_archive_legacy.WakuArchive - wakuLegacyStore*: legacy_store.WakuStore - wakuLegacyStoreClient*: legacy_store_client.WakuStoreClient wakuStore*: store.WakuStore wakuStoreClient*: store_client.WakuStoreClient wakuStoreResume*: StoreResume diff --git a/waku/rest_api/endpoint/admin/handlers.nim b/waku/rest_api/endpoint/admin/handlers.nim index 1efbf7d04..1904d43f9 100644 --- a/waku/rest_api/endpoint/admin/handlers.nim +++ b/waku/rest_api/endpoint/admin/handlers.nim @@ -12,7 +12,6 @@ import waku/[ waku_core, waku_core/topics/pubsub_topic, - waku_store_legacy/common, waku_store/common, waku_filter_v2, waku_lightpush_legacy/common, @@ -172,7 +171,7 @@ proc installAdminV1GetPeersHandler(router: var RestRouter, node: WakuNode) = let peers = populateAdminPeerInfoForCodecs( node, @[ - WakuRelayCodec, WakuFilterSubscribeCodec, WakuStoreCodec, WakuLegacyStoreCodec, + WakuRelayCodec, WakuFilterSubscribeCodec, WakuStoreCodec, WakuLegacyLightPushCodec, WakuLightPushCodec, WakuPeerExchangeCodec, WakuReconciliationCodec, WakuTransferCodec, ], @@ -366,8 +365,6 @@ proc installAdminV1GetPeersHandler(router: var RestRouter, node: WakuNode) = protoStats[WakuFilterPushCodec] = peers.countIt(it.protocols.contains(WakuFilterPushCodec)) protoStats[WakuStoreCodec] = peers.countIt(it.protocols.contains(WakuStoreCodec)) - protoStats[WakuLegacyStoreCodec] = - peers.countIt(it.protocols.contains(WakuLegacyStoreCodec)) protoStats[WakuLightPushCodec] = peers.countIt(it.protocols.contains(WakuLightPushCodec)) protoStats[WakuLegacyLightPushCodec] = diff --git a/waku/rest_api/endpoint/builder.nim b/waku/rest_api/endpoint/builder.nim index cb23bc284..9b4ecf662 100644 --- a/waku/rest_api/endpoint/builder.nim +++ b/waku/rest_api/endpoint/builder.nim @@ -14,7 +14,6 @@ import waku/rest_api/endpoint/legacy_lightpush/handlers as rest_legacy_lightpush_endpoint, waku/rest_api/endpoint/lightpush/handlers as rest_lightpush_endpoint, waku/rest_api/endpoint/store/handlers as rest_store_endpoint, - waku/rest_api/endpoint/legacy_store/handlers as rest_store_legacy_endpoint, waku/rest_api/endpoint/health/handlers as rest_health_endpoint, waku/rest_api/endpoint/admin/handlers as rest_admin_endpoint, waku/waku_core/topics, @@ -195,7 +194,6 @@ proc startRestServerProtocolSupport*( none(DiscoveryHandler) rest_store_endpoint.installStoreApiHandlers(router, node, storeDiscoHandler) - rest_store_legacy_endpoint.installStoreApiHandlers(router, node, storeDiscoHandler) ## Light push API ## Install it either if client is mounted) diff --git a/waku/rest_api/endpoint/legacy_store/client.nim b/waku/rest_api/endpoint/legacy_store/client.nim deleted file mode 100644 index 24ad38d9a..000000000 --- a/waku/rest_api/endpoint/legacy_store/client.nim +++ /dev/null @@ -1,75 +0,0 @@ -{.push raises: [].} - -import - chronicles, json_serialization, json_serialization/std/options, presto/[route, client] -import ../../../waku_store_legacy/common, ../serdes, ../responses, ./types - -export types - -logScope: - topics = "waku node rest legacy store_api" - -proc decodeBytes*( - t: typedesc[StoreResponseRest], - data: openArray[byte], - contentType: Opt[ContentTypeData], -): RestResult[StoreResponseRest] = - if MediaType.init($contentType) == MIMETYPE_JSON: - let decoded = ?decodeFromJsonBytes(StoreResponseRest, data) - return ok(decoded) - - if MediaType.init($contentType) == MIMETYPE_TEXT: - var res: string - if len(data) > 0: - res = newString(len(data)) - copyMem(addr res[0], unsafeAddr data[0], len(data)) - - return ok( - StoreResponseRest( - messages: newSeq[StoreWakuMessage](0), - cursor: none(HistoryCursorRest), - # field that contain error information - errorMessage: some(res), - ) - ) - - # If everything goes wrong - return err(cstring("Unsupported contentType " & $contentType)) - -proc getStoreMessagesV1*( - # URL-encoded reference to the store-node - peerAddr: string = "", - pubsubTopic: string = "", - # URL-encoded comma-separated list of content topics - contentTopics: string = "", - startTime: string = "", - endTime: string = "", - - # Optional cursor fields - senderTime: string = "", - storeTime: string = "", - digest: string = "", # base64-encoded digest - pageSize: string = "", - ascending: string = "", -): RestResponse[StoreResponseRest] {. - rest, endpoint: "/store/v1/messages", meth: HttpMethod.MethodGet -.} - -proc getStoreMessagesV1*( - # URL-encoded reference to the store-node - peerAddr: Option[string], - pubsubTopic: string = "", - # URL-encoded comma-separated list of content topics - contentTopics: string = "", - startTime: string = "", - endTime: string = "", - - # Optional cursor fields - senderTime: string = "", - storeTime: string = "", - digest: string = "", # base64-encoded digest - pageSize: string = "", - ascending: string = "", -): RestResponse[StoreResponseRest] {. - rest, endpoint: "/store/v1/messages", meth: HttpMethod.MethodGet -.} diff --git a/waku/rest_api/endpoint/legacy_store/handlers.nim b/waku/rest_api/endpoint/legacy_store/handlers.nim deleted file mode 100644 index 4ed58f799..000000000 --- a/waku/rest_api/endpoint/legacy_store/handlers.nim +++ /dev/null @@ -1,246 +0,0 @@ -{.push raises: [].} - -import - std/[strformat, sugar], results, chronicles, uri, json_serialization, presto/route -import - ../../../waku_core, - ../../../waku_store_legacy/common, - ../../../waku_store_legacy/self_req_handler, - ../../../waku_node, - ../../../node/peer_manager, - ../../../common/paging, - ../../handlers, - ../responses, - ../serdes, - ./types - -export types - -logScope: - topics = "waku node rest legacy store_api" - -const futTimeout* = 5.seconds # Max time to wait for futures - -const NoPeerNoDiscError* = - RestApiResponse.preconditionFailed("No suitable service peer & no discovery method") - -# Queries the store-node with the query parameters and -# returns a RestApiResponse that is sent back to the api client. -proc performHistoryQuery( - selfNode: WakuNode, histQuery: HistoryQuery, storePeer: RemotePeerInfo -): Future[RestApiResponse] {.async.} = - let queryFut = selfNode.query(histQuery, storePeer) - if not await queryFut.withTimeout(futTimeout): - const msg = "No history response received (timeout)" - error msg - return RestApiResponse.internalServerError(msg) - - let storeResp = queryFut.read().map(res => res.toStoreResponseRest()).valueOr: - const msg = "Error occurred in queryFut.read()" - error msg, error = error - return RestApiResponse.internalServerError(fmt("{msg} [{error}]")) - - let resp = RestApiResponse.jsonResponse(storeResp, status = Http200).valueOr: - const msg = "Error building the json respose" - error msg, error = error - return RestApiResponse.internalServerError(fmt("{msg} [{error}]")) - - return resp - -# Converts a string time representation into an Option[Timestamp]. -# Only positive time is considered a valid Timestamp in the request -proc parseTime(input: Option[string]): Result[Option[Timestamp], string] = - if input.isSome() and input.get() != "": - try: - let time = parseInt(input.get()) - if time > 0: - return ok(some(Timestamp(time))) - except ValueError: - return err("Problem parsing time [" & getCurrentExceptionMsg() & "]") - - return ok(none(Timestamp)) - -# Generates a history query cursor as per the given params -proc parseCursor( - parsedPubsubTopic: Option[string], - senderTime: Option[string], - storeTime: Option[string], - digest: Option[string], -): Result[Option[HistoryCursor], string] = - # Parse sender time - let parsedSenderTime = ?parseTime(senderTime) - - # Parse store time - let parsedStoreTime = ?parseTime(storeTime) - - # Parse message digest - let parsedMsgDigest = ?parseMsgDigest(digest) - - # Parse cursor information - if parsedPubsubTopic.isSome() and parsedSenderTime.isSome() and - parsedStoreTime.isSome() and parsedMsgDigest.isSome(): - return ok( - some( - HistoryCursor( - pubsubTopic: parsedPubsubTopic.get(), - senderTime: parsedSenderTime.get(), - storeTime: parsedStoreTime.get(), - digest: parsedMsgDigest.get(), - ) - ) - ) - else: - return ok(none(HistoryCursor)) - -# Creates a HistoryQuery from the given params -proc createHistoryQuery( - pubsubTopic: Option[string], - contentTopics: Option[string], - senderTime: Option[string], - storeTime: Option[string], - digest: Option[string], - startTime: Option[string], - endTime: Option[string], - pageSize: Option[string], - direction: Option[string], -): Result[HistoryQuery, string] = - # Parse pubsubTopic parameter - var parsedPubsubTopic = none(string) - if pubsubTopic.isSome(): - let decodedPubsubTopic = decodeUrl(pubsubTopic.get()) - if decodedPubsubTopic != "": - parsedPubsubTopic = some(decodedPubsubTopic) - - # Parse the content topics - var parsedContentTopics = newSeq[ContentTopic](0) - if contentTopics.isSome(): - let ctList = decodeUrl(contentTopics.get()) - if ctList != "": - for ct in ctList.split(','): - parsedContentTopics.add(ct) - - # Parse cursor information - let parsedCursor = ?parseCursor(parsedPubsubTopic, senderTime, storeTime, digest) - - # Parse page size field - var parsedPagedSize = DefaultPageSize - if pageSize.isSome() and pageSize.get() != "": - try: - parsedPagedSize = uint64(parseInt(pageSize.get())) - except CatchableError: - return err("Problem parsing page size [" & getCurrentExceptionMsg() & "]") - - # Parse start time - let parsedStartTime = ?parseTime(startTime) - - # Parse end time - let parsedEndTime = ?parseTime(endTime) - - # Parse ascending field - var parsedDirection = default() - if direction.isSome() and direction.get() != "": - parsedDirection = direction.get().into() - - return ok( - HistoryQuery( - pubsubTopic: parsedPubsubTopic, - contentTopics: parsedContentTopics, - startTime: parsedStartTime, - endTime: parsedEndTime, - direction: parsedDirection, - pageSize: parsedPagedSize, - cursor: parsedCursor, - ) - ) - -# Simple type conversion. The "Option[Result[string, cstring]]" -# type is used by the nim-presto library. -proc toOpt(self: Option[Result[string, cstring]]): Option[string] = - if not self.isSome() or self.get().value == "": - return none(string) - if self.isSome() and self.get().value != "": - return some(self.get().value) - -proc retrieveMsgsFromSelfNode( - self: WakuNode, histQuery: HistoryQuery -): Future[RestApiResponse] {.async.} = - ## Performs a "store" request to the local node (self node.) - ## Notice that this doesn't follow the regular store libp2p channel because a node - ## it is not allowed to libp2p-dial a node to itself, by default. - ## - - let selfResp = (await self.wakuLegacyStore.handleSelfStoreRequest(histQuery)).valueOr: - return RestApiResponse.internalServerError($error) - - let storeResp = selfResp.toStoreResponseRest() - let resp = RestApiResponse.jsonResponse(storeResp, status = Http200).valueOr: - const msg = "Error building the json respose" - let e = $error - error msg, error = e - return RestApiResponse.internalServerError(fmt("{msg} [{e}]")) - - return resp - -# Subscribes the rest handler to attend "/store/v1/messages" requests -proc installStoreApiHandlers*( - router: var RestRouter, - node: WakuNode, - discHandler: Option[DiscoveryHandler] = none(DiscoveryHandler), -) = - # Handles the store-query request according to the passed parameters - router.api(MethodGet, "/store/v1/messages") do( - peerAddr: Option[string], - pubsubTopic: Option[string], - contentTopics: Option[string], - senderTime: Option[string], - storeTime: Option[string], - digest: Option[string], - startTime: Option[string], - endTime: Option[string], - pageSize: Option[string], - ascending: Option[string] - ) -> RestApiResponse: - info "REST-GET /store/v1/messages ", peer_addr = $peerAddr - - # All the GET parameters are URL-encoded (https://en.wikipedia.org/wiki/URL_encoding) - # Example: - # /store/v1/messages?peerAddr=%2Fip4%2F127.0.0.1%2Ftcp%2F60001%2Fp2p%2F16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\&pubsubTopic=my-waku-topic - - # Parse the rest of the parameters and create a HistoryQuery - let histQuery = createHistoryQuery( - pubsubTopic.toOpt(), - contentTopics.toOpt(), - senderTime.toOpt(), - storeTime.toOpt(), - digest.toOpt(), - startTime.toOpt(), - endTime.toOpt(), - pageSize.toOpt(), - ascending.toOpt(), - ).valueOr: - return RestApiResponse.badRequest(error) - - if peerAddr.isNone() and not node.wakuLegacyStore.isNil(): - ## The user didn't specify a peer address and self-node is configured as a store node. - ## In this case we assume that the user is willing to retrieve the messages stored by - ## the local/self store node. - return await node.retrieveMsgsFromSelfNode(histQuery) - - # Parse the peer address parameter - let parsedPeerAddr = parseUrlPeerAddr(peerAddr.toOpt()).valueOr: - return RestApiResponse.badRequest(error) - - let peerAddr = parsedPeerAddr.valueOr: - node.peerManager.selectPeer(WakuLegacyStoreCodec).valueOr: - let handler = discHandler.valueOr: - return NoPeerNoDiscError - - let peerOp = (await handler()).valueOr: - return RestApiResponse.internalServerError($error) - - peerOp.valueOr: - return RestApiResponse.preconditionFailed( - "No suitable service peer & none discovered" - ) - - return await node.performHistoryQuery(histQuery, peerAddr) diff --git a/waku/rest_api/endpoint/legacy_store/types.nim b/waku/rest_api/endpoint/legacy_store/types.nim deleted file mode 100644 index 0c547c7cc..000000000 --- a/waku/rest_api/endpoint/legacy_store/types.nim +++ /dev/null @@ -1,375 +0,0 @@ -{.push raises: [].} - -import - std/[sets, strformat, uri], - stew/byteutils, - chronicles, - json_serialization, - json_serialization/std/options, - presto/[route, client, common] -import - ../../../waku_store_legacy/common as waku_store_common, - ../../../common/base64, - ../../../waku_core, - ../serdes - -#### Types - -type - HistoryCursorRest* = object - pubsubTopic*: PubsubTopic - senderTime*: Timestamp - storeTime*: Timestamp - digest*: waku_store_common.MessageDigest - - StoreRequestRest* = object - # inspired by https://github.com/waku-org/nwaku/blob/f95147f5b7edfd45f914586f2d41cd18fb0e0d18/waku/v2//waku_store/common.nim#L52 - pubsubTopic*: Option[PubsubTopic] - contentTopics*: seq[ContentTopic] - cursor*: Option[HistoryCursorRest] - startTime*: Option[Timestamp] - endTime*: Option[Timestamp] - pageSize*: uint64 - ascending*: bool - - StoreWakuMessage* = object - payload*: Base64String - contentTopic*: Option[ContentTopic] - version*: Option[uint32] - timestamp*: Option[Timestamp] - ephemeral*: Option[bool] - meta*: Option[Base64String] - - StoreResponseRest* = object # inspired by https://rfc.vac.dev/spec/16/#storeresponse - messages*: seq[StoreWakuMessage] - cursor*: Option[HistoryCursorRest] - # field that contains error information - errorMessage*: Option[string] - -createJsonFlavor RestJson - -Json.setWriter JsonWriter, PreferredOutput = string - -#### Type conversion - -# Converts a URL-encoded-base64 string into a 'MessageDigest' -proc parseMsgDigest*( - input: Option[string] -): Result[Option[waku_store_common.MessageDigest], string] = - if not input.isSome() or input.get() == "": - return ok(none(waku_store_common.MessageDigest)) - - let decodedUrl = decodeUrl(input.get()) - let base64DecodedArr = ?base64.decode(Base64String(decodedUrl)) - - var messageDigest = waku_store_common.MessageDigest() - - # Next snippet inspired by "nwaku/waku/waku_archive/archive.nim" - # TODO: Improve coherence of MessageDigest type - messageDigest = block: - var data: array[32, byte] - for i in 0 ..< min(base64DecodedArr.len, 32): - data[i] = base64DecodedArr[i] - - waku_store_common.MessageDigest(data: data) - - return ok(some(messageDigest)) - -# Converts a given MessageDigest object into a suitable -# Base64-URL-encoded string suitable to be transmitted in a Rest -# request-response. The MessageDigest is first base64 encoded -# and this result is URL-encoded. -proc toRestStringMessageDigest*(self: waku_store_common.MessageDigest): string = - let base64Encoded = base64.encode(self.data) - encodeUrl($base64Encoded) - -proc toWakuMessage*(message: StoreWakuMessage): WakuMessage = - WakuMessage( - payload: base64.decode(message.payload).get(), - contentTopic: message.contentTopic.get(), - version: message.version.get(), - timestamp: message.timestamp.get(), - ephemeral: message.ephemeral.get(), - meta: message.meta.get(Base64String("")).decode().get(), - ) - -# Converts a 'HistoryResponse' object to an 'StoreResponseRest' -# that can be serialized to a json object. -proc toStoreResponseRest*(histResp: HistoryResponse): StoreResponseRest = - proc toStoreWakuMessage(message: WakuMessage): StoreWakuMessage = - StoreWakuMessage( - payload: base64.encode(message.payload), - contentTopic: some(message.contentTopic), - version: some(message.version), - timestamp: some(message.timestamp), - ephemeral: some(message.ephemeral), - meta: - if message.meta.len > 0: - some(base64.encode(message.meta)) - else: - none(Base64String), - ) - - var storeWakuMsgs: seq[StoreWakuMessage] - for m in histResp.messages: - storeWakuMsgs.add(m.toStoreWakuMessage()) - - var cursor = none(HistoryCursorRest) - if histResp.cursor.isSome: - cursor = some( - HistoryCursorRest( - pubsubTopic: histResp.cursor.get().pubsubTopic, - senderTime: histResp.cursor.get().senderTime, - storeTime: histResp.cursor.get().storeTime, - digest: histResp.cursor.get().digest, - ) - ) - - StoreResponseRest(messages: storeWakuMsgs, cursor: cursor) - -## Beginning of StoreWakuMessage serde - -proc writeValue*( - writer: var JsonWriter, value: StoreWakuMessage -) {.gcsafe, raises: [IOError].} = - writer.beginRecord() - writer.writeField("payload", $value.payload) - if value.contentTopic.isSome(): - writer.writeField("contentTopic", value.contentTopic.get()) - if value.version.isSome(): - writer.writeField("version", value.version.get()) - if value.timestamp.isSome(): - writer.writeField("timestamp", value.timestamp.get()) - if value.ephemeral.isSome(): - writer.writeField("ephemeral", value.ephemeral.get()) - if value.meta.isSome(): - writer.writeField("meta", value.meta.get()) - writer.endRecord() - -proc readValue*( - reader: var JsonReader, value: var StoreWakuMessage -) {.gcsafe, raises: [SerializationError, IOError].} = - var - payload = none(Base64String) - contentTopic = none(ContentTopic) - version = none(uint32) - timestamp = none(Timestamp) - ephemeral = none(bool) - meta = none(Base64String) - - var keys = initHashSet[string]() - for fieldName in readObjectFields(reader): - # Check for reapeated keys - if keys.containsOrIncl(fieldName): - let err = - try: - fmt"Multiple `{fieldName}` fields found" - except CatchableError: - "Multiple fields with the same name found" - reader.raiseUnexpectedField(err, "StoreWakuMessage") - - case fieldName - of "payload": - payload = some(reader.readValue(Base64String)) - of "contentTopic": - contentTopic = some(reader.readValue(ContentTopic)) - of "version": - version = some(reader.readValue(uint32)) - of "timestamp": - timestamp = some(reader.readValue(Timestamp)) - of "ephemeral": - ephemeral = some(reader.readValue(bool)) - of "meta": - meta = some(reader.readValue(Base64String)) - else: - reader.raiseUnexpectedField("Unrecognided field", cstring(fieldName)) - - if payload.isNone(): - reader.raiseUnexpectedValue("Field `payload` is missing") - - value = StoreWakuMessage( - payload: payload.get(), - contentTopic: contentTopic, - version: version, - timestamp: timestamp, - ephemeral: ephemeral, - meta: meta, - ) - -## End of StoreWakuMessage serde - -## Beginning of MessageDigest serde - -proc writeValue*( - writer: var JsonWriter, value: waku_store_common.MessageDigest -) {.gcsafe, raises: [IOError].} = - writer.beginRecord() - writer.writeField("data", base64.encode(value.data)) - writer.endRecord() - -proc readValue*( - reader: var JsonReader, value: var waku_store_common.MessageDigest -) {.gcsafe, raises: [SerializationError, IOError].} = - var data = none(seq[byte]) - - for fieldName in readObjectFields(reader): - case fieldName - of "data": - if data.isSome(): - reader.raiseUnexpectedField("Multiple `data` fields found", "MessageDigest") - let decoded = base64.decode(reader.readValue(Base64String)).valueOr: - reader.raiseUnexpectedField("Failed decoding data", "MessageDigest") - data = some(decoded) - else: - reader.raiseUnexpectedField("Unrecognided field", cstring(fieldName)) - - if data.isNone(): - reader.raiseUnexpectedValue("Field `data` is missing") - - for i in 0 ..< 32: - value.data[i] = data.get()[i] - -## End of MessageDigest serde - -## Beginning of HistoryCursorRest serde - -proc writeValue*( - writer: var JsonWriter, value: HistoryCursorRest -) {.gcsafe, raises: [IOError].} = - writer.beginRecord() - writer.writeField("pubsubTopic", value.pubsubTopic) - writer.writeField("senderTime", value.senderTime) - writer.writeField("storeTime", value.storeTime) - writer.writeField("digest", value.digest) - writer.endRecord() - -proc readValue*( - reader: var JsonReader, value: var HistoryCursorRest -) {.gcsafe, raises: [SerializationError, IOError].} = - var - pubsubTopic = none(PubsubTopic) - senderTime = none(Timestamp) - storeTime = none(Timestamp) - digest = none(waku_store_common.MessageDigest) - - for fieldName in readObjectFields(reader): - case fieldName - of "pubsubTopic": - if pubsubTopic.isSome(): - reader.raiseUnexpectedField( - "Multiple `pubsubTopic` fields found", "HistoryCursorRest" - ) - pubsubTopic = some(reader.readValue(PubsubTopic)) - of "senderTime": - if senderTime.isSome(): - reader.raiseUnexpectedField( - "Multiple `senderTime` fields found", "HistoryCursorRest" - ) - senderTime = some(reader.readValue(Timestamp)) - of "storeTime": - if storeTime.isSome(): - reader.raiseUnexpectedField( - "Multiple `storeTime` fields found", "HistoryCursorRest" - ) - storeTime = some(reader.readValue(Timestamp)) - of "digest": - if digest.isSome(): - reader.raiseUnexpectedField( - "Multiple `digest` fields found", "HistoryCursorRest" - ) - digest = some(reader.readValue(waku_store_common.MessageDigest)) - else: - reader.raiseUnexpectedField("Unrecognided field", cstring(fieldName)) - - if pubsubTopic.isNone(): - reader.raiseUnexpectedValue("Field `pubsubTopic` is missing") - - if senderTime.isNone(): - reader.raiseUnexpectedValue("Field `senderTime` is missing") - - if storeTime.isNone(): - reader.raiseUnexpectedValue("Field `storeTime` is missing") - - if digest.isNone(): - reader.raiseUnexpectedValue("Field `digest` is missing") - - value = HistoryCursorRest( - pubsubTopic: pubsubTopic.get(), - senderTime: senderTime.get(), - storeTime: storeTime.get(), - digest: digest.get(), - ) - -## End of HistoryCursorRest serde - -## Beginning of StoreResponseRest serde - -proc writeValue*( - writer: var JsonWriter, value: StoreResponseRest -) {.gcsafe, raises: [IOError].} = - writer.beginRecord() - writer.writeField("messages", value.messages) - if value.cursor.isSome(): - writer.writeField("cursor", value.cursor.get()) - if value.errorMessage.isSome(): - writer.writeField("errorMessage", value.errorMessage.get()) - writer.endRecord() - -proc readValue*( - reader: var JsonReader, value: var StoreResponseRest -) {.gcsafe, raises: [SerializationError, IOError].} = - var - messages = none(seq[StoreWakuMessage]) - cursor = none(HistoryCursorRest) - errorMessage = none(string) - - for fieldName in readObjectFields(reader): - case fieldName - of "messages": - if messages.isSome(): - reader.raiseUnexpectedField( - "Multiple `messages` fields found", "StoreResponseRest" - ) - messages = some(reader.readValue(seq[StoreWakuMessage])) - of "cursor": - if cursor.isSome(): - reader.raiseUnexpectedField( - "Multiple `cursor` fields found", "StoreResponseRest" - ) - cursor = some(reader.readValue(HistoryCursorRest)) - of "errorMessage": - if errorMessage.isSome(): - reader.raiseUnexpectedField( - "Multiple `errorMessage` fields found", "StoreResponseRest" - ) - errorMessage = some(reader.readValue(string)) - else: - reader.raiseUnexpectedField("Unrecognided field", cstring(fieldName)) - - if messages.isNone(): - reader.raiseUnexpectedValue("Field `messages` is missing") - - value = StoreResponseRest( - messages: messages.get(), cursor: cursor, errorMessage: errorMessage - ) - -## End of StoreResponseRest serde - -## Beginning of StoreRequestRest serde - -proc writeValue*( - writer: var JsonWriter, value: StoreRequestRest -) {.gcsafe, raises: [IOError].} = - writer.beginRecord() - if value.pubsubTopic.isSome(): - writer.writeField("pubsubTopic", value.pubsubTopic.get()) - writer.writeField("contentTopics", value.contentTopics) - if value.startTime.isSome(): - writer.writeField("startTime", value.startTime.get()) - if value.endTime.isSome(): - writer.writeField("endTime", value.endTime.get()) - writer.writeField("pageSize", value.pageSize) - writer.writeField("ascending", value.ascending) - writer.endRecord() - -## End of StoreRequestRest serde diff --git a/waku/waku_archive_legacy.nim b/waku/waku_archive_legacy.nim deleted file mode 100644 index bcb6b6a54..000000000 --- a/waku/waku_archive_legacy.nim +++ /dev/null @@ -1,6 +0,0 @@ -import - ./waku_archive_legacy/common, - ./waku_archive_legacy/archive, - ./waku_archive_legacy/driver - -export common, archive, driver diff --git a/waku/waku_archive_legacy/archive.nim b/waku/waku_archive_legacy/archive.nim deleted file mode 100644 index 7bf5685a5..000000000 --- a/waku/waku_archive_legacy/archive.nim +++ /dev/null @@ -1,285 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import - std/[times, options, sequtils, algorithm], - stew/byteutils, - chronicles, - chronos, - metrics, - results -import - ../common/paging, - ./driver, - ../waku_core, - ../waku_core/message/digest, - ./common, - ./archive_metrics - -logScope: - topics = "waku archive" - -const - DefaultPageSize*: uint = 20 - MaxPageSize*: uint = 100 - - # Retention policy - WakuArchiveDefaultRetentionPolicyInterval* = chronos.minutes(30) - - # Metrics reporting - WakuArchiveDefaultMetricsReportInterval* = chronos.minutes(30) - - # Message validation - # 20 seconds maximum allowable sender timestamp "drift" - MaxMessageTimestampVariance* = getNanoSecondTime(20) - -type MessageValidator* = - proc(msg: WakuMessage): Result[void, string] {.closure, gcsafe, raises: [].} - -## Archive - -type WakuArchive* = ref object - driver: ArchiveDriver - - validator: MessageValidator - -proc validate*(msg: WakuMessage): Result[void, string] = - if msg.ephemeral: - # Ephemeral message, do not store - return - - if msg.timestamp == 0: - return ok() - - let - now = getNanosecondTime(getTime().toUnixFloat()) - lowerBound = now - MaxMessageTimestampVariance - upperBound = now + MaxMessageTimestampVariance - - if msg.timestamp < lowerBound: - return err(invalidMessageOld) - - if upperBound < msg.timestamp: - return err(invalidMessageFuture) - - return ok() - -proc new*( - T: type WakuArchive, driver: ArchiveDriver, validator: MessageValidator = validate -): Result[T, string] = - if driver.isNil(): - return err("archive driver is Nil") - - let archive = WakuArchive(driver: driver, validator: validator) - - return ok(archive) - -proc handleMessage*( - self: WakuArchive, pubsubTopic: PubsubTopic, msg: WakuMessage -) {.async.} = - let - msgDigest = computeDigest(msg) - msgDigestHex = msgDigest.data.to0xHex() - msgHash = computeMessageHash(pubsubTopic, msg) - msgHashHex = msgHash.to0xHex() - msgTimestamp = - if msg.timestamp > 0: - msg.timestamp - else: - getNanosecondTime(getTime().toUnixFloat()) - - trace "handling message", - msg_hash = msgHashHex, - pubsubTopic = pubsubTopic, - contentTopic = msg.contentTopic, - msgTimestamp = msg.timestamp, - digest = msgDigestHex - - self.validator(msg).isOkOr: - waku_legacy_archive_errors.inc(labelValues = [error]) - trace "invalid message", - msg_hash = msgHashHex, - pubsubTopic = pubsubTopic, - contentTopic = msg.contentTopic, - timestamp = msg.timestamp, - error = error - return - - let insertStartTime = getTime().toUnixFloat() - - (await self.driver.put(pubsubTopic, msg, msgDigest, msgHash, msgTimestamp)).isOkOr: - waku_legacy_archive_errors.inc(labelValues = [insertFailure]) - error "failed to insert message", - msg_hash = msgHashHex, - pubsubTopic = pubsubTopic, - contentTopic = msg.contentTopic, - timestamp = msg.timestamp, - error = error - return - - let insertDuration = getTime().toUnixFloat() - insertStartTime - waku_legacy_archive_insert_duration_seconds.observe(insertDuration) - - info "message archived", - msg_hash = msgHashHex, - pubsubTopic = pubsubTopic, - contentTopic = msg.contentTopic, - msgTimestamp = msg.timestamp, - digest = msgDigestHex, - insertDuration = insertDuration - -proc findMessages*( - self: WakuArchive, query: ArchiveQuery -): Future[ArchiveResult] {.async, gcsafe.} = - ## Search the archive to return a single page of messages matching the query criteria - - let maxPageSize = - if query.pageSize <= 0: - DefaultPageSize - else: - min(query.pageSize, MaxPageSize) - - let isAscendingOrder = query.direction.into() - - if query.contentTopics.len > 10: - return err(ArchiveError.invalidQuery("too many content topics")) - - if query.cursor.isSome() and query.cursor.get().hash.len != 32: - return err(ArchiveError.invalidQuery("invalid cursor hash length")) - - let queryStartTime = getTime().toUnixFloat() - - let rows = ( - await self.driver.getMessages( - includeData = query.includeData, - contentTopic = query.contentTopics, - pubsubTopic = query.pubsubTopic, - cursor = query.cursor, - startTime = query.startTime, - endTime = query.endTime, - hashes = query.hashes, - maxPageSize = maxPageSize + 1, - ascendingOrder = isAscendingOrder, - requestId = query.requestId, - ) - ).valueOr: - return err(ArchiveError(kind: ArchiveErrorKind.DRIVER_ERROR, cause: error)) - - let queryDuration = getTime().toUnixFloat() - queryStartTime - waku_legacy_archive_query_duration_seconds.observe(queryDuration) - - var hashes = newSeq[WakuMessageHash]() - var messages = newSeq[WakuMessage]() - var topics = newSeq[PubsubTopic]() - var cursor = none(ArchiveCursor) - - if rows.len == 0: - return ok(ArchiveResponse(hashes: hashes, messages: messages, cursor: cursor)) - - ## Messages - let pageSize = min(rows.len, int(maxPageSize)) - - if query.includeData: - topics = rows[0 ..< pageSize].mapIt(it[0]) - messages = rows[0 ..< pageSize].mapIt(it[1]) - - hashes = rows[0 ..< pageSize].mapIt(it[4]) - - ## Cursor - if rows.len > int(maxPageSize): - ## Build last message cursor - ## The cursor is built from the last message INCLUDED in the response - ## (i.e. the second last message in the rows list) - - let (pubsubTopic, message, digest, storeTimestamp, hash) = rows[^2] - - cursor = some( - ArchiveCursor( - digest: MessageDigest.fromBytes(digest), - storeTime: storeTimestamp, - sendertime: message.timestamp, - pubsubTopic: pubsubTopic, - hash: hash, - ) - ) - - # All messages MUST be returned in chronological order - if not isAscendingOrder: - reverse(hashes) - reverse(messages) - reverse(topics) - - return ok( - ArchiveResponse(hashes: hashes, messages: messages, topics: topics, cursor: cursor) - ) - -proc findMessagesV2*( - self: WakuArchive, query: ArchiveQuery -): Future[ArchiveResult] {.async, deprecated, gcsafe.} = - ## Search the archive to return a single page of messages matching the query criteria - - let maxPageSize = - if query.pageSize <= 0: - DefaultPageSize - else: - min(query.pageSize, MaxPageSize) - - let isAscendingOrder = query.direction.into() - - if query.contentTopics.len > 10: - return err(ArchiveError.invalidQuery("too many content topics")) - - let queryStartTime = getTime().toUnixFloat() - - let rows = ( - await self.driver.getMessagesV2( - contentTopic = query.contentTopics, - pubsubTopic = query.pubsubTopic, - cursor = query.cursor, - startTime = query.startTime, - endTime = query.endTime, - maxPageSize = maxPageSize + 1, - ascendingOrder = isAscendingOrder, - requestId = query.requestId, - ) - ).valueOr: - return err(ArchiveError(kind: ArchiveErrorKind.DRIVER_ERROR, cause: error)) - - let queryDuration = getTime().toUnixFloat() - queryStartTime - waku_legacy_archive_query_duration_seconds.observe(queryDuration) - - var messages = newSeq[WakuMessage]() - var cursor = none(ArchiveCursor) - - if rows.len == 0: - return ok(ArchiveResponse(messages: messages, cursor: cursor)) - - ## Messages - let pageSize = min(rows.len, int(maxPageSize)) - - messages = rows[0 ..< pageSize].mapIt(it[1]) - - ## Cursor - if rows.len > int(maxPageSize): - ## Build last message cursor - ## The cursor is built from the last message INCLUDED in the response - ## (i.e. the second last message in the rows list) - - let (pubsubTopic, message, digest, storeTimestamp, _) = rows[^2] - - cursor = some( - ArchiveCursor( - digest: MessageDigest.fromBytes(digest), - storeTime: storeTimestamp, - sendertime: message.timestamp, - pubsubTopic: pubsubTopic, - ) - ) - - # All messages MUST be returned in chronological order - if not isAscendingOrder: - reverse(messages) - - return ok(ArchiveResponse(messages: messages, cursor: cursor)) diff --git a/waku/waku_archive_legacy/archive_metrics.nim b/waku/waku_archive_legacy/archive_metrics.nim deleted file mode 100644 index c3569a1ea..000000000 --- a/waku/waku_archive_legacy/archive_metrics.nim +++ /dev/null @@ -1,22 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import metrics - -declarePublicGauge waku_legacy_archive_messages, - "number of historical messages", ["type"] -declarePublicCounter waku_legacy_archive_errors, - "number of store protocol errors", ["type"] -declarePublicHistogram waku_legacy_archive_insert_duration_seconds, - "message insertion duration" -declarePublicHistogram waku_legacy_archive_query_duration_seconds, - "history query duration" - -# Error types (metric label values) -const - invalidMessageOld* = "invalid_message_too_old" - invalidMessageFuture* = "invalid_message_future_timestamp" - insertFailure* = "insert_failure" - retPolicyFailure* = "retpolicy_failure" diff --git a/waku/waku_archive_legacy/common.nim b/waku/waku_archive_legacy/common.nim deleted file mode 100644 index ed2b7272d..000000000 --- a/waku/waku_archive_legacy/common.nim +++ /dev/null @@ -1,88 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import std/options, results, stew/byteutils, stew/arrayops, nimcrypto/sha2 -import ../waku_core, ../common/paging - -## Waku message digest - -type MessageDigest* = MDigest[256] - -proc fromBytes*(T: type MessageDigest, src: seq[byte]): T = - var data: array[32, byte] - - let byteCount = copyFrom[byte](data, src) - - assert byteCount == 32 - - return MessageDigest(data: data) - -proc computeDigest*(msg: WakuMessage): MessageDigest = - var ctx: sha256 - ctx.init() - defer: - ctx.clear() - - ctx.update(msg.contentTopic.toBytes()) - ctx.update(msg.payload) - - # Computes the hash - return ctx.finish() - -## API types - -type - #TODO Once Store v2 is removed, the cursor becomes the hash of the last message - ArchiveCursor* = object - digest*: MessageDigest - storeTime*: Timestamp - senderTime*: Timestamp - pubsubTopic*: PubsubTopic - hash*: WakuMessageHash - - ArchiveQuery* = object - includeData*: bool # indicate if messages should be returned in addition to hashes. - pubsubTopic*: Option[PubsubTopic] - contentTopics*: seq[ContentTopic] - cursor*: Option[ArchiveCursor] - startTime*: Option[Timestamp] - endTime*: Option[Timestamp] - hashes*: seq[WakuMessageHash] - pageSize*: uint - direction*: PagingDirection - requestId*: string - - ArchiveResponse* = object - hashes*: seq[WakuMessageHash] - messages*: seq[WakuMessage] - topics*: seq[PubsubTopic] - cursor*: Option[ArchiveCursor] - - ArchiveErrorKind* {.pure.} = enum - UNKNOWN = uint32(0) - DRIVER_ERROR = uint32(1) - INVALID_QUERY = uint32(2) - - ArchiveError* = object - case kind*: ArchiveErrorKind - of DRIVER_ERROR, INVALID_QUERY: - # TODO: Add an enum to be able to distinguish between error causes - cause*: string - else: - discard - - ArchiveResult* = Result[ArchiveResponse, ArchiveError] - -proc `$`*(err: ArchiveError): string = - case err.kind - of ArchiveErrorKind.DRIVER_ERROR: - "DRIVER_ERROR: " & err.cause - of ArchiveErrorKind.INVALID_QUERY: - "INVALID_QUERY: " & err.cause - of ArchiveErrorKind.UNKNOWN: - "UNKNOWN" - -proc invalidQuery*(T: type ArchiveError, cause: string): T = - ArchiveError(kind: ArchiveErrorKind.INVALID_QUERY, cause: cause) diff --git a/waku/waku_archive_legacy/driver.nim b/waku/waku_archive_legacy/driver.nim deleted file mode 100644 index 8ff8df029..000000000 --- a/waku/waku_archive_legacy/driver.nim +++ /dev/null @@ -1,121 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import std/options, results, chronos -import ../waku_core, ./common - -const DefaultPageSize*: uint = 25 - -type - ArchiveDriverResult*[T] = Result[T, string] - ArchiveDriver* = ref object of RootObj - -#TODO Once Store v2 is removed keep only messages and hashes -type ArchiveRow* = (PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash) - -# ArchiveDriver interface - -method put*( - driver: ArchiveDriver, - pubsubTopic: PubsubTopic, - message: WakuMessage, - digest: MessageDigest, - messageHash: WakuMessageHash, - receivedTime: Timestamp, -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method getAllMessages*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.base, async.} = - discard - -method getMessagesV2*( - driver: ArchiveDriver, - contentTopic = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.base, deprecated, async.} = - discard - -method getMessages*( - driver: ArchiveDriver, - includeData = true, - contentTopic = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = newSeq[WakuMessageHash](0), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId = "", -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.base, async.} = - discard - -method getMessagesCount*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[int64]] {.base, async.} = - discard - -method getPagesCount*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[int64]] {.base, async.} = - discard - -method getPagesSize*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[int64]] {.base, async.} = - discard - -method getDatabaseSize*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[int64]] {.base, async.} = - discard - -method performVacuum*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method getOldestMessageTimestamp*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[Timestamp]] {.base, async.} = - discard - -method getNewestMessageTimestamp*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[Timestamp]] {.base, async.} = - discard - -method deleteMessagesOlderThanTimestamp*( - driver: ArchiveDriver, ts: Timestamp -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method deleteOldestMessagesNotWithinLimit*( - driver: ArchiveDriver, limit: int -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method decreaseDatabaseSize*( - driver: ArchiveDriver, targetSizeInBytes: int64, forceRemoval: bool = false -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method close*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method existsTable*( - driver: ArchiveDriver, tableName: string -): Future[ArchiveDriverResult[bool]] {.base, async.} = - discard diff --git a/waku/waku_archive_legacy/driver/builder.nim b/waku/waku_archive_legacy/driver/builder.nim deleted file mode 100644 index 0f19b3669..000000000 --- a/waku/waku_archive_legacy/driver/builder.nim +++ /dev/null @@ -1,89 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import results, chronicles, chronos -import - ../driver, - ../../common/databases/dburl, - ../../common/databases/db_sqlite, - ../../common/error_handling, - ./sqlite_driver, - ./sqlite_driver/migrations as archive_driver_sqlite_migrations, - ./queue_driver - -export sqlite_driver, queue_driver - -when defined(postgres): - import ## These imports add dependency with an external libpq library - ./postgres_driver - export postgres_driver - -proc new*( - T: type ArchiveDriver, - url: string, - vacuum: bool, - migrate: bool, - maxNumConn: int, - onFatalErrorAction: OnFatalErrorHandler, -): Future[Result[T, string]] {.async.} = - ## url - string that defines the database - ## vacuum - if true, a cleanup operation will be applied to the database - ## migrate - if true, the database schema will be updated - ## maxNumConn - defines the maximum number of connections to handle simultaneously (Postgres) - ## onFatalErrorAction - called if, e.g., the connection with db got lost - - dburl.validateDbUrl(url).isOkOr: - return err("DbUrl failure in ArchiveDriver.new: " & error) - - let engine = dburl.getDbEngine(url).valueOr: - return err("error getting db engine in setupWakuArchiveDriver: " & error) - - case engine - of "sqlite": - let path = dburl.getDbPath(url).valueOr: - return err("error get path in setupWakuArchiveDriver: " & error) - - let db = SqliteDatabase.new(path).valueOr: - return err("error in setupWakuArchiveDriver: " & error) - - # SQLite vacuum - let (pageSize, pageCount, freelistCount) = db.gatherSqlitePageStats().valueOr: - return err("error while gathering sqlite stats: " & $error) - - info "sqlite database page stats", - pageSize = pageSize, pages = pageCount, freePages = freelistCount - - if vacuum and (pageCount > 0 and freelistCount > 0): - db.performSqliteVacuum().isOkOr: - return err("error in vacuum sqlite: " & $error) - - # Database migration - if migrate: - archive_driver_sqlite_migrations.migrate(db).isOkOr: - return err("error in migrate sqlite: " & $error) - - info "setting up sqlite waku archive driver" - let res = SqliteDriver.new(db).valueOr: - return err("failed to init sqlite archive driver: " & error) - - return ok(res) - of "postgres": - when defined(postgres): - let driver = PostgresDriver.new( - dbUrl = url, - maxConnections = maxNumConn, - onFatalErrorAction = onFatalErrorAction, - ).valueOr: - return err("failed to init postgres archive driver: " & error) - - return ok(driver) - else: - return err( - "Postgres has been configured but not been compiled. Check compiler definitions." - ) - else: - info "setting up in-memory waku archive driver" - let driver = QueueDriver.new() # Defaults to a capacity of 25.000 messages - return ok(driver) diff --git a/waku/waku_archive_legacy/driver/postgres_driver.nim b/waku/waku_archive_legacy/driver/postgres_driver.nim deleted file mode 100644 index 496005cbe..000000000 --- a/waku/waku_archive_legacy/driver/postgres_driver.nim +++ /dev/null @@ -1,8 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import ./postgres_driver/postgres_driver - -export postgres_driver diff --git a/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim deleted file mode 100644 index a6784e4f8..000000000 --- a/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim +++ /dev/null @@ -1,976 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import - std/[options, sequtils, strutils, strformat, times], - stew/[byteutils, arrayops], - results, - chronos, - db_connector/[postgres, db_common], - chronicles -import - ../../../common/error_handling, - ../../../waku_core, - ../../common, - ../../driver, - ./postgres_healthcheck, - ../../../common/databases/db_postgres as waku_postgres - -type PostgresDriver* = ref object of ArchiveDriver - ## Establish a separate pools for read/write operations - writeConnPool: PgAsyncPool - readConnPool: PgAsyncPool - -const InsertRowStmtName = "InsertRow" -const InsertRowStmtDefinition = # TODO: get the sql queries from a file - """INSERT INTO messages (id, messageHash, contentTopic, payload, pubsubTopic, - version, timestamp, meta) VALUES ($1, $2, $3, $4, $5, $6, $7, CASE WHEN $8 = '' THEN NULL ELSE $8 END) ON CONFLICT DO NOTHING;""" - -const InsertRowInMessagesLookupStmtName = "InsertRowMessagesLookup" -const InsertRowInMessagesLookupStmtDefinition = - """INSERT INTO messages_lookup (messageHash, timestamp) VALUES ($1, $2) ON CONFLICT DO NOTHING;""" - -const SelectNoCursorAscStmtName = "SelectWithoutCursorAsc" -const SelectNoCursorAscStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - messageHash IN ($2) AND - pubsubTopic = $3 AND - timestamp >= $4 AND - timestamp <= $5 - ORDER BY timestamp ASC, messageHash ASC LIMIT $6;""" - -const SelectNoCursorDescStmtName = "SelectWithoutCursorDesc" -const SelectNoCursorDescStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - messageHash IN ($2) AND - pubsubTopic = $3 AND - timestamp >= $4 AND - timestamp <= $5 - ORDER BY timestamp DESC, messageHash DESC LIMIT $6;""" - -const SelectWithCursorDescStmtName = "SelectWithCursorDesc" -const SelectWithCursorDescStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - messageHash IN ($2) AND - pubsubTopic = $3 AND - (timestamp, messageHash) < ($4,$5) AND - timestamp >= $6 AND - timestamp <= $7 - ORDER BY timestamp DESC, messageHash DESC LIMIT $8;""" - -const SelectWithCursorAscStmtName = "SelectWithCursorAsc" -const SelectWithCursorAscStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - messageHash IN ($2) AND - pubsubTopic = $3 AND - (timestamp, messageHash) > ($4,$5) AND - timestamp >= $6 AND - timestamp <= $7 - ORDER BY timestamp ASC, messageHash ASC LIMIT $8;""" - -const SelectMessageByHashName = "SelectMessageByHash" -const SelectMessageByHashDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages WHERE messageHash = $1""" - -const SelectNoCursorV2AscStmtName = "SelectWithoutCursorV2Asc" -const SelectNoCursorV2AscStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - pubsubTopic = $2 AND - timestamp >= $3 AND - timestamp <= $4 - ORDER BY timestamp ASC LIMIT $5;""" - -const SelectNoCursorV2DescStmtName = "SelectWithoutCursorV2Desc" -const SelectNoCursorV2DescStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - pubsubTopic = $2 AND - timestamp >= $3 AND - timestamp <= $4 - ORDER BY timestamp DESC LIMIT $5;""" - -const SelectWithCursorV2DescStmtName = "SelectWithCursorV2Desc" -const SelectWithCursorV2DescStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - pubsubTopic = $2 AND - (timestamp, id) < ($3,$4) AND - timestamp >= $5 AND - timestamp <= $6 - ORDER BY timestamp DESC LIMIT $7;""" - -const SelectWithCursorV2AscStmtName = "SelectWithCursorV2Asc" -const SelectWithCursorV2AscStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - pubsubTopic = $2 AND - (timestamp, id) > ($3,$4) AND - timestamp >= $5 AND - timestamp <= $6 - ORDER BY timestamp ASC LIMIT $7;""" - -const DefaultMaxNumConns = 50 - -proc new*( - T: type PostgresDriver, - dbUrl: string, - maxConnections = DefaultMaxNumConns, - onFatalErrorAction: OnFatalErrorHandler = nil, -): ArchiveDriverResult[T] = - ## Very simplistic split of max connections - let maxNumConnOnEachPool = int(maxConnections / 2) - - let readConnPool = PgAsyncPool.new(dbUrl, maxNumConnOnEachPool).valueOr: - return err("error creating read conn pool PgAsyncPool") - - let writeConnPool = PgAsyncPool.new(dbUrl, maxNumConnOnEachPool).valueOr: - return err("error creating write conn pool PgAsyncPool") - - if not isNil(onFatalErrorAction): - asyncSpawn checkConnectivity(readConnPool, onFatalErrorAction) - - if not isNil(onFatalErrorAction): - asyncSpawn checkConnectivity(writeConnPool, onFatalErrorAction) - - let driver = PostgresDriver(writeConnPool: writeConnPool, readConnPool: readConnPool) - return ok(driver) - -proc reset*(s: PostgresDriver): Future[ArchiveDriverResult[void]] {.async.} = - ## Clear the database partitions - let targetSize = 0 - let forceRemoval = true - let ret = await s.decreaseDatabaseSize(targetSize, forceRemoval) - return ret - -proc rowCallbackImpl( - pqResult: ptr PGresult, - outRows: var seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)], -) = - ## Proc aimed to contain the logic of the callback passed to the `psasyncpool`. - ## That callback is used in "SELECT" queries. - ## - ## pqResult - contains the query results - ## outRows - seq of Store-rows. This is populated from the info contained in pqResult - - let numFields = pqResult.pqnfields() - if numFields != 8: - error "Wrong number of fields, expected 8", numFields - return - - for iRow in 0 ..< pqResult.pqNtuples(): - var wakuMessage: WakuMessage - var timestamp: Timestamp - var version: uint - var pubSubTopic: string - var contentTopic: string - var digest: string - var payload: string - var hashHex: string - var msgHash: WakuMessageHash - var meta: string - - try: - contentTopic = $(pqgetvalue(pqResult, iRow, 0)) - payload = parseHexStr($(pqgetvalue(pqResult, iRow, 1))) - pubSubTopic = $(pqgetvalue(pqResult, iRow, 2)) - version = parseUInt($(pqgetvalue(pqResult, iRow, 3))) - timestamp = parseInt($(pqgetvalue(pqResult, iRow, 4))) - digest = parseHexStr($(pqgetvalue(pqResult, iRow, 5))) - hashHex = parseHexStr($(pqgetvalue(pqResult, iRow, 6))) - meta = parseHexStr($(pqgetvalue(pqResult, iRow, 7))) - msgHash = fromBytes(hashHex.toOpenArrayByte(0, 31)) - except ValueError: - error "could not parse correctly", error = getCurrentExceptionMsg() - - wakuMessage.timestamp = timestamp - wakuMessage.version = uint32(version) - wakuMessage.contentTopic = contentTopic - wakuMessage.payload = @(payload.toOpenArrayByte(0, payload.high)) - wakuMessage.meta = @(meta.toOpenArrayByte(0, meta.high)) - - outRows.add( - ( - pubSubTopic, - wakuMessage, - @(digest.toOpenArrayByte(0, digest.high)), - timestamp, - msgHash, - ) - ) - -method put*( - s: PostgresDriver, - pubsubTopic: PubsubTopic, - message: WakuMessage, - digest: MessageDigest, - messageHash: WakuMessageHash, - receivedTime: Timestamp, -): Future[ArchiveDriverResult[void]] {.async.} = - let digest = byteutils.toHex(digest.data) - let messageHash = byteutils.toHex(messageHash) - let contentTopic = message.contentTopic - let payload = byteutils.toHex(message.payload) - let version = $message.version - let timestamp = $message.timestamp - let meta = byteutils.toHex(message.meta) - - trace "put PostgresDriver", timestamp = timestamp - - ( - await s.writeConnPool.runStmt( - InsertRowStmtName, - InsertRowStmtDefinition, - @[ - digest, messageHash, contentTopic, payload, pubsubTopic, version, timestamp, - meta, - ], - @[ - int32(digest.len), - int32(messageHash.len), - int32(contentTopic.len), - int32(payload.len), - int32(pubsubTopic.len), - int32(version.len), - int32(timestamp.len), - int32(meta.len), - ], - @[int32(0), int32(0), int32(0), int32(0), int32(0), int32(0), int32(0), int32(0)], - ) - ).isOkOr: - return err("could not put msg in messages table: " & $error) - - ## Now add the row to messages_lookup - return await s.writeConnPool.runStmt( - InsertRowInMessagesLookupStmtName, - InsertRowInMessagesLookupStmtDefinition, - @[messageHash, timestamp], - @[int32(messageHash.len), int32(timestamp.len)], - @[int32(0), int32(0)], - ) - -method getAllMessages*( - s: PostgresDriver -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - ## Retrieve all messages from the store. - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - ( - await s.readConnPool.pgQuery( - """SELECT contentTopic, - payload, pubsubTopic, version, timestamp, - id, messageHash, meta FROM messages ORDER BY timestamp ASC""", - newSeq[string](0), - rowCallback, - ) - ).isOkOr: - return err("failed in query: " & $error) - - return ok(rows) - -proc getMessagesArbitraryQuery( - s: PostgresDriver, - contentTopic: seq[ContentTopic] = @[], - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hexHashes: seq[string] = @[], - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - ## This proc allows to handle atypical queries. We don't use prepared statements for those. - - var query = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages""" - var statements: seq[string] - var args: seq[string] - - if contentTopic.len > 0: - let cstmt = "contentTopic IN (" & "?".repeat(contentTopic.len).join(",") & ")" - statements.add(cstmt) - for t in contentTopic: - args.add(t) - - if hexHashes.len > 0: - let cstmt = "messageHash IN (" & "?".repeat(hexHashes.len).join(",") & ")" - statements.add(cstmt) - for t in hexHashes: - args.add(t) - - if pubsubTopic.isSome(): - statements.add("pubsubTopic = ?") - args.add(pubsubTopic.get()) - - if cursor.isSome(): - let hashHex = byteutils.toHex(cursor.get().hash) - - var entree: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc entreeCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, entree) - - ( - await s.readConnPool.runStmt( - SelectMessageByHashName, - SelectMessageByHashDef, - @[hashHex], - @[int32(hashHex.len)], - @[int32(0)], - entreeCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query with cursor: " & $error) - - if entree.len == 0: - return ok(entree) - - let storetime = entree[0][3] - - let comp = if ascendingOrder: ">" else: "<" - statements.add("(timestamp, messageHash) " & comp & " (?,?)") - args.add($storetime) - args.add(hashHex) - - if startTime.isSome(): - statements.add("timestamp >= ?") - args.add($startTime.get()) - - if endTime.isSome(): - statements.add("timestamp <= ?") - args.add($endTime.get()) - - if statements.len > 0: - query &= " WHERE " & statements.join(" AND ") - - var direction: string - if ascendingOrder: - direction = "ASC" - else: - direction = "DESC" - - query &= " ORDER BY timestamp " & direction & ", messageHash " & direction - - query &= " LIMIT ?" - args.add($maxPageSize) - - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - (await s.readConnPool.pgQuery(query, args, rowCallback, requestId)).isOkOr: - return err("failed to run query: " & $error) - - return ok(rows) - -proc getMessagesV2ArbitraryQuery( - s: PostgresDriver, - contentTopic: seq[ContentTopic] = @[], - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async, deprecated.} = - ## This proc allows to handle atypical queries. We don't use prepared statements for those. - - var query = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages""" - var statements: seq[string] - var args: seq[string] - - if contentTopic.len > 0: - let cstmt = "contentTopic IN (" & "?".repeat(contentTopic.len).join(",") & ")" - statements.add(cstmt) - for t in contentTopic: - args.add(t) - - if pubsubTopic.isSome(): - statements.add("pubsubTopic = ?") - args.add(pubsubTopic.get()) - - if cursor.isSome(): - let comp = if ascendingOrder: ">" else: "<" - statements.add("(timestamp, id) " & comp & " (?,?)") - args.add($cursor.get().storeTime) - args.add(toHex(cursor.get().digest.data)) - - if startTime.isSome(): - statements.add("timestamp >= ?") - args.add($startTime.get()) - - if endTime.isSome(): - statements.add("timestamp <= ?") - args.add($endTime.get()) - - if statements.len > 0: - query &= " WHERE " & statements.join(" AND ") - - var direction: string - if ascendingOrder: - direction = "ASC" - else: - direction = "DESC" - - query &= " ORDER BY timestamp " & direction & ", id " & direction - - query &= " LIMIT ?" - args.add($maxPageSize) - - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - (await s.readConnPool.pgQuery(query, args, rowCallback, requestId)).isOkOr: - return err("failed to run query: " & $error) - - return ok(rows) - -proc getMessagesPreparedStmt( - s: PostgresDriver, - contentTopic: string, - pubsubTopic: PubsubTopic, - cursor = none(ArchiveCursor), - startTime: Timestamp, - endTime: Timestamp, - hashes: string, - maxPageSize = DefaultPageSize, - ascOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - ## This proc aims to run the most typical queries in a more performant way, i.e. by means of - ## prepared statements. - ## - ## contentTopic - string with list of conten topics. e.g: "'ctopic1','ctopic2','ctopic3'" - - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - let startTimeStr = $startTime - let endTimeStr = $endTime - let limit = $maxPageSize - - if cursor.isSome(): - let hash = byteutils.toHex(cursor.get().hash) - - var entree: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - - proc entreeCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, entree) - - ( - await s.readConnPool.runStmt( - SelectMessageByHashName, - SelectMessageByHashDef, - @[hash], - @[int32(hash.len)], - @[int32(0)], - entreeCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query with cursor: " & $error) - - if entree.len == 0: - return ok(entree) - - let timestamp = $entree[0][3] - - var stmtName = - if ascOrder: SelectWithCursorAscStmtName else: SelectWithCursorDescStmtName - var stmtDef = - if ascOrder: SelectWithCursorAscStmtDef else: SelectWithCursorDescStmtDef - - ( - await s.readConnPool.runStmt( - stmtName, - stmtDef, - @[ - contentTopic, hashes, pubsubTopic, timestamp, hash, startTimeStr, endTimeStr, - limit, - ], - @[ - int32(contentTopic.len), - int32(hashes.len), - int32(pubsubTopic.len), - int32(timestamp.len), - int32(hash.len), - int32(startTimeStr.len), - int32(endTimeStr.len), - int32(limit.len), - ], - @[ - int32(0), int32(0), int32(0), int32(0), int32(0), int32(0), int32(0), int32(0) - ], - rowCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query with cursor: " & $error) - else: - var stmtName = - if ascOrder: SelectNoCursorAscStmtName else: SelectNoCursorDescStmtName - var stmtDef = if ascOrder: SelectNoCursorAscStmtDef else: SelectNoCursorDescStmtDef - - ( - await s.readConnPool.runStmt( - stmtName, - stmtDef, - @[contentTopic, hashes, pubsubTopic, startTimeStr, endTimeStr, limit], - @[ - int32(contentTopic.len), - int32(hashes.len), - int32(pubsubTopic.len), - int32(startTimeStr.len), - int32(endTimeStr.len), - int32(limit.len), - ], - @[int32(0), int32(0), int32(0), int32(0), int32(0), int32(0)], - rowCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query without cursor: " & $error) - - return ok(rows) - -proc getMessagesV2PreparedStmt( - s: PostgresDriver, - contentTopic: string, - pubsubTopic: PubsubTopic, - cursor = none(ArchiveCursor), - startTime: Timestamp, - endTime: Timestamp, - maxPageSize = DefaultPageSize, - ascOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async, deprecated.} = - ## This proc aims to run the most typical queries in a more performant way, i.e. by means of - ## prepared statements. - ## - ## contentTopic - string with list of conten topics. e.g: "'ctopic1','ctopic2','ctopic3'" - - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - let startTimeStr = $startTime - let endTimeStr = $endTime - let limit = $maxPageSize - - if cursor.isSome(): - var stmtName = - if ascOrder: SelectWithCursorV2AscStmtName else: SelectWithCursorV2DescStmtName - var stmtDef = - if ascOrder: SelectWithCursorV2AscStmtDef else: SelectWithCursorV2DescStmtDef - - let digest = byteutils.toHex(cursor.get().digest.data) - let timestamp = $cursor.get().storeTime - - ( - await s.readConnPool.runStmt( - stmtName, - stmtDef, - @[contentTopic, pubsubTopic, timestamp, digest, startTimeStr, endTimeStr, limit], - @[ - int32(contentTopic.len), - int32(pubsubTopic.len), - int32(timestamp.len), - int32(digest.len), - int32(startTimeStr.len), - int32(endTimeStr.len), - int32(limit.len), - ], - @[int32(0), int32(0), int32(0), int32(0), int32(0), int32(0), int32(0)], - rowCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query with cursor: " & $error) - else: - var stmtName = - if ascOrder: SelectNoCursorV2AscStmtName else: SelectNoCursorV2DescStmtName - var stmtDef = - if ascOrder: SelectNoCursorV2AscStmtDef else: SelectNoCursorV2DescStmtDef - - ( - await s.readConnPool.runStmt( - stmtName, - stmtDef, - @[contentTopic, pubsubTopic, startTimeStr, endTimeStr, limit], - @[ - int32(contentTopic.len), - int32(pubsubTopic.len), - int32(startTimeStr.len), - int32(endTimeStr.len), - int32(limit.len), - ], - @[int32(0), int32(0), int32(0), int32(0), int32(0)], - rowCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query without cursor: " & $error) - - return ok(rows) - -proc getMessagesByMessageHashes( - s: PostgresDriver, hashes: string, maxPageSize: uint, requestId: string -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - ## Retrieves information only filtering by a given messageHashes list. - ## This proc levarages on the messages_lookup table to have better query performance - ## and only query the desired partitions in the partitioned messages table - var query = - fmt""" - WITH min_timestamp AS ( - SELECT MIN(timestamp) AS min_ts - FROM messages_lookup - WHERE messagehash IN ( - {hashes} - ) - ) - SELECT contentTopic, payload, pubsubTopic, version, m.timestamp, id, m.messageHash, meta - FROM messages m - INNER JOIN - messages_lookup l - ON - m.timestamp = l.timestamp - AND m.messagehash = l.messagehash - WHERE - l.timestamp >= (SELECT min_ts FROM min_timestamp) - AND l.messagehash IN ( - {hashes} - ) - ORDER BY - m.timestamp DESC, - m.messagehash DESC - LIMIT {maxPageSize}; - """ - - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - ( - await s.readConnPool.pgQuery( - query = query, rowCallback = rowCallback, requestId = requestId - ) - ).isOkOr: - return err("failed to run query: " & $error) - - return ok(rows) - -method getMessages*( - s: PostgresDriver, - includeData = true, - contentTopicSeq = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = newSeq[WakuMessageHash](0), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId = "", -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - let hexHashes = hashes.mapIt(toHex(it)) - - if cursor.isNone() and pubsubTopic.isNone() and contentTopicSeq.len == 0 and - startTime.isNone() and endTime.isNone() and hexHashes.len > 0: - return await s.getMessagesByMessageHashes( - "'" & hexHashes.join("','") & "'", maxPageSize, requestId - ) - - if contentTopicSeq.len == 1 and hexHashes.len == 1 and pubsubTopic.isSome() and - startTime.isSome() and endTime.isSome(): - ## Considered the most common query. Therefore, we use prepared statements to optimize it. - return await s.getMessagesPreparedStmt( - contentTopicSeq.join(","), - PubsubTopic(pubsubTopic.get()), - cursor, - startTime.get(), - endTime.get(), - hexHashes.join(","), - maxPageSize, - ascendingOrder, - requestId, - ) - else: - ## We will run atypical query. In this case we don't use prepared statemets - return await s.getMessagesArbitraryQuery( - contentTopicSeq, pubsubTopic, cursor, startTime, endTime, hexHashes, maxPageSize, - ascendingOrder, requestId, - ) - -method getMessagesV2*( - s: PostgresDriver, - contentTopicSeq = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async, deprecated.} = - if contentTopicSeq.len == 1 and pubsubTopic.isSome() and startTime.isSome() and - endTime.isSome(): - ## Considered the most common query. Therefore, we use prepared statements to optimize it. - return await s.getMessagesV2PreparedStmt( - contentTopicSeq.join(","), - PubsubTopic(pubsubTopic.get()), - cursor, - startTime.get(), - endTime.get(), - maxPageSize, - ascendingOrder, - requestId, - ) - else: - ## We will run atypical query. In this case we don't use prepared statemets - return await s.getMessagesV2ArbitraryQuery( - contentTopicSeq, pubsubTopic, cursor, startTime, endTime, maxPageSize, - ascendingOrder, requestId, - ) - -proc getStr( - s: PostgresDriver, query: string -): Future[ArchiveDriverResult[string]] {.async.} = - # Performs a query that is expected to return a single string - - var ret: string - proc rowCallback(pqResult: ptr PGresult) = - if pqResult.pqnfields() != 1: - error "Wrong number of fields in getStr" - return - - if pqResult.pqNtuples() != 1: - error "Wrong number of rows in getStr" - return - - ret = $(pqgetvalue(pqResult, 0, 0)) - - (await s.readConnPool.pgQuery(query, newSeq[string](0), rowCallback)).isOkOr: - return err("failed in getRow: " & $error) - - return ok(ret) - -proc getInt( - s: PostgresDriver, query: string -): Future[ArchiveDriverResult[int64]] {.async.} = - # Performs a query that is expected to return a single numeric value (int64) - - var retInt = 0'i64 - let str = (await s.getStr(query)).valueOr: - return err("could not get str in getInt: " & $error) - - try: - retInt = parseInt(str) - except ValueError: - return err( - "exception in getInt, parseInt, str: " & str & " query: " & query & " exception: " & - getCurrentExceptionMsg() - ) - - return ok(retInt) - -method getDatabaseSize*( - s: PostgresDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - let intRes = (await s.getInt("SELECT pg_database_size(current_database())")).valueOr: - return err("error in getDatabaseSize: " & error) - - let databaseSize: int64 = int64(intRes) - return ok(databaseSize) - -method getMessagesCount*( - s: PostgresDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - let intRes = (await s.getInt("SELECT COUNT(1) FROM messages")).valueOr: - return err("error in getMessagesCount: " & error) - - return ok(intRes) - -method getOldestMessageTimestamp*( - s: PostgresDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - return err("not implemented because legacy will get deprecated") - -method getNewestMessageTimestamp*( - s: PostgresDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - let intRes = (await s.getInt("SELECT MAX(timestamp) FROM messages")).valueOr: - return err("error in getNewestMessageTimestamp: " & error) - - return ok(Timestamp(intRes)) - -method deleteOldestMessagesNotWithinLimit*( - s: PostgresDriver, limit: int -): Future[ArchiveDriverResult[void]] {.async.} = - ## Will be completely removed when deprecating store legacy - # let execRes = await s.writeConnPool.pgQuery( - # """DELETE FROM messages WHERE id NOT IN - # ( - # SELECT id FROM messages ORDER BY timestamp DESC LIMIT ? - # );""", - # @[$limit], - # ) - # if execRes.isErr(): - # return err("error in deleteOldestMessagesNotWithinLimit: " & execRes.error) - - return ok() - -method close*(s: PostgresDriver): Future[ArchiveDriverResult[void]] {.async.} = - ## Close the database connection - let writeCloseRes = await s.writeConnPool.close() - let readCloseRes = await s.readConnPool.close() - - writeCloseRes.isOkOr: - return err("error closing write pool: " & $error) - - readCloseRes.isOkOr: - return err("error closing read pool: " & $error) - - return ok() - -proc sleep*( - s: PostgresDriver, seconds: int -): Future[ArchiveDriverResult[void]] {.async.} = - # This is for testing purposes only. It is aimed to test the proper - # implementation of asynchronous requests. It merely triggers a sleep in the - # database for the amount of seconds given as a parameter. - - proc rowCallback(result: ptr PGresult) = - ## We are not interested in any value in this case - discard - - try: - let params = @[$seconds] - (await s.writeConnPool.pgQuery("SELECT pg_sleep(?)", params, rowCallback)).isOkOr: - return err("error in postgres_driver sleep: " & $error) - except DbError: - # This always raises an exception although the sleep works - return err("exception sleeping: " & getCurrentExceptionMsg()) - - return ok() - -proc performWriteQuery*( - s: PostgresDriver, query: string -): Future[ArchiveDriverResult[void]] {.async.} = - ## Performs a query that somehow changes the state of the database - - (await s.writeConnPool.pgQuery(query)).isOkOr: - return err("error in performWriteQuery: " & $error) - - return ok() - -method decreaseDatabaseSize*( - driver: PostgresDriver, targetSizeInBytes: int64, forceRemoval: bool = false -): Future[ArchiveDriverResult[void]] {.async.} = - ## This is completely disabled and only the non-legacy driver - ## will take care of that - # var dbSize = (await driver.getDatabaseSize()).valueOr: - # return err("decreaseDatabaseSize failed to get database size: " & $error) - - # ## database size in bytes - # var totalSizeOfDB: int64 = int64(dbSize) - - # if totalSizeOfDB <= targetSizeInBytes: - # return ok() - - # info "start reducing database size", - # targetSize = $targetSizeInBytes, currentSize = $totalSizeOfDB - - # while totalSizeOfDB > targetSizeInBytes and driver.containsAnyPartition(): - # (await driver.removeOldestPartition(forceRemoval)).isOkOr: - # return err( - # "decreaseDatabaseSize inside loop failed to remove oldest partition: " & $error - # ) - - # dbSize = (await driver.getDatabaseSize()).valueOr: - # return - # err("decreaseDatabaseSize inside loop failed to get database size: " & $error) - - # let newCurrentSize = int64(dbSize) - # if newCurrentSize == totalSizeOfDB: - # return err("the previous partition removal didn't clear database size") - - # totalSizeOfDB = newCurrentSize - - # info "reducing database size", - # targetSize = $targetSizeInBytes, newCurrentSize = $totalSizeOfDB - - return ok() - -method existsTable*( - s: PostgresDriver, tableName: string -): Future[ArchiveDriverResult[bool]] {.async.} = - let query: string = - fmt""" - SELECT EXISTS ( - SELECT FROM - pg_tables - WHERE - tablename = '{tableName}' - ); - """ - - var exists: string - proc rowCallback(pqResult: ptr PGresult) = - if pqResult.pqnfields() != 1: - error "Wrong number of fields in existsTable" - return - - if pqResult.pqNtuples() != 1: - error "Wrong number of rows in existsTable" - return - - exists = $(pqgetvalue(pqResult, 0, 0)) - - (await s.readConnPool.pgQuery(query, newSeq[string](0), rowCallback)).isOkOr: - return err("existsTable failed in getRow: " & $error) - - return ok(exists == "t") - -proc getCurrentVersion*( - s: PostgresDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - let existsVersionTable = (await s.existsTable("version")).valueOr: - return err("error in getCurrentVersion-existsTable: " & $error) - - if not existsVersionTable: - return ok(0) - - let res = (await s.getInt(fmt"SELECT version FROM version")).valueOr: - return err("error in getMessagesCount: " & $error) - - return ok(res) - -method deleteMessagesOlderThanTimestamp*( - s: PostgresDriver, tsNanoSec: Timestamp -): Future[ArchiveDriverResult[void]] {.async.} = - ## First of all, let's remove the older partitions so that we can reduce - ## the database size. - # (await s.removePartitionsOlderThan(tsNanoSec)).isOkOr: - # return err("error while removing older partitions: " & $error) - - # ( - # await s.writeConnPool.pgQuery( - # "DELETE FROM messages WHERE timestamp < " & $tsNanoSec - # ) - # ).isOkOr: - # return err("error in deleteMessagesOlderThanTimestamp: " & $error) - - return ok() diff --git a/waku/waku_archive_legacy/driver/postgres_driver/postgres_healthcheck.nim b/waku/waku_archive_legacy/driver/postgres_driver/postgres_healthcheck.nim deleted file mode 100644 index 23678538e..000000000 --- a/waku/waku_archive_legacy/driver/postgres_driver/postgres_healthcheck.nim +++ /dev/null @@ -1,37 +0,0 @@ -{.push raises: [].} - -import chronos, chronicles, results -import ../../../common/databases/db_postgres, ../../../common/error_handling - -## Simple query to validate that the postgres is working and attending requests -const HealthCheckQuery = "SELECT version();" -const CheckConnectivityInterval = 60.seconds -const MaxNumTrials = 20 -const TrialInterval = 1.seconds - -proc checkConnectivity*( - connPool: PgAsyncPool, onFatalErrorAction: OnFatalErrorHandler -) {.async.} = - while true: - (await connPool.pgQuery(HealthCheckQuery)).isOkOr: - ## The connection failed once. Let's try reconnecting for a while. - ## Notice that the 'pgQuery' proc tries to establish a new connection. - - block errorBlock: - ## Force close all the opened connections. No need to close gracefully. - (await connPool.resetConnPool()).isOkOr: - onFatalErrorAction("checkConnectivity legacy resetConnPool error: " & error) - - var numTrial = 0 - while numTrial < MaxNumTrials: - (await connPool.pgQuery(HealthCheckQuery)).isErrOr: - ## Connection resumed. Let's go back to the normal healthcheck. - break errorBlock - - await sleepAsync(TrialInterval) - numTrial.inc() - - ## The connection couldn't be resumed. Let's inform the upper layers. - onFatalErrorAction("postgres legacy health check error: " & error) - - await sleepAsync(CheckConnectivityInterval) diff --git a/waku/waku_archive_legacy/driver/queue_driver.nim b/waku/waku_archive_legacy/driver/queue_driver.nim deleted file mode 100644 index 1ea8a29d3..000000000 --- a/waku/waku_archive_legacy/driver/queue_driver.nim +++ /dev/null @@ -1,8 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import ./queue_driver/queue_driver, ./queue_driver/index - -export queue_driver, index diff --git a/waku/waku_archive_legacy/driver/queue_driver/index.nim b/waku/waku_archive_legacy/driver/queue_driver/index.nim deleted file mode 100644 index 2328870d0..000000000 --- a/waku/waku_archive_legacy/driver/queue_driver/index.nim +++ /dev/null @@ -1,91 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import nimcrypto/sha2 -import ../../../waku_core, ../../common - -type Index* = object - ## This type contains the description of an Index used in the pagination of WakuMessages - pubsubTopic*: string - senderTime*: Timestamp # the time at which the message is generated - receiverTime*: Timestamp - digest*: MessageDigest # calculated over payload and content topic - hash*: WakuMessageHash - -proc compute*( - T: type Index, msg: WakuMessage, receivedTime: Timestamp, pubsubTopic: PubsubTopic -): T = - ## Takes a WakuMessage with received timestamp and returns its Index. - let - digest = computeDigest(msg) - senderTime = msg.timestamp - hash = computeMessageHash(pubsubTopic, msg) - - return Index( - pubsubTopic: pubsubTopic, - senderTime: senderTime, - receiverTime: receivedTime, - digest: digest, - hash: hash, - ) - -proc tohistoryCursor*(index: Index): ArchiveCursor = - return ArchiveCursor( - pubsubTopic: index.pubsubTopic, - senderTime: index.senderTime, - storeTime: index.receiverTime, - digest: index.digest, - hash: index.hash, - ) - -proc toIndex*(index: ArchiveCursor): Index = - return Index( - pubsubTopic: index.pubsubTopic, - senderTime: index.senderTime, - receiverTime: index.storeTime, - digest: index.digest, - hash: index.hash, - ) - -proc `==`*(x, y: Index): bool = - ## receiverTime plays no role in index equality - return - ( - (x.senderTime == y.senderTime) and (x.digest == y.digest) and - (x.pubsubTopic == y.pubsubTopic) - ) or (x.hash == y.hash) # this applies to store v3 queries only - -proc cmp*(x, y: Index): int = - ## compares x and y - ## returns 0 if they are equal - ## returns -1 if x < y - ## returns 1 if x > y - ## - ## Default sorting order priority is: - ## 1. senderTimestamp - ## 2. receiverTimestamp (a fallback only if senderTimestamp unset on either side, and all other fields unequal) - ## 3. message digest - ## 4. pubsubTopic - - if x == y: - # Quick exit ensures receiver time does not affect index equality - return 0 - - # Timestamp has a higher priority for comparison - let - # Use receiverTime where senderTime is unset - xTimestamp = if x.senderTime == 0: x.receiverTime else: x.senderTime - yTimestamp = if y.senderTime == 0: y.receiverTime else: y.senderTime - - let timecmp = cmp(xTimestamp, yTimestamp) - if timecmp != 0: - return timecmp - - # Continue only when timestamps are equal - let digestcmp = cmp(x.digest.data, y.digest.data) - if digestcmp != 0: - return digestcmp - - return cmp(x.pubsubTopic, y.pubsubTopic) diff --git a/waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim b/waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim deleted file mode 100644 index 530a84034..000000000 --- a/waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim +++ /dev/null @@ -1,363 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import std/options, results, stew/sorted_set, chronicles, chronos -import ../../../waku_core, ../../common, ../../driver, ./index - -logScope: - topics = "waku archive queue_store" - -const QueueDriverDefaultMaxCapacity* = 25_000 - -type - QueryFilterMatcher = - proc(index: Index, msg: WakuMessage): bool {.gcsafe, raises: [], closure.} - - QueueDriver* = ref object of ArchiveDriver - ## Bounded repository for indexed messages - ## - ## The store queue will keep messages up to its - ## configured capacity. As soon as this capacity - ## is reached and a new message is added, the oldest - ## item will be removed to make space for the new one. - ## This implies both a `delete` and `add` operation - ## for new items. - - # TODO: a circular/ring buffer may be a more efficient implementation - items: SortedSet[Index, WakuMessage] # sorted set of stored messages - capacity: int # Maximum amount of messages to keep - - QueueDriverErrorKind {.pure.} = enum - INVALID_CURSOR - - QueueDriverGetPageResult = Result[seq[ArchiveRow], QueueDriverErrorKind] - -proc `$`(error: QueueDriverErrorKind): string = - case error - of INVALID_CURSOR: "invalid_cursor" - -### Helpers - -proc walkToCursor( - w: SortedSetWalkRef[Index, WakuMessage], startCursor: Index, forward: bool -): SortedSetResult[Index, WakuMessage] = - ## Walk to util we find the cursor - ## TODO: Improve performance here with a binary/tree search - - var nextItem = - if forward: - w.first() - else: - w.last() - - ## Fast forward until we reach the startCursor - while nextItem.isOk(): - if nextItem.value.key == startCursor: - break - - # Not yet at cursor. Continue advancing - nextItem = - if forward: - w.next() - else: - w.prev() - - return nextItem - -#### API - -proc new*(T: type QueueDriver, capacity: int = QueueDriverDefaultMaxCapacity): T = - var items = SortedSet[Index, WakuMessage].init() - return QueueDriver(items: items, capacity: capacity) - -proc contains*(driver: QueueDriver, index: Index): bool = - ## Return `true` if the store queue already contains the `index`, `false` otherwise. - return driver.items.eq(index).isOk() - -proc len*(driver: QueueDriver): int {.noSideEffect.} = - return driver.items.len - -proc getPage( - driver: QueueDriver, - pageSize: uint = 0, - forward: bool = true, - cursor: Option[Index] = none(Index), - predicate: QueryFilterMatcher = nil, -): QueueDriverGetPageResult {.raises: [].} = - ## Populate a single page in forward direction - ## Start at the `startCursor` (exclusive), or first entry (inclusive) if not defined. - ## Page size must not exceed `maxPageSize` - ## Each entry must match the `pred` - var outSeq: seq[ArchiveRow] - - var w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - defer: - w.destroy() - - var currentEntry: SortedSetResult[Index, WakuMessage] - - # Find starting entry - if cursor.isSome(): - w.walkToCursor(cursor.get(), forward).isOkOr: - return err(QueueDriverErrorKind.INVALID_CURSOR) - - # Advance walker once more - currentEntry = - if forward: - w.next() - else: - w.prev() - else: - # Start from the beginning of the queue - currentEntry = - if forward: - w.first() - else: - w.last() - - trace "Starting page query", currentEntry = currentEntry - - ## This loop walks forward over the queue: - ## 1. from the given cursor (or first/last entry, if not provided) - ## 2. adds entries matching the predicate function to output page - ## 3. until either the end of the queue or maxPageSize is reached - var numberOfItems: uint = 0 - while currentEntry.isOk() and numberOfItems < pageSize: - trace "Continuing page query", - currentEntry = currentEntry, numberOfItems = numberOfItems - - let - key = currentEntry.value.key - data = currentEntry.value.data - - if predicate.isNil() or predicate(key, data): - numberOfItems += 1 - - outSeq.add( - (key.pubsubTopic, data, @(key.digest.data), key.receiverTime, key.hash) - ) - - currentEntry = - if forward: - w.next() - else: - w.prev() - - trace "Successfully retrieved page", len = outSeq.len - - return ok(outSeq) - -## --- SortedSet accessors --- - -iterator fwdIterator*(driver: QueueDriver): (Index, WakuMessage) = - ## Forward iterator over the entire store queue - var - w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - res = w.first() - - while res.isOk(): - yield (res.value.key, res.value.data) - res = w.next() - - w.destroy() - -iterator bwdIterator*(driver: QueueDriver): (Index, WakuMessage) = - ## Backwards iterator over the entire store queue - var - w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - res = w.last() - - while res.isOk(): - yield (res.value.key, res.value.data) - res = w.prev() - - w.destroy() - -proc first*(driver: QueueDriver): ArchiveDriverResult[Index] = - var - w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - res = w.first() - w.destroy() - - res.isOkOr: - return err("Not found") - - return ok(res.value.key) - -proc last*(driver: QueueDriver): ArchiveDriverResult[Index] = - var - w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - res = w.last() - w.destroy() - - res.isOkOr: - return err("Not found") - - return ok(res.value.key) - -## --- Queue API --- - -proc add*( - driver: QueueDriver, index: Index, msg: WakuMessage -): ArchiveDriverResult[void] = - ## Add a message to the queue - ## - ## If we're at capacity, we will be removing, the oldest (first) item - if driver.contains(index): - trace "could not add item to store queue. Index already exists", index = index - return err("duplicate") - - # TODO: the below delete block can be removed if we convert to circular buffer - if driver.items.len >= driver.capacity: - var - w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - firstItem = w.first - - if cmp(index, firstItem.value.key) < 0: - # When at capacity, we won't add if message index is smaller (older) than our oldest item - w.destroy # Clean up walker - return err("too_old") - - discard driver.items.delete(firstItem.value.key) - w.destroy # better to destroy walker after a delete operation - - driver.items.insert(index).value.data = msg - - return ok() - -method put*( - driver: QueueDriver, - pubsubTopic: PubsubTopic, - message: WakuMessage, - digest: MessageDigest, - messageHash: WakuMessageHash, - receivedTime: Timestamp, -): Future[ArchiveDriverResult[void]] {.async.} = - let index = Index( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - receiverTime: receivedTime, - digest: digest, - hash: messageHash, - ) - - return driver.add(index, message) - -method getAllMessages*( - driver: QueueDriver -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - # TODO: Implement this message_store method - return err("interface method not implemented") - -method existsTable*( - driver: QueueDriver, tableName: string -): Future[ArchiveDriverResult[bool]] {.async.} = - return err("interface method not implemented") - -method getMessages*( - driver: QueueDriver, - includeData = true, - contentTopic: seq[ContentTopic] = @[], - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes: seq[WakuMessageHash] = @[], - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId = "", -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - let cursor = cursor.map(toIndex) - - let matchesQuery: QueryFilterMatcher = - func (index: Index, msg: WakuMessage): bool = - if pubsubTopic.isSome() and index.pubsubTopic != pubsubTopic.get(): - return false - - if contentTopic.len > 0 and msg.contentTopic notin contentTopic: - return false - - if startTime.isSome() and msg.timestamp < startTime.get(): - return false - - if endTime.isSome() and msg.timestamp > endTime.get(): - return false - - if hashes.len > 0 and index.hash notin hashes: - return false - - return true - - var pageRes: QueueDriverGetPageResult - try: - pageRes = driver.getPage(maxPageSize, ascendingOrder, cursor, matchesQuery) - except CatchableError, Exception: - return err(getCurrentExceptionMsg()) - - pageRes.isOkOr: - return err($error) - - return ok(pageRes.value) - -method getMessagesCount*( - driver: QueueDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - return ok(int64(driver.len())) - -method getPagesCount*( - driver: QueueDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - return ok(int64(driver.len())) - -method getPagesSize*( - driver: QueueDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - return ok(int64(driver.len())) - -method getDatabaseSize*( - driver: QueueDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - return ok(int64(driver.len())) - -method performVacuum*( - driver: QueueDriver -): Future[ArchiveDriverResult[void]] {.async.} = - return err("interface method not implemented") - -method getOldestMessageTimestamp*( - driver: QueueDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - return driver.first().map( - proc(index: Index): Timestamp = - index.receiverTime - ) - -method getNewestMessageTimestamp*( - driver: QueueDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - return driver.last().map( - proc(index: Index): Timestamp = - index.receiverTime - ) - -method deleteMessagesOlderThanTimestamp*( - driver: QueueDriver, ts: Timestamp -): Future[ArchiveDriverResult[void]] {.async.} = - # TODO: Implement this message_store method - return err("interface method not implemented") - -method deleteOldestMessagesNotWithinLimit*( - driver: QueueDriver, limit: int -): Future[ArchiveDriverResult[void]] {.async.} = - # TODO: Implement this message_store method - return err("interface method not implemented") - -method decreaseDatabaseSize*( - driver: QueueDriver, targetSizeInBytes: int64, forceRemoval: bool = false -): Future[ArchiveDriverResult[void]] {.async.} = - return err("interface method not implemented") - -method close*(driver: QueueDriver): Future[ArchiveDriverResult[void]] {.async.} = - return ok() diff --git a/waku/waku_archive_legacy/driver/sqlite_driver.nim b/waku/waku_archive_legacy/driver/sqlite_driver.nim deleted file mode 100644 index 027e00488..000000000 --- a/waku/waku_archive_legacy/driver/sqlite_driver.nim +++ /dev/null @@ -1,8 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import ./sqlite_driver/sqlite_driver - -export sqlite_driver diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/cursor.nim b/waku/waku_archive_legacy/driver/sqlite_driver/cursor.nim deleted file mode 100644 index 9729f0ff7..000000000 --- a/waku/waku_archive_legacy/driver/sqlite_driver/cursor.nim +++ /dev/null @@ -1,11 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import ../../../waku_core, ../../common - -type DbCursor* = (Timestamp, seq[byte], PubsubTopic) - -proc toDbCursor*(c: ArchiveCursor): DbCursor = - (c.storeTime, @(c.digest.data), c.pubsubTopic) diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim b/waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim deleted file mode 100644 index 3d8905e7e..000000000 --- a/waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim +++ /dev/null @@ -1,71 +0,0 @@ -{.push raises: [].} - -import - std/[tables, strutils, os], results, chronicles, sqlite3_abi # sqlite3_column_int64 -import ../../../common/databases/db_sqlite, ../../../common/databases/common - -logScope: - topics = "waku archive migration" - -const SchemaVersion* = 9 # increase this when there is an update in the database schema - -template projectRoot(): string = - currentSourcePath.rsplit(DirSep, 1)[0] / ".." / ".." / ".." / ".." - -const MessageStoreMigrationPath: string = projectRoot / "migrations" / "message_store" - -proc isSchemaVersion7*(db: SqliteDatabase): DatabaseResult[bool] = - ## Temporary proc created to analyse when the table actually belongs to the SchemaVersion 7. - ## - ## During many nwaku versions, 0.14.0 until 0.18.0, the SchemaVersion wasn't set or checked. - ## Docker `nwaku` nodes that start working from these versions, 0.14.0 until 0.18.0, they started - ## with this discrepancy: `user_version`== 0 (not set) but Message table with SchemaVersion 7. - ## - ## We found issues where `user_version` (SchemaVersion) was set to 0 in the database even though - ## its scheme structure reflected SchemaVersion 7. In those cases, when `nwaku` re-started to - ## apply the migration scripts (in 0.19.0) the node didn't start properly because it tried to - ## migrate a database that already had the Schema structure #7, so it failed when changing the PK. - ## - ## TODO: This was added in version 0.20.0. We might remove this in version 0.30.0, as we - ## could consider that many users use +0.20.0. - - var pkColumns = newSeq[string]() - proc queryRowCallback(s: ptr sqlite3_stmt) = - let colName = cstring sqlite3_column_text(s, 0) - pkColumns.add($colName) - - let query = - """SELECT l.name FROM pragma_table_info("Message") as l WHERE l.pk != 0;""" - db.query(query, queryRowCallback).isOkOr: - return err("failed to determine the current SchemaVersion: " & $error) - - if pkColumns == @["pubsubTopic", "id", "storedAt"]: - return ok(true) - else: - info "Not considered schema version 7" - return ok(false) - -proc migrate*(db: SqliteDatabase, targetVersion = SchemaVersion): DatabaseResult[void] = - ## Compares the `user_version` of the sqlite database with the provided `targetVersion`, then - ## it runs migration scripts if the `user_version` is outdated. The `migrationScriptsDir` path - ## points to the directory holding the migrations scripts once the db is updated, it sets the - ## `user_version` to the `tragetVersion`. - ## - ## If not `targetVersion` is provided, it defaults to `SchemaVersion`. - ## - ## NOTE: Down migration it is not currently supported - info "starting message store's sqlite database migration" - - let userVersion = ?db.getUserVersion() - let isSchemaVersion7 = ?db.isSchemaVersion7() - - if userVersion == 0'i64 and isSchemaVersion7: - info "We found user_version 0 but the database schema reflects the user_version 7" - ## Force the correct schema version - ?db.setUserVersion(7) - - migrate(db, targetVersion, migrationsScriptsDir = MessageStoreMigrationPath).isOkOr: - return err("failed to execute migration scripts: " & error) - - info "finished message store's sqlite database migration" - return ok() diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim b/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim deleted file mode 100644 index 4590a8df1..000000000 --- a/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim +++ /dev/null @@ -1,729 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import std/[options, sequtils], stew/byteutils, sqlite3_abi, results -import - ../../../common/databases/db_sqlite, - ../../../common/databases/common, - ../../../waku_core, - ./cursor - -const DbTable = "Message" - -type SqlQueryStr = string - -### SQLite column helper methods - -proc queryRowWakuMessageCallback( - s: ptr sqlite3_stmt, - contentTopicCol, payloadCol, versionCol, senderTimestampCol, metaCol: cint, -): WakuMessage = - let - topic = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, contentTopicCol)) - topicLength = sqlite3_column_bytes(s, contentTopicCol) - contentTopic = string.fromBytes(@(toOpenArray(topic, 0, topicLength - 1))) - - p = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, payloadCol)) - m = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, metaCol)) - - payloadLength = sqlite3_column_bytes(s, payloadCol) - metaLength = sqlite3_column_bytes(s, metaCol) - payload = @(toOpenArray(p, 0, payloadLength - 1)) - version = sqlite3_column_int64(s, versionCol) - senderTimestamp = sqlite3_column_int64(s, senderTimestampCol) - meta = @(toOpenArray(m, 0, metaLength - 1)) - - return WakuMessage( - contentTopic: ContentTopic(contentTopic), - payload: payload, - version: uint32(version), - timestamp: Timestamp(senderTimestamp), - meta: meta, - ) - -proc queryRowReceiverTimestampCallback( - s: ptr sqlite3_stmt, storedAtCol: cint -): Timestamp = - let storedAt = sqlite3_column_int64(s, storedAtCol) - return Timestamp(storedAt) - -proc queryRowPubsubTopicCallback( - s: ptr sqlite3_stmt, pubsubTopicCol: cint -): PubsubTopic = - let - pubsubTopicPointer = - cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, pubsubTopicCol)) - pubsubTopicLength = sqlite3_column_bytes(s, pubsubTopicCol) - pubsubTopic = - string.fromBytes(@(toOpenArray(pubsubTopicPointer, 0, pubsubTopicLength - 1))) - - return pubsubTopic - -proc queryRowDigestCallback(s: ptr sqlite3_stmt, digestCol: cint): seq[byte] = - let - digestPointer = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, digestCol)) - digestLength = sqlite3_column_bytes(s, digestCol) - digest = @(toOpenArray(digestPointer, 0, digestLength - 1)) - - return digest - -proc queryRowWakuMessageHashCallback( - s: ptr sqlite3_stmt, hashCol: cint -): WakuMessageHash = - let - hashPointer = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, hashCol)) - hashLength = sqlite3_column_bytes(s, hashCol) - hash = fromBytes(toOpenArray(hashPointer, 0, hashLength - 1)) - - return hash - -### SQLite queries - -## Create table - -proc createTableQuery(table: string): SqlQueryStr = - "CREATE TABLE IF NOT EXISTS " & table & " (" & " pubsubTopic BLOB NOT NULL," & - " contentTopic BLOB NOT NULL," & " payload BLOB," & " version INTEGER NOT NULL," & - " timestamp INTEGER NOT NULL," & " id BLOB," & " messageHash BLOB," & - " storedAt INTEGER NOT NULL," & " meta BLOB," & - " CONSTRAINT messageIndex PRIMARY KEY (messageHash)" & ") WITHOUT ROWID;" - -proc createTable*(db: SqliteDatabase): DatabaseResult[void] = - let query = createTableQuery(DbTable) - discard ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) - return ok() - -## Create indices - -proc createOldestMessageTimestampIndexQuery(table: string): SqlQueryStr = - "CREATE INDEX IF NOT EXISTS i_ts ON " & table & " (storedAt);" - -proc createOldestMessageTimestampIndex*(db: SqliteDatabase): DatabaseResult[void] = - let query = createOldestMessageTimestampIndexQuery(DbTable) - discard ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) - return ok() - -proc createHistoryQueryIndexQuery(table: string): SqlQueryStr = - "CREATE INDEX IF NOT EXISTS i_query ON " & table & - " (contentTopic, pubsubTopic, storedAt, id);" - -proc createHistoryQueryIndex*(db: SqliteDatabase): DatabaseResult[void] = - let query = createHistoryQueryIndexQuery(DbTable) - discard ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) - return ok() - -## Insert message -type InsertMessageParams* = ( - seq[byte], - seq[byte], - Timestamp, - seq[byte], - seq[byte], - seq[byte], - int64, - Timestamp, - seq[byte], -) - -proc insertMessageQuery(table: string): SqlQueryStr = - return - "INSERT INTO " & table & - "(id, messageHash, storedAt, contentTopic, payload, pubsubTopic, version, timestamp, meta)" & - " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);" - -proc prepareInsertMessageStmt*( - db: SqliteDatabase -): SqliteStmt[InsertMessageParams, void] = - let query = insertMessageQuery(DbTable) - return - db.prepareStmt(query, InsertMessageParams, void).expect("this is a valid statement") - -## Count table messages - -proc countMessagesQuery(table: string): SqlQueryStr = - return "SELECT COUNT(*) FROM " & table - -proc getMessageCount*(db: SqliteDatabase): DatabaseResult[int64] = - var count: int64 - proc queryRowCallback(s: ptr sqlite3_stmt) = - count = sqlite3_column_int64(s, 0) - - let query = countMessagesQuery(DbTable) - db.query(query, queryRowCallback).isOkOr: - return err("failed to count number of messages in the database") - - return ok(count) - -## Get oldest message receiver timestamp - -proc selectOldestMessageTimestampQuery(table: string): SqlQueryStr = - return "SELECT MIN(storedAt) FROM " & table - -proc selectOldestReceiverTimestamp*( - db: SqliteDatabase -): DatabaseResult[Timestamp] {.inline.} = - var timestamp: Timestamp - proc queryRowCallback(s: ptr sqlite3_stmt) = - timestamp = queryRowReceiverTimestampCallback(s, 0) - - let query = selectOldestMessageTimestampQuery(DbTable) - db.query(query, queryRowCallback).isOkOr: - return err("failed to get the oldest receiver timestamp from the database") - - return ok(timestamp) - -## Get newest message receiver timestamp - -proc selectNewestMessageTimestampQuery(table: string): SqlQueryStr = - return "SELECT MAX(storedAt) FROM " & table - -proc selectNewestReceiverTimestamp*( - db: SqliteDatabase -): DatabaseResult[Timestamp] {.inline.} = - var timestamp: Timestamp - proc queryRowCallback(s: ptr sqlite3_stmt) = - timestamp = queryRowReceiverTimestampCallback(s, 0) - - let query = selectNewestMessageTimestampQuery(DbTable) - db.query(query, queryRowCallback).isOkOr: - return err("failed to get the newest receiver timestamp from the database") - - return ok(timestamp) - -## Delete messages older than timestamp - -proc deleteMessagesOlderThanTimestampQuery(table: string, ts: Timestamp): SqlQueryStr = - return "DELETE FROM " & table & " WHERE storedAt < " & $ts - -proc deleteMessagesOlderThanTimestamp*( - db: SqliteDatabase, ts: int64 -): DatabaseResult[void] = - let query = deleteMessagesOlderThanTimestampQuery(DbTable, ts) - discard ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) - return ok() - -## Delete oldest messages not within limit - -proc deleteOldestMessagesNotWithinLimitQuery(table: string, limit: int): SqlQueryStr = - return - "DELETE FROM " & table & " WHERE (storedAt, id, pubsubTopic) NOT IN (" & - " SELECT storedAt, id, pubsubTopic FROM " & table & - " ORDER BY storedAt DESC, id DESC" & " LIMIT " & $limit & ");" - -proc deleteOldestMessagesNotWithinLimit*( - db: SqliteDatabase, limit: int -): DatabaseResult[void] = - # NOTE: The word `limit` here refers the store capacity/maximum number-of-messages allowed limit - let query = deleteOldestMessagesNotWithinLimitQuery(DbTable, limit = limit) - discard ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) - return ok() - -## Select all messages - -proc selectAllMessagesQuery(table: string): SqlQueryStr = - return - "SELECT storedAt, contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta" & - " FROM " & table & " ORDER BY storedAt ASC" - -proc selectAllMessages*( - db: SqliteDatabase -): DatabaseResult[ - seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] -] {.gcsafe.} = - ## Retrieve all messages from the store. - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc queryRowCallback(s: ptr sqlite3_stmt) = - let - pubsubTopic = queryRowPubsubTopicCallback(s, pubsubTopicCol = 3) - wakuMessage = queryRowWakuMessageCallback( - s, - contentTopicCol = 1, - payloadCol = 2, - versionCol = 4, - senderTimestampCol = 5, - metaCol = 8, - ) - digest = queryRowDigestCallback(s, digestCol = 6) - storedAt = queryRowReceiverTimestampCallback(s, storedAtCol = 0) - hash = queryRowWakuMessageHashCallback(s, hashCol = 7) - - rows.add((pubsubTopic, wakuMessage, digest, storedAt, hash)) - - let query = selectAllMessagesQuery(DbTable) - discard ?db.query(query, queryRowCallback) - - return ok(rows) - -## Select messages by history query with limit - -proc combineClauses(clauses: varargs[Option[string]]): Option[string] = - let whereSeq = @clauses.filterIt(it.isSome()).mapIt(it.get()) - if whereSeq.len <= 0: - return none(string) - - var where: string = whereSeq[0] - for clause in whereSeq[1 ..^ 1]: - where &= " AND " & clause - return some(where) - -proc whereClausev2( - cursor: bool, - pubsubTopic: Option[PubsubTopic], - contentTopic: seq[ContentTopic], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - ascending: bool, -): Option[string] {.deprecated.} = - let cursorClause = - if cursor: - let comp = if ascending: ">" else: "<" - - some("(storedAt, id) " & comp & " (?, ?)") - else: - none(string) - - let pubsubTopicClause = - if pubsubTopic.isNone(): - none(string) - else: - some("pubsubTopic = (?)") - - let contentTopicClause = - if contentTopic.len <= 0: - none(string) - else: - var where = "contentTopic IN (" - where &= "?" - for _ in 1 ..< contentTopic.len: - where &= ", ?" - where &= ")" - some(where) - - let startTimeClause = - if startTime.isNone(): - none(string) - else: - some("storedAt >= (?)") - - let endTimeClause = - if endTime.isNone(): - none(string) - else: - some("storedAt <= (?)") - - return combineClauses( - cursorClause, pubsubTopicClause, contentTopicClause, startTimeClause, endTimeClause - ) - -proc selectMessagesWithLimitQueryv2( - table: string, where: Option[string], limit: uint, ascending = true, v3 = false -): SqlQueryStr {.deprecated.} = - let order = if ascending: "ASC" else: "DESC" - - var query: string - - query = - "SELECT storedAt, contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta" - query &= " FROM " & table - - if where.isSome(): - query &= " WHERE " & where.get() - - query &= " ORDER BY storedAt " & order & ", id " & order - - query &= " LIMIT " & $limit & ";" - - return query - -proc prepareStmt( - db: SqliteDatabase, stmt: string -): DatabaseResult[SqliteStmt[void, void]] = - var s: RawStmtPtr - checkErr sqlite3_prepare_v2(db.env, stmt, stmt.len.cint, addr s, nil) - return ok(SqliteStmt[void, void](s)) - -proc execSelectMessagesV2WithLimitStmt( - s: SqliteStmt, - cursor: Option[DbCursor], - pubsubTopic: Option[PubsubTopic], - contentTopic: seq[ContentTopic], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - onRowCallback: DataProc, -): DatabaseResult[void] {.deprecated.} = - let s = RawStmtPtr(s) - - # Bind params - var paramIndex = 1 - - if cursor.isSome(): - let (storedAt, id, _) = cursor.get() - checkErr bindParam(s, paramIndex, storedAt) - paramIndex += 1 - checkErr bindParam(s, paramIndex, id) - paramIndex += 1 - - if pubsubTopic.isSome(): - let pubsubTopic = toBytes(pubsubTopic.get()) - checkErr bindParam(s, paramIndex, pubsubTopic) - paramIndex += 1 - - for topic in contentTopic: - checkErr bindParam(s, paramIndex, topic.toBytes()) - paramIndex += 1 - - if startTime.isSome(): - let time = startTime.get() - checkErr bindParam(s, paramIndex, time) - paramIndex += 1 - - if endTime.isSome(): - let time = endTime.get() - checkErr bindParam(s, paramIndex, time) - paramIndex += 1 - - try: - while true: - let v = sqlite3_step(s) - case v - of SQLITE_ROW: - onRowCallback(s) - of SQLITE_DONE: - return ok() - else: - return err($sqlite3_errstr(v)) - except Exception, CatchableError: - # release implicit transaction - discard sqlite3_reset(s) # same return information as step - discard sqlite3_clear_bindings(s) # no errors possible - -proc selectMessagesByHistoryQueryWithLimit*( - db: SqliteDatabase, - contentTopic: seq[ContentTopic], - pubsubTopic: Option[PubsubTopic], - cursor: Option[DbCursor], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - limit: uint, - ascending: bool, -): DatabaseResult[ - seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] -] {.deprecated.} = - var messages: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] = - @[] - - proc queryRowCallback(s: ptr sqlite3_stmt) = - let - pubsubTopic = queryRowPubsubTopicCallback(s, pubsubTopicCol = 3) - message = queryRowWakuMessageCallback( - s, - contentTopicCol = 1, - payloadCol = 2, - versionCol = 4, - senderTimestampCol = 5, - metaCol = 8, - ) - digest = queryRowDigestCallback(s, digestCol = 6) - storedAt = queryRowReceiverTimestampCallback(s, storedAtCol = 0) - hash = queryRowWakuMessageHashCallback(s, hashCol = 7) - - messages.add((pubsubTopic, message, digest, storedAt, hash)) - - let query = block: - let where = whereClausev2( - cursor.isSome(), pubsubTopic, contentTopic, startTime, endTime, ascending - ) - - selectMessagesWithLimitQueryv2(DbTable, where, limit, ascending) - - let dbStmt = ?db.prepareStmt(query) - ?dbStmt.execSelectMessagesV2WithLimitStmt( - cursor, pubsubTopic, contentTopic, startTime, endTime, queryRowCallback - ) - dbStmt.dispose() - - return ok(messages) - -### Store v3 ### - -proc execSelectMessageByHash( - s: SqliteStmt, hash: WakuMessageHash, onRowCallback: DataProc -): DatabaseResult[void] = - let s = RawStmtPtr(s) - - checkErr bindParam(s, 1, toSeq(hash)) - - try: - while true: - let v = sqlite3_step(s) - case v - of SQLITE_ROW: - onRowCallback(s) - of SQLITE_DONE: - return ok() - else: - return err($sqlite3_errstr(v)) - except Exception, CatchableError: - # release implicit transaction - discard sqlite3_reset(s) # same return information as step - discard sqlite3_clear_bindings(s) # no errors possible - -proc selectMessageByHashQuery(): SqlQueryStr = - var query: string - - query = "SELECT contentTopic, payload, version, timestamp, meta, messageHash" - query &= " FROM " & DbTable - query &= " WHERE messageHash = (?)" - - return query - -proc whereClause( - cursor: bool, - pubsubTopic: Option[PubsubTopic], - contentTopic: seq[ContentTopic], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - hashes: seq[WakuMessageHash], - ascending: bool, -): Option[string] = - let cursorClause = - if cursor: - let comp = if ascending: ">" else: "<" - - some("(timestamp, messageHash) " & comp & " (?, ?)") - else: - none(string) - - let pubsubTopicClause = - if pubsubTopic.isNone(): - none(string) - else: - some("pubsubTopic = (?)") - - let contentTopicClause = - if contentTopic.len <= 0: - none(string) - else: - var where = "contentTopic IN (" - where &= "?" - for _ in 1 ..< contentTopic.len: - where &= ", ?" - where &= ")" - some(where) - - let startTimeClause = - if startTime.isNone(): - none(string) - else: - some("storedAt >= (?)") - - let endTimeClause = - if endTime.isNone(): - none(string) - else: - some("storedAt <= (?)") - - let hashesClause = - if hashes.len <= 0: - none(string) - else: - var where = "messageHash IN (" - where &= "?" - for _ in 1 ..< hashes.len: - where &= ", ?" - where &= ")" - some(where) - - return combineClauses( - cursorClause, pubsubTopicClause, contentTopicClause, startTimeClause, endTimeClause, - hashesClause, - ) - -proc execSelectMessagesWithLimitStmt( - s: SqliteStmt, - cursor: Option[(Timestamp, WakuMessageHash)], - pubsubTopic: Option[PubsubTopic], - contentTopic: seq[ContentTopic], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - hashes: seq[WakuMessageHash], - onRowCallback: DataProc, -): DatabaseResult[void] = - let s = RawStmtPtr(s) - - # Bind params - var paramIndex = 1 - - if cursor.isSome(): - let (time, hash) = cursor.get() - checkErr bindParam(s, paramIndex, time) - paramIndex += 1 - checkErr bindParam(s, paramIndex, toSeq(hash)) - paramIndex += 1 - - if pubsubTopic.isSome(): - let pubsubTopic = toBytes(pubsubTopic.get()) - checkErr bindParam(s, paramIndex, pubsubTopic) - paramIndex += 1 - - for topic in contentTopic: - checkErr bindParam(s, paramIndex, topic.toBytes()) - paramIndex += 1 - - for hash in hashes: - checkErr bindParam(s, paramIndex, toSeq(hash)) - paramIndex += 1 - - if startTime.isSome(): - let time = startTime.get() - checkErr bindParam(s, paramIndex, time) - paramIndex += 1 - - if endTime.isSome(): - let time = endTime.get() - checkErr bindParam(s, paramIndex, time) - paramIndex += 1 - - try: - while true: - let v = sqlite3_step(s) - case v - of SQLITE_ROW: - onRowCallback(s) - of SQLITE_DONE: - return ok() - else: - return err($sqlite3_errstr(v)) - except Exception, CatchableError: - # release implicit transaction - discard sqlite3_reset(s) # same return information as step - discard sqlite3_clear_bindings(s) # no errors possible - -proc selectMessagesWithLimitQuery( - table: string, where: Option[string], limit: uint, ascending = true, v3 = false -): SqlQueryStr = - let order = if ascending: "ASC" else: "DESC" - - var query: string - - query = - "SELECT storedAt, contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta" - query &= " FROM " & table - - if where.isSome(): - query &= " WHERE " & where.get() - - query &= " ORDER BY storedAt " & order & ", messageHash " & order - - query &= " LIMIT " & $limit & ";" - - return query - -proc selectMessagesByStoreQueryWithLimit*( - db: SqliteDatabase, - contentTopic: seq[ContentTopic], - pubsubTopic: Option[PubsubTopic], - cursor: Option[WakuMessageHash], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - hashes: seq[WakuMessageHash], - limit: uint, - ascending: bool, -): DatabaseResult[ - seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] -] = - # Must first get the message timestamp before paginating by time - let newCursor = - if cursor.isSome() and cursor.get() != EmptyWakuMessageHash: - let hash: WakuMessageHash = cursor.get() - - var wakuMessage: Option[WakuMessage] - - proc queryRowCallback(s: ptr sqlite3_stmt) = - wakuMessage = some( - queryRowWakuMessageCallback( - s, - contentTopicCol = 0, - payloadCol = 1, - versionCol = 2, - senderTimestampCol = 3, - metaCol = 4, - ) - ) - - let query = selectMessageByHashQuery() - let dbStmt = ?db.prepareStmt(query) - ?dbStmt.execSelectMessageByHash(hash, queryRowCallback) - dbStmt.dispose() - - if wakuMessage.isSome(): - let time = wakuMessage.get().timestamp - - some((time, hash)) - else: - return err("cursor not found") - else: - none((Timestamp, WakuMessageHash)) - - var messages: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] = - @[] - - proc queryRowCallback(s: ptr sqlite3_stmt) = - let - pubsubTopic = queryRowPubsubTopicCallback(s, pubsubTopicCol = 3) - message = queryRowWakuMessageCallback( - s, - contentTopicCol = 1, - payloadCol = 2, - versionCol = 4, - senderTimestampCol = 5, - metaCol = 8, - ) - digest = queryRowDigestCallback(s, digestCol = 6) - storedAt = queryRowReceiverTimestampCallback(s, storedAtCol = 0) - hash = queryRowWakuMessageHashCallback(s, hashCol = 7) - - messages.add((pubsubTopic, message, digest, storedAt, hash)) - - let query = block: - let where = whereClause( - newCursor.isSome(), - pubsubTopic, - contentTopic, - startTime, - endTime, - hashes, - ascending, - ) - - selectMessagesWithLimitQuery(DbTable, where, limit, ascending, true) - - let dbStmt = ?db.prepareStmt(query) - ?dbStmt.execSelectMessagesWithLimitStmt( - newCursor, pubsubTopic, contentTopic, startTime, endTime, hashes, queryRowCallback - ) - dbStmt.dispose() - - return ok(messages) diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim b/waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim deleted file mode 100644 index 63e7c7eac..000000000 --- a/waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim +++ /dev/null @@ -1,220 +0,0 @@ -# The code in this file is an adaptation of the Sqlite KV Store found in nim-eth. -# https://github.com/status-im/nim-eth/blob/master/eth/db/kvstore_sqlite3.nim -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import std/options, stew/byteutils, chronicles, chronos, results -import - ../../../common/databases/db_sqlite, - ../../../waku_core, - ../../../waku_core/message/digest, - ../../common, - ../../driver, - ./cursor, - ./queries - -logScope: - topics = "waku archive sqlite" - -proc init(db: SqliteDatabase): ArchiveDriverResult[void] = - ## Misconfiguration can lead to nil DB - if db.isNil(): - return err("db not initialized") - - # Create table, if doesn't exist - createTable(db).isOkOr: - return err("failed to create table: " & error) - - # Create indices, if don't exist - createOldestMessageTimestampIndex(db).isOkOr: - return err("failed to create i_rt index: " & error) - - createHistoryQueryIndex(db).isOkOr: - return err("failed to create i_query index: " & error) - - return ok() - -type SqliteDriver* = ref object of ArchiveDriver - db: SqliteDatabase - insertStmt: SqliteStmt[InsertMessageParams, void] - -proc new*(T: type SqliteDriver, db: SqliteDatabase): ArchiveDriverResult[T] = - # Database initialization - ?init(db) - - # General initialization - let insertStmt = db.prepareInsertMessageStmt() - return ok(SqliteDriver(db: db, insertStmt: insertStmt)) - -method put*( - s: SqliteDriver, - pubsubTopic: PubsubTopic, - message: WakuMessage, - digest: MessageDigest, - messageHash: WakuMessageHash, - receivedTime: Timestamp, -): Future[ArchiveDriverResult[void]] {.async.} = - ## Inserts a message into the store - let res = s.insertStmt.exec( - ( - @(digest.data), # id - @(messageHash), # messageHash - receivedTime, # storedAt - toBytes(message.contentTopic), # contentTopic - message.payload, # payload - toBytes(pubsubTopic), # pubsubTopic - int64(message.version), # version - message.timestamp, # senderTimestamp - message.meta, # meta - ) - ) - - return res - -method getAllMessages*( - s: SqliteDriver -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - ## Retrieve all messages from the store. - return s.db.selectAllMessages() - -method getMessagesV2*( - s: SqliteDriver, - contentTopic = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async, deprecated.} = - let cursor = cursor.map(toDbCursor) - - let rowsRes = s.db.selectMessagesByHistoryQueryWithLimit( - contentTopic, - pubsubTopic, - cursor, - startTime, - endTime, - limit = maxPageSize, - ascending = ascendingOrder, - ) - - return rowsRes - -method getMessages*( - s: SqliteDriver, - includeData = true, - contentTopic = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = newSeq[WakuMessageHash](0), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId = "", -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - let cursor = - if cursor.isSome(): - some(cursor.get().hash) - else: - none(WakuMessageHash) - - let rowsRes = s.db.selectMessagesByStoreQueryWithLimit( - contentTopic, - pubsubTopic, - cursor, - startTime, - endTime, - hashes, - limit = maxPageSize, - ascending = ascendingOrder, - ) - - return rowsRes - -method getMessagesCount*( - s: SqliteDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - return s.db.getMessageCount() - -method getPagesCount*(s: SqliteDriver): Future[ArchiveDriverResult[int64]] {.async.} = - return s.db.getPageCount() - -method getPagesSize*(s: SqliteDriver): Future[ArchiveDriverResult[int64]] {.async.} = - return s.db.getPageSize() - -method getDatabaseSize*(s: SqliteDriver): Future[ArchiveDriverResult[int64]] {.async.} = - return s.db.getDatabaseSize() - -method performVacuum*(s: SqliteDriver): Future[ArchiveDriverResult[void]] {.async.} = - return s.db.performSqliteVacuum() - -method getOldestMessageTimestamp*( - s: SqliteDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - return s.db.selectOldestReceiverTimestamp() - -method getNewestMessageTimestamp*( - s: SqliteDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - return s.db.selectnewestReceiverTimestamp() - -method deleteMessagesOlderThanTimestamp*( - s: SqliteDriver, ts: Timestamp -): Future[ArchiveDriverResult[void]] {.async.} = - return s.db.deleteMessagesOlderThanTimestamp(ts) - -method deleteOldestMessagesNotWithinLimit*( - s: SqliteDriver, limit: int -): Future[ArchiveDriverResult[void]] {.async.} = - return s.db.deleteOldestMessagesNotWithinLimit(limit) - -method decreaseDatabaseSize*( - driver: SqliteDriver, targetSizeInBytes: int64, forceRemoval: bool = false -): Future[ArchiveDriverResult[void]] {.async.} = - ## To remove 20% of the outdated data from database - const DeleteLimit = 0.80 - - ## when db size overshoots the database limit, shread 20% of outdated messages - ## get size of database - let dbSize = (await driver.getDatabaseSize()).valueOr: - return err("failed to get database size: " & $error) - - ## database size in bytes - let totalSizeOfDB: int64 = int64(dbSize) - - if totalSizeOfDB < targetSizeInBytes: - return ok() - - ## to shread/delete messsges, get the total row/message count - let numMessages = (await driver.getMessagesCount()).valueOr: - return err("failed to get messages count: " & error) - - ## NOTE: Using SQLite vacuuming is done manually, we delete a percentage of rows - ## if vacumming is done automatically then we aim to check DB size periodially for efficient - ## retention policy implementation. - - ## 80% of the total messages are to be kept, delete others - let pageDeleteWindow = int(float(numMessages) * DeleteLimit) - - (await driver.deleteOldestMessagesNotWithinLimit(limit = pageDeleteWindow)).isOkOr: - return err("deleting oldest messages failed: " & error) - - return ok() - -method close*(s: SqliteDriver): Future[ArchiveDriverResult[void]] {.async.} = - ## Close the database connection - # Dispose statements - s.insertStmt.dispose() - # Close connection - s.db.close() - return ok() - -method existsTable*( - s: SqliteDriver, tableName: string -): Future[ArchiveDriverResult[bool]] {.async.} = - return err("existsTable method not implemented in sqlite_driver") diff --git a/waku/waku_core/codecs.nim b/waku/waku_core/codecs.nim index 0d9394c71..f0f0c977e 100644 --- a/waku/waku_core/codecs.nim +++ b/waku/waku_core/codecs.nim @@ -9,5 +9,4 @@ const WakuTransferCodec* = "/vac/waku/transfer/1.0.0" WakuMetadataCodec* = "/vac/waku/metadata/1.0.0" WakuPeerExchangeCodec* = "/vac/waku/peer-exchange/2.0.0-alpha1" - WakuLegacyStoreCodec* = "/vac/waku/store/2.0.0-beta4" WakuRendezVousCodec* = "/vac/waku/rendezvous/1.0.0" diff --git a/waku/waku_store_legacy.nim b/waku/waku_store_legacy.nim deleted file mode 100644 index 9dac194c7..000000000 --- a/waku/waku_store_legacy.nim +++ /dev/null @@ -1,3 +0,0 @@ -import ./waku_store_legacy/common, ./waku_store_legacy/protocol - -export common, protocol diff --git a/waku/waku_store_legacy/README.md b/waku/waku_store_legacy/README.md deleted file mode 100644 index f2068734f..000000000 --- a/waku/waku_store_legacy/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Waku Store protocol - -The store protocol implements historical message support. See https://rfc.vac.dev/spec/13/ for more information. diff --git a/waku/waku_store_legacy/client.nim b/waku/waku_store_legacy/client.nim deleted file mode 100644 index 3965e06cf..000000000 --- a/waku/waku_store_legacy/client.nim +++ /dev/null @@ -1,241 +0,0 @@ -{.push raises: [].} - -import std/options, results, chronicles, chronos, metrics, bearssl/rand -import - ../node/peer_manager, - ../utils/requests, - ./protocol_metrics, - ./common, - ./rpc, - ./rpc_codec - -when defined(waku_exp_store_resume): - import std/[sequtils, times] - import ../waku_archive - import ../waku_core/message/digest - -logScope: - topics = "waku legacy store client" - -const DefaultPageSize*: uint = 20 - # A recommended default number of waku messages per page - -type WakuStoreClient* = ref object - peerManager: PeerManager - rng: ref rand.HmacDrbgContext - - # TODO: Move outside of the client - when defined(waku_exp_store_resume): - store: ArchiveDriver - -proc new*( - T: type WakuStoreClient, peerManager: PeerManager, rng: ref rand.HmacDrbgContext -): T = - WakuStoreClient(peerManager: peerManager, rng: rng) - -proc sendHistoryQueryRPC( - w: WakuStoreClient, req: HistoryQuery, peer: RemotePeerInfo -): Future[HistoryResult] {.async, gcsafe.} = - let connOpt = await w.peerManager.dialPeer(peer, WakuLegacyStoreCodec) - if connOpt.isNone(): - waku_legacy_store_errors.inc(labelValues = [dialFailure]) - return err(HistoryError(kind: HistoryErrorKind.PEER_DIAL_FAILURE, address: $peer)) - - let connection = connOpt.get() - - defer: - await connection.closeWithEof() - - let requestId = - if req.requestId != "": - req.requestId - else: - generateRequestId(w.rng) - - let reqRpc = HistoryRPC(requestId: requestId, query: some(req.toRPC())) - await connection.writeLP(reqRpc.encode().buffer) - - #TODO: I see a challenge here, if storeNode uses a different MaxRPCSize this read will fail. - # Need to find a workaround for this. - let buf = await connection.readLp(DefaultMaxRpcSize.int) - let respRpc = HistoryRPC.decode(buf).valueOr: - waku_legacy_store_errors.inc(labelValues = [decodeRpcFailure]) - return - err(HistoryError(kind: HistoryErrorKind.BAD_RESPONSE, cause: decodeRpcFailure)) - - # Disabled ,for now, since the default response is a possible case (no messages, pagesize = 0, error = NONE(0)) - # TODO: Rework the RPC protocol to differentiate the default value from an empty value (e.g., status = 200 (OK)) - # and rework the protobuf parsing to return Option[T] when empty values are received - if respRpc.response.isNone(): - waku_legacy_store_errors.inc(labelValues = [emptyRpcResponseFailure]) - return err( - HistoryError(kind: HistoryErrorKind.BAD_RESPONSE, cause: emptyRpcResponseFailure) - ) - - let resp = respRpc.response.get() - - return resp.toAPI() - -proc query*( - w: WakuStoreClient, req: HistoryQuery, peer: RemotePeerInfo -): Future[HistoryResult] {.async, gcsafe.} = - return await w.sendHistoryQueryRPC(req, peer) - -# TODO: Move outside of the client -when defined(waku_exp_store_resume): - ## Resume store - - const StoreResumeTimeWindowOffset: Timestamp = getNanosecondTime(20) - ## Adjust the time window with an offset of 20 seconds - - proc new*( - T: type WakuStoreClient, - peerManager: PeerManager, - rng: ref rand.HmacDrbgContext, - store: ArchiveDriver, - ): T = - WakuStoreClient(peerManager: peerManager, rng: rng, store: store) - - proc queryAll( - w: WakuStoreClient, query: HistoryQuery, peer: RemotePeerInfo - ): Future[WakuStoreResult[seq[WakuMessage]]] {.async, gcsafe.} = - ## A thin wrapper for query. Sends the query to the given peer. when the query has a valid pagingInfo, - ## it retrieves the historical messages in pages. - ## Returns all the fetched messages, if error occurs, returns an error string - - # Make a copy of the query - var req = query - - var messageList: seq[WakuMessage] = @[] - - while true: - let response = (await w.query(req, peer)).valueOr: - return err($error) - - messageList.add(response.messages) - - # Check whether it is the last page - if response.cursor.isNone(): - break - - # Update paging cursor - req.cursor = response.cursor - - return ok(messageList) - - proc queryLoop( - w: WakuStoreClient, req: HistoryQuery, peers: seq[RemotePeerInfo] - ): Future[WakuStoreResult[seq[WakuMessage]]] {.async, gcsafe.} = - ## Loops through the peers candidate list in order and sends the query to each - ## - ## Once all responses have been received, the retrieved messages are consolidated into one deduplicated list. - ## if no messages have been retrieved, the returned future will resolve into a result holding an empty seq. - let queryFuturesList = peers.mapIt(w.queryAll(req, it)) - - await allFutures(queryFuturesList) - - let messagesList = queryFuturesList - .map( - proc(fut: Future[WakuStoreResult[seq[WakuMessage]]]): seq[WakuMessage] = - try: - # fut.read() can raise a CatchableError - # These futures have been awaited before using allFutures(). Call completed() just as a sanity check. - if not fut.completed() or fut.read().isErr(): - return @[] - - fut.read().value - except CatchableError: - return @[] - ) - .concat() - .deduplicate() - - return ok(messagesList) - - proc put( - store: ArchiveDriver, pubsubTopic: PubsubTopic, message: WakuMessage - ): Result[void, string] = - let - digest = waku_archive.computeDigest(message) - messageHash = computeMessageHash(pubsubTopic, message) - receivedTime = - if message.timestamp > 0: - message.timestamp - else: - getNanosecondTime(getTime().toUnixFloat()) - - store.put(pubsubTopic, message, digest, messageHash, receivedTime) - - proc resume*( - w: WakuStoreClient, - peerList = none(seq[RemotePeerInfo]), - pageSize = DefaultPageSize, - pubsubTopic = DefaultPubsubTopic, - ): Future[WakuStoreResult[uint64]] {.async, gcsafe.} = - ## resume proc retrieves the history of waku messages published on the default waku pubsub topic since the last time the waku store node has been online - ## messages are stored in the store node's messages field and in the message db - ## the offline time window is measured as the difference between the current time and the timestamp of the most recent persisted waku message - ## an offset of 20 second is added to the time window to count for nodes asynchrony - ## peerList indicates the list of peers to query from. - ## The history is fetched from all available peers in this list and then consolidated into one deduplicated list. - ## Such candidates should be found through a discovery method (to be developed). - ## if no peerList is passed, one of the peers in the underlying peer manager unit of the store protocol is picked randomly to fetch the history from. - ## The history gets fetched successfully if the dialed peer has been online during the queried time window. - ## the resume proc returns the number of retrieved messages if no error occurs, otherwise returns the error string - - # If store has not been provided, don't even try - if w.store.isNil(): - return err("store not provided (nil)") - - # NOTE: Original implementation is based on the message's sender timestamp. At the moment - # of writing, the sqlite store implementation returns the last message's receiver - # timestamp. - # lastSeenTime = lastSeenItem.get().msg.timestamp - let - lastSeenTime = w.store.getNewestMessageTimestamp().get(Timestamp(0)) - now = getNanosecondTime(getTime().toUnixFloat()) - - info "resuming with offline time window", - lastSeenTime = lastSeenTime, currentTime = now - - let - queryEndTime = now + StoreResumeTimeWindowOffset - queryStartTime = max(lastSeenTime - StoreResumeTimeWindowOffset, 0) - - let req = HistoryQuery( - pubsubTopic: some(pubsubTopic), - startTime: some(queryStartTime), - endTime: some(queryEndTime), - pageSize: uint64(pageSize), - direction: default(), - ) - - var res: WakuStoreResult[seq[WakuMessage]] - if peerList.isSome(): - info "trying the candidate list to fetch the history" - res = await w.queryLoop(req, peerList.get()) - else: - info "no candidate list is provided, selecting a random peer" - # if no peerList is set then query from one of the peers stored in the peer manager - let peerOpt = w.peerManager.selectPeer(WakuLegacyStoreCodec) - if peerOpt.isNone(): - warn "no suitable remote peers" - waku_legacy_store_errors.inc(labelValues = [peerNotFoundFailure]) - return err("no suitable remote peers") - - info "a peer is selected from peer manager" - res = await w.queryAll(req, peerOpt.get()) - - res.isOkOr: - info "failed to resume the history" - return err("failed to resume the history") - - # Save the retrieved messages in the store - var added: uint = 0 - for msg in res.get(): - w.store.put(pubsubTopic, msg).isOkOr: - continue - - added.inc() - - return ok(added) diff --git a/waku/waku_store_legacy/common.nim b/waku/waku_store_legacy/common.nim deleted file mode 100644 index 6da7f267e..000000000 --- a/waku/waku_store_legacy/common.nim +++ /dev/null @@ -1,108 +0,0 @@ -{.push raises: [].} - -import std/[options, sequtils], results, stew/byteutils, nimcrypto/sha2 -import ../waku_core, ../common/paging - -from ../waku_core/codecs import WakuLegacyStoreCodec -export WakuLegacyStoreCodec - -const - DefaultPageSize*: uint64 = 20 - - MaxPageSize*: uint64 = 100 - -type WakuStoreResult*[T] = Result[T, string] - -## Waku message digest - -type MessageDigest* = MDigest[256] - -proc computeDigest*(msg: WakuMessage): MessageDigest = - var ctx: sha256 - ctx.init() - defer: - ctx.clear() - - ctx.update(msg.contentTopic.toBytes()) - ctx.update(msg.payload) - - # Computes the hash - return ctx.finish() - -## API types - -type - HistoryCursor* = object - pubsubTopic*: PubsubTopic - senderTime*: Timestamp - storeTime*: Timestamp - digest*: MessageDigest - - HistoryQuery* = object - pubsubTopic*: Option[PubsubTopic] - contentTopics*: seq[ContentTopic] - cursor*: Option[HistoryCursor] - startTime*: Option[Timestamp] - endTime*: Option[Timestamp] - pageSize*: uint64 - direction*: PagingDirection - requestId*: string - - HistoryResponse* = object - messages*: seq[WakuMessage] - cursor*: Option[HistoryCursor] - - HistoryErrorKind* {.pure.} = enum - UNKNOWN = uint32(000) - BAD_RESPONSE = uint32(300) - BAD_REQUEST = uint32(400) - TOO_MANY_REQUESTS = uint32(429) - SERVICE_UNAVAILABLE = uint32(503) - PEER_DIAL_FAILURE = uint32(504) - - HistoryError* = object - case kind*: HistoryErrorKind - of PEER_DIAL_FAILURE: - address*: string - of BAD_RESPONSE, BAD_REQUEST: - cause*: string - else: - discard - - HistoryResult* = Result[HistoryResponse, HistoryError] - -proc parse*(T: type HistoryErrorKind, kind: uint32): T = - case kind - of 000, 200, 300, 400, 429, 503: - HistoryErrorKind(kind) - else: - HistoryErrorKind.UNKNOWN - -proc `$`*(err: HistoryError): string = - case err.kind - of HistoryErrorKind.PEER_DIAL_FAILURE: - "PEER_DIAL_FAILURE: " & err.address - of HistoryErrorKind.BAD_RESPONSE: - "BAD_RESPONSE: " & err.cause - of HistoryErrorKind.BAD_REQUEST: - "BAD_REQUEST: " & err.cause - of HistoryErrorKind.TOO_MANY_REQUESTS: - "TOO_MANY_REQUESTS" - of HistoryErrorKind.SERVICE_UNAVAILABLE: - "SERVICE_UNAVAILABLE" - of HistoryErrorKind.UNKNOWN: - "UNKNOWN" - -proc checkHistCursor*(self: HistoryCursor): Result[void, HistoryError] = - if self.pubsubTopic.len == 0: - return err(HistoryError(kind: BAD_REQUEST, cause: "empty pubsubTopic")) - if self.senderTime == 0: - return err(HistoryError(kind: BAD_REQUEST, cause: "invalid senderTime")) - if self.storeTime == 0: - return err(HistoryError(kind: BAD_REQUEST, cause: "invalid storeTime")) - if self.digest.data.all( - proc(x: byte): bool = - x == 0 - ): - return err(HistoryError(kind: BAD_REQUEST, cause: "empty digest")) - return ok() diff --git a/waku/waku_store_legacy/protocol.nim b/waku/waku_store_legacy/protocol.nim deleted file mode 100644 index 8916e8ac0..000000000 --- a/waku/waku_store_legacy/protocol.nim +++ /dev/null @@ -1,188 +0,0 @@ -## Waku Store protocol for historical messaging support. -## See spec for more details: -## https://github.com/vacp2p/specs/blob/master/specs/waku/v2/waku-store.md -{.push raises: [].} - -import - std/[options, times], - results, - chronicles, - chronos, - bearssl/rand, - libp2p/crypto/crypto, - libp2p/protocols/protocol, - libp2p/protobuf/minprotobuf, - libp2p/stream/connection, - metrics -import - ../waku_core, - ../node/peer_manager, - ./common, - ./rpc, - ./rpc_codec, - ./protocol_metrics, - ../common/rate_limit/request_limiter - -logScope: - topics = "waku legacy store" - -type HistoryQueryHandler* = - proc(req: HistoryQuery): Future[HistoryResult] {.async, gcsafe.} - -type WakuStore* = ref object of LPProtocol - peerManager: PeerManager - rng: ref rand.HmacDrbgContext - queryHandler*: HistoryQueryHandler - requestRateLimiter*: RequestRateLimiter - -## Protocol - -type StoreResp = tuple[resp: seq[byte], requestId: string] - -proc handleLegacyQueryRequest( - self: WakuStore, requestor: PeerId, raw_request: seq[byte] -): Future[StoreResp] {.async.} = - let reqRpc = HistoryRPC.decode(raw_request).valueOr: - error "failed to decode rpc", peerId = requestor, error = $error - waku_legacy_store_errors.inc(labelValues = [decodeRpcFailure]) - return (newSeq[byte](), "failed to decode rpc") - - if reqRpc.query.isNone(): - error "empty query rpc", peerId = requestor, requestId = reqRpc.requestId - waku_legacy_store_errors.inc(labelValues = [emptyRpcQueryFailure]) - return (newSeq[byte](), "empty query rpc") - - let requestId = reqRpc.requestId - var request = reqRpc.query.get().toAPI() - request.requestId = requestId - - info "received history query", - peerId = requestor, requestId = requestId, query = request - waku_legacy_store_queries.inc() - - var responseRes: HistoryResult - try: - responseRes = await self.queryHandler(request) - except Exception: - error "history query failed", - peerId = requestor, requestId = requestId, error = getCurrentExceptionMsg() - - let error = HistoryError(kind: HistoryErrorKind.UNKNOWN).toRPC() - let response = HistoryResponseRPC(error: error) - return ( - HistoryRPC(requestId: requestId, response: some(response)).encode().buffer, - requestId, - ) - - responseRes.isOkOr: - error "history query failed", - peerId = requestor, requestId = requestId, error = error - - let response = responseRes.toRPC() - return ( - HistoryRPC(requestId: requestId, response: some(response)).encode().buffer, - requestId, - ) - - let response = responseRes.toRPC() - - info "sending history response", - peerId = requestor, requestId = requestId, messages = response.messages.len - - return ( - HistoryRPC(requestId: requestId, response: some(response)).encode().buffer, - requestId, - ) - -proc initProtocolHandler(ws: WakuStore) = - let rejectResponseBuf = HistoryRPC( - ## We will not copy and decode RPC buffer from stream only for requestId - ## in reject case as it is comparably too expensive and opens possible - ## attack surface - requestId: "N/A", - response: some( - HistoryResponseRPC( - error: HistoryError(kind: HistoryErrorKind.TOO_MANY_REQUESTS).toRPC() - ) - ), - ).encode().buffer - - proc handler(conn: Connection, proto: string) {.async: (raises: [CancelledError]).} = - var successfulQuery = false ## only consider the correct queries in metrics - var resBuf: StoreResp - var queryDuration: float - - defer: - await conn.closeWithEof() - - ws.requestRateLimiter.checkUsageLimit(WakuLegacyStoreCodec, conn): - let readRes = catch: - await conn.readLp(DefaultMaxRpcSize.int) - - let reqBuf = readRes.valueOr: - error "Connection read error", error = error.msg - return - - waku_service_network_bytes.inc( - amount = reqBuf.len().int64, labelValues = [WakuLegacyStoreCodec, "in"] - ) - - let queryStartTime = getTime().toUnixFloat() - try: - resBuf = await ws.handleLegacyQueryRequest(conn.peerId, reqBuf) - except CatchableError: - error "legacy store query handler failed", - remote_peer_id = conn.peerId, error = getCurrentExceptionMsg() - return - - queryDuration = getTime().toUnixFloat() - queryStartTime - waku_legacy_store_time_seconds.set(queryDuration, ["query-db-time"]) - successfulQuery = true - do: - info "Legacy store query request rejected due rate limit exceeded", - peerId = conn.peerId, limit = $ws.requestRateLimiter.setting - resBuf = (rejectResponseBuf, "rejected") - - let writeRespStartTime = getTime().toUnixFloat() - let writeRes = catch: - await conn.writeLp(resBuf.resp) - - writeRes.isOkOr: - error "Connection write error", error = error.msg - return - - if successfulQuery: - let writeDuration = getTime().toUnixFloat() - writeRespStartTime - waku_legacy_store_time_seconds.set(writeDuration, ["send-store-resp-time"]) - info "after sending response", - requestId = resBuf.requestId, - queryDurationSecs = queryDuration, - writeStreamDurationSecs = writeDuration - - waku_service_network_bytes.inc( - amount = resBuf.resp.len().int64, labelValues = [WakuLegacyStoreCodec, "out"] - ) - - ws.handler = handler - ws.codec = WakuLegacyStoreCodec - -proc new*( - T: type WakuStore, - peerManager: PeerManager, - rng: ref rand.HmacDrbgContext, - queryHandler: HistoryQueryHandler, - rateLimitSetting: Option[RateLimitSetting] = none[RateLimitSetting](), -): T = - # Raise a defect if history query handler is nil - if queryHandler.isNil(): - raise newException(NilAccessDefect, "history query handler is nil") - - let ws = WakuStore( - rng: rng, - peerManager: peerManager, - queryHandler: queryHandler, - requestRateLimiter: newRequestRateLimiter(rateLimitSetting), - ) - ws.initProtocolHandler() - setServiceLimitMetric(WakuLegacyStoreCodec, rateLimitSetting) - ws diff --git a/waku/waku_store_legacy/protocol_metrics.nim b/waku/waku_store_legacy/protocol_metrics.nim deleted file mode 100644 index 45a848998..000000000 --- a/waku/waku_store_legacy/protocol_metrics.nim +++ /dev/null @@ -1,21 +0,0 @@ -{.push raises: [].} - -import metrics - -declarePublicCounter waku_legacy_store_errors, - "number of legacy store protocol errors", ["type"] -declarePublicCounter waku_legacy_store_queries, - "number of legacy store queries received" - -## "query-db-time" phase considers the time when node performs the query to the database. -## "send-store-resp-time" phase is the time when node writes the store response to the store-client. -declarePublicGauge waku_legacy_store_time_seconds, - "Time in seconds spent by each store phase", labels = ["phase"] - -# Error types (metric label values) -const - dialFailure* = "dial_failure_legacy" - decodeRpcFailure* = "decode_rpc_failure_legacy" - peerNotFoundFailure* = "peer_not_found_failure_legacy" - emptyRpcQueryFailure* = "empty_rpc_query_failure_legacy" - emptyRpcResponseFailure* = "empty_rpc_response_failure_legacy" diff --git a/waku/waku_store_legacy/rpc.nim b/waku/waku_store_legacy/rpc.nim deleted file mode 100644 index 44aad8d07..000000000 --- a/waku/waku_store_legacy/rpc.nim +++ /dev/null @@ -1,218 +0,0 @@ -{.push raises: [].} - -import std/[options, sequtils], results -import ../waku_core, ../common/paging, ./common - -## Wire protocol - -const HistoryQueryDirectionDefaultValue = default(type HistoryQuery.direction) - -type PagingIndexRPC* = object - ## This type contains the description of an Index used in the pagination of WakuMessages - pubsubTopic*: PubsubTopic - senderTime*: Timestamp # the time at which the message is generated - receiverTime*: Timestamp - digest*: MessageDigest # calculated over payload and content topic - -proc `==`*(x, y: PagingIndexRPC): bool = - ## receiverTime plays no role in index equality - (x.senderTime == y.senderTime) and (x.digest == y.digest) and - (x.pubsubTopic == y.pubsubTopic) - -proc compute*( - T: type PagingIndexRPC, - msg: WakuMessage, - receivedTime: Timestamp, - pubsubTopic: PubsubTopic, -): T = - ## Takes a WakuMessage with received timestamp and returns its Index. - let - digest = computeDigest(msg) - senderTime = msg.timestamp - - PagingIndexRPC( - pubsubTopic: pubsubTopic, - senderTime: senderTime, - receiverTime: receivedTime, - digest: digest, - ) - -type PagingInfoRPC* = object - ## This type holds the information needed for the pagination - pageSize*: Option[uint64] - cursor*: Option[PagingIndexRPC] - direction*: Option[PagingDirection] - -type - HistoryContentFilterRPC* = object - contentTopic*: ContentTopic - - HistoryQueryRPC* = object - contentFilters*: seq[HistoryContentFilterRPC] - pubsubTopic*: Option[PubsubTopic] - pagingInfo*: Option[PagingInfoRPC] - startTime*: Option[int64] - endTime*: Option[int64] - - HistoryResponseErrorRPC* {.pure.} = enum - ## HistoryResponseErrorRPC contains error message to inform the querying node about - ## the state of its request - NONE = uint32(0) - INVALID_CURSOR = uint32(1) - TOO_MANY_REQUESTS = uint32(429) - SERVICE_UNAVAILABLE = uint32(503) - - HistoryResponseRPC* = object - messages*: seq[WakuMessage] - pagingInfo*: Option[PagingInfoRPC] - error*: HistoryResponseErrorRPC - - HistoryRPC* = object - requestId*: string - query*: Option[HistoryQueryRPC] - response*: Option[HistoryResponseRPC] - -proc parse*(T: type HistoryResponseErrorRPC, kind: uint32): T = - case kind - of 0, 1, 429, 503: - cast[HistoryResponseErrorRPC](kind) - else: - # TODO: Improve error variants/move to satus codes - HistoryResponseErrorRPC.INVALID_CURSOR - -## Wire protocol type mappings - -proc toRPC*(cursor: HistoryCursor): PagingIndexRPC {.gcsafe.} = - PagingIndexRPC( - pubsubTopic: cursor.pubsubTopic, - senderTime: cursor.senderTime, - receiverTime: cursor.storeTime, - digest: cursor.digest, - ) - -proc toAPI*(rpc: PagingIndexRPC): HistoryCursor = - HistoryCursor( - pubsubTopic: rpc.pubsubTopic, - senderTime: rpc.senderTime, - storeTime: rpc.receiverTime, - digest: rpc.digest, - ) - -proc toRPC*(query: HistoryQuery): HistoryQueryRPC = - var rpc = HistoryQueryRPC() - - rpc.contentFilters = - query.contentTopics.mapIt(HistoryContentFilterRPC(contentTopic: it)) - - rpc.pubsubTopic = query.pubsubTopic - - rpc.pagingInfo = block: - if query.cursor.isNone() and query.pageSize == default(type query.pageSize) and - query.direction == HistoryQueryDirectionDefaultValue: - none(PagingInfoRPC) - else: - let - pageSize = some(query.pageSize) - cursor = query.cursor.map(toRPC) - direction = some(query.direction) - - some(PagingInfoRPC(pageSize: pageSize, cursor: cursor, direction: direction)) - - rpc.startTime = query.startTime - rpc.endTime = query.endTime - - rpc - -proc toAPI*(rpc: HistoryQueryRPC): HistoryQuery = - let - pubsubTopic = rpc.pubsubTopic - - contentTopics = rpc.contentFilters.mapIt(it.contentTopic) - - cursor = - if rpc.pagingInfo.isNone() or rpc.pagingInfo.get().cursor.isNone(): - none(HistoryCursor) - else: - rpc.pagingInfo.get().cursor.map(toAPI) - - startTime = rpc.startTime - - endTime = rpc.endTime - - pageSize = - if rpc.pagingInfo.isNone() or rpc.pagingInfo.get().pageSize.isNone(): - 0'u64 - else: - rpc.pagingInfo.get().pageSize.get() - - direction = - if rpc.pagingInfo.isNone() or rpc.pagingInfo.get().direction.isNone(): - HistoryQueryDirectionDefaultValue - else: - rpc.pagingInfo.get().direction.get() - - HistoryQuery( - pubsubTopic: pubsubTopic, - contentTopics: contentTopics, - cursor: cursor, - startTime: startTime, - endTime: endTime, - pageSize: pageSize, - direction: direction, - ) - -proc toRPC*(err: HistoryError): HistoryResponseErrorRPC = - # TODO: Better error mappings/move to error codes - case err.kind - of HistoryErrorKind.BAD_REQUEST: - # TODO: Respond aksi with the reason - HistoryResponseErrorRPC.INVALID_CURSOR - of HistoryErrorKind.TOO_MANY_REQUESTS: - HistoryResponseErrorRPC.TOO_MANY_REQUESTS - of HistoryErrorKind.SERVICE_UNAVAILABLE: - HistoryResponseErrorRPC.SERVICE_UNAVAILABLE - else: - HistoryResponseErrorRPC.INVALID_CURSOR - -proc toAPI*(err: HistoryResponseErrorRPC): HistoryError = - # TODO: Better error mappings/move to error codes - case err - of HistoryResponseErrorRPC.INVALID_CURSOR: - HistoryError(kind: HistoryErrorKind.BAD_REQUEST, cause: "invalid cursor") - of HistoryResponseErrorRPC.TOO_MANY_REQUESTS: - HistoryError(kind: HistoryErrorKind.TOO_MANY_REQUESTS) - of HistoryResponseErrorRPC.SERVICE_UNAVAILABLE: - HistoryError(kind: HistoryErrorKind.SERVICE_UNAVAILABLE) - else: - HistoryError(kind: HistoryErrorKind.UNKNOWN) - -proc toRPC*(res: HistoryResult): HistoryResponseRPC = - let resp = res.valueOr: - return HistoryResponseRPC(error: error.toRPC()) - let - messages = resp.messages - - pagingInfo = block: - if resp.cursor.isNone(): - none(PagingInfoRPC) - else: - some(PagingInfoRPC(cursor: resp.cursor.map(toRPC))) - - error = HistoryResponseErrorRPC.NONE - - HistoryResponseRPC(messages: messages, pagingInfo: pagingInfo, error: error) - -proc toAPI*(rpc: HistoryResponseRPC): HistoryResult = - if rpc.error != HistoryResponseErrorRPC.NONE: - err(rpc.error.toAPI()) - else: - let - messages = rpc.messages - - cursor = - if rpc.pagingInfo.isNone(): - none(HistoryCursor) - else: - rpc.pagingInfo.get().cursor.map(toAPI) - - ok(HistoryResponse(messages: messages, cursor: cursor)) diff --git a/waku/waku_store_legacy/rpc_codec.nim b/waku/waku_store_legacy/rpc_codec.nim deleted file mode 100644 index f9c518e83..000000000 --- a/waku/waku_store_legacy/rpc_codec.nim +++ /dev/null @@ -1,255 +0,0 @@ -{.push raises: [].} - -import std/options, nimcrypto/hash -import ../common/[protobuf, paging], ../waku_core, ./common, ./rpc - -const DefaultMaxRpcSize* = -1 - -## Pagination - -proc encode*(index: PagingIndexRPC): ProtoBuffer = - ## Encode an Index object into a ProtoBuffer - ## returns the resultant ProtoBuffer - var pb = initProtoBuffer() - - pb.write3(1, index.digest.data) - pb.write3(2, zint64(index.receiverTime)) - pb.write3(3, zint64(index.senderTime)) - pb.write3(4, index.pubsubTopic) - pb.finish3() - - pb - -proc decode*(T: type PagingIndexRPC, buffer: seq[byte]): ProtobufResult[T] = - ## creates and returns an Index object out of buffer - var rpc = PagingIndexRPC() - let pb = initProtoBuffer(buffer) - - var data: seq[byte] - if not ?pb.getField(1, data): - return err(ProtobufError.missingRequiredField("digest")) - else: - var digest = MessageDigest() - for count, b in data: - digest.data[count] = b - - rpc.digest = digest - - var receiverTime: zint64 - if not ?pb.getField(2, receiverTime): - return err(ProtobufError.missingRequiredField("receiver_time")) - else: - rpc.receiverTime = int64(receiverTime) - - var senderTime: zint64 - if not ?pb.getField(3, senderTime): - return err(ProtobufError.missingRequiredField("sender_time")) - else: - rpc.senderTime = int64(senderTime) - - var pubsubTopic: string - if not ?pb.getField(4, pubsubTopic): - return err(ProtobufError.missingRequiredField("pubsub_topic")) - else: - rpc.pubsubTopic = pubsubTopic - - ok(rpc) - -proc encode*(rpc: PagingInfoRPC): ProtoBuffer = - ## Encodes a PagingInfo object into a ProtoBuffer - ## returns the resultant ProtoBuffer - var pb = initProtoBuffer() - - pb.write3(1, rpc.pageSize) - pb.write3(2, rpc.cursor.map(encode)) - pb.write3( - 3, - rpc.direction.map( - proc(d: PagingDirection): uint32 = - uint32(ord(d)) - ), - ) - pb.finish3() - - pb - -proc decode*(T: type PagingInfoRPC, buffer: seq[byte]): ProtobufResult[T] = - ## creates and returns a PagingInfo object out of buffer - var rpc = PagingInfoRPC() - let pb = initProtoBuffer(buffer) - - var pageSize: uint64 - if not ?pb.getField(1, pageSize): - rpc.pageSize = none(uint64) - else: - rpc.pageSize = some(pageSize) - - var cursorBuffer: seq[byte] - if not ?pb.getField(2, cursorBuffer): - rpc.cursor = none(PagingIndexRPC) - else: - let cursor = ?PagingIndexRPC.decode(cursorBuffer) - rpc.cursor = some(cursor) - - var direction: uint32 - if not ?pb.getField(3, direction): - rpc.direction = none(PagingDirection) - else: - rpc.direction = some(PagingDirection(direction)) - - ok(rpc) - -## Wire protocol - -proc encode*(rpc: HistoryContentFilterRPC): ProtoBuffer = - var pb = initProtoBuffer() - - pb.write3(1, rpc.contentTopic) - pb.finish3() - - pb - -proc decode*(T: type HistoryContentFilterRPC, buffer: seq[byte]): ProtobufResult[T] = - let pb = initProtoBuffer(buffer) - - var contentTopic: ContentTopic - if not ?pb.getField(1, contentTopic): - return err(ProtobufError.missingRequiredField("content_topic")) - ok(HistoryContentFilterRPC(contentTopic: contentTopic)) - -proc encode*(rpc: HistoryQueryRPC): ProtoBuffer = - var pb = initProtoBuffer() - pb.write3(2, rpc.pubsubTopic) - - for filter in rpc.contentFilters: - pb.write3(3, filter.encode()) - - pb.write3(4, rpc.pagingInfo.map(encode)) - pb.write3( - 5, - rpc.startTime.map( - proc(time: int64): zint64 = - zint64(time) - ), - ) - pb.write3( - 6, - rpc.endTime.map( - proc(time: int64): zint64 = - zint64(time) - ), - ) - pb.finish3() - - pb - -proc decode*(T: type HistoryQueryRPC, buffer: seq[byte]): ProtobufResult[T] = - var rpc = HistoryQueryRPC() - let pb = initProtoBuffer(buffer) - - var pubsubTopic: string - if not ?pb.getField(2, pubsubTopic): - rpc.pubsubTopic = none(string) - else: - rpc.pubsubTopic = some(pubsubTopic) - - var buffs: seq[seq[byte]] - if not ?pb.getRepeatedField(3, buffs): - rpc.contentFilters = @[] - else: - for pb in buffs: - let filter = ?HistoryContentFilterRPC.decode(pb) - rpc.contentFilters.add(filter) - - var pagingInfoBuffer: seq[byte] - if not ?pb.getField(4, pagingInfoBuffer): - rpc.pagingInfo = none(PagingInfoRPC) - else: - let pagingInfo = ?PagingInfoRPC.decode(pagingInfoBuffer) - rpc.pagingInfo = some(pagingInfo) - - var startTime: zint64 - if not ?pb.getField(5, startTime): - rpc.startTime = none(int64) - else: - rpc.startTime = some(int64(startTime)) - - var endTime: zint64 - if not ?pb.getField(6, endTime): - rpc.endTime = none(int64) - else: - rpc.endTime = some(int64(endTime)) - - ok(rpc) - -proc encode*(response: HistoryResponseRPC): ProtoBuffer = - var pb = initProtoBuffer() - - for rpc in response.messages: - pb.write3(2, rpc.encode()) - - pb.write3(3, response.pagingInfo.map(encode)) - pb.write3(4, uint32(ord(response.error))) - pb.finish3() - - pb - -proc decode*(T: type HistoryResponseRPC, buffer: seq[byte]): ProtobufResult[T] = - var rpc = HistoryResponseRPC() - let pb = initProtoBuffer(buffer) - - var messages: seq[seq[byte]] - if ?pb.getRepeatedField(2, messages): - for pb in messages: - let message = ?WakuMessage.decode(pb) - rpc.messages.add(message) - else: - rpc.messages = @[] - - var pagingInfoBuffer: seq[byte] - if ?pb.getField(3, pagingInfoBuffer): - let pagingInfo = ?PagingInfoRPC.decode(pagingInfoBuffer) - rpc.pagingInfo = some(pagingInfo) - else: - rpc.pagingInfo = none(PagingInfoRPC) - - var error: uint32 - if not ?pb.getField(4, error): - return err(ProtobufError.missingRequiredField("error")) - else: - rpc.error = HistoryResponseErrorRPC.parse(error) - - ok(rpc) - -proc encode*(rpc: HistoryRPC): ProtoBuffer = - var pb = initProtoBuffer() - - pb.write3(1, rpc.requestId) - pb.write3(2, rpc.query.map(encode)) - pb.write3(3, rpc.response.map(encode)) - pb.finish3() - - pb - -proc decode*(T: type HistoryRPC, buffer: seq[byte]): ProtobufResult[T] = - var rpc = HistoryRPC() - let pb = initProtoBuffer(buffer) - - if not ?pb.getField(1, rpc.requestId): - return err(ProtobufError.missingRequiredField("request_id")) - - var queryBuffer: seq[byte] - if not ?pb.getField(2, queryBuffer): - rpc.query = none(HistoryQueryRPC) - else: - let query = ?HistoryQueryRPC.decode(queryBuffer) - rpc.query = some(query) - - var responseBuffer: seq[byte] - if not ?pb.getField(3, responseBuffer): - rpc.response = none(HistoryResponseRPC) - else: - let response = ?HistoryResponseRPC.decode(responseBuffer) - rpc.response = some(response) - - ok(rpc) diff --git a/waku/waku_store_legacy/self_req_handler.nim b/waku/waku_store_legacy/self_req_handler.nim deleted file mode 100644 index e465d9e5b..000000000 --- a/waku/waku_store_legacy/self_req_handler.nim +++ /dev/null @@ -1,31 +0,0 @@ -## -## This file is aimed to attend the requests that come directly -## from the 'self' node. It is expected to attend the store requests that -## come from REST-store endpoint when those requests don't indicate -## any store-peer address. -## -## Notice that the REST-store requests normally assume that the REST -## server is acting as a store-client. In this module, we allow that -## such REST-store node can act as store-server as well by retrieving -## its own stored messages. The typical use case for that is when -## using `nwaku-compose`, which spawn a Waku node connected to a local -## database, and the user is interested in retrieving the messages -## stored by that local store node. -## - -import results, chronos -import ./protocol, ./common - -proc handleSelfStoreRequest*( - self: WakuStore, histQuery: HistoryQuery -): Future[WakuStoreResult[HistoryResponse]] {.async.} = - ## Handles the store requests made by the node to itself. - ## Normally used in REST-store requests - - try: - let resp: HistoryResponse = (await self.queryHandler(histQuery)).valueOr: - return err("error in handleSelfStoreRequest: " & $error) - - return WakuStoreResult[HistoryResponse].ok(resp) - except Exception: - return err("exception in handleSelfStoreRequest: " & getCurrentExceptionMsg()) From dc026bbff19050155f0ef1abb3e03a64c8bba81e Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 30 Mar 2026 08:30:34 -0300 Subject: [PATCH 109/155] feat: active filter subscription management for edge nodes (#3773) feat: active filter subscription management for edge nodes ## Subscription Manager * edgeFilterSubLoop reconciles desired vs actual filter subscriptions * edgeFilterHealthLoop pings filter peers, evicts stale ones * EdgeFilterSubState per-shard tracking of confirmed peers and health * best-effort unsubscribe on peer removal * RequestEdgeShardHealth and RequestEdgeFilterPeerCount broker providers ## WakuNode * Remove old edge health loop (loopEdgeHealth, edgeHealthEvent, calculateEdgeTopicHealth) * Register MessageSeenEvent push handler on filter client during start * startDeliveryService now returns `Result[void, string]` and propagates errors ## Health Monitor * getFilterClientHealth queries RequestEdgeFilterPeerCount via broker * Shard/content health providers fall back to RequestEdgeShardHealth when relay inactive * Listen to EventShardTopicHealthChange for health recalculation * Add missing return p.notReady() on failed edge filter peer count request * HealthyThreshold constant moved to `connection_status.nim` ## Broker types * RequestEdgeShardHealth, RequestEdgeFilterPeerCount request types * EventShardTopicHealthChange event type ## Filter Client * Add timeout parameter to ping proc ## Tests * Health monitor event tests with per-node lockNewGlobalBrokerContext * Edge (light client) health update test * Edge health driven by confirmed filter subscriptions test * API subscription tests: sub/receive, failover, peer replacement Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Co-authored by Zoltan Nagy --- tests/api/test_api_subscription.nim | 433 +++++++++++++++- tests/node/test_wakunode_health_monitor.nim | 209 ++++++-- waku/events/peer_events.nim | 2 +- waku/factory/waku.nim | 3 +- .../delivery_service/delivery_service.nim | 14 +- .../recv_service/recv_service.nim | 12 +- .../delivery_service/subscription_manager.nim | 464 ++++++++++++++++-- .../node/health_monitor/connection_status.nim | 3 + .../health_monitor/node_health_monitor.nim | 35 +- waku/node/peer_manager/peer_manager.nim | 37 +- waku/node/waku_node.nim | 93 +--- waku/requests/health_requests.nim | 12 + waku/waku_core/subscription.nim | 4 +- .../subscription/subscription_manager.nim | 52 -- waku/waku_filter_v2/client.nim | 10 +- waku/waku_relay/protocol.nim | 9 +- 16 files changed, 1126 insertions(+), 266 deletions(-) delete mode 100644 waku/waku_core/subscription/subscription_manager.nim diff --git a/tests/api/test_api_subscription.nim b/tests/api/test_api_subscription.nim index 6639e3dea..e0ceb9226 100644 --- a/tests/api/test_api_subscription.nim +++ b/tests/api/test_api_subscription.nim @@ -1,6 +1,6 @@ {.used.} -import std/[strutils, net, options, sets] +import std/[strutils, sequtils, net, options, sets, tables] import chronos, testutils/unittests, stew/byteutils import libp2p/[peerid, peerinfo, multiaddress, crypto/crypto] import ../testlib/[common, wakucore, wakunode, testasync] @@ -13,12 +13,12 @@ import common/broker/broker_context, events/message_events, waku_relay/protocol, + node/kernel_api/filter, + node/delivery_service/subscription_manager, ] import waku/factory/waku_conf import tools/confutils/cli_args -# TODO: Edge testing (after MAPI edge support is completed) - const TestTimeout = chronos.seconds(10) const NegativeTestTimeout = chronos.seconds(2) @@ -60,8 +60,10 @@ proc waitForEvents( return await manager.receivedEvent.wait().withTimeout(timeout) type TestNetwork = ref object - publisher: WakuNode + publisher: WakuNode # Relay node that publishes messages in tests. + meshBuddy: WakuNode # Extra relay peer for publisher's mesh (Edge tests only). subscriber: Waku + # The receiver node in tests. Edge node in edge tests, Core node in relay tests. publisherPeerInfo: RemotePeerInfo proc createApiNodeConf( @@ -94,8 +96,12 @@ proc setupNetwork( lockNewGlobalBrokerContext: net.publisher = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) - net.publisher.mountMetadata(3, @[0'u16]).expect("Failed to mount metadata") + net.publisher.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata" + ) (await net.publisher.mountRelay()).expect("Failed to mount relay") + if mode == cli_args.WakuMode.Edge: + await net.publisher.mountFilter() await net.publisher.mountLibp2pPing() await net.publisher.start() @@ -104,16 +110,32 @@ proc setupNetwork( proc dummyHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = discard - # Subscribe the publisher to all shards to guarantee a GossipSub mesh with the subscriber. - # Currently, Core/Relay nodes auto-subscribe to all network shards on boot, but if - # that changes, this will be needed to cause the publisher to have shard interest - # for any shards the subscriber may want to use, which is required for waitForMesh to work. + var shards: seq[PubsubTopic] for i in 0 ..< numShards.int: - let shard = PubsubTopic("/waku/2/rs/3/" & $i) + shards.add(PubsubTopic("/waku/2/rs/3/" & $i)) + + for shard in shards: net.publisher.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( "Failed to sub publisher" ) + if mode == cli_args.WakuMode.Edge: + lockNewGlobalBrokerContext: + net.meshBuddy = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + net.meshBuddy.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on meshBuddy" + ) + (await net.meshBuddy.mountRelay()).expect("Failed to mount relay on meshBuddy") + await net.meshBuddy.start() + + for shard in shards: + net.meshBuddy.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub meshBuddy" + ) + + await net.meshBuddy.connectToNodes(@[net.publisherPeerInfo]) + net.subscriber = await setupSubscriberNode(createApiNodeConf(mode, numShards)) await net.subscriber.node.connectToNodes(@[net.publisherPeerInfo]) @@ -125,6 +147,10 @@ proc teardown(net: TestNetwork) {.async.} = (await net.subscriber.stop()).expect("Failed to stop subscriber node") net.subscriber = nil + if not isNil(net.meshBuddy): + await net.meshBuddy.stop() + net.meshBuddy = nil + if not isNil(net.publisher): await net.publisher.stop() net.publisher = nil @@ -141,18 +167,34 @@ proc waitForMesh(node: WakuNode, shard: PubsubTopic) {.async.} = await sleepAsync(100.milliseconds) raise newException(ValueError, "GossipSub Mesh failed to stabilize on " & shard) +proc waitForEdgeSubs(w: Waku, shard: PubsubTopic) {.async.} = + let sm = w.deliveryService.subscriptionManager + for _ in 0 ..< 50: + if sm.edgeFilterPeerCount(shard) > 0: + return + await sleepAsync(100.milliseconds) + raise newException(ValueError, "Edge filter subscription failed on " & shard) + proc publishToMesh( net: TestNetwork, contentTopic: ContentTopic, payload: seq[byte] ): Future[Result[int, string]] {.async.} = + # Publishes a message from "publisher" via relay into the gossipsub mesh. let shard = net.subscriber.node.getRelayShard(contentTopic) - await waitForMesh(net.publisher, shard) - let msg = WakuMessage( payload: payload, contentTopic: contentTopic, version: 0, timestamp: now() ) return await net.publisher.publish(some(shard), msg) +proc publishToMeshAfterEdgeReady( + net: TestNetwork, contentTopic: ContentTopic, payload: seq[byte] +): Future[Result[int, string]] {.async.} = + # First, ensure "subscriber" node (an edge node) is subscribed and ready to receive. + # Afterwards, "publisher" (relay node) sends the message in the gossipsub network. + let shard = net.subscriber.node.getRelayShard(contentTopic) + await waitForEdgeSubs(net.subscriber, shard) + return await net.publishToMesh(contentTopic, payload) + suite "Messaging API, SubscriptionManager": asyncTest "Subscription API, relay node auto subscribe and receive message": let net = await setupNetwork(1) @@ -398,3 +440,370 @@ suite "Messaging API, SubscriptionManager": activeSubs.add(t) await verifyNetworkState(activeSubs) + + asyncTest "Subscription API, edge node subscribe and receive message": + let net = await setupNetwork(1, cli_args.WakuMode.Edge) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/test-content/proto") + (await net.subscriber.subscribe(testTopic)).expect("failed to subscribe") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + discard (await net.publishToMeshAfterEdgeReady(testTopic, "Hello, edge!".toBytes())).expect( + "Publish failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + require eventManager.receivedMessages.len == 1 + check eventManager.receivedMessages[0].contentTopic == testTopic + + asyncTest "Subscription API, edge node ignores unsubscribed content topics": + let net = await setupNetwork(1, cli_args.WakuMode.Edge) + defer: + await net.teardown() + + let subbedTopic = ContentTopic("/waku/2/subbed-topic/proto") + let ignoredTopic = ContentTopic("/waku/2/ignored-topic/proto") + (await net.subscriber.subscribe(subbedTopic)).expect("failed to subscribe") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + discard (await net.publishToMesh(ignoredTopic, "Ghost Msg".toBytes())).expect( + "Publish failed" + ) + + check not await eventManager.waitForEvents(NegativeTestTimeout) + check eventManager.receivedMessages.len == 0 + + asyncTest "Subscription API, edge node unsubscribe stops message receipt": + let net = await setupNetwork(1, cli_args.WakuMode.Edge) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/unsub-test/proto") + + (await net.subscriber.subscribe(testTopic)).expect("failed to subscribe") + net.subscriber.unsubscribe(testTopic).expect("failed to unsubscribe") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + discard (await net.publishToMesh(testTopic, "Should be dropped".toBytes())).expect( + "Publish failed" + ) + + check not await eventManager.waitForEvents(NegativeTestTimeout) + check eventManager.receivedMessages.len == 0 + + asyncTest "Subscription API, edge node overlapping topics isolation": + let net = await setupNetwork(1, cli_args.WakuMode.Edge) + defer: + await net.teardown() + + let topicA = ContentTopic("/waku/2/topic-a/proto") + let topicB = ContentTopic("/waku/2/topic-b/proto") + (await net.subscriber.subscribe(topicA)).expect("failed to sub A") + (await net.subscriber.subscribe(topicB)).expect("failed to sub B") + + let shard = net.subscriber.node.getRelayShard(topicA) + await waitForEdgeSubs(net.subscriber, shard) + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + net.subscriber.unsubscribe(topicA).expect("failed to unsub A") + + discard (await net.publishToMesh(topicA, "Dropped Message".toBytes())).expect( + "Publish A failed" + ) + discard + (await net.publishToMesh(topicB, "Kept Msg".toBytes())).expect("Publish B failed") + + require await eventManager.waitForEvents(TestTimeout) + require eventManager.receivedMessages.len == 1 + check eventManager.receivedMessages[0].contentTopic == topicB + + asyncTest "Subscription API, edge node resubscribe after unsubscribe": + let net = await setupNetwork(1, cli_args.WakuMode.Edge) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/resub-test/proto") + + (await net.subscriber.subscribe(testTopic)).expect("Initial sub failed") + + var eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + discard (await net.publishToMeshAfterEdgeReady(testTopic, "Msg 1".toBytes())).expect( + "Pub 1 failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + eventManager.teardown() + + net.subscriber.unsubscribe(testTopic).expect("Unsub failed") + eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + + discard + (await net.publishToMesh(testTopic, "Ghost".toBytes())).expect("Ghost pub failed") + + check not await eventManager.waitForEvents(NegativeTestTimeout) + eventManager.teardown() + + (await net.subscriber.subscribe(testTopic)).expect("Resub failed") + eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + + discard (await net.publishToMeshAfterEdgeReady(testTopic, "Msg 2".toBytes())).expect( + "Pub 2 failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + check eventManager.receivedMessages[0].payload == "Msg 2".toBytes() + + asyncTest "Subscription API, edge node failover after service peer dies": + # NOTE: This test is a bit more verbose because it defines a custom topology. + # It doesn't use the shared TestNetwork helper. + # This mounts two service peers for the edge node then fails one. + let numShards: uint16 = 1 + let shards = @[PubsubTopic("/waku/2/rs/3/0")] + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + discard + + var publisher: WakuNode + lockNewGlobalBrokerContext: + publisher = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + publisher.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on publisher" + ) + (await publisher.mountRelay()).expect("Failed to mount relay on publisher") + await publisher.mountFilter() + await publisher.mountLibp2pPing() + await publisher.start() + + for shard in shards: + publisher.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub publisher" + ) + + let publisherPeerInfo = publisher.peerInfo.toRemotePeerInfo() + + var meshBuddy: WakuNode + lockNewGlobalBrokerContext: + meshBuddy = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + meshBuddy.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on meshBuddy" + ) + (await meshBuddy.mountRelay()).expect("Failed to mount relay on meshBuddy") + await meshBuddy.mountFilter() + await meshBuddy.mountLibp2pPing() + await meshBuddy.start() + + for shard in shards: + meshBuddy.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub meshBuddy" + ) + + let meshBuddyPeerInfo = meshBuddy.peerInfo.toRemotePeerInfo() + + await meshBuddy.connectToNodes(@[publisherPeerInfo]) + + let conf = createApiNodeConf(cli_args.WakuMode.Edge, numShards) + var subscriber: Waku + lockNewGlobalBrokerContext: + subscriber = (await createNode(conf)).expect("Failed to create edge subscriber") + (await startWaku(addr subscriber)).expect("Failed to start edge subscriber") + + # Connect edge subscriber to both filter servers so selectPeers finds both + await subscriber.node.connectToNodes(@[publisherPeerInfo, meshBuddyPeerInfo]) + + let testTopic = ContentTopic("/waku/2/failover-test/proto") + let shard = subscriber.node.getRelayShard(testTopic) + + (await subscriber.subscribe(testTopic)).expect("Failed to subscribe") + + # Wait for dialing both filter servers (HealthyThreshold = 2) + for _ in 0 ..< 100: + if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2: + break + await sleepAsync(100.milliseconds) + + check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2 + + # Verify message delivery with both servers alive + await waitForMesh(publisher, shard) + + var eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) + let msg1 = WakuMessage( + payload: "Before failover".toBytes(), + contentTopic: testTopic, + version: 0, + timestamp: now(), + ) + discard (await publisher.publish(some(shard), msg1)).expect("Publish 1 failed") + + require await eventManager.waitForEvents(TestTimeout) + check eventManager.receivedMessages[0].payload == "Before failover".toBytes() + eventManager.teardown() + + # Disconnect meshBuddy from edge (keeps relay mesh alive for publishing) + await subscriber.node.disconnectNode(meshBuddyPeerInfo) + + # Wait for the dead peer to be pruned + for _ in 0 ..< 50: + if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) < 2: + break + await sleepAsync(100.milliseconds) + + check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 1 + + # Verify messages still arrive through the surviving filter server (publisher) + eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) + let msg2 = WakuMessage( + payload: "After failover".toBytes(), + contentTopic: testTopic, + version: 0, + timestamp: now(), + ) + discard (await publisher.publish(some(shard), msg2)).expect("Publish 2 failed") + + require await eventManager.waitForEvents(TestTimeout) + check eventManager.receivedMessages[0].payload == "After failover".toBytes() + eventManager.teardown() + + (await subscriber.stop()).expect("Failed to stop subscriber") + await meshBuddy.stop() + await publisher.stop() + + asyncTest "Subscription API, edge node dials replacement after peer eviction": + # 3 service peers: publisher, meshBuddy, sparePeer. Edge subscribes and + # confirms 2 (HealthyThreshold). After one is disconnected, the sub loop + # should detect the loss and dial the spare to recover back to threshold. + let numShards: uint16 = 1 + let shards = @[PubsubTopic("/waku/2/rs/3/0")] + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + discard + + var publisher: WakuNode + lockNewGlobalBrokerContext: + publisher = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + publisher.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on publisher" + ) + (await publisher.mountRelay()).expect("Failed to mount relay on publisher") + await publisher.mountFilter() + await publisher.mountLibp2pPing() + await publisher.start() + + for shard in shards: + publisher.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub publisher" + ) + + let publisherPeerInfo = publisher.peerInfo.toRemotePeerInfo() + + var meshBuddy: WakuNode + lockNewGlobalBrokerContext: + meshBuddy = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + meshBuddy.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on meshBuddy" + ) + (await meshBuddy.mountRelay()).expect("Failed to mount relay on meshBuddy") + await meshBuddy.mountFilter() + await meshBuddy.mountLibp2pPing() + await meshBuddy.start() + + for shard in shards: + meshBuddy.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub meshBuddy" + ) + + let meshBuddyPeerInfo = meshBuddy.peerInfo.toRemotePeerInfo() + + var sparePeer: WakuNode + lockNewGlobalBrokerContext: + sparePeer = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + sparePeer.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on sparePeer" + ) + (await sparePeer.mountRelay()).expect("Failed to mount relay on sparePeer") + await sparePeer.mountFilter() + await sparePeer.mountLibp2pPing() + await sparePeer.start() + + for shard in shards: + sparePeer.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub sparePeer" + ) + + let sparePeerInfo = sparePeer.peerInfo.toRemotePeerInfo() + + await meshBuddy.connectToNodes(@[publisherPeerInfo]) + await sparePeer.connectToNodes(@[publisherPeerInfo]) + + let conf = createApiNodeConf(cli_args.WakuMode.Edge, numShards) + var subscriber: Waku + lockNewGlobalBrokerContext: + subscriber = (await createNode(conf)).expect("Failed to create edge subscriber") + (await startWaku(addr subscriber)).expect("Failed to start edge subscriber") + + await subscriber.node.connectToNodes( + @[publisherPeerInfo, meshBuddyPeerInfo, sparePeerInfo] + ) + + let testTopic = ContentTopic("/waku/2/replacement-test/proto") + let shard = subscriber.node.getRelayShard(testTopic) + + (await subscriber.subscribe(testTopic)).expect("Failed to subscribe") + + # Wait for 2 confirmed peers (HealthyThreshold). The 3rd is available but not dialed. + for _ in 0 ..< 100: + if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2: + break + await sleepAsync(100.milliseconds) + + require subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) == + 2 + + await subscriber.node.disconnectNode(meshBuddyPeerInfo) + + # Wait for the sub loop to detect the loss and dial a replacement + for _ in 0 ..< 100: + if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2: + break + await sleepAsync(100.milliseconds) + + check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2 + + await waitForMesh(publisher, shard) + + var eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) + let msg = WakuMessage( + payload: "After replacement".toBytes(), + contentTopic: testTopic, + version: 0, + timestamp: now(), + ) + discard (await publisher.publish(some(shard), msg)).expect("Publish failed") + + require await eventManager.waitForEvents(TestTimeout) + check eventManager.receivedMessages[0].payload == "After replacement".toBytes() + eventManager.teardown() + + (await subscriber.stop()).expect("Failed to stop subscriber") + await sparePeer.stop() + await meshBuddy.stop() + await publisher.stop() diff --git a/tests/node/test_wakunode_health_monitor.nim b/tests/node/test_wakunode_health_monitor.nim index 416dc9dda..8a3ddd104 100644 --- a/tests/node/test_wakunode_health_monitor.nim +++ b/tests/node/test_wakunode_health_monitor.nim @@ -12,12 +12,18 @@ import node/health_monitor/health_status, node/health_monitor/connection_status, node/health_monitor/protocol_health, + node/health_monitor/topic_health, node/health_monitor/node_health_monitor, + node/delivery_service/delivery_service, + node/delivery_service/subscription_manager, node/kernel_api/relay, node/kernel_api/store, node/kernel_api/lightpush, node/kernel_api/filter, + events/health_events, + events/peer_events, waku_archive, + common/broker/broker_context, ] import ../testlib/[wakunode, wakucore], ../waku_archive/archive_utils @@ -129,13 +135,12 @@ suite "Health Monitor - health state calculation": suite "Health Monitor - events": asyncTest "Core (relay) health update": - let - nodeAKey = generateSecp256k1Key() + var nodeA: WakuNode + lockNewGlobalBrokerContext: + let nodeAKey = generateSecp256k1Key() nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0)) - - (await nodeA.mountRelay()).expect("Node A failed to mount Relay") - - await nodeA.start() + (await nodeA.mountRelay()).expect("Node A failed to mount Relay") + await nodeA.start() let monitorA = NodeHealthMonitor.new(nodeA) @@ -151,17 +156,15 @@ suite "Health Monitor - events": monitorA.startHealthMonitor().expect("Health monitor failed to start") - let - nodeBKey = generateSecp256k1Key() + var nodeB: WakuNode + lockNewGlobalBrokerContext: + let nodeBKey = generateSecp256k1Key() nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0)) - - let driver = newSqliteArchiveDriver() - nodeB.mountArchive(driver).expect("Node B failed to mount archive") - - (await nodeB.mountRelay()).expect("Node B failed to mount relay") - await nodeB.mountStore() - - await nodeB.start() + let driver = newSqliteArchiveDriver() + nodeB.mountArchive(driver).expect("Node B failed to mount archive") + (await nodeB.mountRelay()).expect("Node B failed to mount relay") + await nodeB.mountStore() + await nodeB.start() await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()]) @@ -214,15 +217,20 @@ suite "Health Monitor - events": await nodeA.stop() asyncTest "Edge (light client) health update": - let - nodeAKey = generateSecp256k1Key() + var nodeA: WakuNode + lockNewGlobalBrokerContext: + let nodeAKey = generateSecp256k1Key() nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0)) + nodeA.mountLightpushClient() + await nodeA.mountFilterClient() + nodeA.mountStoreClient() + require nodeA.mountAutoSharding(1, 8).isOk + nodeA.mountMetadata(1, @[0'u16]).expect("Node A failed to mount metadata") + await nodeA.start() - nodeA.mountLightpushClient() - await nodeA.mountFilterClient() - nodeA.mountStoreClient() - - await nodeA.start() + let ds = + DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService") + ds.startDeliveryService().expect("Failed to start DeliveryService") let monitorA = NodeHealthMonitor.new(nodeA) @@ -238,23 +246,40 @@ suite "Health Monitor - events": monitorA.startHealthMonitor().expect("Health monitor failed to start") - let - nodeBKey = generateSecp256k1Key() + var nodeB: WakuNode + lockNewGlobalBrokerContext: + let nodeBKey = generateSecp256k1Key() nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0)) + let driver = newSqliteArchiveDriver() + nodeB.mountArchive(driver).expect("Node B failed to mount archive") + (await nodeB.mountRelay()).expect("Node B failed to mount relay") + (await nodeB.mountLightpush()).expect("Node B failed to mount lightpush") + await nodeB.mountFilter() + await nodeB.mountStore() + require nodeB.mountAutoSharding(1, 8).isOk + nodeB.mountMetadata(1, toSeq(0'u16 ..< 8'u16)).expect( + "Node B failed to mount metadata" + ) + await nodeB.start() - let driver = newSqliteArchiveDriver() - nodeB.mountArchive(driver).expect("Node B failed to mount archive") - - (await nodeB.mountRelay()).expect("Node B failed to mount relay") - - (await nodeB.mountLightpush()).expect("Node B failed to mount lightpush") - await nodeB.mountFilter() - await nodeB.mountStore() - - await nodeB.start() + var metadataFut = newFuture[void]("waitForMetadata") + let metadataLis = WakuPeerEvent + .listen( + nodeA.brokerCtx, + proc(evt: WakuPeerEvent): Future[void] {.async: (raises: []), gcsafe.} = + if not metadataFut.finished and + evt.kind == WakuPeerEventKind.EventMetadataUpdated: + metadataFut.complete() + , + ) + .expect("Failed to listen for metadata") await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()]) + let metadataOk = await metadataFut.withTimeout(TestConnectivityTimeLimit) + WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis) + require metadataOk + let connectTimeLimit = Moment.now() + TestConnectivityTimeLimit var gotConnected = false @@ -292,4 +317,118 @@ suite "Health Monitor - events": lastStatus == ConnectionStatus.Disconnected await monitorA.stopHealthMonitor() + await ds.stopDeliveryService() + await nodeA.stop() + + asyncTest "Edge health driven by confirmed filter subscriptions": + var nodeA: WakuNode + lockNewGlobalBrokerContext: + let nodeAKey = generateSecp256k1Key() + nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0)) + await nodeA.mountFilterClient() + nodeA.mountLightpushClient() + nodeA.mountStoreClient() + require nodeA.mountAutoSharding(1, 8).isOk + nodeA.mountMetadata(1, @[0'u16]).expect("Node A failed to mount metadata") + await nodeA.start() + + let ds = + DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService") + ds.startDeliveryService().expect("Failed to start DeliveryService") + let subMgr = ds.subscriptionManager + + var nodeB: WakuNode + lockNewGlobalBrokerContext: + let nodeBKey = generateSecp256k1Key() + nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0)) + let driver = newSqliteArchiveDriver() + nodeB.mountArchive(driver).expect("Node B failed to mount archive") + (await nodeB.mountRelay()).expect("Node B failed to mount relay") + (await nodeB.mountLightpush()).expect("Node B failed to mount lightpush") + await nodeB.mountFilter() + await nodeB.mountStore() + require nodeB.mountAutoSharding(1, 8).isOk + nodeB.mountMetadata(1, toSeq(0'u16 ..< 8'u16)).expect( + "Node B failed to mount metadata" + ) + await nodeB.start() + + let monitorA = NodeHealthMonitor.new(nodeA) + + var + lastStatus = ConnectionStatus.Disconnected + healthSignal = newAsyncEvent() + + monitorA.onConnectionStatusChange = proc(status: ConnectionStatus) {.async.} = + lastStatus = status + healthSignal.fire() + + monitorA.startHealthMonitor().expect("Health monitor failed to start") + + var metadataFut = newFuture[void]("waitForMetadata") + let metadataLis = WakuPeerEvent + .listen( + nodeA.brokerCtx, + proc(evt: WakuPeerEvent): Future[void] {.async: (raises: []), gcsafe.} = + if not metadataFut.finished and + evt.kind == WakuPeerEventKind.EventMetadataUpdated: + metadataFut.complete() + , + ) + .expect("Failed to listen for metadata") + + await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()]) + + let metadataOk = await metadataFut.withTimeout(TestConnectivityTimeLimit) + WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis) + require metadataOk + + var deadline = Moment.now() + TestConnectivityTimeLimit + while Moment.now() < deadline: + if lastStatus == ConnectionStatus.PartiallyConnected: + break + if await healthSignal.wait().withTimeout(deadline - Moment.now()): + healthSignal.clear() + + check lastStatus == ConnectionStatus.PartiallyConnected + + var shardHealthFut = newFuture[EventShardTopicHealthChange]("waitForShardHealth") + + let shardHealthLis = EventShardTopicHealthChange + .listen( + nodeA.brokerCtx, + proc( + evt: EventShardTopicHealthChange + ): Future[void] {.async: (raises: []), gcsafe.} = + if not shardHealthFut.finished and ( + evt.health == TopicHealth.MINIMALLY_HEALTHY or + evt.health == TopicHealth.SUFFICIENTLY_HEALTHY + ): + shardHealthFut.complete(evt) + , + ) + .expect("Failed to listen for shard health") + + let contentTopic = ContentTopic("/waku/2/default-content/proto") + subMgr.subscribe(contentTopic).expect("Failed to subscribe") + + let shardHealthOk = await shardHealthFut.withTimeout(TestConnectivityTimeLimit) + EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis) + + check shardHealthOk == true + check subMgr.edgeFilterSubStates.len > 0 + + healthSignal.clear() + deadline = Moment.now() + TestConnectivityTimeLimit + while Moment.now() < deadline: + if lastStatus == ConnectionStatus.PartiallyConnected: + break + if await healthSignal.wait().withTimeout(deadline - Moment.now()): + healthSignal.clear() + + check lastStatus == ConnectionStatus.PartiallyConnected + + await ds.stopDeliveryService() + await monitorA.stopHealthMonitor() + await nodeB.stop() await nodeA.stop() diff --git a/waku/events/peer_events.nim b/waku/events/peer_events.nim index 49dfa9f9a..dd02841f7 100644 --- a/waku/events/peer_events.nim +++ b/waku/events/peer_events.nim @@ -8,6 +8,6 @@ type WakuPeerEventKind* {.pure.} = enum EventMetadataUpdated EventBroker: - type EventWakuPeer* = object + type WakuPeerEvent* = object peerId*: PeerId kind*: WakuPeerEventKind diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index dbee8d093..45e0edee0 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -416,7 +416,8 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: ## Reliability if not waku[].deliveryService.isNil(): - waku[].deliveryService.startDeliveryService() + waku[].deliveryService.startDeliveryService().isOkOr: + return err("failed to start delivery service: " & $error) ## Health Monitor waku[].healthMonitor.startHealthMonitor().isOkOr: diff --git a/waku/node/delivery_service/delivery_service.nim b/waku/node/delivery_service/delivery_service.nim index 258c01e95..fd728d048 100644 --- a/waku/node/delivery_service/delivery_service.nim +++ b/waku/node/delivery_service/delivery_service.nim @@ -1,18 +1,13 @@ ## This module helps to ensure the correct transmission and reception of messages import results -import chronos +import chronos, chronicles import ./recv_service, ./send_service, ./subscription_manager, waku/[ - waku_core, - waku_node, - waku_store/client, - waku_relay/protocol, - waku_lightpush/client, - waku_filter_v2/client, + waku_core, waku_node, waku_store/client, waku_relay/protocol, waku_lightpush/client ] type DeliveryService* = ref object @@ -37,10 +32,11 @@ proc new*( ) ) -proc startDeliveryService*(self: DeliveryService) = - self.subscriptionManager.startSubscriptionManager() +proc startDeliveryService*(self: DeliveryService): Result[void, string] = + ?self.subscriptionManager.startSubscriptionManager() self.recvService.startRecvService() self.sendService.startSendService() + return ok() proc stopDeliveryService*(self: DeliveryService) {.async.} = await self.sendService.stopSendService() diff --git a/waku/node/delivery_service/recv_service/recv_service.nim b/waku/node/delivery_service/recv_service/recv_service.nim index 0eba2c450..9a85df2f9 100644 --- a/waku/node/delivery_service/recv_service/recv_service.nim +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -91,20 +91,20 @@ proc msgChecker(self: RecvService) {.async.} = self.endTimeToCheck = getNowInNanosecondTime() var msgHashesInStore = newSeq[WakuMessageHash](0) - for sub in self.subscriptionManager.getActiveSubscriptions(): + for pubsubTopic, contentTopics in self.subscriptionManager.subscribedTopics: let storeResp: StoreQueryResponse = ( await self.node.wakuStoreClient.queryToAny( StoreQueryRequest( includeData: false, - pubsubTopic: some(PubsubTopic(sub.pubsubTopic)), - contentTopics: sub.contentTopics, + pubsubTopic: some(pubsubTopic), + contentTopics: toSeq(contentTopics), startTime: some(self.startTimeToCheck - DelayExtra.nanos), endTime: some(self.endTimeToCheck + DelayExtra.nanos), ) ) ).valueOr: error "msgChecker failed to get remote msgHashes", - pubsubTopic = sub.pubsubTopic, cTopics = sub.contentTopics, error = $error + pubsubTopic = pubsubTopic, cTopics = toSeq(contentTopics), error = $error continue msgHashesInStore.add(storeResp.messages.mapIt(it.messageHash)) @@ -154,10 +154,6 @@ proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T = recentReceivedMsgs: @[], ) - # TODO: For MAPI Edge support, either call node.wakuFilterClient.registerPushHandler - # so that the RecvService listens to incoming filter messages, - # or have the filter client emit MessageSeenEvent. - return recvService proc loopPruneOldMessages(self: RecvService) {.async.} = diff --git a/waku/node/delivery_service/subscription_manager.nim b/waku/node/delivery_service/subscription_manager.nim index 22df47413..70d8df7f0 100644 --- a/waku/node/delivery_service/subscription_manager.nim +++ b/waku/node/delivery_service/subscription_manager.nim @@ -1,4 +1,5 @@ -import std/[sets, tables, options, strutils], chronos, chronicles, results +import std/[sequtils, sets, tables, options, strutils], chronos, chronicles, results +import libp2p/[peerid, peerinfo] import waku/[ waku_core, @@ -6,16 +7,67 @@ import waku_core/topics/sharding, waku_node, waku_relay, + waku_filter_v2/common as filter_common, + waku_filter_v2/client as filter_client, + waku_filter_v2/protocol as filter_protocol, common/broker/broker_context, - events/delivery_events, + events/health_events, + events/peer_events, + requests/health_requests, + node/peer_manager, + node/health_monitor/topic_health, + node/health_monitor/connection_status, ] +# --------------------------------------------------------------------------- +# Logos Messaging API SubscriptionManager +# +# Maps all topic subscription intent and centralizes all consistency +# maintenance of the pubsub and content topic subscription model across +# the various network drivers that handle topics (Edge/Filter and Core/Relay). +# --------------------------------------------------------------------------- + +type EdgeFilterSubState* = object + peers: seq[RemotePeerInfo] + ## Filter service peers with confirmed subscriptions on this shard. + pending: seq[Future[void]] ## In-flight dial futures for peers not yet confirmed. + pendingPeers: HashSet[PeerId] ## PeerIds of peers currently being dialed. + currentHealth: TopicHealth + ## Cached health derived from peers.len; updated on every peer set change. + +func toTopicHealth*(peersCount: int): TopicHealth = + if peersCount >= HealthyThreshold: + TopicHealth.SUFFICIENTLY_HEALTHY + elif peersCount > 0: + TopicHealth.MINIMALLY_HEALTHY + else: + TopicHealth.UNHEALTHY + type SubscriptionManager* = ref object of RootObj node: WakuNode contentTopicSubs: Table[PubsubTopic, HashSet[ContentTopic]] ## Map of Shard to ContentTopic needed because e.g. WakuRelay is PubsubTopic only. ## A present key with an empty HashSet value means pubsubtopic already subscribed ## (via subscribePubsubTopics()) but there's no specific content topic interest yet. + edgeFilterSubStates*: Table[PubsubTopic, EdgeFilterSubState] + ## Per-shard filter subscription state for edge mode. + edgeFilterWakeup: AsyncEvent + ## Signalled when the edge filter sub loop should re-reconcile. + edgeFilterSubLoopFut: Future[void] + edgeFilterHealthLoopFut: Future[void] + peerEventListener: WakuPeerEventListener + ## Listener for peer connect/disconnect events (edge filter wakeup). + +iterator subscribedTopics*( + self: SubscriptionManager +): (PubsubTopic, HashSet[ContentTopic]) = + for pubsub, topics in self.contentTopicSubs.pairs: + yield (pubsub, topics) + +proc edgeFilterPeerCount*(sm: SubscriptionManager, shard: PubsubTopic): int = + sm.edgeFilterSubStates.withValue(shard, state): + return state.peers.len + return 0 proc new*(T: typedesc[SubscriptionManager], node: WakuNode): T = SubscriptionManager( @@ -25,30 +77,35 @@ proc new*(T: typedesc[SubscriptionManager], node: WakuNode): T = proc addContentTopicInterest( self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic ): Result[void, string] = + var changed = false if not self.contentTopicSubs.hasKey(shard): self.contentTopicSubs[shard] = initHashSet[ContentTopic]() + changed = true self.contentTopicSubs.withValue(shard, cTopics): if not cTopics[].contains(topic): cTopics[].incl(topic) + changed = true - # TODO: Call a "subscribe(shard, topic)" on filter client here, - # so the filter client can know that subscriptions changed. + if changed and not isNil(self.edgeFilterWakeup): + self.edgeFilterWakeup.fire() return ok() proc removeContentTopicInterest( self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic ): Result[void, string] = + var changed = false self.contentTopicSubs.withValue(shard, cTopics): if cTopics[].contains(topic): cTopics[].excl(topic) + changed = true if cTopics[].len == 0 and isNil(self.node.wakuRelay): self.contentTopicSubs.del(shard) # We're done with cTopics here - # TODO: Call a "unsubscribe(shard, topic)" on filter client here, - # so the filter client can know that subscriptions changed. + if changed and not isNil(self.edgeFilterWakeup): + self.edgeFilterWakeup.fire() return ok() @@ -73,46 +130,6 @@ proc subscribePubsubTopics( return ok() -proc startSubscriptionManager*(self: SubscriptionManager) = - if isNil(self.node.wakuRelay): - return - - if self.node.wakuAutoSharding.isSome(): - # Subscribe relay to all shards in autosharding. - let autoSharding = self.node.wakuAutoSharding.get() - let clusterId = autoSharding.clusterId - let numShards = autoSharding.shardCountGenZero - - if numShards > 0: - var clusterPubsubTopics = newSeqOfCap[PubsubTopic](numShards) - - for i in 0 ..< numShards: - let shardObj = RelayShard(clusterId: clusterId, shardId: uint16(i)) - clusterPubsubTopics.add(PubsubTopic($shardObj)) - - self.subscribePubsubTopics(clusterPubsubTopics).isOkOr: - error "Failed to auto-subscribe Relay to cluster shards: ", error = error - else: - info "SubscriptionManager has no AutoSharding configured; skipping auto-subscribe." - -proc stopSubscriptionManager*(self: SubscriptionManager) {.async.} = - discard - -proc getActiveSubscriptions*( - self: SubscriptionManager -): seq[tuple[pubsubTopic: string, contentTopics: seq[ContentTopic]]] = - var activeSubs: seq[tuple[pubsubTopic: string, contentTopics: seq[ContentTopic]]] = - @[] - - for pubsub, cTopicSet in self.contentTopicSubs.pairs: - if cTopicSet.len > 0: - var cTopicSeq = newSeqOfCap[ContentTopic](cTopicSet.len) - for t in cTopicSet: - cTopicSeq.add(t) - activeSubs.add((pubsub, cTopicSeq)) - - return activeSubs - proc getShardForContentTopic( self: SubscriptionManager, topic: ContentTopic ): Result[PubsubTopic, string] = @@ -162,3 +179,358 @@ proc unsubscribe*( ?self.removeContentTopicInterest(shard, topic) return ok() + +# --------------------------------------------------------------------------- +# Edge Filter driver for the Logos Messaging API +# +# The SubscriptionManager absorbs natively the responsibility of using the +# Edge Filter protocol to effect subscriptions and message receipt for edge. +# --------------------------------------------------------------------------- + +const EdgeFilterSubscribeTimeout = chronos.seconds(15) + ## Timeout for a single filter subscribe/unsubscribe RPC to a service peer. +const EdgeFilterPingTimeout = chronos.seconds(5) + ## Timeout for a filter ping health check. +const EdgeFilterLoopInterval = chronos.seconds(30) + ## Interval for the edge filter health ping loop. +const EdgeFilterSubLoopDebounce = chronos.seconds(1) + ## Debounce delay to coalesce rapid-fire wakeups into a single reconciliation pass. + +proc updateShardHealth( + self: SubscriptionManager, shard: PubsubTopic, state: var EdgeFilterSubState +) = + ## Recompute and emit health for a shard after its peer set changed. + let newHealth = toTopicHealth(state.peers.len) + if newHealth != state.currentHealth: + state.currentHealth = newHealth + EventShardTopicHealthChange.emit(self.node.brokerCtx, shard, newHealth) + +proc removePeer(self: SubscriptionManager, shard: PubsubTopic, peerId: PeerId) = + ## Remove a peer from edgeFilterSubStates for the given shard, + ## update health, and wake the sub loop to dial a replacement. + ## Best-effort unsubscribe so the service peer stops pushing to us. + self.edgeFilterSubStates.withValue(shard, state): + var peer: RemotePeerInfo + var found = false + for p in state.peers: + if p.peerId == peerId: + peer = p + found = true + break + if not found: + return + + state.peers.keepItIf(it.peerId != peerId) + self.updateShardHealth(shard, state[]) + self.edgeFilterWakeup.fire() + + if not self.node.wakuFilterClient.isNil(): + self.contentTopicSubs.withValue(shard, topics): + let ct = toSeq(topics[]) + if ct.len > 0: + proc doUnsubscribe() {.async.} = + discard await self.node.wakuFilterClient.unsubscribe(peer, shard, ct) + + asyncSpawn doUnsubscribe() + +type SendChunkedFilterRpcKind = enum + FilterSubscribe + FilterUnsubscribe + +proc sendChunkedFilterRpc( + self: SubscriptionManager, + peer: RemotePeerInfo, + shard: PubsubTopic, + topics: seq[ContentTopic], + kind: SendChunkedFilterRpcKind, +): Future[bool] {.async.} = + ## Send a chunked filter subscribe or unsubscribe RPC. Returns true on + ## success. On failure the peer is removed and false is returned. + try: + var i = 0 + while i < topics.len: + let chunk = + topics[i ..< min(i + filter_protocol.MaxContentTopicsPerRequest, topics.len)] + let fut = + case kind + of FilterSubscribe: + self.node.wakuFilterClient.subscribe(peer, shard, chunk) + of FilterUnsubscribe: + self.node.wakuFilterClient.unsubscribe(peer, shard, chunk) + if not (await fut.withTimeout(EdgeFilterSubscribeTimeout)) or fut.read().isErr(): + trace "sendChunkedFilterRpc: chunk failed", + op = kind, shard = shard, peer = peer.peerId + self.removePeer(shard, peer.peerId) + return false + i += filter_protocol.MaxContentTopicsPerRequest + except CatchableError as exc: + debug "sendChunkedFilterRpc: failed", + op = kind, shard = shard, peer = peer.peerId, err = exc.msg + self.removePeer(shard, peer.peerId) + return false + return true + +proc syncFilterDeltas( + self: SubscriptionManager, + peer: RemotePeerInfo, + shard: PubsubTopic, + added: seq[ContentTopic], + removed: seq[ContentTopic], +) {.async.} = + ## Push content topic changes (adds/removes) to an already-tracked peer. + if added.len > 0: + if not await self.sendChunkedFilterRpc(peer, shard, added, FilterSubscribe): + return + + if removed.len > 0: + discard await self.sendChunkedFilterRpc(peer, shard, removed, FilterUnsubscribe) + +proc dialFilterPeer( + self: SubscriptionManager, + peer: RemotePeerInfo, + shard: PubsubTopic, + contentTopics: seq[ContentTopic], +) {.async.} = + ## Subscribe a new peer to all content topics on a shard and start tracking it. + self.edgeFilterSubStates.withValue(shard, state): + state.pendingPeers.incl(peer.peerId) + + try: + if not await self.sendChunkedFilterRpc(peer, shard, contentTopics, FilterSubscribe): + return + + self.edgeFilterSubStates.withValue(shard, state): + if state.peers.anyIt(it.peerId == peer.peerId): + trace "dialFilterPeer: peer already tracked, skipping duplicate", + shard = shard, peer = peer.peerId + return + + state.peers.add(peer) + self.updateShardHealth(shard, state[]) + trace "dialFilterPeer: successfully subscribed to all chunks", + shard = shard, peer = peer.peerId, totalPeers = state.peers.len + do: + trace "dialFilterPeer: shard removed while subscribing, discarding result", + shard = shard, peer = peer.peerId + finally: + self.edgeFilterSubStates.withValue(shard, state): + state.pendingPeers.excl(peer.peerId) + +proc edgeFilterHealthLoop*(self: SubscriptionManager) {.async.} = + ## Periodically pings all connected filter service peers to verify they are + ## still alive at the application layer. Peers that fail the ping are removed. + while true: + await sleepAsync(EdgeFilterLoopInterval) + + if self.node.wakuFilterClient.isNil(): + warn "filter client is nil within edge filter health loop" + continue + + var connected = initTable[PeerId, RemotePeerInfo]() + for state in self.edgeFilterSubStates.values: + for peer in state.peers: + if self.node.peerManager.switch.peerStore.isConnected(peer.peerId): + connected[peer.peerId] = peer + + var alive = initHashSet[PeerId]() + + if connected.len > 0: + var pingTasks: seq[(PeerId, Future[FilterSubscribeResult])] = @[] + for peer in connected.values: + pingTasks.add( + (peer.peerId, self.node.wakuFilterClient.ping(peer, EdgeFilterPingTimeout)) + ) + + # extract future tasks from (PeerId, Future) tuples and await them + await allFutures(pingTasks.mapIt(it[1])) + + for (peerId, task) in pingTasks: + if task.read().isOk(): + alive.incl(peerId) + + var changed = false + for shard, state in self.edgeFilterSubStates.mpairs: + let oldLen = state.peers.len + state.peers.keepItIf(it.peerId notin connected or alive.contains(it.peerId)) + + if state.peers.len < oldLen: + changed = true + self.updateShardHealth(shard, state) + trace "Edge Filter health degraded by Ping failure", + shard = shard, new = state.currentHealth + + if changed: + self.edgeFilterWakeup.fire() + +proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = + ## Reconciles filter subscriptions with the desired state from SubscriptionManager. + var lastSynced = initTable[PubsubTopic, HashSet[ContentTopic]]() + + while true: + await self.edgeFilterWakeup.wait() + await sleepAsync(EdgeFilterSubLoopDebounce) + self.edgeFilterWakeup.clear() + trace "edgeFilterSubLoop: woke up" + + if isNil(self.node.wakuFilterClient): + trace "edgeFilterSubLoop: wakuFilterClient is nil, skipping" + continue + + let desired = self.contentTopicSubs + + trace "edgeFilterSubLoop: desired state", numShards = desired.len + + let allShards = toHashSet(toSeq(desired.keys)) + toHashSet(toSeq(lastSynced.keys)) + + for shard in allShards: + let currTopics = desired.getOrDefault(shard) + let prevTopics = lastSynced.getOrDefault(shard) + + if shard notin self.edgeFilterSubStates: + self.edgeFilterSubStates[shard] = + EdgeFilterSubState(currentHealth: TopicHealth.UNHEALTHY) + + let addedTopics = toSeq(currTopics - prevTopics) + let removedTopics = toSeq(prevTopics - currTopics) + + self.edgeFilterSubStates.withValue(shard, state): + state.peers.keepItIf( + self.node.peerManager.switch.peerStore.isConnected(it.peerId) + ) + state.pending.keepItIf(not it.finished) + + if addedTopics.len > 0 or removedTopics.len > 0: + for peer in state.peers: + asyncSpawn self.syncFilterDeltas(peer, shard, addedTopics, removedTopics) + + if currTopics.len == 0: + for fut in state.pending: + if not fut.finished: + await fut.cancelAndWait() + self.edgeFilterSubStates.del(shard) + # invalidates `state` — do not use after this + else: + self.updateShardHealth(shard, state[]) + + let needed = max(0, HealthyThreshold - state.peers.len - state.pending.len) + + if needed > 0: + let tracked = state.peers.mapIt(it.peerId).toHashSet() + state.pendingPeers + var candidates = self.node.peerManager.selectPeers( + filter_common.WakuFilterSubscribeCodec, some(shard) + ) + candidates.keepItIf(it.peerId notin tracked) + + let toDial = min(needed, candidates.len) + + trace "edgeFilterSubLoop: shard reconciliation", + shard = shard, + num_peers = state.peers.len, + num_pending = state.pending.len, + num_needed = needed, + num_available = candidates.len, + toDial = toDial + + for i in 0 ..< toDial: + let fut = self.dialFilterPeer(candidates[i], shard, toSeq(currTopics)) + state.pending.add(fut) + + lastSynced = desired + +proc startEdgeFilterLoops(self: SubscriptionManager): Result[void, string] = + ## Start the edge filter orchestration loops. + ## Caller must ensure this is only called in edge mode (relay nil, filter client present). + self.edgeFilterWakeup = newAsyncEvent() + + self.peerEventListener = WakuPeerEvent.listen( + self.node.brokerCtx, + proc(evt: WakuPeerEvent) {.async: (raises: []), gcsafe.} = + if evt.kind == WakuPeerEventKind.EventDisconnected or + evt.kind == WakuPeerEventKind.EventMetadataUpdated: + self.edgeFilterWakeup.fire() + , + ).valueOr: + return err("Failed to listen to peer events for edge filter: " & error) + + self.edgeFilterSubLoopFut = self.edgeFilterSubLoop() + self.edgeFilterHealthLoopFut = self.edgeFilterHealthLoop() + return ok() + +proc stopEdgeFilterLoops(self: SubscriptionManager) {.async: (raises: []).} = + ## Stop the edge filter orchestration loops and clean up pending futures. + if not isNil(self.edgeFilterSubLoopFut): + await self.edgeFilterSubLoopFut.cancelAndWait() + self.edgeFilterSubLoopFut = nil + + if not isNil(self.edgeFilterHealthLoopFut): + await self.edgeFilterHealthLoopFut.cancelAndWait() + self.edgeFilterHealthLoopFut = nil + + for shard, state in self.edgeFilterSubStates: + for fut in state.pending: + if not fut.finished: + await fut.cancelAndWait() + + WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener) + +# --------------------------------------------------------------------------- +# SubscriptionManager Lifecycle (calls Edge behavior above) +# +# startSubscriptionManager and stopSubscriptionManager orchestrate both the +# core (relay) and edge (filter) paths, and register/clear broker providers. +# --------------------------------------------------------------------------- + +proc startSubscriptionManager*(self: SubscriptionManager): Result[void, string] = + # Register edge filter broker providers. The shard/content health providers + # in WakuNode query these via the broker as a fallback when relay health is + # not available. If edge mode is not active, these providers simply return + # NOT_SUBSCRIBED / strength 0, which is harmless. + RequestEdgeShardHealth.setProvider( + self.node.brokerCtx, + proc(shard: PubsubTopic): Result[RequestEdgeShardHealth, string] = + self.edgeFilterSubStates.withValue(shard, state): + return ok(RequestEdgeShardHealth(health: state.currentHealth)) + return ok(RequestEdgeShardHealth(health: TopicHealth.NOT_SUBSCRIBED)), + ).isOkOr: + error "Can't set provider for RequestEdgeShardHealth", error = error + + RequestEdgeFilterPeerCount.setProvider( + self.node.brokerCtx, + proc(): Result[RequestEdgeFilterPeerCount, string] = + var minPeers = high(int) + for state in self.edgeFilterSubStates.values: + minPeers = min(minPeers, state.peers.len) + if minPeers == high(int): + minPeers = 0 + return ok(RequestEdgeFilterPeerCount(peerCount: minPeers)), + ).isOkOr: + error "Can't set provider for RequestEdgeFilterPeerCount", error = error + + if self.node.wakuRelay.isNil(): + return self.startEdgeFilterLoops() + + # Core mode: auto-subscribe relay to all shards in autosharding. + if self.node.wakuAutoSharding.isSome(): + let autoSharding = self.node.wakuAutoSharding.get() + let clusterId = autoSharding.clusterId + let numShards = autoSharding.shardCountGenZero + + if numShards > 0: + var clusterPubsubTopics = newSeqOfCap[PubsubTopic](numShards) + + for i in 0 ..< numShards: + let shardObj = RelayShard(clusterId: clusterId, shardId: uint16(i)) + clusterPubsubTopics.add(PubsubTopic($shardObj)) + + self.subscribePubsubTopics(clusterPubsubTopics).isOkOr: + error "Failed to auto-subscribe Relay to cluster shards: ", error = error + else: + info "SubscriptionManager has no AutoSharding configured; skipping auto-subscribe." + + return ok() + +proc stopSubscriptionManager*(self: SubscriptionManager) {.async: (raises: []).} = + if self.node.wakuRelay.isNil(): + await self.stopEdgeFilterLoops() + RequestEdgeShardHealth.clearProvider(self.node.brokerCtx) + RequestEdgeFilterPeerCount.clearProvider(self.node.brokerCtx) diff --git a/waku/node/health_monitor/connection_status.nim b/waku/node/health_monitor/connection_status.nim index 77696130a..68ec9d4be 100644 --- a/waku/node/health_monitor/connection_status.nim +++ b/waku/node/health_monitor/connection_status.nim @@ -2,6 +2,9 @@ import chronos, results, std/strutils, ../../api/types export ConnectionStatus +const HealthyThreshold* = 2 + ## Minimum peers required per service protocol for a "Connected" status (excluding Relay). + proc init*( t: typedesc[ConnectionStatus], strRep: string ): Result[ConnectionStatus, string] = diff --git a/waku/node/health_monitor/node_health_monitor.nim b/waku/node/health_monitor/node_health_monitor.nim index 79bf9f92a..066e7776a 100644 --- a/waku/node/health_monitor/node_health_monitor.nim +++ b/waku/node/health_monitor/node_health_monitor.nim @@ -21,6 +21,7 @@ import node/health_monitor/health_report, node/health_monitor/connection_status, node/health_monitor/protocol_health, + requests/health_requests, ] ## This module is aimed to check the state of the "self" Waku Node @@ -29,9 +30,6 @@ import # if not called, the outcome of randomization procedures will be the same in every run random.randomize() -const HealthyThreshold* = 2 - ## minimum peers required for all services for a Connected status, excluding Relay - type NodeHealthMonitor* = ref object nodeHealth: HealthStatus node: WakuNode @@ -48,7 +46,8 @@ type NodeHealthMonitor* = ref object ## latest known connectivity strength (e.g. connected peer count) metric for each protocol. ## if it doesn't make sense for the protocol in question, this is set to zero. relayObserver: PubSubObserver - peerEventListener: EventWakuPeerListener + peerEventListener: WakuPeerEventListener + shardHealthListener: EventShardTopicHealthChangeListener func getHealth*(report: HealthReport, kind: WakuProtocol): ProtocolHealth = for h in report.protocolsHealth: @@ -198,6 +197,17 @@ proc getFilterClientHealth(hm: NodeHealthMonitor): ProtocolHealth = hm.strength[WakuProtocol.FilterClientProtocol] = 0 return p.notMounted() + if isNil(hm.node.wakuRelay): + let edgeRes = RequestEdgeFilterPeerCount.request(hm.node.brokerCtx) + if edgeRes.isOk(): + let peerCount = edgeRes.get().peerCount + if peerCount > 0: + hm.strength[WakuProtocol.FilterClientProtocol] = peerCount + return p.ready() + else: + error "Failed to request edge filter peer count", error = edgeRes.error + return p.notReady("Failed to request edge filter peer count: " & edgeRes.error) + let peerCount = countCapablePeers(hm, WakuFilterSubscribeCodec) hm.strength[WakuProtocol.FilterClientProtocol] = peerCount @@ -663,14 +673,23 @@ proc startHealthMonitor*(hm: NodeHealthMonitor): Result[void, string] = ) hm.node.wakuRelay.addObserver(hm.relayObserver) - hm.peerEventListener = EventWakuPeer.listen( + hm.peerEventListener = WakuPeerEvent.listen( hm.node.brokerCtx, - proc(evt: EventWakuPeer): Future[void] {.async: (raises: []), gcsafe.} = + proc(evt: WakuPeerEvent): Future[void] {.async: (raises: []), gcsafe.} = ## Recompute health on any peer changing anything (join, leave, identify, metadata update) hm.healthUpdateEvent.fire(), ).valueOr: return err("Failed to subscribe to peer events: " & error) + hm.shardHealthListener = EventShardTopicHealthChange.listen( + hm.node.brokerCtx, + proc( + evt: EventShardTopicHealthChange + ): Future[void] {.async: (raises: []), gcsafe.} = + hm.healthUpdateEvent.fire(), + ).valueOr: + return err("Failed to subscribe to shard health events: " & error) + hm.healthUpdateEvent = newAsyncEvent() hm.healthUpdateEvent.fire() @@ -690,8 +709,8 @@ proc stopHealthMonitor*(hm: NodeHealthMonitor) {.async.} = if not isNil(hm.healthLoopFut): await hm.healthLoopFut.cancelAndWait() - if hm.peerEventListener.id != 0: - EventWakuPeer.dropListener(hm.node.brokerCtx, hm.peerEventListener) + WakuPeerEvent.dropListener(hm.node.brokerCtx, hm.peerEventListener) + EventShardTopicHealthChange.dropListener(hm.node.brokerCtx, hm.shardHealthListener) if not isNil(hm.node.wakuRelay) and not isNil(hm.relayObserver): hm.node.wakuRelay.removeObserver(hm.relayObserver) diff --git a/waku/node/peer_manager/peer_manager.nim b/waku/node/peer_manager/peer_manager.nim index dc0da9624..e3eb8d75b 100644 --- a/waku/node/peer_manager/peer_manager.nim +++ b/waku/node/peer_manager/peer_manager.nim @@ -215,31 +215,34 @@ proc loadFromStorage(pm: PeerManager) {.gcsafe.} = trace "recovered peers from storage", amount = amount -proc selectPeer*( +proc selectPeers*( pm: PeerManager, proto: string, shard: Option[PubsubTopic] = none(PubsubTopic) -): Option[RemotePeerInfo] = - # Selects the best peer for a given protocol - +): seq[RemotePeerInfo] = + ## Returns all peers that support the given protocol (and optionally shard), + ## shuffled randomly. Callers can further filter or pick from this list. var peers = pm.switch.peerStore.getPeersByProtocol(proto) - trace "Selecting peer from peerstore", - protocol = proto, peers, address = cast[uint](pm.switch.peerStore) + trace "Selecting peers from peerstore", + protocol = proto, num_peers = peers.len, address = cast[uint](pm.switch.peerStore) if shard.isSome(): - # Parse the shard from the pubsub topic to get cluster and shard ID let shardInfo = RelayShard.parse(shard.get()).valueOr: trace "Failed to parse shard from pubsub topic", topic = shard.get() - return none(RemotePeerInfo) + return @[] - # Filter peers that support the requested shard - # Check both ENR (if present) and the shards field on RemotePeerInfo peers.keepItIf( - # Check ENR if available (it.enr.isSome() and it.enr.get().containsShard(shard.get())) or - # Otherwise check the shards field directly - (it.shards.len > 0 and it.shards.contains(shardInfo.shardId)) + (it.shards.len > 0 and it.shards.contains(shardInfo.shardId)) ) shuffle(peers) + return peers + +proc selectPeer*( + pm: PeerManager, proto: string, shard: Option[PubsubTopic] = none(PubsubTopic) +): Option[RemotePeerInfo] = + ## Selects a single peer for a given protocol, checking service slots first + ## (for non-relay protocols). + let peers = pm.selectPeers(proto, shard) # No criteria for selecting a peer for WakuRelay, random one if proto == WakuRelayCodec: @@ -742,7 +745,7 @@ proc refreshPeerMetadata(pm: PeerManager, peerId: PeerId) {.async.} = # TODO: should only trigger an event if metadata actually changed # should include the shard subscription delta in the event when # it is a MetadataUpdated event - EventWakuPeer.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventMetadataUpdated) + WakuPeerEvent.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventMetadataUpdated) return info "disconnecting from peer", peerId = peerId, reason = reason @@ -787,7 +790,7 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = asyncSpawn(pm.switch.disconnect(peerId)) peerStore.delete(peerId) - EventWakuPeer.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventConnected) + WakuPeerEvent.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventConnected) if not pm.onConnectionChange.isNil(): # we don't want to await for the callback to finish @@ -804,7 +807,7 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = pm.ipTable.del(ip) break - EventWakuPeer.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventDisconnected) + WakuPeerEvent.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventDisconnected) if not pm.onConnectionChange.isNil(): # we don't want to await for the callback to finish @@ -812,7 +815,7 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = of PeerEventKind.Identified: info "event identified", peerId = peerId - EventWakuPeer.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventIdentified) + WakuPeerEvent.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventIdentified) peerStore[ConnectionBook][peerId] = connectedness peerStore[DirectionBook][peerId] = direction diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 92528c7b9..506a3e592 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -1,7 +1,7 @@ {.push raises: [].} import - std/[options, tables, strutils, sequtils, os, net, random], + std/[options, tables, strutils, sequtils, os, net, random, sets], chronos, chronicles, metrics, @@ -38,7 +38,6 @@ import waku_store/resume, waku_store_sync, waku_filter_v2, - waku_filter_v2/common as filter_common, waku_filter_v2/client as filter_client, waku_metadata, waku_rendezvous/protocol, @@ -60,7 +59,7 @@ import requests/node_requests, requests/health_requests, events/health_events, - events/peer_events, + events/message_events, ], waku/discovery/waku_kademlia, ./net_config, @@ -95,9 +94,6 @@ const clientId* = "Nimbus Waku v2 node" const WakuNodeVersionString* = "version / git commit hash: " & git_version -const EdgeTopicHealthyThreshold = 2 - ## Lightpush server and filter server requirement for a healthy topic in edge mode - # key and crypto modules different type # TODO: Move to application instance (e.g., `WakuNode2`) @@ -142,10 +138,6 @@ type legacyAppHandlers*: Table[PubsubTopic, WakuRelayHandler] ## Kernel API Relay appHandlers (if any) wakuMix*: WakuMix - edgeTopicsHealth*: Table[PubsubTopic, TopicHealth] - edgeHealthEvent*: AsyncEvent - edgeHealthLoop: Future[void] - peerEventListener*: EventWakuPeerListener kademliaDiscoveryLoop*: Future[void] wakuKademlia*: WakuKademlia @@ -498,52 +490,7 @@ proc updateAnnouncedAddrWithPrimaryIpAddr*(node: WakuNode): Result[void, string] return ok() -proc calculateEdgeTopicHealth(node: WakuNode, shard: PubsubTopic): TopicHealth = - let filterPeers = - node.peerManager.getPeersForShard(filter_common.WakuFilterSubscribeCodec, shard) - let lightpushPeers = - node.peerManager.getPeersForShard(lightpush_protocol.WakuLightPushCodec, shard) - - if filterPeers >= EdgeTopicHealthyThreshold and - lightpushPeers >= EdgeTopicHealthyThreshold: - return TopicHealth.SUFFICIENTLY_HEALTHY - elif filterPeers > 0 and lightpushPeers > 0: - return TopicHealth.MINIMALLY_HEALTHY - - return TopicHealth.UNHEALTHY - -proc loopEdgeHealth(node: WakuNode) {.async.} = - while node.started: - await node.edgeHealthEvent.wait() - node.edgeHealthEvent.clear() - - try: - for shard in node.edgeTopicsHealth.keys: - if not node.wakuRelay.isNil and node.wakuRelay.isSubscribed(shard): - continue - - let oldHealth = node.edgeTopicsHealth.getOrDefault(shard, TopicHealth.UNHEALTHY) - let newHealth = node.calculateEdgeTopicHealth(shard) - if newHealth != oldHealth: - node.edgeTopicsHealth[shard] = newHealth - EventShardTopicHealthChange.emit(node.brokerCtx, shard, newHealth) - except CancelledError: - break - except CatchableError as e: - warn "Error in edge health check", error = e.msg - - # safety cooldown to protect from edge cases - await sleepAsync(100.milliseconds) - proc startProvidersAndListeners*(node: WakuNode) = - node.peerEventListener = EventWakuPeer.listen( - node.brokerCtx, - proc(evt: EventWakuPeer) {.async: (raises: []), gcsafe.} = - node.edgeHealthEvent.fire(), - ).valueOr: - error "Failed to listen to peer events", error = error - return - RequestRelayShard.setProvider( node.brokerCtx, proc( @@ -561,14 +508,23 @@ proc startProvidersAndListeners*(node: WakuNode) = var response: RequestShardTopicsHealth for shard in topics: - var healthStatus = TopicHealth.UNHEALTHY + # Health resolution order: + # 1. Relay topicsHealth (computed from gossipsub mesh state) + # 2. If relay is active but topicsHealth hasn't computed yet, UNHEALTHY + # 3. Otherwise, ask edge filter (via broker; no-op if no provider set) + var healthStatus = TopicHealth.NOT_SUBSCRIBED if not node.wakuRelay.isNil: healthStatus = node.wakuRelay.topicsHealth.getOrDefault(shard, TopicHealth.NOT_SUBSCRIBED) if healthStatus == TopicHealth.NOT_SUBSCRIBED: - healthStatus = node.calculateEdgeTopicHealth(shard) + if not node.wakuRelay.isNil and node.wakuRelay.isSubscribed(shard): + healthStatus = TopicHealth.UNHEALTHY + else: + let edgeRes = RequestEdgeShardHealth.request(node.brokerCtx, shard) + if edgeRes.isOk(): + healthStatus = edgeRes.get().health response.topicHealth.add((shard, healthStatus)) @@ -594,9 +550,10 @@ proc startProvidersAndListeners*(node: WakuNode) = pubsubTopic, TopicHealth.NOT_SUBSCRIBED ) - if topicHealth == TopicHealth.NOT_SUBSCRIBED and - pubsubTopic in node.edgeTopicsHealth: - topicHealth = node.calculateEdgeTopicHealth(pubsubTopic) + if topicHealth == TopicHealth.NOT_SUBSCRIBED: + let edgeRes = RequestEdgeShardHealth.request(node.brokerCtx, pubsubTopic) + if edgeRes.isOk(): + topicHealth = edgeRes.get().health response.contentTopicHealth.add((topic: contentTopic, health: topicHealth)) @@ -605,7 +562,6 @@ proc startProvidersAndListeners*(node: WakuNode) = error "Can't set provider for RequestContentTopicsHealth", error = error proc stopProvidersAndListeners*(node: WakuNode) = - EventWakuPeer.dropListener(node.brokerCtx, node.peerEventListener) RequestRelayShard.clearProvider(node.brokerCtx) RequestContentTopicsHealth.clearProvider(node.brokerCtx) RequestShardTopicsHealth.clearProvider(node.brokerCtx) @@ -658,13 +614,16 @@ proc start*(node: WakuNode) {.async.} = ## The switch will update addresses after start using the addressMapper await node.switch.start() - node.edgeHealthEvent = newAsyncEvent() - node.edgeHealthLoop = loopEdgeHealth(node) + node.started = true + + if not node.wakuFilterClient.isNil(): + node.wakuFilterClient.registerPushHandler( + proc(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + MessageSeenEvent.emit(node.brokerCtx, pubsubTopic, msg) + ) node.startProvidersAndListeners() - node.started = true - if not zeroPortPresent: updateAnnouncedAddrWithPrimaryIpAddr(node).isOkOr: error "failed update announced addr", error = $error @@ -678,10 +637,6 @@ proc stop*(node: WakuNode) {.async.} = node.stopProvidersAndListeners() - if not node.edgeHealthLoop.isNil: - await node.edgeHealthLoop.cancelAndWait() - node.edgeHealthLoop = nil - await node.switch.stop() node.peerManager.stop() diff --git a/waku/requests/health_requests.nim b/waku/requests/health_requests.nim index 3554922b3..c3a0ce286 100644 --- a/waku/requests/health_requests.nim +++ b/waku/requests/health_requests.nim @@ -37,3 +37,15 @@ RequestBroker: healthStatus*: ProtocolHealth proc signature(protocol: WakuProtocol): Future[Result[RequestProtocolHealth, string]] + +# Get edge filter health for a single shard (set by DeliveryService when edge mode is active) +RequestBroker(sync): + type RequestEdgeShardHealth* = object + health*: TopicHealth + + proc signature(shard: PubsubTopic): Result[RequestEdgeShardHealth, string] + +# Get edge filter confirmed peer count (set by DeliveryService when edge mode is active) +RequestBroker(sync): + type RequestEdgeFilterPeerCount* = object + peerCount*: int diff --git a/waku/waku_core/subscription.nim b/waku/waku_core/subscription.nim index 19f3386ef..8694efa1b 100644 --- a/waku/waku_core/subscription.nim +++ b/waku/waku_core/subscription.nim @@ -1,3 +1,3 @@ -import ./subscription/subscription_manager, ./subscription/push_handler +import ./subscription/push_handler -export subscription_manager, push_handler +export push_handler diff --git a/waku/waku_core/subscription/subscription_manager.nim b/waku/waku_core/subscription/subscription_manager.nim deleted file mode 100644 index ccade763b..000000000 --- a/waku/waku_core/subscription/subscription_manager.nim +++ /dev/null @@ -1,52 +0,0 @@ -{.push raises: [].} - -import std/tables, results, chronicles, chronos - -import ./push_handler, ../topics, ../message - -## Subscription manager -type LegacySubscriptionManager* = object - subscriptions: TableRef[(string, ContentTopic), FilterPushHandler] - -proc init*(T: type LegacySubscriptionManager): T = - LegacySubscriptionManager( - subscriptions: newTable[(string, ContentTopic), FilterPushHandler]() - ) - -proc clear*(m: var LegacySubscriptionManager) = - m.subscriptions.clear() - -proc registerSubscription*( - m: LegacySubscriptionManager, - pubsubTopic: PubsubTopic, - contentTopic: ContentTopic, - handler: FilterPushHandler, -) = - try: - # TODO: Handle over subscription surprises - m.subscriptions[(pubsubTopic, contentTopic)] = handler - except CatchableError: - error "failed to register filter subscription", error = getCurrentExceptionMsg() - -proc removeSubscription*( - m: LegacySubscriptionManager, pubsubTopic: PubsubTopic, contentTopic: ContentTopic -) = - m.subscriptions.del((pubsubTopic, contentTopic)) - -proc notifySubscriptionHandler*( - m: LegacySubscriptionManager, - pubsubTopic: PubsubTopic, - contentTopic: ContentTopic, - message: WakuMessage, -) = - if not m.subscriptions.hasKey((pubsubTopic, contentTopic)): - return - - try: - let handler = m.subscriptions[(pubsubTopic, contentTopic)] - asyncSpawn handler(pubsubTopic, message) - except CatchableError: - discard - -proc getSubscriptionsCount*(m: LegacySubscriptionManager): int = - m.subscriptions.len() diff --git a/waku/waku_filter_v2/client.nim b/waku/waku_filter_v2/client.nim index ba8cd3d0c..265bf5e7b 100644 --- a/waku/waku_filter_v2/client.nim +++ b/waku/waku_filter_v2/client.nim @@ -101,12 +101,20 @@ proc sendSubscribeRequest( return ok() proc ping*( - wfc: WakuFilterClient, servicePeer: RemotePeerInfo + wfc: WakuFilterClient, servicePeer: RemotePeerInfo, timeout = chronos.seconds(0) ): Future[FilterSubscribeResult] {.async.} = info "sending ping", servicePeer = shortLog($servicePeer) let requestId = generateRequestId(wfc.rng) let filterSubscribeRequest = FilterSubscribeRequest.ping(requestId) + if timeout > chronos.seconds(0): + let fut = wfc.sendSubscribeRequest(servicePeer, filterSubscribeRequest) + if not await fut.withTimeout(timeout): + return err( + FilterSubscribeError.parse(uint32(FilterSubscribeErrorKind.PEER_DIAL_FAILURE)) + ) + return fut.read() + return await wfc.sendSubscribeRequest(servicePeer, filterSubscribeRequest) proc subscribe*( diff --git a/waku/waku_relay/protocol.nim b/waku/waku_relay/protocol.nim index 17470af29..490feae87 100644 --- a/waku/waku_relay/protocol.nim +++ b/waku/waku_relay/protocol.nim @@ -157,7 +157,7 @@ type ): Future[ValidationResult] {.gcsafe, raises: [Defect].} WakuRelay* = ref object of GossipSub brokerCtx: BrokerContext - peerEventListener: EventWakuPeerListener + peerEventListener: WakuPeerEventListener # seq of tuples: the first entry in the tuple contains the validators are called for every topic # the second entry contains the error messages to be returned when the validator fails wakuValidators: seq[tuple[handler: WakuValidatorHandler, errorMessage: string]] @@ -376,9 +376,9 @@ proc new*( w.initProtocolHandler() w.initRelayObservers() - w.peerEventListener = EventWakuPeer.listen( + w.peerEventListener = WakuPeerEvent.listen( w.brokerCtx, - proc(evt: EventWakuPeer): Future[void] {.async: (raises: []), gcsafe.} = + proc(evt: WakuPeerEvent): Future[void] {.async: (raises: []), gcsafe.} = if evt.kind == WakuPeerEventKind.EventDisconnected: w.topicHealthCheckAll = true w.topicHealthUpdateEvent.fire() @@ -524,8 +524,7 @@ method stop*(w: WakuRelay) {.async, base.} = info "stop" await procCall GossipSub(w).stop() - if w.peerEventListener.id != 0: - EventWakuPeer.dropListener(w.brokerCtx, w.peerEventListener) + WakuPeerEvent.dropListener(w.brokerCtx, w.peerEventListener) if not w.topicHealthLoopHandle.isNil(): await w.topicHealthLoopHandle.cancelAndWait() From b0c0e0b63746d0f345ed741900aca5a3b2abd721 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Thu, 2 Apr 2026 07:10:02 -0300 Subject: [PATCH 110/155] chore: optimize release builds for speed (#3735) (#3777) * Add -flto (lto_incremental, link-time optimization) for release builds * Add -s (strip symbols) for release builds * Switch library builds from --opt:size to --opt:speed * Change -d:marchOptimized to x86-64-v2 target from broadwell * Remove obsolete chronicles_colors=off for Windows * Remove obsolete withoutPCRE define --- Makefile | 2 +- config.nims | 14 +++++--------- waku.nimble | 8 ++++---- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 8f98e90bd..afd2389d2 100644 --- a/Makefile +++ b/Makefile @@ -144,7 +144,7 @@ deps: | deps-common nat-libs waku.nims # "-d:release" implies "--stacktrace:off" and it cannot be added to config.nims ifeq ($(DEBUG), 0) -NIM_PARAMS := $(NIM_PARAMS) -d:release +NIM_PARAMS := $(NIM_PARAMS) -d:release -d:lto_incremental -d:strip else NIM_PARAMS := $(NIM_PARAMS) -d:debug endif diff --git a/config.nims b/config.nims index f74fe183f..0655bf092 100644 --- a/config.nims +++ b/config.nims @@ -26,10 +26,6 @@ if defined(windows): # set the IMAGE_FILE_LARGE_ADDRESS_AWARE flag so we can use PAE, if enabled, and access more than 2 GiB of RAM switch("passL", "-Wl,--large-address-aware") - # The dynamic Chronicles output currently prevents us from using colors on Windows - # because these require direct manipulations of the stdout File object. - switch("define", "chronicles_colors=off") - # https://github.com/status-im/nimbus-eth2/blob/stable/docs/cpu_features.md#ssse3-supplemental-sse3 # suggests that SHA256 hashing with SSSE3 is 20% faster than without SSSE3, so # given its near-ubiquity in the x86 installed base, it renders a distribution @@ -52,9 +48,10 @@ if defined(disableMarchNative): switch("passL", "-march=haswell -mtune=generic") else: if defined(marchOptimized): - # https://github.com/status-im/nimbus-eth2/blob/stable/docs/cpu_features.md#bmi2--adx - switch("passC", "-march=broadwell -mtune=generic") - switch("passL", "-march=broadwell -mtune=generic") + # -march=broadwell: https://github.com/status-im/nimbus-eth2/blob/stable/docs/cpu_features.md#bmi2--adx + # Changed to x86-64-v2 for broader support + switch("passC", "-march=x86-64-v2 -mtune=generic") + switch("passL", "-march=x86-64-v2 -mtune=generic") else: switch("passC", "-mssse3") switch("passL", "-mssse3") @@ -76,6 +73,7 @@ else: on --opt: speed + --excessiveStackTrace: on # enable metric collection @@ -85,8 +83,6 @@ else: --define: nimTypeNames -switch("define", "withoutPCRE") - # the default open files limit is too low on macOS (512), breaking the # "--debugger:native" build. It can be increased with `ulimit -n 1024`. if not defined(macosx) and not defined(android): diff --git a/waku.nimble b/waku.nimble index d879bc0e1..cbbe6aa97 100644 --- a/waku.nimble +++ b/waku.nimble @@ -73,11 +73,11 @@ proc buildLibrary(lib_name: string, srcDir = "./", params = "", `type` = "static extra_params &= " " & paramStr(i) if `type` == "static": exec "nim c" & " --out:build/" & lib_name & - " --threads:on --app:staticlib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:on -d:discv5_protocol_id=d5waku " & + " --threads:on --app:staticlib --opt:speed --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:on -d:discv5_protocol_id=d5waku " & extra_params & " " & srcDir & srcFile else: exec "nim c" & " --out:build/" & lib_name & - " --threads:on --app:lib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:off -d:discv5_protocol_id=d5waku " & + " --threads:on --app:lib --opt:speed --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:off -d:discv5_protocol_id=d5waku " & extra_params & " " & srcDir & srcFile proc buildMobileAndroid(srcDir = ".", params = "") = @@ -93,7 +93,7 @@ proc buildMobileAndroid(srcDir = ".", params = "") = extra_params &= " " & paramStr(i) exec "nim c" & " --out:" & outDir & - "/libwaku.so --threads:on --app:lib --opt:size --noMain --mm:refc -d:chronicles_sinks=textlines[dynamic] --header -d:chronosEventEngine=epoll --passL:-L" & + "/libwaku.so --threads:on --app:lib --opt:speed --noMain --mm:refc -d:chronicles_sinks=textlines[dynamic] --header -d:chronosEventEngine=epoll --passL:-L" & outdir & " --passL:-lrln --passL:-llog --cpu:" & cpu & " --os:android -d:androidNDK " & extra_params & " " & srcDir & "/libwaku.nim" @@ -266,7 +266,7 @@ proc buildMobileIOS(srcDir = ".", params = "") = " --os:ios --cpu:" & cpu & " --compileOnly:on" & " --noMain --mm:refc" & - " --threads:on --opt:size --header" & + " --threads:on --opt:speed --header" & " -d:metrics -d:discv5_protocol_id=d5waku" & " --nimMainPrefix:libwaku --skipParentCfg:on" & " --cc:clang" & From 39719e124770d62346ad79cdc1aaad165462e445 Mon Sep 17 00:00:00 2001 From: Darshan <35736874+darshankabariya@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:53:45 +0530 Subject: [PATCH 111/155] increase default timeout to 20s and add debug logging (#3792) --- apps/wakucanary/wakucanary.nim | 40 +++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/wakucanary/wakucanary.nim b/apps/wakucanary/wakucanary.nim index bb68f7237..e7b1ff9aa 100644 --- a/apps/wakucanary/wakucanary.nim +++ b/apps/wakucanary/wakucanary.nim @@ -6,12 +6,15 @@ import os import libp2p/protocols/ping, + libp2p/protocols/protocol, libp2p/crypto/[crypto, secp], libp2p/nameresolving/dnsresolver, libp2p/multicodec import ./certsgenerator, - waku/[waku_enr, node/peer_manager, waku_core, waku_node, factory/builder] + waku/[waku_enr, node/peer_manager, waku_core, waku_node, factory/builder], + waku/waku_metadata/protocol, + waku/common/callbacks # protocols and their tag const ProtocolsTable = { @@ -45,7 +48,7 @@ type WakuCanaryConf* = object timeout* {. desc: "Timeout to consider that the connection failed", - defaultValue: chronos.seconds(10), + defaultValue: chronos.seconds(20), name: "timeout", abbr: "t" .}: chronos.Duration @@ -251,12 +254,26 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = error "failed to mount libp2p ping protocol: " & getCurrentExceptionMsg() quit(QuitFailure) - node.mountMetadata(conf.clusterId, conf.shards).isOkOr: - error "failed to mount metadata protocol", error + # Mount metadata with a custom getter that returns CLI shards directly, + # since the canary doesn't mount relay (which is what the default getter reads from). + # Without this fix, the canary always sends remoteShards=[] in metadata requests. + let cliShards = conf.shards + let shardsGetter: GetShards = proc(): seq[uint16] {.closure, gcsafe, raises: [].} = + return cliShards + + let metadata = WakuMetadata.new(conf.clusterId, shardsGetter) + node.wakuMetadata = metadata + node.peerManager.wakuMetadata = metadata + let mountRes = catch: + node.switch.mount(metadata, protocolMatcher(WakuMetadataCodec)) + mountRes.isOkOr: + error "failed to mount metadata protocol", error = error.msg quit(QuitFailure) await node.start() + debug "Connecting to peer", peer = peer, timeout = conf.timeout + var pingFut: Future[bool] if conf.ping: pingFut = pingNode(node, peer).withTimeout(conf.timeout) @@ -266,8 +283,18 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = error "Timedout after", timeout = conf.timeout quit(QuitFailure) + # Clean disconnect with defer so the remote node doesn't see + # "Stream Underlying Connection Closed!" when we exit + defer: + debug "Cleanly disconnecting from peer", peerId = peer.peerId + await node.peerManager.disconnectNode(peer.peerId) + await node.stop() + + debug "Connected, checking connection status", peerId = peer.peerId + let lp2pPeerStore = node.switch.peerStore let conStatus = node.peerManager.switch.peerStore[ConnectionBook][peer.peerId] + debug "Connection status", peerId = peer.peerId, conStatus = conStatus var pingSuccess = true if conf.ping: @@ -283,14 +310,15 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = if conStatus in [Connected, CanConnect]: let nodeProtocols = lp2pPeerStore[ProtoBook][peer.peerId] + debug "Peer protocols", peerId = peer.peerId, protocols = nodeProtocols if not areProtocolsSupported(conf.protocols, nodeProtocols): error "Not all protocols are supported", expected = conf.protocols, supported = nodeProtocols - quit(QuitFailure) + return 1 elif conStatus == CannotConnect: error "Could not connect", peerId = peer.peerId - quit(QuitFailure) + return 1 return 0 when isMainModule: From 56359e49ed60cb78c03f5e9c1519e43fe90deaab Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 6 Apr 2026 11:08:47 -0300 Subject: [PATCH 112/155] prefer reusing service peers across shards in edge filter reconciliation (#3789) * selectFilterCandidates prefers peers already serving other shards * restructure edgeFilterSubLoop (plan all dials then execute) for safety --- .../delivery_service/subscription_manager.nim | 78 +++++++++++++++---- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/waku/node/delivery_service/subscription_manager.nim b/waku/node/delivery_service/subscription_manager.nim index 70d8df7f0..f00d9024c 100644 --- a/waku/node/delivery_service/subscription_manager.nim +++ b/waku/node/delivery_service/subscription_manager.nim @@ -115,7 +115,7 @@ proc subscribePubsubTopics( if isNil(self.node.wakuRelay): return err("subscribePubsubTopics requires a Relay") - var errors: seq[string] = @[] + var errors: seq[string] for shard in shards: if not self.contentTopicSubs.hasKey(shard): @@ -196,6 +196,11 @@ const EdgeFilterLoopInterval = chronos.seconds(30) const EdgeFilterSubLoopDebounce = chronos.seconds(1) ## Debounce delay to coalesce rapid-fire wakeups into a single reconciliation pass. +type EdgeDialTask = object + peer: RemotePeerInfo + shard: PubsubTopic + topics: seq[ContentTopic] + proc updateShardHealth( self: SubscriptionManager, shard: PubsubTopic, state: var EdgeFilterSubState ) = @@ -335,7 +340,7 @@ proc edgeFilterHealthLoop*(self: SubscriptionManager) {.async.} = var alive = initHashSet[PeerId]() if connected.len > 0: - var pingTasks: seq[(PeerId, Future[FilterSubscribeResult])] = @[] + var pingTasks: seq[(PeerId, Future[FilterSubscribeResult])] for peer in connected.values: pingTasks.add( (peer.peerId, self.node.wakuFilterClient.ping(peer, EdgeFilterPingTimeout)) @@ -362,6 +367,36 @@ proc edgeFilterHealthLoop*(self: SubscriptionManager) {.async.} = if changed: self.edgeFilterWakeup.fire() +proc selectFilterCandidates( + self: SubscriptionManager, shard: PubsubTopic, exclude: HashSet[PeerId], needed: int +): seq[RemotePeerInfo] = + ## Select filter service peer candidates for a shard. + + # Start with every filter server peer that can serve the shard + var allCandidates = self.node.peerManager.selectPeers( + filter_common.WakuFilterSubscribeCodec, some(shard) + ) + + # Remove all already used in this shard or being dialed for it + allCandidates.keepItIf(it.peerId notin exclude) + + # Collect peer IDs already tracked on other shards + var trackedOnOther = initHashSet[PeerId]() + for otherShard, otherState in self.edgeFilterSubStates.pairs: + if otherShard != shard: + for peer in otherState.peers: + trackedOnOther.incl(peer.peerId) + + # Prefer peers we already have a connection to first, preserving shuffle + var candidates = + allCandidates.filterIt(it.peerId in trackedOnOther) & + allCandidates.filterIt(it.peerId notin trackedOnOther) + + # We need to return 'needed' peers only + if candidates.len > needed: + candidates.setLen(needed) + return candidates + proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = ## Reconciles filter subscriptions with the desired state from SubscriptionManager. var lastSynced = initTable[PubsubTopic, HashSet[ContentTopic]]() @@ -382,6 +417,12 @@ proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = let allShards = toHashSet(toSeq(desired.keys)) + toHashSet(toSeq(lastSynced.keys)) + # Step 1: read state across all shards at once and + # create a list of peer dial tasks and shard tracking to delete. + + var dialTasks: seq[EdgeDialTask] + var shardsToDelete: seq[PubsubTopic] + for shard in allShards: let currTopics = desired.getOrDefault(shard) let prevTopics = lastSynced.getOrDefault(shard) @@ -404,11 +445,7 @@ proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = asyncSpawn self.syncFilterDeltas(peer, shard, addedTopics, removedTopics) if currTopics.len == 0: - for fut in state.pending: - if not fut.finished: - await fut.cancelAndWait() - self.edgeFilterSubStates.del(shard) - # invalidates `state` — do not use after this + shardsToDelete.add(shard) else: self.updateShardHealth(shard, state[]) @@ -416,11 +453,7 @@ proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = if needed > 0: let tracked = state.peers.mapIt(it.peerId).toHashSet() + state.pendingPeers - var candidates = self.node.peerManager.selectPeers( - filter_common.WakuFilterSubscribeCodec, some(shard) - ) - candidates.keepItIf(it.peerId notin tracked) - + let candidates = self.selectFilterCandidates(shard, tracked, needed) let toDial = min(needed, candidates.len) trace "edgeFilterSubLoop: shard reconciliation", @@ -432,8 +465,25 @@ proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = toDial = toDial for i in 0 ..< toDial: - let fut = self.dialFilterPeer(candidates[i], shard, toSeq(currTopics)) - state.pending.add(fut) + dialTasks.add( + EdgeDialTask( + peer: candidates[i], shard: shard, topics: toSeq(currTopics) + ) + ) + + # Step 2: execute deferred shard tracking deletion and dial tasks. + + for shard in shardsToDelete: + self.edgeFilterSubStates.withValue(shard, state): + for fut in state.pending: + if not fut.finished: + await fut.cancelAndWait() + self.edgeFilterSubStates.del(shard) + + for task in dialTasks: + let fut = self.dialFilterPeer(task.peer, task.shard, task.topics) + self.edgeFilterSubStates.withValue(task.shard, state): + state.pending.add(fut) lastSynced = desired From 549bf8bc4309aadb30530a181fa1933bced2c9a7 Mon Sep 17 00:00:00 2001 From: Danish Arora <35004822+danisharora099@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:14:32 +0530 Subject: [PATCH 113/155] fix(nix): fetch git submodules automatically via inputs.self (#3738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Nix build fails when consumers use `nix build github:logos-messaging/logos-delivery#liblogosdelivery` without appending `?submodules=1` — vendor/nimbus-build-system is missing, causing patchShebangs and substituteInPlace to fail. Two fixes: 1. Add `inputs.self.submodules = true` to flake.nix (Nix >= 2.27) so submodules are fetched automatically without requiring callers to pass `?submodules=1`. 2. Fix the assertion in nix/default.nix: `(src.submodules or true)` always evaluates to true, silently masking the missing-submodules error. Changed to `builtins.pathExists` check on the actual submodule directory so it fails with a helpful message when submodules are genuinely absent. --- flake.nix | 4 ++++ nix/default.nix | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index ee24c8f13..13ca5e618 100644 --- a/flake.nix +++ b/flake.nix @@ -7,6 +7,10 @@ }; inputs = { + # Ensure Nix fetches git submodules (vendor/*) when evaluating this flake. + # Requires Nix >= 2.27. Consumers no longer need '?submodules=1' in the URL. + self.submodules = true; + # We are pinning the commit because ultimately we want to use same commit across different projects. # A commit from nixpkgs 24.11 release : https://github.com/NixOS/nixpkgs/tree/release-24.11 nixpkgs.url = "github:NixOS/nixpkgs/0ef228213045d2cdb5a169a95d63ded38670b293"; diff --git a/nix/default.nix b/nix/default.nix index 7df58df60..816d0aed8 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -12,7 +12,7 @@ zerokitRln, }: -assert pkgs.lib.assertMsg ((src.submodules or true) == true) +assert pkgs.lib.assertMsg (builtins.pathExists "${src}/vendor/nimbus-build-system/scripts") "Unable to build without submodules. Append '?submodules=1#' to the URI."; let From 9a344553e765775bb9859b58d0f5e284c51e9a98 Mon Sep 17 00:00:00 2001 From: Darshan <35736874+darshankabariya@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:30:57 +0530 Subject: [PATCH 114/155] chore: update master changelog after v0.37.4 (#3802) --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be2b795f6..ac15321c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## v0.37.4 (2026-04-03) + +### Changes + +- Optimize release builds for speed ([#3735](https://github.com/logos-messaging/logos-delivery/pull/3735)) ([#3777](https://github.com/logos-messaging/logos-delivery/pull/3777)) + +### Bug Fixes + +- Properly add DEBUG flag into Dockerfile + ## v0.37.3 (2026-03-25) ### Features From f5762af4c4834972d98cdda961b796c7e30613bf Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:42:14 +0200 Subject: [PATCH 115/155] Start using nimble and deprecate vendor dependencies (#3798) Co-authored-by: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Co-authored-by: Darshan K <35736874+darshankabariya@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bump_dependencies.md | 46 +- .github/ISSUE_TEMPLATE/prepare_release.md | 1 + .github/workflows/ci-daily.yml | 2 +- .github/workflows/ci-nix.yml | 15 +- .github/workflows/ci.yml | 109 ++-- .github/workflows/container-image.yml | 36 +- .github/workflows/pre-release.yml | 4 +- .github/workflows/windows-build.yml | 70 ++- .gitignore | 6 - .gitmodules | 187 ------ BearSSL.mk | 39 ++ Dockerfile | 3 +- Makefile | 446 +++++++-------- Nat.mk | 54 ++ apps/wakunode2/wakunode2.nim | 3 +- config.nims | 11 +- env.sh | 8 - flake.lock | 57 +- flake.nix | 112 ++-- nimble.lock | 551 ++++++++++++++++++ nix/default.nix | 212 +++---- nix/deps.nix | 272 +++++++++ nix/nimble.nix | 12 - nix/shell.nix | 3 +- tests/tools/test_confutils_envvar.nim | 10 +- tests/waku_core/test_peers.nim | 1 + tools/confutils/cli_args.nim | 13 +- tools/gen-nix-deps.sh | 80 +++ vendor/db_connector | 1 - vendor/dnsclient.nim | 1 - vendor/nim-bearssl | 1 - vendor/nim-chronicles | 1 - vendor/nim-chronos | 1 - vendor/nim-confutils | 1 - vendor/nim-dnsdisc | 1 - vendor/nim-eth | 1 - vendor/nim-faststreams | 1 - vendor/nim-ffi | 1 - vendor/nim-http-utils | 1 - vendor/nim-json-rpc | 1 - vendor/nim-json-serialization | 1 - vendor/nim-jwt | 1 - vendor/nim-libbacktrace | 1 - vendor/nim-libp2p | 1 - vendor/nim-lsquic | 1 - vendor/nim-metrics | 1 - vendor/nim-minilru | 1 - vendor/nim-nat-traversal | 1 - vendor/nim-presto | 1 - vendor/nim-regex | 1 - vendor/nim-results | 1 - vendor/nim-secp256k1 | 1 - vendor/nim-serialization | 1 - vendor/nim-sqlite3-abi | 1 - vendor/nim-stew | 1 - vendor/nim-stint | 1 - vendor/nim-taskpools | 1 - vendor/nim-testutils | 1 - vendor/nim-toml-serialization | 1 - vendor/nim-unicodedb | 1 - vendor/nim-unittest2 | 1 - vendor/nim-web3 | 1 - vendor/nim-websock | 1 - vendor/nim-zlib | 1 - vendor/nimbus-build-system | 1 - vendor/nimcrypto | 1 - vendor/nph | 1 - waku.nimble | 602 ++++++++++++-------- waku/api/api_conf.nim | 12 +- 69 files changed, 1916 insertions(+), 1100 deletions(-) create mode 100644 BearSSL.mk create mode 100644 Nat.mk delete mode 100755 env.sh create mode 100644 nimble.lock create mode 100644 nix/deps.nix delete mode 100644 nix/nimble.nix create mode 100755 tools/gen-nix-deps.sh delete mode 160000 vendor/db_connector delete mode 160000 vendor/dnsclient.nim delete mode 160000 vendor/nim-bearssl delete mode 160000 vendor/nim-chronicles delete mode 160000 vendor/nim-chronos delete mode 160000 vendor/nim-confutils delete mode 160000 vendor/nim-dnsdisc delete mode 160000 vendor/nim-eth delete mode 160000 vendor/nim-faststreams delete mode 160000 vendor/nim-ffi delete mode 160000 vendor/nim-http-utils delete mode 160000 vendor/nim-json-rpc delete mode 160000 vendor/nim-json-serialization delete mode 160000 vendor/nim-jwt delete mode 160000 vendor/nim-libbacktrace delete mode 160000 vendor/nim-libp2p delete mode 160000 vendor/nim-lsquic delete mode 160000 vendor/nim-metrics delete mode 160000 vendor/nim-minilru delete mode 160000 vendor/nim-nat-traversal delete mode 160000 vendor/nim-presto delete mode 160000 vendor/nim-regex delete mode 160000 vendor/nim-results delete mode 160000 vendor/nim-secp256k1 delete mode 160000 vendor/nim-serialization delete mode 160000 vendor/nim-sqlite3-abi delete mode 160000 vendor/nim-stew delete mode 160000 vendor/nim-stint delete mode 160000 vendor/nim-taskpools delete mode 160000 vendor/nim-testutils delete mode 160000 vendor/nim-toml-serialization delete mode 160000 vendor/nim-unicodedb delete mode 160000 vendor/nim-unittest2 delete mode 160000 vendor/nim-web3 delete mode 160000 vendor/nim-websock delete mode 160000 vendor/nim-zlib delete mode 160000 vendor/nimbus-build-system delete mode 160000 vendor/nimcrypto delete mode 160000 vendor/nph diff --git a/.github/ISSUE_TEMPLATE/bump_dependencies.md b/.github/ISSUE_TEMPLATE/bump_dependencies.md index 0413cbfd2..59f46f08b 100644 --- a/.github/ISSUE_TEMPLATE/bump_dependencies.md +++ b/.github/ISSUE_TEMPLATE/bump_dependencies.md @@ -1,7 +1,7 @@ --- name: Bump dependencies -about: Bump vendor dependencies for release -title: 'Bump vendor dependencies for release 0.0.0' +about: Bump dependencies for release +title: 'Bump dependencies for release 0.X.0' labels: dependencies assignees: '' @@ -9,40 +9,10 @@ assignees: '' -Update `nwaku` "vendor" dependencies. +### Bumped items +- [ ] Update nimble dependencies + 1. Edit manually waku.nimble. For some dependencies, we want to bump versions manually and use a pinned version, f.e., nim-libp2p and all its dependencies. + 2. Run `nimble lock` (make sure `nimble --version` shows the Nimble version pinned in waku.nimble) + 3. Run `./tools/gen-nix-deps.sh nimble.lock nix/deps.nix` to update nix deps -### Items to bump -- [ ] dnsclient.nim ( update to the latest tag version ) -- [ ] nim-bearssl -- [ ] nimbus-build-system -- [ ] nim-chronicles -- [ ] nim-chronos -- [ ] nim-confutils -- [ ] nimcrypto -- [ ] nim-dnsdisc -- [ ] nim-eth -- [ ] nim-faststreams -- [ ] nim-http-utils -- [ ] nim-json-rpc -- [ ] nim-json-serialization -- [ ] nim-libbacktrace -- [ ] nim-libp2p ( update to the latest tag version ) -- [ ] nim-metrics -- [ ] nim-nat-traversal -- [ ] nim-presto -- [ ] nim-regex ( update to the latest tag version ) -- [ ] nim-results -- [ ] nim-secp256k1 -- [ ] nim-serialization -- [ ] nim-sqlite3-abi ( update to the latest tag version ) -- [ ] nim-stew -- [ ] nim-stint -- [ ] nim-taskpools ( update to the latest tag version ) -- [ ] nim-testutils ( update to the latest tag version ) -- [ ] nim-toml-serialization -- [ ] nim-unicodedb -- [ ] nim-unittest2 ( update to the latest tag version ) -- [ ] nim-web3 ( update to the latest tag version ) -- [ ] nim-websock ( update to the latest tag version ) -- [ ] nim-zlib -- [ ] zerokit ( this should be kept in version `v0.7.0` ) +- [ ] Update vendor/zerokit dependency. diff --git a/.github/ISSUE_TEMPLATE/prepare_release.md b/.github/ISSUE_TEMPLATE/prepare_release.md index 83456e79a..de67b3eaf 100644 --- a/.github/ISSUE_TEMPLATE/prepare_release.md +++ b/.github/ISSUE_TEMPLATE/prepare_release.md @@ -18,6 +18,7 @@ For detailed info on the release process refer to https://github.com/logos-messa All items below are to be completed by the owner of the given release. - [ ] Create release branch with major and minor only ( e.g. release/v0.X ) if it doesn't exist. +- [ ] Update the `version` field in `waku.nimble` to match the release version (e.g. `version = "0.X.0"`). - [ ] Assign release candidate tag to the release branch HEAD (e.g. `v0.X.0-rc.0`, `v0.X.0-rc.1`, ... `v0.X.0-rc.N`). - [ ] Generate and edit release notes in CHANGELOG.md. diff --git a/.github/workflows/ci-daily.yml b/.github/workflows/ci-daily.yml index b442014a6..a4cf39340 100644 --- a/.github/workflows/ci-daily.yml +++ b/.github/workflows/ci-daily.yml @@ -40,7 +40,7 @@ jobs: run: make update - name: Build binaries - run: make V=1 QUICK_AND_DIRTY_COMPILER=1 examples tools + run: make V=1 examples tools - name: Notify Discord if: always() diff --git a/.github/workflows/ci-nix.yml b/.github/workflows/ci-nix.yml index 8fc7ac985..7319f64aa 100644 --- a/.github/workflows/ci-nix.yml +++ b/.github/workflows/ci-nix.yml @@ -16,14 +16,7 @@ jobs: - aarch64-darwin - x86_64-linux nixpkg: - - libwaku - - libwaku-android-arm64 - - wakucanary - - exclude: - # Android SDK limitation - - system: aarch64-darwin - nixpkg: libwaku-android-arm64 + - liblogosdelivery include: - system: aarch64-darwin @@ -36,12 +29,10 @@ jobs: runs-on: ${{ matrix.runs_on }} steps: - uses: actions/checkout@v4 - with: - submodules: recursive - - name: 'Run Nix build for {{ matrix.nixpkg }}' + - name: 'Run Nix build for ${{ matrix.nixpkg }}' shell: bash - run: nix build -L '.?submodules=1#${{ matrix.nixpkg }}' + run: nix build -L '.#${{ matrix.nixpkg }}' - name: 'Show result contents' shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c84f5c6f..b2de4e50e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,8 @@ env: NPROC: 2 MAKEFLAGS: "-j${NPROC}" NIMFLAGS: "--parallelBuild:${NPROC} --colors:off -d:chronicles_colors:none" + NIM_VERSION: '2.2.4' + NIMBLE_VERSION: '0.18.2' jobs: changes: # changes detection @@ -30,9 +32,9 @@ jobs: filters: | common: - '.github/workflows/**' - - 'vendor/**' - - 'Makefile' + - 'nimble.lock' - 'waku.nimble' + - 'Makefile' - 'library/**' v2: - 'waku/**' @@ -63,24 +65,36 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Get submodules hash - id: submodules - run: | - echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT + - name: Install Nim ${{ env.NIM_VERSION }} + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Cache submodules + - name: Install Nimble ${{ env.NIMBLE_VERSION }} + run: | + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH + + - name: Cache nimble deps + id: cache-nimbledeps uses: actions/cache@v3 with: path: | - vendor/ - .git/modules - key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + nimbledeps/ + nimble.paths + key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock') }} - - name: Make update - run: make update + - name: Install nimble deps + if: steps.cache-nimbledeps.outputs.cache-hit != 'true' + run: | + nimble setup --localdeps -y + make rebuild-nat-libs-nimbledeps + make rebuild-bearssl-nimbledeps + touch nimbledeps/.nimble-setup - name: Build binaries - run: make V=1 QUICK_AND_DIRTY_COMPILER=1 all + run: make V=1 all build-windows: needs: changes @@ -104,21 +118,33 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Get submodules hash - id: submodules - run: | - echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT + - name: Install Nim ${{ env.NIM_VERSION }} + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Cache submodules + - name: Install Nimble ${{ env.NIMBLE_VERSION }} + run: | + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH + + - name: Cache nimble deps + id: cache-nimbledeps uses: actions/cache@v3 with: path: | - vendor/ - .git/modules - key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + nimbledeps/ + nimble.paths + key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock') }} - - name: Make update - run: make update + - name: Install nimble deps + if: steps.cache-nimbledeps.outputs.cache-hit != 'true' + run: | + nimble setup --localdeps -y + make rebuild-nat-libs-nimbledeps + make rebuild-bearssl-nimbledeps + touch nimbledeps/.nimble-setup - name: Run tests run: | @@ -132,13 +158,13 @@ jobs: export NIMFLAGS="--colors:off -d:chronicles_colors:none" export USE_LIBBACKTRACE=0 - make V=1 LOG_LEVEL=DEBUG QUICK_AND_DIRTY_COMPILER=1 POSTGRES=$postgres_enabled test - make V=1 LOG_LEVEL=DEBUG QUICK_AND_DIRTY_COMPILER=1 POSTGRES=$postgres_enabled testwakunode2 + make V=1 POSTGRES=$postgres_enabled test + make V=1 POSTGRES=$postgres_enabled testwakunode2 build-docker-image: needs: changes if: ${{ needs.changes.outputs.v2 == 'true' || needs.changes.outputs.common == 'true' || needs.changes.outputs.docker == 'true' }} - uses: logos-messaging/logos-delivery/.github/workflows/container-image.yml@10dc3d3eb4b6a3d4313f7b2cc4a85a925e9ce039 + uses: ./.github/workflows/container-image.yml secrets: inherit nwaku-nwaku-interop-tests: @@ -171,18 +197,33 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Get submodules hash - id: submodules - run: | - echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT + - name: Install Nim ${{ env.NIM_VERSION }} + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Cache submodules + - name: Install Nimble ${{ env.NIMBLE_VERSION }} + run: | + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH + + - name: Cache nimble deps + id: cache-nimbledeps uses: actions/cache@v3 with: path: | - vendor/ - .git/modules - key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + nimbledeps/ + nimble.paths + key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock') }} + + - name: Install nimble deps + if: steps.cache-nimbledeps.outputs.cache-hit != 'true' + run: | + nimble setup --localdeps -y + make rebuild-nat-libs-nimbledeps + make rebuild-bearssl-nimbledeps + touch nimbledeps/.nimble-setup - name: Build nph run: | diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml index 2bc08be2f..ae132a477 100644 --- a/.github/workflows/container-image.yml +++ b/.github/workflows/container-image.yml @@ -15,6 +15,8 @@ env: NPROC: 2 MAKEFLAGS: "-j${NPROC}" NIMFLAGS: "--parallelBuild:${NPROC}" + NIM_VERSION: '2.2.4' + NIMBLE_VERSION: '0.18.2' # This workflow should not run for outside contributors # If org secrets are not available, we'll avoid building and publishing the docker image and we'll pass the workflow @@ -46,28 +48,42 @@ jobs: if: ${{ steps.secrets.outcome == 'success' }} uses: actions/checkout@v4 - - name: Get submodules hash - id: submodules + - name: Install Nim ${{ env.NIM_VERSION }} + if: ${{ steps.secrets.outcome == 'success' }} + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Nimble ${{ env.NIMBLE_VERSION }} if: ${{ steps.secrets.outcome == 'success' }} run: | - echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH - - name: Cache submodules + - name: Cache nimble deps if: ${{ steps.secrets.outcome == 'success' }} + id: cache-nimbledeps uses: actions/cache@v3 with: path: | - vendor/ - .git/modules - key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + nimbledeps/ + nimble.paths + key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock') }} + + - name: Install nimble deps + if: ${{ steps.secrets.outcome == 'success' && steps.cache-nimbledeps.outputs.cache-hit != 'true' }} + run: | + nimble setup --localdeps -y + make rebuild-nat-libs-nimbledeps + make rebuild-bearssl-nimbledeps + touch nimbledeps/.nimble-setup - name: Build binaries id: build if: ${{ steps.secrets.outcome == 'success' }} run: | - make update - - make -j${NPROC} V=1 QUICK_AND_DIRTY_COMPILER=1 NIMFLAGS="-d:disableMarchNative -d:postgres -d:chronicles_colors:none" wakunode2 + make -j${NPROC} V=1 NIMFLAGS="-d:disableMarchNative -d:postgres -d:chronicles_colors:none" wakunode2 SHORT_REF=$(git rev-parse --short HEAD) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index e145e28ae..e3c8bb575 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -63,10 +63,10 @@ jobs: run: | OS=$([[ "${{runner.os}}" == "macOS" ]] && echo "macosx" || echo "linux") - make QUICK_AND_DIRTY_COMPILER=1 V=1 CI=false NIMFLAGS="-d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" \ + make V=1 CI=false NIMFLAGS="-d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" \ update - make QUICK_AND_DIRTY_COMPILER=1 V=1 CI=false\ + make V=1 CI=false\ NIMFLAGS="-d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}} -d:postgres" \ wakunode2\ chat2\ diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 9c1b1eab0..09ef05a5d 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -7,20 +7,25 @@ on: required: true type: string +env: + NPROC: 4 + NIM_VERSION: '2.2.4' + NIMBLE_VERSION: '0.18.2' + jobs: build: runs-on: windows-latest defaults: run: - shell: msys2 {0} + shell: msys2 {0} env: MSYSTEM: MINGW64 steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup MSYS2 uses: msys2/setup-msys2@v2 @@ -51,50 +56,61 @@ jobs: run: | bash scripts/install_nasm_in_windows.sh source $HOME/.bashrc - + - name: Add UPX to PATH run: | echo "/usr/bin:$PATH" >> $GITHUB_PATH echo "/mingw64/bin:$PATH" >> $GITHUB_PATH echo "/usr/lib:$PATH" >> $GITHUB_PATH - echo "/mingw64/lib:$PATH" >> $GITHUB_PATH + echo "/mingw64/lib:$PATH" >> $GITHUB_PATH - name: Verify dependencies run: | which upx gcc g++ make cmake cargo rustc python nasm - - name: Updating submodules - run: git submodule update --init --recursive + - name: Install Nim ${{ env.NIM_VERSION }} + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Nimble ${{ env.NIMBLE_VERSION }} + run: | + export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$PATH" + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH + + - name: Cache nimble deps + id: cache-nimbledeps + uses: actions/cache@v4 + with: + path: | + nimbledeps/ + nimble.paths + key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock') }} + + - name: Install nimble deps + if: steps.cache-nimbledeps.outputs.cache-hit != 'true' + run: | + export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH" + nimble setup --localdeps -y + make rebuild-nat-libs-nimbledeps CC=gcc + make rebuild-bearssl-nimbledeps CC=gcc + touch nimbledeps/.nimble-setup - name: Creating tmp directory run: mkdir -p tmp - - name: Building Nim - run: | - cd vendor/nimbus-build-system/vendor/Nim - ./build_all.bat - cd ../../../.. - - - name: Building miniupnpc - run: | - cd vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc - make -f Makefile.mingw CC=gcc CXX=g++ libminiupnpc.a V=1 - cd ../../../../.. - - - name: Building libnatpmp - run: | - cd ./vendor/nim-nat-traversal/vendor/libnatpmp-upstream - make CC="gcc -fPIC -D_WIN32_WINNT=0x0600 -DNATPMP_STATICLIB" libnatpmp.a V=1 - cd ../../../../ - - name: Building wakunode2.exe run: | - make wakunode2 LOG_LEVEL=DEBUG V=3 -j8 + export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH" + make wakunode2 V=3 -j${{ env.NPROC }} - name: Building libwaku.dll run: | - make libwaku STATIC=0 LOG_LEVEL=DEBUG V=1 -j - + export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH" + make libwaku STATIC=0 V=1 -j + - name: Check Executable run: | if [ -f "./build/wakunode2.exe" ]; then diff --git a/.gitignore b/.gitignore index 5222a0d5e..188090b19 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,6 @@ # Executables shall be put in an ignored build/ directory /build -# Nimble packages -/vendor/.nimble - # Generated Files *.generated.nim @@ -45,9 +42,6 @@ node_modules/ rlnKeystore.json *.tar.gz -# Nimbus Build System -nimbus-build-system.paths - # sqlite db *.db *.db-shm diff --git a/.gitmodules b/.gitmodules index 6a63491e3..ac07235b8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,197 +1,10 @@ -[submodule "vendor/nim-eth"] - path = vendor/nim-eth - url = https://github.com/status-im/nim-eth.git - ignore = dirty - branch = master -[submodule "vendor/nim-secp256k1"] - path = vendor/nim-secp256k1 - url = https://github.com/status-im/nim-secp256k1.git - ignore = dirty - branch = master -[submodule "vendor/nim-libp2p"] - path = vendor/nim-libp2p - url = https://github.com/vacp2p/nim-libp2p.git - ignore = dirty - branch = master -[submodule "vendor/nim-stew"] - path = vendor/nim-stew - url = https://github.com/status-im/nim-stew.git - ignore = dirty - branch = master -[submodule "vendor/nimbus-build-system"] - path = vendor/nimbus-build-system - url = https://github.com/status-im/nimbus-build-system.git - ignore = dirty - branch = master -[submodule "vendor/nim-nat-traversal"] - path = vendor/nim-nat-traversal - url = https://github.com/status-im/nim-nat-traversal.git - ignore = dirty - branch = master -[submodule "vendor/nim-libbacktrace"] - path = vendor/nim-libbacktrace - url = https://github.com/status-im/nim-libbacktrace.git - ignore = dirty - branch = master -[submodule "vendor/nim-confutils"] - path = vendor/nim-confutils - url = https://github.com/status-im/nim-confutils.git - ignore = dirty - branch = master -[submodule "vendor/nim-chronicles"] - path = vendor/nim-chronicles - url = https://github.com/status-im/nim-chronicles.git - ignore = dirty - branch = master -[submodule "vendor/nim-faststreams"] - path = vendor/nim-faststreams - url = https://github.com/status-im/nim-faststreams.git - ignore = dirty - branch = master -[submodule "vendor/nim-chronos"] - path = vendor/nim-chronos - url = https://github.com/status-im/nim-chronos.git - ignore = dirty - branch = master -[submodule "vendor/nim-json-serialization"] - path = vendor/nim-json-serialization - url = https://github.com/status-im/nim-json-serialization.git - ignore = dirty - branch = master -[submodule "vendor/nim-serialization"] - path = vendor/nim-serialization - url = https://github.com/status-im/nim-serialization.git - ignore = dirty - branch = master -[submodule "vendor/nimcrypto"] - path = vendor/nimcrypto - url = https://github.com/cheatfate/nimcrypto.git - ignore = dirty - branch = master -[submodule "vendor/nim-metrics"] - path = vendor/nim-metrics - url = https://github.com/status-im/nim-metrics.git - ignore = dirty - branch = master -[submodule "vendor/nim-stint"] - path = vendor/nim-stint - url = https://github.com/status-im/nim-stint.git - ignore = dirty - branch = master -[submodule "vendor/nim-json-rpc"] - path = vendor/nim-json-rpc - url = https://github.com/status-im/nim-json-rpc.git - ignore = dirty - branch = master -[submodule "vendor/nim-http-utils"] - path = vendor/nim-http-utils - url = https://github.com/status-im/nim-http-utils.git - ignore = dirty - branch = master -[submodule "vendor/nim-bearssl"] - path = vendor/nim-bearssl - url = https://github.com/status-im/nim-bearssl.git - ignore = dirty - branch = master -[submodule "vendor/nim-sqlite3-abi"] - path = vendor/nim-sqlite3-abi - url = https://github.com/arnetheduck/nim-sqlite3-abi.git - ignore = dirty - branch = master -[submodule "vendor/nim-web3"] - path = vendor/nim-web3 - url = https://github.com/status-im/nim-web3.git -[submodule "vendor/nim-testutils"] - path = vendor/nim-testutils - url = https://github.com/status-im/nim-testutils.git - ignore = untracked - branch = master -[submodule "vendor/nim-unittest2"] - path = vendor/nim-unittest2 - url = https://github.com/status-im/nim-unittest2.git - ignore = untracked - branch = master -[submodule "vendor/nim-websock"] - path = vendor/nim-websock - url = https://github.com/status-im/nim-websock.git - ignore = untracked - branch = main -[submodule "vendor/nim-zlib"] - path = vendor/nim-zlib - url = https://github.com/status-im/nim-zlib.git - ignore = untracked - branch = master -[submodule "vendor/nim-dnsdisc"] - path = vendor/nim-dnsdisc - url = https://github.com/status-im/nim-dnsdisc.git - ignore = untracked - branch = main -[submodule "vendor/dnsclient.nim"] - path = vendor/dnsclient.nim - url = https://github.com/ba0f3/dnsclient.nim.git - ignore = untracked - branch = master -[submodule "vendor/nim-toml-serialization"] - path = vendor/nim-toml-serialization - url = https://github.com/status-im/nim-toml-serialization.git -[submodule "vendor/nim-presto"] - path = vendor/nim-presto - url = https://github.com/status-im/nim-presto.git - ignore = untracked - branch = master [submodule "vendor/zerokit"] path = vendor/zerokit url = https://github.com/vacp2p/zerokit.git ignore = dirty branch = v0.5.1 -[submodule "vendor/nim-regex"] - path = vendor/nim-regex - url = https://github.com/nitely/nim-regex.git - ignore = untracked - branch = master -[submodule "vendor/nim-unicodedb"] - path = vendor/nim-unicodedb - url = https://github.com/nitely/nim-unicodedb.git - ignore = untracked - branch = master -[submodule "vendor/nim-taskpools"] - path = vendor/nim-taskpools - url = https://github.com/status-im/nim-taskpools.git - ignore = untracked - branch = stable -[submodule "vendor/nim-results"] - ignore = untracked - branch = master - path = vendor/nim-results - url = https://github.com/arnetheduck/nim-results.git -[submodule "vendor/db_connector"] - path = vendor/db_connector - url = https://github.com/nim-lang/db_connector.git - ignore = untracked - branch = devel -[submodule "vendor/nph"] - ignore = untracked - branch = master - path = vendor/nph - url = https://github.com/arnetheduck/nph.git -[submodule "vendor/nim-minilru"] - path = vendor/nim-minilru - url = https://github.com/status-im/nim-minilru.git - ignore = untracked - branch = master [submodule "vendor/waku-rlnv2-contract"] path = vendor/waku-rlnv2-contract url = https://github.com/logos-messaging/waku-rlnv2-contract.git ignore = untracked branch = master -[submodule "vendor/nim-lsquic"] - path = vendor/nim-lsquic - url = https://github.com/vacp2p/nim-lsquic -[submodule "vendor/nim-jwt"] - path = vendor/nim-jwt - url = https://github.com/vacp2p/nim-jwt.git -[submodule "vendor/nim-ffi"] - path = vendor/nim-ffi - url = https://github.com/logos-messaging/nim-ffi/ - ignore = untracked - branch = master diff --git a/BearSSL.mk b/BearSSL.mk new file mode 100644 index 000000000..98e933ebd --- /dev/null +++ b/BearSSL.mk @@ -0,0 +1,39 @@ +# Copyright (c) 2022 Status Research & Development GmbH. Licensed under +# either of: +# - Apache License, version 2.0 +# - MIT license +# at your option. This file may not be copied, modified, or distributed except +# according to those terms. + +########################### +## bearssl (nimbledeps) ## +########################### +# Rebuilds libbearssl.a from the package installed by nimble under +# nimbledeps/pkgs2/. Used by `make update` / $(NIMBLEDEPS_STAMP). +# +# BEARSSL_NIMBLEDEPS_DIR is evaluated at parse time, so targets that +# depend on it must be invoked via a recursive $(MAKE) call so the sub-make +# re-evaluates the variable after nimble setup has populated nimbledeps/. +# +# `ls -dt` (sort by modification time, newest first) is used to pick the +# latest installed version and is portable across Linux, macOS, and +# Windows (MSYS/MinGW). + +BEARSSL_NIMBLEDEPS_DIR := $(shell ls -dt $(CURDIR)/nimbledeps/pkgs2/bearssl-* 2>/dev/null | head -1) +BEARSSL_CSOURCES_DIR := $(BEARSSL_NIMBLEDEPS_DIR)/bearssl/csources + +.PHONY: clean-bearssl-nimbledeps rebuild-bearssl-nimbledeps + +clean-bearssl-nimbledeps: +ifeq ($(BEARSSL_NIMBLEDEPS_DIR),) + $(error No bearssl package found under nimbledeps/pkgs2/ — run 'make update' first) +endif + + [ -e "$(BEARSSL_CSOURCES_DIR)/build" ] && \ + "$(MAKE)" -C "$(BEARSSL_CSOURCES_DIR)" clean || true + +rebuild-bearssl-nimbledeps: | clean-bearssl-nimbledeps +ifeq ($(BEARSSL_NIMBLEDEPS_DIR),) + $(error No bearssl package found under nimbledeps/pkgs2/ — run 'make update' first) +endif + @echo "Rebuilding bearssl from $(BEARSSL_CSOURCES_DIR)" + + "$(MAKE)" -C "$(BEARSSL_CSOURCES_DIR)" lib \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5b16b9eee..412d0977a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,6 @@ FROM rustlang/rust:nightly-alpine3.19 AS nim-build ARG NIMFLAGS ARG MAKE_TARGET=wakunode2 ARG NIM_COMMIT -ARG LOG_LEVEL=TRACE ARG HEAPTRACK_BUILD=0 # Get build tools and required header files @@ -27,7 +26,7 @@ RUN if [ "$HEAPTRACK_BUILD" = "1" ]; then \ RUN make -j$(nproc) deps QUICK_AND_DIRTY_COMPILER=1 ${NIM_COMMIT} # Build the final node binary -RUN make -j$(nproc) ${NIM_COMMIT} $MAKE_TARGET LOG_LEVEL=${LOG_LEVEL} NIMFLAGS="${NIMFLAGS}" +RUN make -j$(nproc) ${NIM_COMMIT} $MAKE_TARGET NIMFLAGS="${NIMFLAGS}" # PRODUCTION IMAGE ------------------------------------------------------------- diff --git a/Makefile b/Makefile index afd2389d2..cabeec80f 100644 --- a/Makefile +++ b/Makefile @@ -4,28 +4,13 @@ # - MIT license # at your option. This file may not be copied, modified, or distributed except # according to those terms. -export BUILD_SYSTEM_DIR := vendor/nimbus-build-system -export EXCLUDED_NIM_PACKAGES := vendor/nim-dnsdisc/vendor + +include Nat.mk +include BearSSL.mk + LINK_PCRE := 0 FORMAT_MSG := "\\x1B[95mFormatting:\\x1B[39m" -# we don't want an error here, so we can handle things later, in the ".DEFAULT" target --include $(BUILD_SYSTEM_DIR)/makefiles/variables.mk - - -ifeq ($(NIM_PARAMS),) -# "variables.mk" was not included, so we update the submodules. -GIT_SUBMODULE_UPDATE := git submodule update --init --recursive -.DEFAULT: - +@ echo -e "Git submodules not found. Running '$(GIT_SUBMODULE_UPDATE)'.\n"; \ - $(GIT_SUBMODULE_UPDATE); \ - echo -# Now that the included *.mk files appeared, and are newer than this file, Make will restart itself: -# https://www.gnu.org/software/make/manual/make.html#Remaking-Makefiles -# -# After restarting, it will execute its original goal, so we don't have to start a child Make here -# with "$(MAKE) $(MAKECMDGOALS)". Isn't hidden control flow great? - -else # "variables.mk" was included. Business as usual until the end of this file. +BUILD_MSG := "Building:" # Determine the OS detected_OS := $(shell uname -s) @@ -33,29 +18,31 @@ ifneq (,$(findstring MINGW,$(detected_OS))) detected_OS := Windows endif +# NIM binary location +NIM_BINARY := $(shell which nim) +NPH := $(HOME)/.nimble/bin/nph +NIMBLEDEPS_STAMP := nimbledeps/.nimble-setup + +# Compilation parameters +NIM_PARAMS ?= + ifeq ($(detected_OS),Windows) - # Update MINGW_PATH to standard MinGW location MINGW_PATH = /mingw64 NIM_PARAMS += --passC:"-I$(MINGW_PATH)/include" NIM_PARAMS += --passL:"-L$(MINGW_PATH)/lib" - NIM_PARAMS += --passL:"-Lvendor/nim-nat-traversal/vendor/miniupnp/miniupnpc" - NIM_PARAMS += --passL:"-Lvendor/nim-nat-traversal/vendor/libnatpmp-upstream" - - LIBS = -lws2_32 -lbcrypt -liphlpapi -luserenv -lntdll -lminiupnpc -lnatpmp -lpq + LIBS = -lws2_32 -lbcrypt -liphlpapi -luserenv -lntdll -lpq NIM_PARAMS += $(foreach lib,$(LIBS),--passL:"$(lib)") NIM_PARAMS += --passL:"-Wl,--allow-multiple-definition" - export PATH := /c/msys64/usr/bin:/c/msys64/mingw64/bin:/c/msys64/usr/lib:/c/msys64/mingw64/lib:$(PATH) - endif ########## ## Main ## ########## -.PHONY: all test update clean examples +.PHONY: all test update clean examples deps nimble -# default target, because it's the first one that doesn't start with '.' -all: | wakunode2 libwaku +# default target +all: | wakunode2 libwaku liblogosdelivery examples: | example2 chat2 chat2bridge @@ -71,102 +58,116 @@ ifeq ($(strip $(test_file)),) else $(MAKE) compile-test TEST_FILE="$(test_file)" TEST_NAME="$(call test_name)" endif -# this prevents make from erroring on unknown targets like "Index" + +# this prevents make from erroring on unknown targets %: @true waku.nims: ln -s waku.nimble $@ -update: | update-common - rm -rf waku.nims && \ - $(MAKE) waku.nims $(HANDLE_OUTPUT) +$(NIMBLEDEPS_STAMP): nimble.lock | waku.nims + @if ! command -v nimble > /dev/null 2>&1; then $(MAKE) install-nimble; fi + nimble setup --localdeps $(MAKE) build-nph + $(MAKE) rebuild-bearssl-nimbledeps + touch $@ + +update: + rm -f $(NIMBLEDEPS_STAMP) + $(MAKE) $(NIMBLEDEPS_STAMP) + nimble lock clean: - rm -rf build + rm -rf build 2> /dev/null || true + rm -rf nimbledeps 2> /dev/null || true + rm nimble.lock 2> /dev/null || true + rm -fr nimcache 2> /dev/null || true + rm nimble.paths 2> /dev/null || true + nimble clean -# must be included after the default target --include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk +REQUIRED_NIM_VERSION := $(shell grep -E '^const NimVersion\s*=' waku.nimble | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') +REQUIRED_NIMBLE_VERSION := $(shell grep -E '^const NimbleVersion\s*=' waku.nimble | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') + +install-nim: + $(eval NIM_OS := $(shell uname -s | tr 'A-Z' 'a-z' | sed 's/darwin/macosx/')) + $(eval NIM_ARCH := $(shell uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')) + $(eval NIM_INSTALL_DIR := $(HOME)/.nim_runtime) + @nim_ver=$$(nim --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ + if [ "$$nim_ver" = "$(REQUIRED_NIM_VERSION)" ]; then \ + echo "nim $(REQUIRED_NIM_VERSION) already installed, skipping."; \ + else \ + curl -L "https://github.com/nim-lang/Nim/releases/download/v$(REQUIRED_NIM_VERSION)/nim-$(REQUIRED_NIM_VERSION)-$(NIM_OS)_$(NIM_ARCH).tar.xz" \ + -o /tmp/nim-$(REQUIRED_NIM_VERSION).tar.xz && \ + tar -xJf /tmp/nim-$(REQUIRED_NIM_VERSION).tar.xz -C /tmp && \ + mkdir -p $(NIM_INSTALL_DIR) && \ + cd /tmp/nim-$(REQUIRED_NIM_VERSION) && ./install.sh $(NIM_INSTALL_DIR); \ + fi + +install-nimble: install-nim + @nimble_ver=$$(nimble --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ + if [ "$$nimble_ver" = "$(REQUIRED_NIMBLE_VERSION)" ]; then \ + echo "nimble $(REQUIRED_NIMBLE_VERSION) already installed, skipping."; \ + else \ + cd /tmp && PATH="$(HOME)/.nim_runtime/bin:$$PATH" \ + nimble install "nimble@$(REQUIRED_NIMBLE_VERSION)" -y; \ + fi + +build: + @nim_ver=$$(nim --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ + if [ "$$nim_ver" != "$(REQUIRED_NIM_VERSION)" ]; then \ + echo "Error: Nim $(REQUIRED_NIM_VERSION) is required, but found '$$nim_ver'"; \ + exit 1; \ + fi + @nimble_ver=$$(nimble --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ + if [ "$$nimble_ver" != "$(REQUIRED_NIMBLE_VERSION)" ]; then \ + echo "Error: Nimble $(REQUIRED_NIMBLE_VERSION) is required, but found '$$nimble_ver'"; \ + exit 1; \ + fi + mkdir -p build + +nimble: + echo "Inside nimble target, checking for nimble..." && \ + command -v nimble >/dev/null 2>&1 || { \ + mv nimbledeps nimbledeps_backup 2>/dev/null || true; \ + echo "choosenim not found, installing ..."; \ + curl -sSf https://nim-lang.org/choosenim/init.sh | sh; \ + mv nimbledeps_backup nimbledeps 2>/dev/null || true; \ + } ## Possible values: prod; debug TARGET ?= prod ## Git version GIT_VERSION ?= $(shell git describe --abbrev=6 --always --tags) -## Compilation parameters. If defined in the CLI the assignments won't be executed NIM_PARAMS := $(NIM_PARAMS) -d:git_version=\"$(GIT_VERSION)\" ## Heaptracker options HEAPTRACKER ?= 0 HEAPTRACKER_INJECT ?= 0 ifeq ($(HEAPTRACKER), 1) -# Assumes Nim's lib/system/alloc.nim is patched! TARGET := debug-with-heaptrack - ifeq ($(HEAPTRACKER_INJECT), 1) -# the Nim compiler will load 'libheaptrack_inject.so' HEAPTRACK_PARAMS := -d:heaptracker -d:heaptracker_inject NIM_PARAMS := $(NIM_PARAMS) -d:heaptracker -d:heaptracker_inject else -# the Nim compiler will load 'libheaptrack_preload.so' HEAPTRACK_PARAMS := -d:heaptracker NIM_PARAMS := $(NIM_PARAMS) -d:heaptracker endif - -endif -## end of Heaptracker options - -################## -## Dependencies ## -################## -.PHONY: deps libbacktrace - -FOUNDRY_VERSION := 1.5.0 -PNPM_VERSION := 10.23.0 - - -rustup: -ifeq (, $(shell which cargo)) -# Install Rustup if it's not installed -# -y: Assume "yes" for all prompts -# --default-toolchain stable: Install the stable toolchain - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable endif -rln-deps: rustup - ./scripts/install_rln_tests_dependencies.sh $(FOUNDRY_VERSION) $(PNPM_VERSION) - -deps: | deps-common nat-libs waku.nims - - -### nim-libbacktrace - -# "-d:release" implies "--stacktrace:off" and it cannot be added to config.nims +# Debug/Release mode ifeq ($(DEBUG), 0) -NIM_PARAMS := $(NIM_PARAMS) -d:release -d:lto_incremental -d:strip +NIM_PARAMS := $(NIM_PARAMS) -d:release else NIM_PARAMS := $(NIM_PARAMS) -d:debug endif -ifeq ($(USE_LIBBACKTRACE), 0) NIM_PARAMS := $(NIM_PARAMS) -d:disable_libbacktrace -endif # enable experimental exit is dest feature in libp2p mix NIM_PARAMS := $(NIM_PARAMS) -d:libp2p_mix_experimental_exit_is_dest -libbacktrace: - + $(MAKE) -C vendor/nim-libbacktrace --no-print-directory BUILD_CXX_LIB=0 - -clean-libbacktrace: - + $(MAKE) -C vendor/nim-libbacktrace clean $(HANDLE_OUTPUT) - -# Extend deps and clean targets -ifneq ($(USE_LIBBACKTRACE), 0) -deps: | libbacktrace -endif - ifeq ($(POSTGRES), 1) NIM_PARAMS := $(NIM_PARAMS) -d:postgres -d:nimDebugDlOpen endif @@ -175,14 +176,26 @@ ifeq ($(DEBUG_DISCV5), 1) NIM_PARAMS := $(NIM_PARAMS) -d:debugDiscv5 endif -clean: | clean-libbacktrace +# Export NIM_PARAMS so nimble can access it +export NIM_PARAMS -### Create nimble links (used when building with Nix) +################## +## Dependencies ## +################## +.PHONY: deps -nimbus-build-system-nimble-dir: - NIMBLE_DIR="$(CURDIR)/$(NIMBLE_DIR)" \ - PWD_CMD="$(PWD)" \ - $(CURDIR)/scripts/generate_nimble_links.sh +FOUNDRY_VERSION := 1.5.0 +PNPM_VERSION := 10.23.0 + +rustup: +ifeq (, $(shell which cargo)) + curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable +endif + +rln-deps: rustup + ./scripts/install_rln_tests_dependencies.sh $(FOUNDRY_VERSION) $(PNPM_VERSION) + +deps: | nimble ################## ## RLN ## @@ -199,8 +212,9 @@ LIBRLN_FILE ?= librln_$(LIBRLN_VERSION).a endif $(LIBRLN_FILE): + git submodule update --init vendor/zerokit echo -e $(BUILD_MSG) "$@" && \ - ./scripts/build_rln.sh $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(LIBRLN_FILE) + bash scripts/build_rln.sh $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(LIBRLN_FILE) librln: | $(LIBRLN_FILE) $(eval NIM_PARAMS += --passL:$(LIBRLN_FILE) --passL:-lm) @@ -209,7 +223,6 @@ clean-librln: cargo clean --manifest-path vendor/zerokit/rln/Cargo.toml rm -f $(LIBRLN_FILE) -# Extend clean target clean: | clean-librln ################# @@ -217,74 +230,71 @@ clean: | clean-librln ################# .PHONY: testcommon -testcommon: | build deps +testcommon: | build echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim testcommon $(NIM_PARAMS) waku.nims - + nimble testcommon ########## ## Waku ## ########## .PHONY: testwaku wakunode2 testwakunode2 example2 chat2 chat2bridge liteprotocoltester -# install rln-deps only for the testwaku target -testwaku: | build deps rln-deps librln +testwaku: | build rln-deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim test -d:os=$(shell uname) $(NIM_PARAMS) waku.nims + nimble test -wakunode2: | build deps librln +wakunode2: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - \ - $(ENV_SCRIPT) nim wakunode2 $(NIM_PARAMS) waku.nims + nimble wakunode2 -benchmarks: | build deps librln +benchmarks: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim benchmarks $(NIM_PARAMS) waku.nims + nimble benchmarks -testwakunode2: | build deps librln +testwakunode2: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim testwakunode2 $(NIM_PARAMS) waku.nims + nimble testwakunode2 -example2: | build deps librln +example2: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim example2 $(NIM_PARAMS) waku.nims + nimble example2 -chat2: | build deps librln +chat2: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim chat2 $(NIM_PARAMS) waku.nims + nimble chat2 -chat2mix: | build deps librln +chat2mix: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim chat2mix $(NIM_PARAMS) waku.nims + nimble chat2mix -rln-db-inspector: | build deps librln +rln-db-inspector: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim rln_db_inspector $(NIM_PARAMS) waku.nims + nimble rln_db_inspector -chat2bridge: | build deps librln +chat2bridge: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim chat2bridge $(NIM_PARAMS) waku.nims + nimble chat2bridge -liteprotocoltester: | build deps librln +liteprotocoltester: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim liteprotocoltester $(NIM_PARAMS) waku.nims + nimble liteprotocoltester -lightpushwithmix: | build deps librln +lightpushwithmix: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim lightpushwithmix $(NIM_PARAMS) waku.nims + nimble lightpushwithmix -api_example: | build deps librln +api_example: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim api_example $(NIM_PARAMS) waku.nims -build/%: | build deps librln +build/%: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$*" && \ - $(ENV_SCRIPT) nim buildone $(NIM_PARAMS) waku.nims $* + nimble buildone $* -compile-test: | build deps librln +compile-test: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "$(TEST_FILE)" "\"$(TEST_NAME)\"" && \ - $(ENV_SCRIPT) nim buildTest $(NIM_PARAMS) waku.nims $(TEST_FILE) && \ - $(ENV_SCRIPT) nim execTest $(NIM_PARAMS) waku.nims $(TEST_FILE) "\"$(TEST_NAME)\""; \ + nimble buildTest $(TEST_FILE) && \ + nimble execTest $(TEST_FILE) "\"$(TEST_NAME)\"" ################ ## Waku tools ## @@ -293,29 +303,30 @@ compile-test: | build deps librln tools: networkmonitor wakucanary -wakucanary: | build deps librln +wakucanary: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim wakucanary $(NIM_PARAMS) waku.nims + nimble wakucanary -networkmonitor: | build deps librln +networkmonitor: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim networkmonitor $(NIM_PARAMS) waku.nims + nimble networkmonitor ############ ## Format ## ############ -.PHONY: build-nph install-nph clean-nph print-nph-path - -# Default location for nph binary shall be next to nim binary to make it available on the path. -NPH:=$(shell dirname $(NIM_BINARY))/nph +.PHONY: build-nph install-nph print-nph-path build-nph: | build deps -ifeq ("$(wildcard $(NPH))","") - $(ENV_SCRIPT) nim c --skipParentCfg:on vendor/nph/src/nph.nim && \ - mv vendor/nph/src/nph $(shell dirname $(NPH)) - echo "nph utility is available at " $(NPH) +ifneq ($(detected_OS),Windows) + if command -v nph > /dev/null 2>&1; then \ + echo "nph already installed, skipping"; \ + else \ + echo "Installing nph globally"; \ + (cd /tmp && nimble install nph@0.7.0 --accept -g); \ + fi + command -v nph else - echo "nph utility already exists at " $(NPH) + echo "Skipping nph build on Windows (nph is only used on Unix-like systems)" endif GIT_PRE_COMMIT_HOOK := .git/hooks/pre-commit @@ -332,39 +343,30 @@ nph/%: | build-nph echo -e $(FORMAT_MSG) "nph/$*" && \ $(NPH) $* -clean-nph: - rm -f $(NPH) - -# To avoid hardcoding nph binary location in several places print-nph-path: - echo "$(NPH)" + @echo "$(NPH)" -clean: | clean-nph +clean: ################### ## Documentation ## ################### .PHONY: docs coverage -# TODO: Remove unused target docs: | build deps echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim doc --run --index:on --project --out:.gh-pages waku/waku.nim waku.nims + nimble doc --run --index:on --project --out:.gh-pages waku/waku.nim waku.nims coverage: echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) ./scripts/run_cov.sh -y - + ./scripts/run_cov.sh -y ##################### ## Container image ## ##################### -# -d:insecure - Necessary to enable Prometheus HTTP endpoint for metrics -# -d:chronicles_colors:none - Necessary to disable colors in logs for Docker DOCKER_IMAGE_NIMFLAGS ?= -d:chronicles_colors:none -d:insecure -d:postgres DOCKER_IMAGE_NIMFLAGS := $(DOCKER_IMAGE_NIMFLAGS) $(HEAPTRACK_PARAMS) -# build a docker image for the fleet docker-image: MAKE_TARGET ?= wakunode2 docker-image: DOCKER_IMAGE_TAG ?= $(MAKE_TARGET)-$(GIT_VERSION) docker-image: DOCKER_IMAGE_NAME ?= wakuorg/nwaku:$(DOCKER_IMAGE_TAG) @@ -372,8 +374,6 @@ docker-image: docker build \ --build-arg="MAKE_TARGET=$(MAKE_TARGET)" \ --build-arg="NIMFLAGS=$(DOCKER_IMAGE_NIMFLAGS)" \ - --build-arg="NIM_COMMIT=$(DOCKER_NIM_COMMIT)" \ - --build-arg="LOG_LEVEL=$(LOG_LEVEL)" \ --build-arg="HEAPTRACK_BUILD=$(HEAPTRACKER)" \ --label="commit=$(shell git rev-parse HEAD)" \ --label="version=$(GIT_VERSION)" \ @@ -384,7 +384,7 @@ docker-quick-image: MAKE_TARGET ?= wakunode2 docker-quick-image: DOCKER_IMAGE_TAG ?= $(MAKE_TARGET)-$(GIT_VERSION) docker-quick-image: DOCKER_IMAGE_NAME ?= wakuorg/nwaku:$(DOCKER_IMAGE_TAG) docker-quick-image: NIM_PARAMS := $(NIM_PARAMS) -d:chronicles_colors:none -d:insecure -d:postgres --passL:$(LIBRLN_FILE) --passL:-lm -docker-quick-image: | build deps librln wakunode2 +docker-quick-image: | build librln wakunode2 docker build \ --build-arg="MAKE_TARGET=$(MAKE_TARGET)" \ --tag $(DOCKER_IMAGE_NAME) \ @@ -398,20 +398,14 @@ docker-push: #################################### ## Container lite-protocol-tester ## #################################### -# -d:insecure - Necessary to enable Prometheus HTTP endpoint for metrics -# -d:chronicles_colors:none - Necessary to disable colors in logs for Docker DOCKER_LPT_NIMFLAGS ?= -d:chronicles_colors:none -d:insecure -# build a docker image for the fleet docker-liteprotocoltester: DOCKER_LPT_TAG ?= latest docker-liteprotocoltester: DOCKER_LPT_NAME ?= wakuorg/liteprotocoltester:$(DOCKER_LPT_TAG) -# --no-cache docker-liteprotocoltester: docker build \ --build-arg="MAKE_TARGET=liteprotocoltester" \ --build-arg="NIMFLAGS=$(DOCKER_LPT_NIMFLAGS)" \ - --build-arg="NIM_COMMIT=$(DOCKER_NIM_COMMIT)" \ - --build-arg="LOG_LEVEL=TRACE" \ --label="commit=$(shell git rev-parse HEAD)" \ --label="version=$(GIT_VERSION)" \ --target $(if $(filter deploy,$(DOCKER_LPT_TAG)),deployment_lpt,standalone_lpt) \ @@ -430,39 +424,38 @@ docker-quick-liteprotocoltester: | liteprotocoltester docker-liteprotocoltester-push: docker push $(DOCKER_LPT_NAME) - ################ ## C Bindings ## ################ .PHONY: cbindings cwaku_example libwaku liblogosdelivery liblogosdelivery_example +detected_OS ?= Linux +ifeq ($(OS),Windows_NT) +detected_OS := Windows +else +detected_OS := $(shell uname -s) +endif + +BUILD_COMMAND ?= Dynamic STATIC ?= 0 -LIBWAKU_BUILD_COMMAND ?= libwakuDynamic -LIBLOGOSDELIVERY_BUILD_COMMAND ?= liblogosdeliveryDynamic +ifeq ($(STATIC), 1) + BUILD_COMMAND = Static +endif ifeq ($(detected_OS),Windows) - LIB_EXT_DYNAMIC = dll - LIB_EXT_STATIC = lib + BUILD_COMMAND := $(BUILD_COMMAND)Windows else ifeq ($(detected_OS),Darwin) - LIB_EXT_DYNAMIC = dylib - LIB_EXT_STATIC = a + BUILD_COMMAND := $(BUILD_COMMAND)Mac + export IOS_SDK_PATH := $(shell xcrun --sdk iphoneos --show-sdk-path) else ifeq ($(detected_OS),Linux) - LIB_EXT_DYNAMIC = so - LIB_EXT_STATIC = a + BUILD_COMMAND := $(BUILD_COMMAND)Linux endif -LIB_EXT := $(LIB_EXT_DYNAMIC) -ifeq ($(STATIC), 1) - LIB_EXT = $(LIB_EXT_STATIC) - LIBWAKU_BUILD_COMMAND = libwakuStatic - LIBLOGOSDELIVERY_BUILD_COMMAND = liblogosdeliveryStatic -endif +libwaku: | $(NIMBLEDEPS_STAMP) librln + nimble --verbose libwaku$(BUILD_COMMAND) waku.nimble -libwaku: | build deps librln - echo -e $(BUILD_MSG) "build/$@.$(LIB_EXT)" && $(ENV_SCRIPT) nim $(LIBWAKU_BUILD_COMMAND) $(NIM_PARAMS) waku.nims $@.$(LIB_EXT) - -liblogosdelivery: | build deps librln - echo -e $(BUILD_MSG) "build/$@.$(LIB_EXT)" && $(ENV_SCRIPT) nim $(LIBLOGOSDELIVERY_BUILD_COMMAND) $(NIM_PARAMS) waku.nims $@.$(LIB_EXT) +liblogosdelivery: | $(NIMBLEDEPS_STAMP) librln + nimble --verbose liblogosdelivery$(BUILD_COMMAND) waku.nimble logosdelivery_example: | build liblogosdelivery @echo -e $(BUILD_MSG) "build/$@" @@ -492,17 +485,35 @@ else ifeq ($(detected_OS),Windows) -lws2_32 endif +cwaku_example: | build libwaku + echo -e $(BUILD_MSG) "build/$@" && \ + cc -o "build/$@" \ + ./examples/cbindings/waku_example.c \ + ./examples/cbindings/base64.c \ + -lwaku -Lbuild/ \ + -pthread -ldl -lm + +cppwaku_example: | build libwaku + echo -e $(BUILD_MSG) "build/$@" && \ + g++ -o "build/$@" \ + ./examples/cpp/waku.cpp \ + ./examples/cpp/base64.cpp \ + -lwaku -Lbuild/ \ + -pthread -ldl -lm + +nodejswaku: | build deps + echo -e $(BUILD_MSG) "build/$@" && \ + node-gyp build --directory=examples/nodejs/ + ##################### ## Mobile Bindings ## ##################### .PHONY: libwaku-android \ - libwaku-android-precheck \ - libwaku-android-arm64 \ - libwaku-android-amd64 \ - libwaku-android-x86 \ - libwaku-android-arm \ - rebuild-nat-libs \ - build-libwaku-for-android-arch + libwaku-android-precheck \ + libwaku-android-arm64 \ + libwaku-android-amd64 \ + libwaku-android-x86 \ + libwaku-android-arm ANDROID_TARGET ?= 30 ifeq ($(detected_OS),Darwin) @@ -511,22 +522,19 @@ else ANDROID_TOOLCHAIN_DIR := $(ANDROID_NDK_HOME)/toolchains/llvm/prebuilt/linux-x86_64 endif -rebuild-nat-libs: | clean-cross nat-libs - libwaku-android-precheck: ifndef ANDROID_NDK_HOME - $(error ANDROID_NDK_HOME is not set) + $(error ANDROID_NDK_HOME is not set) endif build-libwaku-for-android-arch: ifneq ($(findstring /nix/store,$(LIBRLN_FILE)),) mkdir -p $(CURDIR)/build/android/$(ABIDIR)/ - cp $(LIBRLN_FILE) $(CURDIR)/build/android/$(ABIDIR)/ + CPU=$(CPU) ABIDIR=$(ABIDIR) ANDROID_ARCH=$(ANDROID_ARCH) ANDROID_COMPILER=$(ANDROID_COMPILER) ANDROID_TOOLCHAIN_DIR=$(ANDROID_TOOLCHAIN_DIR) nimble libWakuAndroid else ./scripts/build_rln_android.sh $(CURDIR)/build $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(CROSS_TARGET) $(ABIDIR) endif - $(MAKE) rebuild-nat-libs CC=$(ANDROID_TOOLCHAIN_DIR)/bin/$(ANDROID_COMPILER) - CPU=$(CPU) ABIDIR=$(ABIDIR) ANDROID_ARCH=$(ANDROID_ARCH) ANDROID_COMPILER=$(ANDROID_COMPILER) ANDROID_TOOLCHAIN_DIR=$(ANDROID_TOOLCHAIN_DIR) $(ENV_SCRIPT) nim libWakuAndroid $(NIM_PARAMS) waku.nims + $(MAKE) rebuild-nat-libs-nimbledeps CC=$(ANDROID_TOOLCHAIN_DIR)/bin/$(ANDROID_COMPILER) libwaku-android-arm64: ANDROID_ARCH=aarch64-linux-android libwaku-android-arm64: CPU=arm64 @@ -550,29 +558,23 @@ libwaku-android-arm: ANDROID_ARCH=armv7a-linux-androideabi libwaku-android-arm: CPU=arm libwaku-android-arm: ABIDIR=armeabi-v7a libwaku-android-arm: | libwaku-android-precheck build deps -# cross-rs target architecture name does not match the one used in android $(MAKE) build-libwaku-for-android-arch ANDROID_ARCH=$(ANDROID_ARCH) CROSS_TARGET=armv7-linux-androideabi CPU=$(CPU) ABIDIR=$(ABIDIR) ANDROID_COMPILER=$(ANDROID_ARCH)$(ANDROID_TARGET)-clang libwaku-android: $(MAKE) libwaku-android-amd64 $(MAKE) libwaku-android-arm64 $(MAKE) libwaku-android-x86 -# This target is disabled because on recent versions of cross-rs complain with the following error -# relocation R_ARM_THM_ALU_PREL_11_0 cannot be used against symbol 'stack_init_trampoline_return'; recompile with -fPIC -# It's likely this architecture is not used so we might just not support it. -# $(MAKE) libwaku-android-arm ################# ## iOS Bindings # ################# .PHONY: libwaku-ios-precheck \ - libwaku-ios-device \ - libwaku-ios-simulator \ - libwaku-ios + libwaku-ios-device \ + libwaku-ios-simulator \ + libwaku-ios IOS_DEPLOYMENT_TARGET ?= 18.0 -# Get SDK paths dynamically using xcrun define get_ios_sdk_path $(shell xcrun --sdk $(1) --show-sdk-path 2>/dev/null) endef @@ -584,59 +586,25 @@ else $(error iOS builds are only supported on macOS) endif -# Build for iOS architecture build-libwaku-for-ios-arch: - IOS_SDK=$(IOS_SDK) IOS_ARCH=$(IOS_ARCH) IOS_SDK_PATH=$(IOS_SDK_PATH) $(ENV_SCRIPT) nim libWakuIOS $(NIM_PARAMS) waku.nims + IOS_SDK=$(IOS_SDK) IOS_ARCH=$(IOS_ARCH) IOS_SDK_PATH=$(IOS_SDK_PATH) nimble libWakuIOS -# iOS device (arm64) libwaku-ios-device: IOS_ARCH=arm64 libwaku-ios-device: IOS_SDK=iphoneos libwaku-ios-device: IOS_SDK_PATH=$(call get_ios_sdk_path,iphoneos) libwaku-ios-device: | libwaku-ios-precheck build deps $(MAKE) build-libwaku-for-ios-arch IOS_ARCH=$(IOS_ARCH) IOS_SDK=$(IOS_SDK) IOS_SDK_PATH=$(IOS_SDK_PATH) -# iOS simulator (arm64 - Apple Silicon Macs) libwaku-ios-simulator: IOS_ARCH=arm64 libwaku-ios-simulator: IOS_SDK=iphonesimulator libwaku-ios-simulator: IOS_SDK_PATH=$(call get_ios_sdk_path,iphonesimulator) libwaku-ios-simulator: | libwaku-ios-precheck build deps $(MAKE) build-libwaku-for-ios-arch IOS_ARCH=$(IOS_ARCH) IOS_SDK=$(IOS_SDK) IOS_SDK_PATH=$(IOS_SDK_PATH) -# Build all iOS targets libwaku-ios: $(MAKE) libwaku-ios-device $(MAKE) libwaku-ios-simulator -cwaku_example: | build libwaku - echo -e $(BUILD_MSG) "build/$@" && \ - cc -o "build/$@" \ - ./examples/cbindings/waku_example.c \ - ./examples/cbindings/base64.c \ - -lwaku -Lbuild/ \ - -pthread -ldl -lm \ - -lminiupnpc -Lvendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/build/ \ - -lnatpmp -Lvendor/nim-nat-traversal/vendor/libnatpmp-upstream/ \ - vendor/nim-libbacktrace/libbacktrace_wrapper.o \ - vendor/nim-libbacktrace/install/usr/lib/libbacktrace.a - -cppwaku_example: | build libwaku - echo -e $(BUILD_MSG) "build/$@" && \ - g++ -o "build/$@" \ - ./examples/cpp/waku.cpp \ - ./examples/cpp/base64.cpp \ - -lwaku -Lbuild/ \ - -pthread -ldl -lm \ - -lminiupnpc -Lvendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/build/ \ - -lnatpmp -Lvendor/nim-nat-traversal/vendor/libnatpmp-upstream/ \ - vendor/nim-libbacktrace/libbacktrace_wrapper.o \ - vendor/nim-libbacktrace/install/usr/lib/libbacktrace.a - -nodejswaku: | build deps - echo -e $(BUILD_MSG) "build/$@" && \ - node-gyp build --directory=examples/nodejs/ - -endif # "variables.mk" was not included - ################### # Release Targets # ################### @@ -649,6 +617,4 @@ release-notes: -u $(shell id -u) \ docker.io/wakuorg/sv4git:latest \ release-notes |\ - sed -E 's@#([0-9]+)@[#\1](https://github.com/waku-org/nwaku/issues/\1)@g' -# I could not get the tool to replace issue ids with links, so using sed for now, -# asked here: https://github.com/bvieira/sv4git/discussions/101 + sed -E 's@#([0-9]+)@[#\1](https://github.com/waku-org/nwaku/issues/\1)@g' \ No newline at end of file diff --git a/Nat.mk b/Nat.mk new file mode 100644 index 000000000..31ad4e018 --- /dev/null +++ b/Nat.mk @@ -0,0 +1,54 @@ +# Copyright (c) 2022 Status Research & Development GmbH. Licensed under +# either of: +# - Apache License, version 2.0 +# - MIT license +# at your option. This file may not be copied, modified, or distributed except +# according to those terms. + +########################### +## nat-libs (nimbledeps) ## +########################### +# Builds miniupnpc and libnatpmp from the package installed by nimble under +# nimbledeps/pkgs2/. Used by `make update` / $(NIMBLEDEPS_STAMP). +# +# NAT_TRAVERSAL_NIMBLEDEPS_DIR is evaluated at parse time, so targets that +# depend on it must be invoked via a recursive $(MAKE) call so the sub-make +# re-evaluates the variable after nimble setup has populated nimbledeps/. +# +# `ls -dt` (sort by modification time, newest first) is used to pick the +# latest installed version and is portable across Linux, macOS, and +# Windows (MSYS/MinGW). + +NAT_TRAVERSAL_NIMBLEDEPS_DIR := $(shell ls -dt $(CURDIR)/nimbledeps/pkgs2/nat_traversal-* 2>/dev/null | head -1) + +.PHONY: clean-cross-nimbledeps rebuild-nat-libs-nimbledeps + +clean-cross-nimbledeps: +ifeq ($(NAT_TRAVERSAL_NIMBLEDEPS_DIR),) + $(error No nat_traversal package found under nimbledeps/pkgs2/ — run 'make update' first) +endif + + [ -e "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc" ] && \ + "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc" CC=$(CC) clean $(HANDLE_OUTPUT) || true + + [ -e "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/libnatpmp-upstream" ] && \ + "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/libnatpmp-upstream" CC=$(CC) clean $(HANDLE_OUTPUT) || true + +rebuild-nat-libs-nimbledeps: | clean-cross-nimbledeps +ifeq ($(NAT_TRAVERSAL_NIMBLEDEPS_DIR),) + $(error No nat_traversal package found under nimbledeps/pkgs2/ — run 'make update' first) +endif + @echo "Rebuilding nat-libs from $(NAT_TRAVERSAL_NIMBLEDEPS_DIR)" +ifeq ($(OS), Windows_NT) + + [ -e "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc/libminiupnpc.a" ] || \ + PATH=".;$${PATH}" "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc" \ + -f Makefile.mingw CC=$(CC) CFLAGS="-Os -fPIC" libminiupnpc.a $(HANDLE_OUTPUT) + + "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/libnatpmp-upstream" \ + OS=mingw CC=$(CC) \ + CFLAGS="-Wall -Wno-cpp -Os -fPIC -DWIN32 -DNATPMP_STATICLIB -DENABLE_STRNATPMPERR -DNATPMP_MAX_RETRIES=4 $(CFLAGS)" \ + libnatpmp.a $(HANDLE_OUTPUT) +else + + "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc" \ + CC=$(CC) CFLAGS="-Os -fPIC" build/libminiupnpc.a $(HANDLE_OUTPUT) + + "$(MAKE)" CFLAGS="-Wall -Wno-cpp -Os -fPIC -DENABLE_STRNATPMPERR -DNATPMP_MAX_RETRIES=4 $(CFLAGS)" \ + -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/libnatpmp-upstream" \ + CC=$(CC) libnatpmp.a $(HANDLE_OUTPUT) +endif diff --git a/apps/wakunode2/wakunode2.nim b/apps/wakunode2/wakunode2.nim index c8132ff4e..484adf68f 100644 --- a/apps/wakunode2/wakunode2.nim +++ b/apps/wakunode2/wakunode2.nim @@ -5,7 +5,6 @@ import chronicles, chronos, metrics, - libbacktrace, system/ansi_c, libp2p/crypto/crypto import @@ -88,7 +87,7 @@ when isMainModule: when defined(posix): proc handleSigsegv(signal: cint) {.noconv.} = # Require --debugger:native - fatal "Shutting down after receiving SIGSEGV", stacktrace = getBacktrace() + fatal "Shutting down after receiving SIGSEGV" # Not available in -d:release mode writeStackTrace() diff --git a/config.nims b/config.nims index 0655bf092..329384ac4 100644 --- a/config.nims +++ b/config.nims @@ -9,12 +9,6 @@ if defined(windows): switch("passL", "rln.lib") switch("define", "postgres=false") - # Automatically add all vendor subdirectories - for dir in walkDir("./vendor"): - if dir.kind == pcDir: - switch("path", dir.path) - switch("path", dir.path / "src") - # disable timestamps in Windows PE headers - https://wiki.debian.org/ReproducibleBuilds/TimestampsInPEBinaries switch("passL", "-Wl,--no-insert-timestamp") # increase stack size @@ -121,3 +115,8 @@ if defined(android): switch("passC", "--sysroot=" & sysRoot) switch("passL", "--sysroot=" & sysRoot) switch("cincludes", sysRoot & "/usr/include/") +# begin Nimble config (version 2) +when withDir(thisDir(), system.fileExists("nimble.paths")): + --noNimblePath + include "nimble.paths" +# end Nimble config diff --git a/env.sh b/env.sh deleted file mode 100755 index f90ba9a74..000000000 --- a/env.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# We use ${BASH_SOURCE[0]} instead of $0 to allow sourcing this file -# and we fall back to a Zsh-specific special var to also support Zsh. -REL_PATH="$(dirname ${BASH_SOURCE[0]:-${(%):-%x}})" -ABS_PATH="$(cd ${REL_PATH}; pwd)" -source ${ABS_PATH}/vendor/nimbus-build-system/scripts/env.sh - diff --git a/flake.lock b/flake.lock index b927e8807..9b5db728d 100644 --- a/flake.lock +++ b/flake.lock @@ -2,27 +2,48 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1757590060, - "narHash": "sha256-EWwwdKLMZALkgHFyKW7rmyhxECO74+N+ZO5xTDnY/5c=", + "lastModified": 1770464364, + "narHash": "sha256-z5NJPSBwsLf/OfD8WTmh79tlSU8XgIbwmk6qB1/TFzY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0ef228213045d2cdb5a169a95d63ded38670b293", + "rev": "23d72dabcb3b12469f57b37170fcbc1789bd7457", "type": "github" }, "original": { "owner": "NixOS", "repo": "nixpkgs", - "rev": "0ef228213045d2cdb5a169a95d63ded38670b293", + "rev": "23d72dabcb3b12469f57b37170fcbc1789bd7457", "type": "github" } }, "root": { "inputs": { "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", "zerokit": "zerokit" } }, "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775099554, + "narHash": "sha256-3xBsGnGDLOFtnPZ1D3j2LU19wpAlYefRKTlkv648rU0=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "8d6387ed6d8e6e6672fd3ed4b61b59d44b124d99", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { "inputs": { "nixpkgs": [ "zerokit", @@ -30,11 +51,11 @@ ] }, "locked": { - "lastModified": 1748399823, - "narHash": "sha256-kahD8D5hOXOsGbNdoLLnqCL887cjHkx98Izc37nDjlA=", + "lastModified": 1771211437, + "narHash": "sha256-lcNK438i4DGtyA+bPXXyVLHVmJjYpVKmpux9WASa3ro=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "d68a69dc71bc19beb3479800392112c2f6218159", + "rev": "c62195b3d6e1bb11e0c2fb2a494117d3b55d410f", "type": "github" }, "original": { @@ -48,21 +69,21 @@ "nixpkgs": [ "nixpkgs" ], - "rust-overlay": "rust-overlay" + "rust-overlay": "rust-overlay_2" }, "locked": { - "lastModified": 1762211504, - "narHash": "sha256-SbDoBElFYJ4cYebltxlO2lYnz6qOaDAVY6aNJ5bqHDE=", - "ref": "refs/heads/master", - "rev": "3160d9504d07791f2fc9b610948a6cf9a58ed488", - "revCount": 342, - "type": "git", - "url": "https://github.com/vacp2p/zerokit" + "lastModified": 1771279884, + "narHash": "sha256-tzkQPwSl4vPTUo1ixHh6NCENjsBDroMKTjifg2q8QX8=", + "owner": "vacp2p", + "repo": "zerokit", + "rev": "53b18098e6d5d046e3eb1ac338a8f4f651432477", + "type": "github" }, "original": { - "rev": "3160d9504d07791f2fc9b610948a6cf9a58ed488", - "type": "git", - "url": "https://github.com/vacp2p/zerokit" + "owner": "vacp2p", + "repo": "zerokit", + "rev": "53b18098e6d5d046e3eb1ac338a8f4f651432477", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index 13ca5e618..57592722b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,92 +1,70 @@ { - description = "Logos Messaging Nim build flake"; + description = "logos-delivery nim build flake"; nixConfig = { extra-substituters = [ "https://nix-cache.status.im/" ]; - extra-trusted-public-keys = [ "nix-cache.status.im-1:x/93lOfLU+duPplwMSBR+OlY4+mo+dCN7n0mr4oPwgY=" ]; + extra-trusted-public-keys = [ + "nix-cache.status.im-1:x/93lOfLU+duPplwMSBR+OlY4+mo+dCN7n0mr4oPwgY=" + ]; }; inputs = { - # Ensure Nix fetches git submodules (vendor/*) when evaluating this flake. - # Requires Nix >= 2.27. Consumers no longer need '?submodules=1' in the URL. - self.submodules = true; + # Pinning the commit to use same commit across different projects. + # A commit from nixpkgs 25.11 release: https://github.com/NixOS/nixpkgs/tree/release-25.11 + nixpkgs.url = "github:NixOS/nixpkgs?rev=23d72dabcb3b12469f57b37170fcbc1789bd7457"; - # We are pinning the commit because ultimately we want to use same commit across different projects. - # A commit from nixpkgs 24.11 release : https://github.com/NixOS/nixpkgs/tree/release-24.11 - nixpkgs.url = "github:NixOS/nixpkgs/0ef228213045d2cdb5a169a95d63ded38670b293"; - # WARNING: Remember to update commit and use 'nix flake update' to update flake.lock. + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # External flake input: Zerokit pinned to a specific commit. + # Update the rev here when a new zerokit version is needed. zerokit = { - url = "git+https://github.com/vacp2p/zerokit?rev=3160d9504d07791f2fc9b610948a6cf9a58ed488"; + url = "github:vacp2p/zerokit/53b18098e6d5d046e3eb1ac338a8f4f651432477"; inputs.nixpkgs.follows = "nixpkgs"; }; }; - outputs = { self, nixpkgs, zerokit }: + outputs = { self, nixpkgs, rust-overlay, zerokit }: let - stableSystems = [ + systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" - "x86_64-windows" "i686-linux" - "i686-windows" + "x86_64-windows" ]; - forAllSystems = f: nixpkgs.lib.genAttrs stableSystems (system: f system); + forAllSystems = nixpkgs.lib.genAttrs systems; - pkgsFor = forAllSystems ( - system: import nixpkgs { - inherit system; - config = { - android_sdk.accept_license = true; - allowUnfree = true; + pkgsFor = system: import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) ]; + }; + in { + packages = forAllSystems (system: + let + pkgs = pkgsFor system; + mkPkg = zerokitRln: import ./nix/default.nix { + inherit pkgs zerokitRln; + src = ./.; }; - overlays = [ - (final: prev: { - androidEnvCustom = prev.callPackage ./nix/pkgs/android-sdk { }; - androidPkgs = final.androidEnvCustom.pkgs; - androidShell = final.androidEnvCustom.shell; - }) - ]; + in rec { + liblogosdelivery = mkPkg zerokit.packages.${system}.rln; + default = liblogosdelivery; } ); - in rec { - packages = forAllSystems (system: let - pkgs = pkgsFor.${system}; - in rec { - libwaku-android-arm64 = pkgs.callPackage ./nix/default.nix { - inherit stableSystems; - src = self; - targets = ["libwaku-android-arm64"]; - abidir = "arm64-v8a"; - zerokitRln = zerokit.packages.${system}.rln-android-arm64; - }; - - libwaku = pkgs.callPackage ./nix/default.nix { - inherit stableSystems; - src = self; - targets = ["libwaku"]; - zerokitRln = zerokit.packages.${system}.rln; - }; - - wakucanary = pkgs.callPackage ./nix/default.nix { - inherit stableSystems; - src = self; - targets = ["wakucanary"]; - zerokitRln = zerokit.packages.${system}.rln; - }; - - liblogosdelivery = pkgs.callPackage ./nix/default.nix { - inherit stableSystems; - src = self; - targets = ["liblogosdelivery"]; - zerokitRln = zerokit.packages.${system}.rln; - }; - - default = libwaku; - }); - - devShells = forAllSystems (system: { - default = pkgsFor.${system}.callPackage ./nix/shell.nix {}; - }); + devShells = forAllSystems (system: + let + pkgs = pkgsFor system; + in { + default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + nim-2_2 + nimble + ]; + }; + } + ); }; } diff --git a/nimble.lock b/nimble.lock new file mode 100644 index 000000000..96f64baf3 --- /dev/null +++ b/nimble.lock @@ -0,0 +1,551 @@ +{ + "version": 2, + "packages": { + "unittest2": { + "version": "0.2.5", + "vcsRevision": "26f2ef3ae0ec72a2a75bfe557e02e88f6a31c189", + "url": "https://github.com/status-im/nim-unittest2", + "downloadMethod": "git", + "dependencies": [], + "checksums": { + "sha1": "02bb3751ba9ddc3c17bfd89f2e41cb6bfb8fc0c9" + } + }, + "bearssl": { + "version": "0.2.7", + "vcsRevision": "3b341f30d8c619b9a75c154243f9a55468a404e2", + "url": "https://github.com/status-im/nim-bearssl", + "downloadMethod": "git", + "dependencies": [ + "unittest2" + ], + "checksums": { + "sha1": "a85aab15b1b9a8b2438e9a128ac2eba41227da79" + } + }, + "bearssl_pkey_decoder": { + "version": "0.1.0", + "vcsRevision": "21dd3710df9345ed2ad8bf8f882761e07863b8e0", + "url": "https://github.com/vacp2p/bearssl_pkey_decoder", + "downloadMethod": "git", + "dependencies": [ + "bearssl" + ], + "checksums": { + "sha1": "21b42e2e6ddca6c875d3fc50f36a5115abf51714" + } + }, + "results": { + "version": "0.5.1", + "vcsRevision": "df8113dda4c2d74d460a8fa98252b0b771bf1f27", + "url": "https://github.com/arnetheduck/nim-results", + "downloadMethod": "git", + "dependencies": [], + "checksums": { + "sha1": "a9c011f74bc9ed5c91103917b9f382b12e82a9e7" + } + }, + "stew": { + "version": "0.5.0", + "vcsRevision": "4382b18f04b3c43c8409bfcd6b62063773b2bbaa", + "url": "https://github.com/status-im/nim-stew", + "downloadMethod": "git", + "dependencies": [ + "results", + "unittest2" + ], + "checksums": { + "sha1": "db22942939773ab7d5a0f2b2668c237240c67dd6" + } + }, + "faststreams": { + "version": "0.5.0", + "vcsRevision": "ce27581a3e881f782f482cb66dc5b07a02bd615e", + "url": "https://github.com/status-im/nim-faststreams", + "downloadMethod": "git", + "dependencies": [ + "stew", + "unittest2" + ], + "checksums": { + "sha1": "ee61e507b805ae1df7ec936f03f2d101b0d72383" + } + }, + "serialization": { + "version": "0.5.2", + "vcsRevision": "b0f2fa32960ea532a184394b0f27be37bd80248b", + "url": "https://github.com/status-im/nim-serialization", + "downloadMethod": "git", + "dependencies": [ + "faststreams", + "unittest2", + "stew" + ], + "checksums": { + "sha1": "fa35c1bb76a0a02a2379fe86eaae0957c7527cb8" + } + }, + "json_serialization": { + "version": "0.4.4", + "vcsRevision": "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44", + "url": "https://github.com/status-im/nim-json-serialization", + "downloadMethod": "git", + "dependencies": [ + "faststreams", + "serialization", + "stew", + "results" + ], + "checksums": { + "sha1": "8b3115354104858a0ac9019356fb29720529c2bd" + } + }, + "testutils": { + "version": "0.8.1", + "vcsRevision": "6ce5e5e2301ccbc04b09d27ff78741ff4d352b4d", + "url": "https://github.com/status-im/nim-testutils", + "downloadMethod": "git", + "dependencies": [ + "unittest2" + ], + "checksums": { + "sha1": "96a11cf8b84fa9bd12d4a553afa1cc4b7f9df4e3" + } + }, + "chronicles": { + "version": "0.12.2", + "vcsRevision": "27ec507429a4eb81edc20f28292ee8ec420be05b", + "url": "https://github.com/status-im/nim-chronicles", + "downloadMethod": "git", + "dependencies": [ + "faststreams", + "serialization", + "json_serialization", + "testutils" + ], + "checksums": { + "sha1": "02febb20d088120b2836d3306cfa21f434f88f65" + } + }, + "httputils": { + "version": "0.4.1", + "vcsRevision": "f142cb2e8bd812dd002a6493b6082827bb248592", + "url": "https://github.com/status-im/nim-http-utils", + "downloadMethod": "git", + "dependencies": [ + "stew", + "results", + "unittest2" + ], + "checksums": { + "sha1": "016774ab31c3afff9a423f7d80584905ee59c570" + } + }, + "chronos": { + "version": "4.2.2", + "vcsRevision": "45f43a9ad8bd8bcf5903b42f365c1c879bd54240", + "url": "https://github.com/status-im/nim-chronos", + "downloadMethod": "git", + "dependencies": [ + "results", + "stew", + "bearssl", + "httputils", + "unittest2" + ], + "checksums": { + "sha1": "3a4c9477df8cef20a04e4f1b54a2d74fdfc2a3d0" + } + }, + "confutils": { + "version": "0.1.0", + "vcsRevision": "7728f6bd81a1eedcfe277d02ea85fdb805bcc05a", + "url": "https://github.com/status-im/nim-confutils", + "downloadMethod": "git", + "dependencies": [ + "stew", + "serialization", + "results" + ], + "checksums": { + "sha1": "8bc8c30b107fdba73b677e5f257c6c42ae1cdc8e" + } + }, + "db_connector": { + "version": "0.1.0", + "vcsRevision": "29450a2063970712422e1ab857695c12d80112a6", + "url": "https://github.com/nim-lang/db_connector", + "downloadMethod": "git", + "dependencies": [], + "checksums": { + "sha1": "4f2e67d0e4b61af9ac5575509305660b473f01a4" + } + }, + "dnsclient": { + "version": "0.3.4", + "vcsRevision": "23214235d4784d24aceed99bbfe153379ea557c8", + "url": "https://github.com/ba0f3/dnsclient.nim", + "downloadMethod": "git", + "dependencies": [], + "checksums": { + "sha1": "65262c7e533ff49d6aca5539da4bc6c6ce132f40" + } + }, + "nimcrypto": { + "version": "0.6.4", + "vcsRevision": "721fb99ee099b632eb86dfad1f0d96ee87583774", + "url": "https://github.com/cheatfate/nimcrypto", + "downloadMethod": "git", + "dependencies": [], + "checksums": { + "sha1": "f9ab24fa940ed03d0fb09729a7303feb50b7eaec" + } + }, + "stint": { + "version": "0.8.2", + "vcsRevision": "470b7892561b5179ab20bd389a69217d6213fe58", + "url": "https://github.com/status-im/nim-stint", + "downloadMethod": "git", + "dependencies": [ + "stew", + "unittest2" + ], + "checksums": { + "sha1": "d8f871fd617e7857192d4609fe003b48942a8ae5" + } + }, + "secp256k1": { + "version": "0.6.0.3.2", + "vcsRevision": "d8f1288b7c72f00be5fc2c5ea72bf5cae1eafb15", + "url": "https://github.com/status-im/nim-secp256k1", + "downloadMethod": "git", + "dependencies": [ + "stew", + "results", + "nimcrypto" + ], + "checksums": { + "sha1": "6618ef9de17121846a8c1d0317026b0ce8584e10" + } + }, + "nat_traversal": { + "version": "0.0.1", + "vcsRevision": "860e18c37667b5dd005b94c63264560c35d88004", + "url": "https://github.com/status-im/nim-nat-traversal", + "downloadMethod": "git", + "dependencies": [ + "results" + ], + "checksums": { + "sha1": "1a376d3e710590ef2c48748a546369755f0a7c97" + } + }, + "metrics": { + "version": "0.2.1", + "vcsRevision": "a1296caf3ebb5f30f51a5feae7749a30df2824c2", + "url": "https://github.com/status-im/nim-metrics", + "downloadMethod": "git", + "dependencies": [ + "chronos", + "results", + "stew" + ], + "checksums": { + "sha1": "84bb09873d7677c06046f391c7b473cd2fcff8a2" + } + }, + "sqlite3_abi": { + "version": "3.52.0.0", + "vcsRevision": "4b79c5e1882b7fc6c00aec311daf1ed50ad653d5", + "url": "https://github.com/arnetheduck/nim-sqlite3-abi", + "downloadMethod": "git", + "dependencies": [], + "checksums": { + "sha1": "b56b489a7cb01eef8821d66d38d411923a14316d" + } + }, + "minilru": { + "version": "0.1.0", + "vcsRevision": "6dd93feb60f4cded3c05e7af7209cf63fb677893", + "url": "https://github.com/status-im/nim-minilru", + "downloadMethod": "git", + "dependencies": [ + "results", + "unittest2" + ], + "checksums": { + "sha1": "0be03a5da29fdd4409ea74a60fd0ccce882601b4" + } + }, + "snappy": { + "version": "0.1.0", + "vcsRevision": "00bfcef94f8ef6981df5d5b994897f6695badfb2", + "url": "https://github.com/status-im/nim-snappy", + "downloadMethod": "git", + "dependencies": [ + "faststreams", + "unittest2", + "results", + "stew" + ], + "checksums": { + "sha1": "e572d60d6a3178c5b1cde2400c51ad771812cd3d" + } + }, + "eth": { + "version": "0.9.0", + "vcsRevision": "d9135e6c3c5d6d819afdfb566aa8d958756b73a8", + "url": "https://github.com/status-im/nim-eth", + "downloadMethod": "git", + "dependencies": [ + "nimcrypto", + "stint", + "secp256k1", + "chronos", + "chronicles", + "stew", + "nat_traversal", + "metrics", + "sqlite3_abi", + "confutils", + "testutils", + "unittest2", + "results", + "minilru", + "snappy" + ], + "checksums": { + "sha1": "2e01b0cfff9523d110562af70d19948280f8013e" + } + }, + "dnsdisc": { + "version": "0.1.0", + "vcsRevision": "38f2e0f52c0a8f032ef4530835e519d550706d9e", + "url": "https://github.com/status-im/nim-dnsdisc", + "downloadMethod": "git", + "dependencies": [ + "bearssl", + "chronicles", + "chronos", + "eth", + "secp256k1", + "stew", + "testutils", + "unittest2", + "nimcrypto", + "results" + ], + "checksums": { + "sha1": "055b882a0f6b1d1e57a25a7af99d2e5ac6268154" + } + }, + "taskpools": { + "version": "0.1.0", + "vcsRevision": "9e8ccc754631ac55ac2fd495e167e74e86293edb", + "url": "https://github.com/status-im/nim-taskpools", + "downloadMethod": "git", + "dependencies": [], + "checksums": { + "sha1": "09e1b2fdad55b973724d61227971afc0df0b7a81" + } + }, + "ffi": { + "version": "0.1.3", + "vcsRevision": "06111de155253b34e47ed2aaed1d61d08d62cc1b", + "url": "https://github.com/logos-messaging/nim-ffi", + "downloadMethod": "git", + "dependencies": [ + "chronos", + "chronicles", + "taskpools" + ], + "checksums": { + "sha1": "6f9d49375ea1dc71add55c72ac80a808f238e5b0" + } + }, + "zlib": { + "version": "0.1.0", + "vcsRevision": "e680f269fb01af2c34a2ba879ff281795a5258fe", + "url": "https://github.com/status-im/nim-zlib", + "downloadMethod": "git", + "dependencies": [ + "stew", + "results" + ], + "checksums": { + "sha1": "bbde4f5a97a84b450fef7d107461e5f35cf2b47f" + } + }, + "websock": { + "version": "0.2.2", + "vcsRevision": "3918ce3900c83e1cc7496232a307709f195f7acd", + "url": "https://github.com/status-im/nim-websock", + "downloadMethod": "git", + "dependencies": [ + "chronos", + "httputils", + "chronicles", + "stew", + "nimcrypto", + "bearssl", + "results", + "zlib" + ], + "checksums": { + "sha1": "3c424661eff56c925b01e1cd1a911ff744e72962" + } + }, + "json_rpc": { + "version": "0.5.4", + "vcsRevision": "b6e40a776fa2d00b97a9366761fb7da18f31ae5c", + "url": "https://github.com/status-im/nim-json-rpc", + "downloadMethod": "git", + "dependencies": [ + "stew", + "nimcrypto", + "stint", + "chronos", + "httputils", + "chronicles", + "websock", + "serialization", + "json_serialization", + "unittest2" + ], + "checksums": { + "sha1": "d8e8be795fcf098f4ce03b5826f6b3153f6a6e07" + } + }, + "jwt": { + "version": "0.2", + "vcsRevision": "18f8378de52b241f321c1f9ea905456e89b95c6f", + "url": "https://github.com/vacp2p/nim-jwt.git", + "downloadMethod": "git", + "dependencies": [ + "bearssl", + "bearssl_pkey_decoder" + ], + "checksums": { + "sha1": "bcfd6fc9c5e10a52b87117219b7ab5c98136bc8e" + } + }, + "lsquic": { + "version": "0.0.1", + "vcsRevision": "4fb03ee7bfb39aecb3316889fdcb60bec3d0936f", + "url": "https://github.com/vacp2p/nim-lsquic", + "downloadMethod": "git", + "dependencies": [ + "zlib", + "stew", + "chronos", + "nimcrypto", + "unittest2", + "chronicles" + ], + "checksums": { + "sha1": "f465fa994346490d0924d162f53d9b5aec62f948" + } + }, + "libp2p": { + "version": "1.15.2", + "vcsRevision": "ff8d51857b4b79a68468e7bcc27b2026cca02996", + "url": "https://github.com/vacp2p/nim-libp2p.git", + "downloadMethod": "git", + "dependencies": [ + "nimcrypto", + "dnsclient", + "bearssl", + "chronicles", + "chronos", + "metrics", + "secp256k1", + "stew", + "websock", + "unittest2", + "results", + "serialization", + "lsquic", + "jwt" + ], + "checksums": { + "sha1": "fa2a7552c6ec860717b77ce34cf0b7afe4570234" + } + }, + "presto": { + "version": "0.1.1", + "vcsRevision": "d66043dd7ede146442e6c39720c76a20bde5225f", + "url": "https://github.com/status-im/nim-presto", + "downloadMethod": "git", + "dependencies": [ + "chronos", + "chronicles", + "metrics", + "results", + "stew" + ], + "checksums": { + "sha1": "8df97c45683abe2337bdff43b844c4fbcc124ca2" + } + }, + "unicodedb": { + "version": "0.13.2", + "vcsRevision": "66f2458710dc641dd4640368f9483c8a0ec70561", + "url": "https://github.com/nitely/nim-unicodedb", + "downloadMethod": "git", + "dependencies": [], + "checksums": { + "sha1": "739102d885d99bb4571b1955f5f12aee423c935b" + } + }, + "regex": { + "version": "0.26.3", + "vcsRevision": "4593305ed1e49731fc75af1dc572dd2559aad19c", + "url": "https://github.com/nitely/nim-regex", + "downloadMethod": "git", + "dependencies": [ + "unicodedb" + ], + "checksums": { + "sha1": "4d24e7d7441137cd202e16f2359a5807ddbdc31f" + } + }, + "toml_serialization": { + "version": "0.2.18", + "vcsRevision": "b5b387e6fb2a7cc75d54a269b07cc6218361bd46", + "url": "https://github.com/status-im/nim-toml-serialization", + "downloadMethod": "git", + "dependencies": [ + "faststreams", + "serialization", + "stew" + ], + "checksums": { + "sha1": "76ae1c2af5dd092849b41750ff29217980dc9ca3" + } + }, + "web3": { + "version": "0.8.0", + "vcsRevision": "cdfe5601d2812a58e54faf53ee634452d01e5918", + "url": "https://github.com/status-im/nim-web3", + "downloadMethod": "git", + "dependencies": [ + "chronicles", + "chronos", + "bearssl", + "eth", + "faststreams", + "json_rpc", + "serialization", + "json_serialization", + "nimcrypto", + "stew", + "stint", + "results" + ], + "checksums": { + "sha1": "26a112af032ef1536f97da2ca7364af618a11b80" + } + } + }, + "tasks": {} +} diff --git a/nix/default.nix b/nix/default.nix index 816d0aed8..f90b8185e 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,149 +1,101 @@ -{ - pkgs, - src ? ../., - targets ? ["libwaku-android-arm64"], - verbosity ? 1, - useSystemNim ? true, - quickAndDirty ? true, - stableSystems ? [ - "x86_64-linux" "aarch64-linux" - ], - abidir ? null, - zerokitRln, -}: - -assert pkgs.lib.assertMsg (builtins.pathExists "${src}/vendor/nimbus-build-system/scripts") - "Unable to build without submodules. Append '?submodules=1#' to the URI."; +{ pkgs, src, zerokitRln }: let - inherit (pkgs) stdenv lib writeScriptBin callPackage; + deps = import ./deps.nix { inherit pkgs; }; - androidManifest = ""; + # nat_traversal is excluded from the static pathArgs; it is handled + # separately in buildPhase (its bundled C libs must be compiled first). + otherDeps = builtins.removeAttrs deps [ "nat_traversal" ]; - tools = pkgs.callPackage ./tools.nix {}; - version = tools.findKeyValue "^version = \"([a-f0-9.-]+)\"$" ../waku.nimble; - revision = lib.substring 0 8 (src.rev or src.dirtyRev or "00000000"); - copyLibwaku = lib.elem "libwaku" targets; - copyLiblogosdelivery = lib.elem "liblogosdelivery" targets; - copyWakunode2 = lib.elem "wakunode2" targets; - hasKnownInstallTarget = copyLibwaku || copyLiblogosdelivery || copyWakunode2; + # Some packages (e.g. regex, unicodedb) put their .nim files under src/ + # while others use the repo root. Pass both so the compiler finds either layout. + pathArgs = + builtins.concatStringsSep " " + (builtins.concatMap (p: [ "--path:${p}" "--path:${p}/src" ]) + (builtins.attrValues otherDeps)); -in stdenv.mkDerivation { - pname = "logos-messaging-nim"; - version = "${version}-${revision}"; + libExt = + if pkgs.stdenv.hostPlatform.isWindows then "dll" + else if pkgs.stdenv.hostPlatform.isDarwin then "dylib" + else "so"; +in +pkgs.stdenv.mkDerivation { + pname = "liblogosdelivery"; + version = "dev"; inherit src; - # Runtime dependencies - buildInputs = with pkgs; [ - openssl gmp zip - ]; + nativeBuildInputs = with pkgs; [ + nim-2_2 + git + gnumake + which + ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ pkgs.darwin.cctools ]; - # Dependencies that should only exist in the build environment. - nativeBuildInputs = let - # Fix for Nim compiler calling 'git rev-parse' and 'lsb_release'. - fakeGit = writeScriptBin "git" "echo ${version}"; - in with pkgs; [ - cmake which zerokitRln nim-unwrapped-2_2 fakeGit - ] ++ lib.optionals stdenv.isDarwin [ - pkgs.darwin.cctools gcc # Necessary for libbacktrace - ]; + buildInputs = [ zerokitRln ]; - # Environment variables required for Android builds - ANDROID_SDK_ROOT = "${pkgs.androidPkgs.sdk}"; - ANDROID_NDK_HOME = "${pkgs.androidPkgs.ndk}"; - NIMFLAGS = "-d:disableMarchNative -d:git_revision_override=${revision}"; - XDG_CACHE_HOME = "/tmp"; + buildPhase = '' + export HOME=$TMPDIR + export XDG_CACHE_HOME=$TMPDIR/.cache + export NIMBLE_DIR=$TMPDIR/.nimble + export NIMCACHE=$TMPDIR/nimcache - makeFlags = targets ++ [ - "V=${toString verbosity}" - "QUICK_AND_DIRTY_COMPILER=${if quickAndDirty then "1" else "0"}" - "QUICK_AND_DIRTY_NIMBLE=${if quickAndDirty then "1" else "0"}" - "USE_SYSTEM_NIM=${if useSystemNim then "1" else "0"}" - "LIBRLN_FILE=${zerokitRln}/lib/librln.${if abidir != null then "so" else "a"}" - "POSTGRES=1" - ]; + mkdir -p build $NIMCACHE - configurePhase = '' - patchShebangs . vendor/nimbus-build-system > /dev/null + # nat_traversal bundles C sub-libraries that must be compiled before linking. + # Copy the fetchgit store path to a writable tmpdir, build, then pass to nim. + NAT_TRAV=$TMPDIR/nat_traversal + cp -r ${deps.nat_traversal} $NAT_TRAV + chmod -R +w $NAT_TRAV - # build_nim.sh guards "rm -rf dist/checksums" with NIX_BUILD_TOP != "/build", - # but on macOS the nix sandbox uses /private/tmp/... so the check fails and - # dist/checksums (provided via preBuild) gets deleted. Fix the check to skip - # the removal whenever NIX_BUILD_TOP is set (i.e. any nix build). - substituteInPlace vendor/nimbus-build-system/scripts/build_nim.sh \ - --replace 'if [[ "''${NIX_BUILD_TOP}" != "/build" ]]; then' \ - 'if [[ -z "''${NIX_BUILD_TOP}" ]]; then' + make -C $NAT_TRAV/vendor/miniupnp/miniupnpc \ + CFLAGS="-Os -fPIC" build/libminiupnpc.a - make nimbus-build-system-paths - make nimbus-build-system-nimble-dir + make -C $NAT_TRAV/vendor/libnatpmp-upstream \ + CFLAGS="-Wall -Os -fPIC -DENABLE_STRNATPMPERR -DNATPMP_MAX_RETRIES=4" libnatpmp.a + + echo "== Building liblogosdelivery (dynamic) ==" + nim c \ + --noNimblePath \ + ${pathArgs} \ + --path:$NAT_TRAV \ + --path:$NAT_TRAV/src \ + --passL:"-L${zerokitRln}/lib -lrln" \ + --define:disable_libbacktrace \ + --out:build/liblogosdelivery.${libExt} \ + --app:lib \ + --threads:on \ + --opt:size \ + --noMain \ + --mm:refc \ + --header \ + --nimMainPrefix:liblogosdelivery \ + --nimcache:$NIMCACHE \ + liblogosdelivery/liblogosdelivery.nim + + echo "== Building liblogosdelivery (static) ==" + nim c \ + --noNimblePath \ + ${pathArgs} \ + --path:$NAT_TRAV \ + --path:$NAT_TRAV/src \ + --passL:"-L${zerokitRln}/lib -lrln" \ + --define:disable_libbacktrace \ + --out:build/liblogosdelivery.a \ + --app:staticlib \ + --threads:on \ + --opt:size \ + --noMain \ + --mm:refc \ + --nimMainPrefix:liblogosdelivery \ + --nimcache:$NIMCACHE \ + liblogosdelivery/liblogosdelivery.nim ''; - # For the Nim v2.2.4 built with NBS we added sat and zippy - preBuild = lib.optionalString (!useSystemNim) '' - pushd vendor/nimbus-build-system/vendor/Nim - mkdir dist - mkdir -p dist/nimble/vendor/sat - mkdir -p dist/nimble/vendor/checksums - mkdir -p dist/nimble/vendor/zippy - - cp -r ${callPackage ./nimble.nix {}}/. dist/nimble - cp -r ${callPackage ./checksums.nix {}}/. dist/checksums - cp -r ${callPackage ./csources.nix {}}/. csources_v2 - cp -r ${callPackage ./sat.nix {}}/. dist/nimble/vendor/sat - cp -r ${callPackage ./checksums.nix {}}/. dist/nimble/vendor/checksums - cp -r ${callPackage ./zippy.nix {}}/. dist/nimble/vendor/zippy - chmod 777 -R dist/nimble csources_v2 - popd - ''; - - installPhase = if abidir != null then '' - mkdir -p $out/jni - cp -r ./build/android/${abidir}/* $out/jni/ - echo '${androidManifest}' > $out/jni/AndroidManifest.xml - cd $out && zip -r libwaku.aar * - '' else '' - mkdir -p $out/bin $out/include - - # Copy artifacts from build directory (created by Make during buildPhase) - # Note: build/ is in the source tree, not result/ (which is a post-build symlink) - if [ -d build ]; then - ${lib.optionalString copyLibwaku '' - cp build/libwaku.{so,dylib,dll,a,lib} $out/bin/ 2>/dev/null || true - ''} - - ${lib.optionalString copyLiblogosdelivery '' - cp build/liblogosdelivery.{so,dylib,dll,a,lib} $out/bin/ 2>/dev/null || true - ''} - - ${lib.optionalString copyWakunode2 '' - cp build/wakunode2 $out/bin/ 2>/dev/null || true - ''} - - ${lib.optionalString (!hasKnownInstallTarget) '' - cp build/lib*.{so,dylib,dll,a,lib} $out/bin/ 2>/dev/null || true - ''} - fi - - # Copy header files - ${lib.optionalString copyLibwaku '' - cp library/libwaku.h $out/include/ 2>/dev/null || true - ''} - - ${lib.optionalString copyLiblogosdelivery '' + installPhase = '' + mkdir -p $out/lib $out/include + cp build/liblogosdelivery.${libExt} $out/lib/ 2>/dev/null || true + cp build/liblogosdelivery.a $out/lib/ 2>/dev/null || true cp liblogosdelivery/liblogosdelivery.h $out/include/ 2>/dev/null || true - ''} - - ${lib.optionalString (!hasKnownInstallTarget) '' - cp library/libwaku.h $out/include/ 2>/dev/null || true - cp liblogosdelivery/liblogosdelivery.h $out/include/ 2>/dev/null || true - ''} ''; - - meta = with pkgs.lib; { - description = "NWaku derivation to build libwaku for mobile targets using Android NDK and Rust."; - homepage = "https://github.com/status-im/nwaku"; - license = licenses.mit; - platforms = stableSystems; - }; } diff --git a/nix/deps.nix b/nix/deps.nix new file mode 100644 index 000000000..2f30a572c --- /dev/null +++ b/nix/deps.nix @@ -0,0 +1,272 @@ +# AUTOGENERATED from nimble.lock — do not edit manually. +# Regenerate with: ./tools/gen-nix-deps.sh nimble.lock nix/deps.nix +{ pkgs }: + +{ + unittest2 = pkgs.fetchgit { + url = "https://github.com/status-im/nim-unittest2"; + rev = "26f2ef3ae0ec72a2a75bfe557e02e88f6a31c189"; + sha256 = "1n8n36kad50m97b64y7bzzknz9n7szffxhp0bqpk3g2v7zpda8sw"; + fetchSubmodules = true; + }; + + bearssl = pkgs.fetchgit { + url = "https://github.com/status-im/nim-bearssl"; + rev = "3b341f30d8c619b9a75c154243f9a55468a404e2"; + sha256 = "059avc2dh39vv9c3a1qayah98fjm5pw04r3dn2bqrgs6vf7licmv"; + fetchSubmodules = true; + }; + + bearssl_pkey_decoder = pkgs.fetchgit { + url = "https://github.com/vacp2p/bearssl_pkey_decoder"; + rev = "21dd3710df9345ed2ad8bf8f882761e07863b8e0"; + sha256 = "0bl3f147zmkazbhdkr4cj1nipf9rqiw3g4hh1j424k9hpl55zdpg"; + fetchSubmodules = true; + }; + + results = pkgs.fetchgit { + url = "https://github.com/arnetheduck/nim-results"; + rev = "df8113dda4c2d74d460a8fa98252b0b771bf1f27"; + sha256 = "1h7amas16sbhlr7zb7n3jb5434k98ji375vzw72k1fsc86vnmcr9"; + fetchSubmodules = true; + }; + + stew = pkgs.fetchgit { + url = "https://github.com/status-im/nim-stew"; + rev = "4382b18f04b3c43c8409bfcd6b62063773b2bbaa"; + sha256 = "0mx9g5m636h3sk5pllcpylk51brf7lx91izx3gc23k3ih3hrxyk2"; + fetchSubmodules = true; + }; + + faststreams = pkgs.fetchgit { + url = "https://github.com/status-im/nim-faststreams"; + rev = "ce27581a3e881f782f482cb66dc5b07a02bd615e"; + sha256 = "0y6bw2scnmr8cxj4fg18w7f34l2bh9qwg5nhlgd84m9fpr5bqarn"; + fetchSubmodules = true; + }; + + serialization = pkgs.fetchgit { + url = "https://github.com/status-im/nim-serialization"; + rev = "b0f2fa32960ea532a184394b0f27be37bd80248b"; + sha256 = "0wip1fjx7ka39ck1g1xvmyarzq1p5dlngpqil6zff8k8z5skiz27"; + fetchSubmodules = true; + }; + + json_serialization = pkgs.fetchgit { + url = "https://github.com/status-im/nim-json-serialization"; + rev = "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44"; + sha256 = "0i8sq51nqj8lshf6bfixaz9a7sq0ahsbvq3chkxdvv4khsqvam91"; + fetchSubmodules = true; + }; + + testutils = pkgs.fetchgit { + url = "https://github.com/status-im/nim-testutils"; + rev = "6ce5e5e2301ccbc04b09d27ff78741ff4d352b4d"; + sha256 = "1vbkr6i5yxhc2ai3b7rbglhmyc98f99x874fqdp6a152a6kqgwxy"; + fetchSubmodules = true; + }; + + chronicles = pkgs.fetchgit { + url = "https://github.com/status-im/nim-chronicles"; + rev = "27ec507429a4eb81edc20f28292ee8ec420be05b"; + sha256 = "1xx9fcfwgcaizq3s7i3s03mclz253r5j8va38l9ycl19fcbc96z9"; + fetchSubmodules = true; + }; + + httputils = pkgs.fetchgit { + url = "https://github.com/status-im/nim-http-utils"; + rev = "f142cb2e8bd812dd002a6493b6082827bb248592"; + sha256 = "03msj4zdxraz4qx9cidb17g7v0asazxv91nng6xxbzjxz0qaqxw6"; + fetchSubmodules = true; + }; + + chronos = pkgs.fetchgit { + url = "https://github.com/status-im/nim-chronos"; + rev = "45f43a9ad8bd8bcf5903b42f365c1c879bd54240"; + sha256 = "1v1n59zfzznp97pvwgs9kf136bqmv4x2s2y9f24msspa7qv27w39"; + fetchSubmodules = true; + }; + + confutils = pkgs.fetchgit { + url = "https://github.com/status-im/nim-confutils"; + rev = "7728f6bd81a1eedcfe277d02ea85fdb805bcc05a"; + sha256 = "18bj1ilx10jm2vmqx2wy2xl9rzy7alymi2m4n9jgpa4sbxnfh0x3"; + fetchSubmodules = true; + }; + + db_connector = pkgs.fetchgit { + url = "https://github.com/nim-lang/db_connector"; + rev = "29450a2063970712422e1ab857695c12d80112a6"; + sha256 = "11dna09ccdhj3pzpqa04j7a95ibx907z6n1ff33yf0n92qa4x59z"; + fetchSubmodules = true; + }; + + dnsclient = pkgs.fetchgit { + url = "https://github.com/ba0f3/dnsclient.nim"; + rev = "23214235d4784d24aceed99bbfe153379ea557c8"; + sha256 = "03mf3lw5c0m5nq9ppa49nylrl8ibkv2zzlc0wyhqg7w09kz6hks6"; + fetchSubmodules = true; + }; + + nimcrypto = pkgs.fetchgit { + url = "https://github.com/cheatfate/nimcrypto"; + rev = "721fb99ee099b632eb86dfad1f0d96ee87583774"; + sha256 = "178vzb3q8wzjq295ik2pd25rrqf32w381ck76hm5x2d8qnzfmkkc"; + fetchSubmodules = true; + }; + + stint = pkgs.fetchgit { + url = "https://github.com/status-im/nim-stint"; + rev = "470b7892561b5179ab20bd389a69217d6213fe58"; + sha256 = "1isfwmbj98qfi5pm9acy0yyvq0vlz38nxp30xl43jx2mmaga2w22"; + fetchSubmodules = true; + }; + + secp256k1 = pkgs.fetchgit { + url = "https://github.com/status-im/nim-secp256k1"; + rev = "d8f1288b7c72f00be5fc2c5ea72bf5cae1eafb15"; + sha256 = "1qjrmwbngb73f6r1fznvig53nyal7wj41d1cmqfksrmivk2sgrn2"; + fetchSubmodules = true; + }; + + nat_traversal = pkgs.fetchgit { + url = "https://github.com/status-im/nim-nat-traversal"; + rev = "860e18c37667b5dd005b94c63264560c35d88004"; + sha256 = "0319k5bbl468phwfnvlrh7725sc80rnf7m9gyj0i3cb5hb9q78bs"; + fetchSubmodules = true; + }; + + metrics = pkgs.fetchgit { + url = "https://github.com/status-im/nim-metrics"; + rev = "a1296caf3ebb5f30f51a5feae7749a30df2824c2"; + sha256 = "02vxqy20g8012ks939ac25ksc25k727q84si0p2cmihy5bw1a3qm"; + fetchSubmodules = true; + }; + + sqlite3_abi = pkgs.fetchgit { + url = "https://github.com/arnetheduck/nim-sqlite3-abi"; + rev = "4b79c5e1882b7fc6c00aec311daf1ed50ad653d5"; + sha256 = "0qa6p2vnxmf6r2w19mfydr5rzv7bg1lfxccnpdhk0akzxnc7i5gy"; + fetchSubmodules = true; + }; + + minilru = pkgs.fetchgit { + url = "https://github.com/status-im/nim-minilru"; + rev = "6dd93feb60f4cded3c05e7af7209cf63fb677893"; + sha256 = "1xgx4j56ais3hk8b51zhnfs9q85g2afkp3y1j9ky5iziqvcs2sml"; + fetchSubmodules = true; + }; + + snappy = pkgs.fetchgit { + url = "https://github.com/status-im/nim-snappy"; + rev = "00bfcef94f8ef6981df5d5b994897f6695badfb2"; + sha256 = "117mam97mkjjj1hs8svc07679k5ayww9yigi74yq8dyqm6fpbl6l"; + fetchSubmodules = true; + }; + + eth = pkgs.fetchgit { + url = "https://github.com/status-im/nim-eth"; + rev = "d9135e6c3c5d6d819afdfb566aa8d958756b73a8"; + sha256 = "15r6aszalnbk6mkyfbv5rnz5vcf1mmgj6yg332wry53xsd2ipg7r"; + fetchSubmodules = true; + }; + + dnsdisc = pkgs.fetchgit { + url = "https://github.com/status-im/nim-dnsdisc"; + rev = "38f2e0f52c0a8f032ef4530835e519d550706d9e"; + sha256 = "0dk787ny49n41bmzhlrvm87giwajr01gwdw9nlmphch89rdqpxxn"; + fetchSubmodules = true; + }; + + taskpools = pkgs.fetchgit { + url = "https://github.com/status-im/nim-taskpools"; + rev = "9e8ccc754631ac55ac2fd495e167e74e86293edb"; + sha256 = "1y78l33vdjxmb9dkr455pbphxa73rgdsh8m9gpkf4d9b1wm1yivy"; + fetchSubmodules = true; + }; + + ffi = pkgs.fetchgit { + url = "https://github.com/logos-messaging/nim-ffi"; + rev = "06111de155253b34e47ed2aaed1d61d08d62cc1b"; + sha256 = "0rb0d2i519amgsp7q0bn6m5465z1vwj4rab89529pyiivh3fgh8j"; + fetchSubmodules = true; + }; + + zlib = pkgs.fetchgit { + url = "https://github.com/status-im/nim-zlib"; + rev = "e680f269fb01af2c34a2ba879ff281795a5258fe"; + sha256 = "1xw9f1gjsgqihdg7kdkbaq1wankgnx2vn9l3ihc6nqk2jzv5bvk5"; + fetchSubmodules = true; + }; + + websock = pkgs.fetchgit { + url = "https://github.com/status-im/nim-websock"; + rev = "3918ce3900c83e1cc7496232a307709f195f7acd"; + sha256 = "16zvdjyasfpb04708d072rpvg12pyz3gmszi3md5brmlhbc3x8jp"; + fetchSubmodules = true; + }; + + json_rpc = pkgs.fetchgit { + url = "https://github.com/status-im/nim-json-rpc"; + rev = "b6e40a776fa2d00b97a9366761fb7da18f31ae5c"; + sha256 = "0c86glijpzcxdb5fagdk98hm9dmsrgw179nn3ixbapl48pvly9nr"; + fetchSubmodules = true; + }; + + jwt = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-jwt.git"; + rev = "18f8378de52b241f321c1f9ea905456e89b95c6f"; + sha256 = "1986czmszdxj6g9yr7xn1fx8y2y9mwpb3f1bn9nc6973qawsdm0p"; + fetchSubmodules = true; + }; + + lsquic = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-lsquic"; + rev = "4fb03ee7bfb39aecb3316889fdcb60bec3d0936f"; + sha256 = "0qdhcd4hyp185szc9sv3jvwdwc9zp3j0syy7glxv13k9bchfmkfg"; + fetchSubmodules = true; + }; + + libp2p = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-libp2p.git"; + rev = "ff8d51857b4b79a68468e7bcc27b2026cca02996"; + sha256 = "08y4s0zhqzsd780bwaixfqbi79km0mcq5g8nyw7awfvcbjqsa53l"; + fetchSubmodules = true; + }; + + presto = pkgs.fetchgit { + url = "https://github.com/status-im/nim-presto"; + rev = "d66043dd7ede146442e6c39720c76a20bde5225f"; + sha256 = "1hrppcak32aigrdv3mqk124w81yy9jv1prs57vqqhfj83gl930vi"; + fetchSubmodules = true; + }; + + unicodedb = pkgs.fetchgit { + url = "https://github.com/nitely/nim-unicodedb"; + rev = "66f2458710dc641dd4640368f9483c8a0ec70561"; + sha256 = "092z3glgdb7rmwajm7dmqzvralkm7ixighixk8ycf8sf17zm72ck"; + fetchSubmodules = true; + }; + + regex = pkgs.fetchgit { + url = "https://github.com/nitely/nim-regex"; + rev = "4593305ed1e49731fc75af1dc572dd2559aad19c"; + sha256 = "1b666qws5sva3n5allin0ycvnqlzdjd7xzprpdvv632ccqddzcl9"; + fetchSubmodules = true; + }; + + toml_serialization = pkgs.fetchgit { + url = "https://github.com/status-im/nim-toml-serialization"; + rev = "b5b387e6fb2a7cc75d54a269b07cc6218361bd46"; + sha256 = "175swdj01rz57h1hvflkyaz4x76qbfn0174ysrk3qk385i1zlg5z"; + fetchSubmodules = true; + }; + + web3 = pkgs.fetchgit { + url = "https://github.com/status-im/nim-web3"; + rev = "cdfe5601d2812a58e54faf53ee634452d01e5918"; + sha256 = "1j52vcqw868qs40bh4wzfw5cvvnywp2q0dnzhfajh31xws98jc27"; + fetchSubmodules = true; + }; + +} diff --git a/nix/nimble.nix b/nix/nimble.nix deleted file mode 100644 index 337ecd672..000000000 --- a/nix/nimble.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ pkgs ? import { } }: - -let - tools = pkgs.callPackage ./tools.nix {}; - sourceFile = ../vendor/nimbus-build-system/vendor/Nim/koch.nim; -in pkgs.fetchFromGitHub { - owner = "nim-lang"; - repo = "nimble"; - rev = tools.findKeyValue "^ +NimbleStableCommit = \"([a-f0-9]+)\".*$" sourceFile; - # WARNING: Requires manual updates when Nim compiler version changes. - hash = "sha256-8iutVgNzDtttZ7V+7S11KfLEuwhKA9TsgS51mlUI08k="; -} diff --git a/nix/shell.nix b/nix/shell.nix index 3b83ac93d..80e3b7930 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -14,6 +14,7 @@ pkgs.mkShell { rustup rustc cmake - nim-unwrapped-2_2 + nim-2_2 + nimble ]; } diff --git a/tests/tools/test_confutils_envvar.nim b/tests/tools/test_confutils_envvar.nim index ed559ad0b..76d9ddd31 100644 --- a/tests/tools/test_confutils_envvar.nim +++ b/tests/tools/test_confutils_envvar.nim @@ -19,7 +19,7 @@ type TestConf = object Option[InputFile] listenAddress* {. - defaultValue: parseIpAddress("127.0.0.1"), + defaultValue: IpAddress(family: IpAddressFamily.IPv4, address_v4: [127u8, 0, 0, 1]), desc: "Listening address", name: "listen-address" .}: IpAddress @@ -62,9 +62,15 @@ suite "nim-confutils - envvar": ## Then check confLoadRes.isOk() + let parsedIpAddress = + try: + parseIpAddress(listenAddress) + except ValueError: + IpAddress(family: IpAddressFamily.IPv4, address_v4: [0u8, 0, 0, 0]) + let conf = confLoadRes.get() check: - conf.listenAddress == parseIpAddress(listenAddress) + conf.listenAddress == parsedIpAddress conf.tcpPort == Port(8080) conf.configFile.isSome() diff --git a/tests/waku_core/test_peers.nim b/tests/waku_core/test_peers.nim index 59ae2e2f3..0ba3e5b04 100644 --- a/tests/waku_core/test_peers.nim +++ b/tests/waku_core/test_peers.nim @@ -1,5 +1,6 @@ {.used.} +import std/options import results, testutils/unittests, diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index a99ba43ee..5c1934712 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -251,7 +251,10 @@ type WakuNodeConf* = object dnsAddrsNameServers* {. desc: "DNS name server IPs to query for DNS multiaddrs resolution. Argument may be repeated.", - defaultValue: @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")], + defaultValue: @[ + IpAddress(family: IpAddressFamily.IPv4, address_v4: [1'u8, 1, 1, 1]), + IpAddress(family: IpAddressFamily.IPv4, address_v4: [1'u8, 0, 0, 1]), + ], name: "dns-addrs-name-server" .}: seq[IpAddress] @@ -480,7 +483,8 @@ with the drawback of consuming some more bandwidth.""", restAddress* {. desc: "Listening address of the REST HTTP server.", - defaultValue: parseIpAddress("127.0.0.1"), + defaultValue: + IpAddress(family: IpAddressFamily.IPv4, address_v4: [127'u8, 0, 0, 1]), name: "rest-address" .}: IpAddress @@ -520,7 +524,8 @@ with the drawback of consuming some more bandwidth.""", metricsServerAddress* {. desc: "Listening address of the metrics server.", - defaultValue: parseIpAddress("127.0.0.1"), + defaultValue: + IpAddress(family: IpAddressFamily.IPv4, address_v4: [127'u8, 0, 0, 1]), name: "metrics-server-address" .}: IpAddress @@ -774,7 +779,7 @@ proc completeCmdArg*(T: type IpAddress, val: string): seq[string] = proc defaultListenAddress*(): IpAddress = # TODO: Should probably listen on both ipv4 and ipv6 by default. - (static parseIpAddress("0.0.0.0")) + (static IpAddress(family: IpAddressFamily.IPv4, address_v4: [0'u8, 0, 0, 0])) proc defaultColocationLimit*(): int = return DefaultColocationLimit diff --git a/tools/gen-nix-deps.sh b/tools/gen-nix-deps.sh new file mode 100755 index 000000000..9bb43e638 --- /dev/null +++ b/tools/gen-nix-deps.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Generates nix/deps.nix from nimble.lock using nix-prefetch-git. +# Usage: ./tools/gen-nix-deps.sh [nimble.lock] [nix/deps.nix] +set -euo pipefail + +usage() { + cat < + +Example: + $0 nimble.lock nix/deps.nix +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage; exit 0 +fi + +if [[ $# -ne 2 ]]; then + usage; exit 1 +fi + +LOCKFILE="$1" +OUTFILE="$2" + +command -v jq >/dev/null || { echo "error: jq required"; exit 1; } +command -v nix-prefetch-git >/dev/null || { echo "error: nix-prefetch-git required"; exit 1; } + +if [[ ! -f "$LOCKFILE" ]]; then + echo "[!] $LOCKFILE not found" + echo "[*] Generating $LOCKFILE via 'nimble lock'" + nimble lock +fi + +echo "[*] Generating $OUTFILE from $LOCKFILE" +mkdir -p "$(dirname "$OUTFILE")" + +cat > "$OUTFILE" <<'EOF' +# AUTOGENERATED from nimble.lock — do not edit manually. +# Regenerate with: ./tools/gen-nix-deps.sh nimble.lock nix/deps.nix +{ pkgs }: + +{ +EOF + +jq -c ' + .packages + | to_entries[] + | select(.value.downloadMethod == "git") + | select(.key != "nim" and .key != "nimble") +' "$LOCKFILE" | while read -r entry; do + name=$(jq -r '.key' <<<"$entry") + url=$(jq -r '.value.url' <<<"$entry") + rev=$(jq -r '.value.vcsRevision' <<<"$entry") + + echo " [*] Prefetching $name @ $rev" + + sha=$(nix-prefetch-git \ + --url "$url" \ + --rev "$rev" \ + --fetch-submodules \ + | jq -r '.sha256') + + cat >> "$OUTFILE" <> "$OUTFILE" <<'EOF' +} +EOF + +echo "[✓] Wrote $OUTFILE" diff --git a/vendor/db_connector b/vendor/db_connector deleted file mode 160000 index 74aef399e..000000000 --- a/vendor/db_connector +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 74aef399e5c232f95c9fc5c987cebac846f09d62 diff --git a/vendor/dnsclient.nim b/vendor/dnsclient.nim deleted file mode 160000 index 23214235d..000000000 --- a/vendor/dnsclient.nim +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 23214235d4784d24aceed99bbfe153379ea557c8 diff --git a/vendor/nim-bearssl b/vendor/nim-bearssl deleted file mode 160000 index 11e798b62..000000000 --- a/vendor/nim-bearssl +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 11e798b62b8e6beabe958e048e9e24c7e0f9ee63 diff --git a/vendor/nim-chronicles b/vendor/nim-chronicles deleted file mode 160000 index 54f5b7260..000000000 --- a/vendor/nim-chronicles +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 54f5b726025e8c7385e3a6529d3aa27454c6e6ff diff --git a/vendor/nim-chronos b/vendor/nim-chronos deleted file mode 160000 index 85af4db76..000000000 --- a/vendor/nim-chronos +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 85af4db764ecd3573c4704139560df3943216cf1 diff --git a/vendor/nim-confutils b/vendor/nim-confutils deleted file mode 160000 index e214b3992..000000000 --- a/vendor/nim-confutils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e214b3992a31acece6a9aada7d0a1ad37c928f3b diff --git a/vendor/nim-dnsdisc b/vendor/nim-dnsdisc deleted file mode 160000 index 203abd2b3..000000000 --- a/vendor/nim-dnsdisc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 203abd2b3e758e0ea3ae325769b20a7e1bcd1010 diff --git a/vendor/nim-eth b/vendor/nim-eth deleted file mode 160000 index d9135e6c3..000000000 --- a/vendor/nim-eth +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d9135e6c3c5d6d819afdfb566aa8d958756b73a8 diff --git a/vendor/nim-faststreams b/vendor/nim-faststreams deleted file mode 160000 index ce27581a3..000000000 --- a/vendor/nim-faststreams +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ce27581a3e881f782f482cb66dc5b07a02bd615e diff --git a/vendor/nim-ffi b/vendor/nim-ffi deleted file mode 160000 index 06111de15..000000000 --- a/vendor/nim-ffi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 06111de155253b34e47ed2aaed1d61d08d62cc1b diff --git a/vendor/nim-http-utils b/vendor/nim-http-utils deleted file mode 160000 index f142cb2e8..000000000 --- a/vendor/nim-http-utils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f142cb2e8bd812dd002a6493b6082827bb248592 diff --git a/vendor/nim-json-rpc b/vendor/nim-json-rpc deleted file mode 160000 index 9665c2650..000000000 --- a/vendor/nim-json-rpc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9665c265035f49f5ff94bbffdeadde68e19d6221 diff --git a/vendor/nim-json-serialization b/vendor/nim-json-serialization deleted file mode 160000 index c343b0e24..000000000 --- a/vendor/nim-json-serialization +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c343b0e243d9e17e2c40f3a8a24340f7c4a71d44 diff --git a/vendor/nim-jwt b/vendor/nim-jwt deleted file mode 160000 index 18f8378de..000000000 --- a/vendor/nim-jwt +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 18f8378de52b241f321c1f9ea905456e89b95c6f diff --git a/vendor/nim-libbacktrace b/vendor/nim-libbacktrace deleted file mode 160000 index d8bd4ce5c..000000000 --- a/vendor/nim-libbacktrace +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d8bd4ce5c46bb6d2f984f6b3f3d7380897d95ecb diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p deleted file mode 160000 index ff8d51857..000000000 --- a/vendor/nim-libp2p +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ff8d51857b4b79a68468e7bcc27b2026cca02996 diff --git a/vendor/nim-lsquic b/vendor/nim-lsquic deleted file mode 160000 index 4fb03ee7b..000000000 --- a/vendor/nim-lsquic +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4fb03ee7bfb39aecb3316889fdcb60bec3d0936f diff --git a/vendor/nim-metrics b/vendor/nim-metrics deleted file mode 160000 index a1296caf3..000000000 --- a/vendor/nim-metrics +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a1296caf3ebb5f30f51a5feae7749a30df2824c2 diff --git a/vendor/nim-minilru b/vendor/nim-minilru deleted file mode 160000 index 0c4b2bce9..000000000 --- a/vendor/nim-minilru +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0c4b2bce959591f0a862e9b541ba43c6d0cf3476 diff --git a/vendor/nim-nat-traversal b/vendor/nim-nat-traversal deleted file mode 160000 index 860e18c37..000000000 --- a/vendor/nim-nat-traversal +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 860e18c37667b5dd005b94c63264560c35d88004 diff --git a/vendor/nim-presto b/vendor/nim-presto deleted file mode 160000 index d66043dd7..000000000 --- a/vendor/nim-presto +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d66043dd7ede146442e6c39720c76a20bde5225f diff --git a/vendor/nim-regex b/vendor/nim-regex deleted file mode 160000 index 4593305ed..000000000 --- a/vendor/nim-regex +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4593305ed1e49731fc75af1dc572dd2559aad19c diff --git a/vendor/nim-results b/vendor/nim-results deleted file mode 160000 index df8113dda..000000000 --- a/vendor/nim-results +++ /dev/null @@ -1 +0,0 @@ -Subproject commit df8113dda4c2d74d460a8fa98252b0b771bf1f27 diff --git a/vendor/nim-secp256k1 b/vendor/nim-secp256k1 deleted file mode 160000 index 9dd3df621..000000000 --- a/vendor/nim-secp256k1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9dd3df62124aae79d564da636bb22627c53c7676 diff --git a/vendor/nim-serialization b/vendor/nim-serialization deleted file mode 160000 index b0f2fa329..000000000 --- a/vendor/nim-serialization +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b0f2fa32960ea532a184394b0f27be37bd80248b diff --git a/vendor/nim-sqlite3-abi b/vendor/nim-sqlite3-abi deleted file mode 160000 index 89ba51f55..000000000 --- a/vendor/nim-sqlite3-abi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 89ba51f557414d3a3e17ab3df8270e1bdaa3ca2a diff --git a/vendor/nim-stew b/vendor/nim-stew deleted file mode 160000 index b66168735..000000000 --- a/vendor/nim-stew +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b66168735d6f3841c5239c3169d3fe5fe98b1257 diff --git a/vendor/nim-stint b/vendor/nim-stint deleted file mode 160000 index 470b78925..000000000 --- a/vendor/nim-stint +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 470b7892561b5179ab20bd389a69217d6213fe58 diff --git a/vendor/nim-taskpools b/vendor/nim-taskpools deleted file mode 160000 index 9e8ccc754..000000000 --- a/vendor/nim-taskpools +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9e8ccc754631ac55ac2fd495e167e74e86293edb diff --git a/vendor/nim-testutils b/vendor/nim-testutils deleted file mode 160000 index e4d37dc16..000000000 --- a/vendor/nim-testutils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e4d37dc1652d5c63afb89907efb5a5e812261797 diff --git a/vendor/nim-toml-serialization b/vendor/nim-toml-serialization deleted file mode 160000 index b5b387e6f..000000000 --- a/vendor/nim-toml-serialization +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b5b387e6fb2a7cc75d54a269b07cc6218361bd46 diff --git a/vendor/nim-unicodedb b/vendor/nim-unicodedb deleted file mode 160000 index 66f245871..000000000 --- a/vendor/nim-unicodedb +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 66f2458710dc641dd4640368f9483c8a0ec70561 diff --git a/vendor/nim-unittest2 b/vendor/nim-unittest2 deleted file mode 160000 index 26f2ef3ae..000000000 --- a/vendor/nim-unittest2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 26f2ef3ae0ec72a2a75bfe557e02e88f6a31c189 diff --git a/vendor/nim-web3 b/vendor/nim-web3 deleted file mode 160000 index 81ee8ce47..000000000 --- a/vendor/nim-web3 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 81ee8ce479d86acb73be7c4f365328e238d9b4a3 diff --git a/vendor/nim-websock b/vendor/nim-websock deleted file mode 160000 index 35ae76f15..000000000 --- a/vendor/nim-websock +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 35ae76f1559e835c80f9c1a3943bf995d3dd9eb5 diff --git a/vendor/nim-zlib b/vendor/nim-zlib deleted file mode 160000 index daa8723fd..000000000 --- a/vendor/nim-zlib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit daa8723fd32299d4ca621c837430c29a5a11e19a diff --git a/vendor/nimbus-build-system b/vendor/nimbus-build-system deleted file mode 160000 index e6c2c9da3..000000000 --- a/vendor/nimbus-build-system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e6c2c9da39c2d368d9cf420ac22692e99715d22c diff --git a/vendor/nimcrypto b/vendor/nimcrypto deleted file mode 160000 index 721fb99ee..000000000 --- a/vendor/nimcrypto +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 721fb99ee099b632eb86dfad1f0d96ee87583774 diff --git a/vendor/nph b/vendor/nph deleted file mode 160000 index 2cacf6cc2..000000000 --- a/vendor/nph +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2cacf6cc28116e4046e0b67a13545af5c4e756bd diff --git a/waku.nimble b/waku.nimble index cbbe6aa97..a6e824528 100644 --- a/waku.nimble +++ b/waku.nimble @@ -4,81 +4,161 @@ import os mode = ScriptMode.Verbose ### Package -version = "0.36.0" +version = "0.37.4" author = "Status Research & Development GmbH" description = "Waku, Private P2P Messaging for Resource-Restricted Devices" license = "MIT or Apache License 2.0" #bin = @["build/waku"] +## This indicates the nim compiler version we are currently working on. It may compile with others +## but we haven't tested. +const NimVersion = "2.2.4" +## This is the underlying nimble version that gets installed after doing `choosenim 2.2.4`. +const NimbleVersion = "0.18.2" + ### Dependencies requires "nim >= 2.2.4", + "chronos >= 4.2.0", + "taskpools", + # Logging & Configuration "chronicles", "confutils", - "chronos", - "dnsdisc", - "eth", - "json_rpc", - "libbacktrace", - "nimcrypto", + # Serialization "serialization", + "json_serialization", + "toml_serialization", + "faststreams", + # Networking & P2P + "https://github.com/vacp2p/nim-libp2p.git#ff8d51857b4b79a68468e7bcc27b2026cca02996", + "eth", + "nat_traversal", + "dnsdisc", + "dnsclient", + "httputils >= 0.4.1", + "websock >= 0.2.1", + # Cryptography + "nimcrypto == 0.6.4", # 0.6.4 used in libp2p. Version 0.7.3 makes test to crash on Ubuntu. + "secp256k1", + "bearssl", + # RPC & APIs + "json_rpc", + "presto", + "web3", + # Database + "db_connector", + "sqlite3_abi", + # Utilities "stew", "stint", "metrics", - "libp2p >= 1.15.0", - "web3", - "presto", "regex", + "unicodedb", "results", - "db_connector", "minilru", - "lsquic", - "jwt", - "ffi" + "zlib", + # Debug & Testing + "testutils", + "unittest2" + +# Packages not on nimble (use git URLs) +requires "https://github.com/logos-messaging/nim-ffi" + +requires "https://github.com/vacp2p/nim-lsquic" +requires "https://github.com/vacp2p/nim-jwt.git#18f8378de52b241f321c1f9ea905456e89b95c6f" + +proc getMyCPU(): string = + ## Need to set cpu more explicit manner to avoid arch issues between dependencies + when defined(macosx) and defined(arm64): + return " --cpu:arm64 --passC:\"-arch arm64\" --passL:\"-arch arm64\" " + elif defined(macosx) and defined(amd64): + return " --cpu:amd64 --passC:\"-arch x86_64\" --passL:\"-arch x86_64\" " + elif defined(arm64): + return " --cpu:arm64 " + elif defined(amd64): + return " --cpu:amd64 " + +proc getNimParams(): string = + return " " & getEnv("NIM_PARAMS") & " " ### Helper functions -proc buildModule(filePath, params = "", lang = "c"): bool = +proc buildModule(filePath, params = ""): bool = if not dirExists "build": mkDir "build" - # allow something like "nim nimbus --verbosity:0 --hints:off nimbus.nims" - var extra_params = params - for i in 2 ..< paramCount() - 1: - extra_params &= " " & paramStr(i) if not fileExists(filePath): echo "File to build not found: " & filePath return false - exec "nim " & lang & " --out:build/" & filepath & ".bin --mm:refc " & extra_params & + exec "nim c --out:build/" & filepath & ".bin --mm:refc " & getMyCPU() & getNimParams() & " " & params & " " & filePath # exec will raise exception if anything goes wrong return true -proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = +proc buildBinary(name: string, srcDir = "./", params = "") = if not dirExists "build": mkDir "build" - # allow something like "nim nimbus --verbosity:0 --hints:off nimbus.nims" - var extra_params = params - for i in 2 ..< paramCount(): - extra_params &= " " & paramStr(i) - exec "nim " & lang & " --out:build/" & name & " --mm:refc " & extra_params & " " & + exec "nim c --out:build/" & name & " --mm:refc " & getMyCPU() & getNimParams() & " " & params & " " & srcDir & name & ".nim" proc buildLibrary(lib_name: string, srcDir = "./", params = "", `type` = "static", srcFile = "libwaku.nim", mainPrefix = "libwaku") = if not dirExists "build": mkDir "build" - # allow something like "nim nimbus --verbosity:0 --hints:off nimbus.nims" - var extra_params = params - for i in 2 ..< (paramCount() - 1): - extra_params &= " " & paramStr(i) + if `type` == "static": exec "nim c" & " --out:build/" & lib_name & " --threads:on --app:staticlib --opt:speed --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:on -d:discv5_protocol_id=d5waku " & - extra_params & " " & srcDir & srcFile + getMyCPU() & getNimParams() & srcDir & "/" & srcFile else: exec "nim c" & " --out:build/" & lib_name & " --threads:on --app:lib --opt:speed --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:off -d:discv5_protocol_id=d5waku " & - extra_params & " " & srcDir & srcFile + getMyCPU() & getNimParams() & " " & srcDir & "/" & srcFile + +proc buildLibDynamicWindows(libName: string, folderName: string) = + buildLibrary libName & ".dll", folderName, + """-d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE """, + "dynamic", libName & ".nim", libname + +proc buildLibDynamicLinux(libName: string, folderName: string) = + buildLibrary libName & ".so", folderName, + """-d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE """, + "dynamic", libName & ".nim", libname + +proc buildLibDynamicMac(libName: string, folderName: string) = + let sdkPath = staticExec("xcrun --show-sdk-path").strip() + when defined(arm64): + let archFlags = "--cpu:arm64 --passC:\"-arch arm64\" --passL:\"-arch arm64\" --passC:\"-isysroot " & sdkPath & "\" --passL:\"-isysroot " & sdkPath & "\"" + elif defined(amd64): + let archFlags = "--cpu:amd64 --passC:\"-arch x86_64\" --passL:\"-arch x86_64\" --passC:\"-isysroot " & sdkPath & "\" --passL:\"-isysroot " & sdkPath & "\"" + else: + {.error: "Unsupported macOS architecture".} + buildLibrary libName & ".dylib", folderName, + archFlags & " -d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE", + "dynamic", libName & ".nim", libname + +proc buildLibStaticWindows(libName: string, folderName: string) = + buildLibrary libName & ".lib", folderName, + """-d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE """, + "static", libName & ".nim", libname + +proc buildLibStaticLinux(libName: string, folderName: string) = + buildLibrary libName & ".a", folderName, + """-d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE """, + "static", libName & ".nim", libname + +proc buildLibStaticMac(libName: string, folderName: string) = + let sdkPath = staticExec("xcrun --show-sdk-path").strip() + when defined(arm64): + let archFlags = "--cpu:arm64 --passC:\"-arch arm64\" --passL:\"-arch arm64\" --passC:\"-isysroot " & sdkPath & "\" --passL:\"-isysroot " & sdkPath & "\"" + elif defined(amd64): + let archFlags = "--cpu:amd64 --passC:\"-arch x86_64\" --passL:\"-arch x86_64\" --passC:\"-isysroot " & sdkPath & "\" --passL:\"-isysroot " & sdkPath & "\"" + else: + {.error: "Unsupported macOS architecture".} + buildLibrary libName & ".a", folderName, + archFlags & " -d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE", + "static", libName & ".nim", libname + +### Mobile Android proc buildMobileAndroid(srcDir = ".", params = "") = let cpu = getEnv("CPU") @@ -88,16 +168,206 @@ proc buildMobileAndroid(srcDir = ".", params = "") = if not dirExists outDir: mkDir outDir - var extra_params = params - for i in 2 ..< paramCount(): - extra_params &= " " & paramStr(i) - exec "nim c" & " --out:" & outDir & - "/libwaku.so --threads:on --app:lib --opt:speed --noMain --mm:refc -d:chronicles_sinks=textlines[dynamic] --header -d:chronosEventEngine=epoll --passL:-L" & - outdir & " --passL:-lrln --passL:-llog --cpu:" & cpu & " --os:android -d:androidNDK " & - extra_params & " " & srcDir & "/libwaku.nim" + "/liblogosdelivery.so --threads:on --app:lib --opt:speed --noMain --mm:refc -d:chronicles_sinks=textlines[dynamic] --header -d:chronosEventEngine=epoll --passL:-L" & + outdir & " --passL:-lrln --passL:-llog --cpu:" & cpu & " --nimMainPrefix:liblogosdelivery --os:android -d:androidNDK " & params & + getNimParams() & " " & srcDir & "/liblogosdelivery.nim" -proc test(name: string, params = "-d:chronicles_log_level=DEBUG", lang = "c") = +task libLogosDeliveryAndroid, "Build the mobile bindings for Android": + let srcDir = "./library" + buildMobileAndroid srcDir, "-d:chronicles_log_level=ERROR" + +### Mobile iOS + +import std/sequtils + +proc buildMobileIOS(srcDir = ".", params = "") = + echo "Building iOS liblogosdelivery library" + + let iosArch = getEnv("IOS_ARCH") + let iosSdk = getEnv("IOS_SDK") + let sdkPath = getEnv("IOS_SDK_PATH") + + if sdkPath.len == 0: + quit "Error: IOS_SDK_PATH not set. Set it to the path of the iOS SDK" + + # Get nimble package paths + let bearsslPath = gorge("nimble path bearssl").strip() + let secp256k1Path = gorge("nimble path secp256k1").strip() + let natTraversalPath = gorge("nimble path nat_traversal").strip() + + # Get Nim standard library path + let nimPath = gorge("nim --fullhelp 2>&1 | head -1 | sed 's/.*\\[//' | sed 's/\\].*//'").strip() + let nimLibPath = nimPath.parentDir.parentDir / "lib" + + # Use SDK name in path to differentiate device vs simulator + let outDir = "build/ios/" & iosSdk & "-" & iosArch + if not dirExists outDir: + mkDir outDir + + var extra_params = params + let args = commandLineParams() + for arg in args: + extra_params &= " " & arg + + let cpu = if iosArch == "arm64": "arm64" else: "amd64" + + # The output static library + let nimcacheDir = outDir & "/nimcache" + let objDir = outDir & "/obj" + let vendorObjDir = outDir & "/vendor_obj" + let aFile = outDir & "/liblogosdelivery.a" + + if not dirExists objDir: + mkDir objDir + if not dirExists vendorObjDir: + mkDir vendorObjDir + + let clangBase = "clang -arch " & iosArch & " -isysroot " & sdkPath & + " -mios-version-min=18.0 -fembed-bitcode -fPIC -O2" + + # Generate C sources from Nim (no linking) + exec "nim c" & + " --nimcache:" & nimcacheDir & + " --os:ios --cpu:" & cpu & + " --compileOnly:on" & + " --noMain --mm:refc" & + " --threads:on --opt:size --header" & + " -d:metrics -d:discv5_protocol_id=d5waku" & + " --nimMainPrefix:liblogosdelivery --skipParentCfg:on" & + " --cc:clang" & + " " & extra_params & + " " & srcDir & "/liblogosdelivery.nim" + + # Compile vendor C libraries for iOS + + # --- BearSSL --- + echo "Compiling BearSSL for iOS..." + let bearSslSrcDir = bearsslPath / "bearssl/csources/src" + let bearSslIncDir = bearsslPath / "bearssl/csources/inc" + for path in walkDirRec(bearSslSrcDir): + if path.endsWith(".c"): + let relPath = path.replace(bearSslSrcDir & "/", "").replace("/", "_") + let baseName = relPath.changeFileExt("o") + let oFile = vendorObjDir / ("bearssl_" & baseName) + if not fileExists(oFile): + exec clangBase & " -I" & bearSslIncDir & " -I" & bearSslSrcDir & " -c " & path & " -o " & oFile + + # --- secp256k1 --- + echo "Compiling secp256k1 for iOS..." + let secp256k1Dir = secp256k1Path / "vendor/secp256k1" + let secp256k1Flags = " -I" & secp256k1Dir & "/include" & + " -I" & secp256k1Dir & "/src" & + " -I" & secp256k1Dir & + " -DENABLE_MODULE_RECOVERY=1" & + " -DENABLE_MODULE_ECDH=1" & + " -DECMULT_WINDOW_SIZE=15" & + " -DECMULT_GEN_PREC_BITS=4" + + # Main secp256k1 source + let secp256k1Obj = vendorObjDir / "secp256k1.o" + if not fileExists(secp256k1Obj): + exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/secp256k1.c -o " & secp256k1Obj + + # Precomputed tables (required for ecmult operations) + let secp256k1PreEcmultObj = vendorObjDir / "secp256k1_precomputed_ecmult.o" + if not fileExists(secp256k1PreEcmultObj): + exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/precomputed_ecmult.c -o " & secp256k1PreEcmultObj + + let secp256k1PreEcmultGenObj = vendorObjDir / "secp256k1_precomputed_ecmult_gen.o" + if not fileExists(secp256k1PreEcmultGenObj): + exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/precomputed_ecmult_gen.c -o " & secp256k1PreEcmultGenObj + + # --- miniupnpc --- + echo "Compiling miniupnpc for iOS..." + let miniupnpcSrcDir = natTraversalPath / "vendor/miniupnp/miniupnpc/src" + let miniupnpcIncDir = natTraversalPath / "vendor/miniupnp/miniupnpc/include" + let miniupnpcBuildDir = natTraversalPath / "vendor/miniupnp/miniupnpc/build" + let miniupnpcFiles = @[ + "addr_is_reserved.c", "connecthostport.c", "igd_desc_parse.c", + "minisoap.c", "minissdpc.c", "miniupnpc.c", "miniwget.c", + "minixml.c", "portlistingparse.c", "receivedata.c", "upnpcommands.c", + "upnpdev.c", "upnperrors.c", "upnpreplyparse.c" + ] + for fileName in miniupnpcFiles: + let srcPath = miniupnpcSrcDir / fileName + let oFile = vendorObjDir / ("miniupnpc_" & fileName.changeFileExt("o")) + if fileExists(srcPath) and not fileExists(oFile): + exec clangBase & + " -I" & miniupnpcIncDir & + " -I" & miniupnpcSrcDir & + " -I" & miniupnpcBuildDir & + " -DMINIUPNPC_SET_SOCKET_TIMEOUT" & + " -D_BSD_SOURCE -D_DEFAULT_SOURCE" & + " -c " & srcPath & " -o " & oFile + + # --- libnatpmp --- + echo "Compiling libnatpmp for iOS..." + let natpmpSrcDir = natTraversalPath / "vendor/libnatpmp-upstream" + # Only compile natpmp.c - getgateway.c uses net/route.h which is not available on iOS + let natpmpObj = vendorObjDir / "natpmp_natpmp.o" + if not fileExists(natpmpObj): + exec clangBase & + " -I" & natpmpSrcDir & + " -DENABLE_STRNATPMPERR" & + " -c " & natpmpSrcDir & "/natpmp.c -o " & natpmpObj + + # Use iOS-specific stub for getgateway + let getgatewayStubSrc = "./library/ios_natpmp_stubs.c" + let getgatewayStubObj = vendorObjDir / "natpmp_getgateway_stub.o" + if fileExists(getgatewayStubSrc) and not fileExists(getgatewayStubObj): + exec clangBase & " -c " & getgatewayStubSrc & " -o " & getgatewayStubObj + + # --- BearSSL stubs (for tools functions not in main library) --- + echo "Compiling BearSSL stubs for iOS..." + let bearSslStubsSrc = "./library/ios_bearssl_stubs.c" + let bearSslStubsObj = vendorObjDir / "bearssl_stubs.o" + if fileExists(bearSslStubsSrc) and not fileExists(bearSslStubsObj): + exec clangBase & " -c " & bearSslStubsSrc & " -o " & bearSslStubsObj + + # Compile all Nim-generated C files to object files + echo "Compiling Nim-generated C files for iOS..." + var cFiles: seq[string] = @[] + for kind, path in walkDir(nimcacheDir): + if kind == pcFile and path.endsWith(".c"): + cFiles.add(path) + + for cFile in cFiles: + let baseName = extractFilename(cFile).changeFileExt("o") + let oFile = objDir / baseName + exec clangBase & + " -DENABLE_STRNATPMPERR" & + " -I" & nimLibPath & + " -I" & bearsslPath & "/bearssl/csources/inc/" & + " -I" & bearsslPath & "/bearssl/csources/tools/" & + " -I" & bearsslPath & "/bearssl/abi/" & + " -I" & secp256k1Path & "/vendor/secp256k1/include/" & + " -I" & natTraversalPath & "/vendor/miniupnp/miniupnpc/include/" & + " -I" & natTraversalPath & "/vendor/libnatpmp-upstream/" & + " -I" & nimcacheDir & + " -c " & cFile & + " -o " & oFile + + # Create static library from all object files + echo "Creating static library..." + var objFiles: seq[string] = @[] + for kind, path in walkDir(objDir): + if kind == pcFile and path.endsWith(".o"): + objFiles.add(path) + for kind, path in walkDir(vendorObjDir): + if kind == pcFile and path.endsWith(".o"): + objFiles.add(path) + + exec "libtool -static -o " & aFile & " " & objFiles.join(" ") + + echo "iOS library created: " & aFile + +task libWakuIOS, "Build the mobile bindings for iOS": + let srcDir = "./library" + let extraParams = "-d:chronicles_log_level=ERROR" + buildMobileIOS srcDir, extraParams + +proc test(name: string, params = "-d:chronicles_log_level=DEBUG") = # XXX: When running `> NIM_PARAMS="-d:chronicles_log_level=INFO" make test2` # I expect compiler flag to be overridden, however it stays with whatever is # specified here. @@ -106,12 +376,12 @@ proc test(name: string, params = "-d:chronicles_log_level=DEBUG", lang = "c") = ### Waku common tasks task testcommon, "Build & run common tests": - test "all_tests_common", "-d:chronicles_log_level=WARN -d:chronosStrictException" + test "all_tests_common", "-d:chronicles_log_level=DEBUG -d:chronosStrictException" ### Waku tasks task wakunode2, "Build Waku v2 cli node": let name = "wakunode2" - buildBinary name, "apps/wakunode2/", " -d:chronicles_log_level='TRACE' " + buildBinary name, "apps/wakunode2/", " -d:chronicles_log_level=TRACE " task benchmarks, "Some benchmarks": let name = "benchmarks" @@ -150,7 +420,7 @@ task chat2, "Build example Waku chat usage": let name = "chat2" buildBinary name, "apps/chat2/", - "-d:chronicles_sinks=textlines[file] -d:chronicles_log_level='TRACE' " + "-d:chronicles_sinks=textlines[file] -d:chronicles_log_level=TRACE " # -d:ssl - cause unlisted exception error in libp2p/utility... task chat2mix, "Build example Waku chat mix usage": @@ -161,7 +431,7 @@ task chat2mix, "Build example Waku chat mix usage": let name = "chat2mix" buildBinary name, "apps/chat2mix/", - "-d:chronicles_sinks=textlines[file] -d:chronicles_log_level='TRACE' " + "-d:chronicles_sinks=textlines[file] -d:chronicles_log_level=TRACE " # -d:ssl - cause unlisted exception error in libp2p/utility... task chat2bridge, "Build chat2bridge": @@ -170,32 +440,33 @@ task chat2bridge, "Build chat2bridge": task liteprotocoltester, "Build liteprotocoltester": let name = "liteprotocoltester" - buildBinary name, "apps/liteprotocoltester/" + buildBinary name, "apps/liteprotocoltester/", "-d:chronicles_log_level=TRACE" task lightpushwithmix, "Build lightpushwithmix": let name = "lightpush_publisher_mix" buildBinary name, "examples/lightpush_mix/" -task api_example, "Build api_example": - let name = "api_example" - buildBinary name, "examples/api_example/" - -task buildone, "Build custom target": - let filepath = paramStr(paramCount()) - discard buildModule filepath - task buildTest, "Test custom target": - let filepath = paramStr(paramCount()) + let args = commandLineParams() + if args.len == 0: + quit "Missing test file" + + let filepath = args[^1] discard buildModule(filepath) import std/strutils task execTest, "Run test": - # Expects to be parameterized with test case name in quotes - # preceded with the nim source file name and path - # If no test case name is given still it requires empty quotes `""` - let filepath = paramStr(paramCount() - 1) - var testSuite = paramStr(paramCount()).strip(chars = {'\"'}) + let args = commandLineParams() + if args.len == 0: + quit "Missing arguments" + # expects: "" + let filepath = + if args.len >= 2: args[^2] + else: args[^1] + var testSuite = + if args.len >= 1: args[^1].strip(chars = {'\"'}) + else: "" if testSuite != "": testSuite = " \"" & testSuite & "\"" exec "build/" & filepath & ".bin " & testSuite @@ -208,203 +479,42 @@ let chroniclesParams = """-d:chronicles_disabled_topics="eth,dnsdisc.client" """ & "--warning:Deprecated:off " & "--warning:UnusedImport:on " & "-d:chronicles_log_level=TRACE" -task libwakuStatic, "Build the cbindings waku node library": - let lib_name = paramStr(paramCount()) - buildLibrary lib_name, "library/", chroniclesParams, "static" +## Libwaku build tasks -task libwakuDynamic, "Build the cbindings waku node library": - let lib_name = paramStr(paramCount()) - buildLibrary lib_name, "library/", chroniclesParams, "dynamic" +task libwakuDynamicWindows, "Generate bindings": + buildLibDynamicWindows("libwaku", "library") -### Mobile Android -task libWakuAndroid, "Build the mobile bindings for Android": - let srcDir = "./library" - let extraParams = "-d:chronicles_log_level=ERROR" - buildMobileAndroid srcDir, extraParams +task libwakuDynamicLinux, "Generate bindings": + buildLibDynamicLinux("libwaku", "library") -### Mobile iOS -import std/sequtils +task libwakuDynamicMac, "Generate bindings": + buildLibDynamicMac("libwaku", "library") -proc buildMobileIOS(srcDir = ".", params = "") = - echo "Building iOS libwaku library" +task libwakuStaticWindows, "Generate bindings": + buildLibStaticWindows("libwaku", "library") - let iosArch = getEnv("IOS_ARCH") - let iosSdk = getEnv("IOS_SDK") - let sdkPath = getEnv("IOS_SDK_PATH") +task libwakuStaticLinux, "Generate bindings": + buildLibStaticLinux("libwaku", "library") - if sdkPath.len == 0: - quit "Error: IOS_SDK_PATH not set. Set it to the path of the iOS SDK" +task libwakuStaticMac, "Generate bindings": + buildLibStaticMac("libwaku", "library") - # Use SDK name in path to differentiate device vs simulator - let outDir = "build/ios/" & iosSdk & "-" & iosArch - if not dirExists outDir: - mkDir outDir +## Liblogosdelivery build tasks - var extra_params = params - for i in 2 ..< paramCount(): - extra_params &= " " & paramStr(i) +task liblogosdeliveryDynamicWindows, "Generate bindings": + buildLibDynamicWindows("liblogosdelivery", "liblogosdelivery") - let cpu = if iosArch == "arm64": "arm64" else: "amd64" +task liblogosdeliveryDynamicLinux, "Generate bindings": + buildLibDynamicLinux("liblogosdelivery", "liblogosdelivery") - # The output static library - let nimcacheDir = outDir & "/nimcache" - let objDir = outDir & "/obj" - let vendorObjDir = outDir & "/vendor_obj" - let aFile = outDir & "/libwaku.a" +task liblogosdeliveryDynamicMac, "Generate bindings": + buildLibDynamicMac("liblogosdelivery", "liblogosdelivery") - if not dirExists objDir: - mkDir objDir - if not dirExists vendorObjDir: - mkDir vendorObjDir +task liblogosdeliveryStaticWindows, "Generate bindings": + buildLibStaticWindows("liblogosdelivery", "liblogosdelivery") - let clangBase = "clang -arch " & iosArch & " -isysroot " & sdkPath & - " -mios-version-min=18.0 -fembed-bitcode -fPIC -O2" +task liblogosdeliveryStaticLinux, "Generate bindings": + buildLibStaticLinux("liblogosdelivery", "liblogosdelivery") - # Generate C sources from Nim (no linking) - exec "nim c" & - " --nimcache:" & nimcacheDir & - " --os:ios --cpu:" & cpu & - " --compileOnly:on" & - " --noMain --mm:refc" & - " --threads:on --opt:speed --header" & - " -d:metrics -d:discv5_protocol_id=d5waku" & - " --nimMainPrefix:libwaku --skipParentCfg:on" & - " --cc:clang" & - " " & extra_params & - " " & srcDir & "/libwaku.nim" - - # Compile vendor C libraries for iOS - - # --- BearSSL --- - echo "Compiling BearSSL for iOS..." - let bearSslSrcDir = "./vendor/nim-bearssl/bearssl/csources/src" - let bearSslIncDir = "./vendor/nim-bearssl/bearssl/csources/inc" - for path in walkDirRec(bearSslSrcDir): - if path.endsWith(".c"): - let relPath = path.replace(bearSslSrcDir & "/", "").replace("/", "_") - let baseName = relPath.changeFileExt("o") - let oFile = vendorObjDir / ("bearssl_" & baseName) - if not fileExists(oFile): - exec clangBase & " -I" & bearSslIncDir & " -I" & bearSslSrcDir & " -c " & path & " -o " & oFile - - # --- secp256k1 --- - echo "Compiling secp256k1 for iOS..." - let secp256k1Dir = "./vendor/nim-secp256k1/vendor/secp256k1" - let secp256k1Flags = " -I" & secp256k1Dir & "/include" & - " -I" & secp256k1Dir & "/src" & - " -I" & secp256k1Dir & - " -DENABLE_MODULE_RECOVERY=1" & - " -DENABLE_MODULE_ECDH=1" & - " -DECMULT_WINDOW_SIZE=15" & - " -DECMULT_GEN_PREC_BITS=4" - - # Main secp256k1 source - let secp256k1Obj = vendorObjDir / "secp256k1.o" - if not fileExists(secp256k1Obj): - exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/secp256k1.c -o " & secp256k1Obj - - # Precomputed tables (required for ecmult operations) - let secp256k1PreEcmultObj = vendorObjDir / "secp256k1_precomputed_ecmult.o" - if not fileExists(secp256k1PreEcmultObj): - exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/precomputed_ecmult.c -o " & secp256k1PreEcmultObj - - let secp256k1PreEcmultGenObj = vendorObjDir / "secp256k1_precomputed_ecmult_gen.o" - if not fileExists(secp256k1PreEcmultGenObj): - exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/precomputed_ecmult_gen.c -o " & secp256k1PreEcmultGenObj - - # --- miniupnpc --- - echo "Compiling miniupnpc for iOS..." - let miniupnpcSrcDir = "./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/src" - let miniupnpcIncDir = "./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/include" - let miniupnpcBuildDir = "./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/build" - let miniupnpcFiles = @[ - "addr_is_reserved.c", "connecthostport.c", "igd_desc_parse.c", - "minisoap.c", "minissdpc.c", "miniupnpc.c", "miniwget.c", - "minixml.c", "portlistingparse.c", "receivedata.c", "upnpcommands.c", - "upnpdev.c", "upnperrors.c", "upnpreplyparse.c" - ] - for fileName in miniupnpcFiles: - let srcPath = miniupnpcSrcDir / fileName - let oFile = vendorObjDir / ("miniupnpc_" & fileName.changeFileExt("o")) - if fileExists(srcPath) and not fileExists(oFile): - exec clangBase & - " -I" & miniupnpcIncDir & - " -I" & miniupnpcSrcDir & - " -I" & miniupnpcBuildDir & - " -DMINIUPNPC_SET_SOCKET_TIMEOUT" & - " -D_BSD_SOURCE -D_DEFAULT_SOURCE" & - " -c " & srcPath & " -o " & oFile - - # --- libnatpmp --- - echo "Compiling libnatpmp for iOS..." - let natpmpSrcDir = "./vendor/nim-nat-traversal/vendor/libnatpmp-upstream" - # Only compile natpmp.c - getgateway.c uses net/route.h which is not available on iOS - let natpmpObj = vendorObjDir / "natpmp_natpmp.o" - if not fileExists(natpmpObj): - exec clangBase & - " -I" & natpmpSrcDir & - " -DENABLE_STRNATPMPERR" & - " -c " & natpmpSrcDir & "/natpmp.c -o " & natpmpObj - - # Use iOS-specific stub for getgateway - let getgatewayStubSrc = "./library/ios_natpmp_stubs.c" - let getgatewayStubObj = vendorObjDir / "natpmp_getgateway_stub.o" - if fileExists(getgatewayStubSrc) and not fileExists(getgatewayStubObj): - exec clangBase & " -c " & getgatewayStubSrc & " -o " & getgatewayStubObj - - # --- BearSSL stubs (for tools functions not in main library) --- - echo "Compiling BearSSL stubs for iOS..." - let bearSslStubsSrc = "./library/ios_bearssl_stubs.c" - let bearSslStubsObj = vendorObjDir / "bearssl_stubs.o" - if fileExists(bearSslStubsSrc) and not fileExists(bearSslStubsObj): - exec clangBase & " -c " & bearSslStubsSrc & " -o " & bearSslStubsObj - - # Compile all Nim-generated C files to object files - echo "Compiling Nim-generated C files for iOS..." - var cFiles: seq[string] = @[] - for kind, path in walkDir(nimcacheDir): - if kind == pcFile and path.endsWith(".c"): - cFiles.add(path) - - for cFile in cFiles: - let baseName = extractFilename(cFile).changeFileExt("o") - let oFile = objDir / baseName - exec clangBase & - " -DENABLE_STRNATPMPERR" & - " -I./vendor/nimbus-build-system/vendor/Nim/lib/" & - " -I./vendor/nim-bearssl/bearssl/csources/inc/" & - " -I./vendor/nim-bearssl/bearssl/csources/tools/" & - " -I./vendor/nim-bearssl/bearssl/abi/" & - " -I./vendor/nim-secp256k1/vendor/secp256k1/include/" & - " -I./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/include/" & - " -I./vendor/nim-nat-traversal/vendor/libnatpmp-upstream/" & - " -I" & nimcacheDir & - " -c " & cFile & - " -o " & oFile - - # Create static library from all object files - echo "Creating static library..." - var objFiles: seq[string] = @[] - for kind, path in walkDir(objDir): - if kind == pcFile and path.endsWith(".o"): - objFiles.add(path) - for kind, path in walkDir(vendorObjDir): - if kind == pcFile and path.endsWith(".o"): - objFiles.add(path) - - exec "libtool -static -o " & aFile & " " & objFiles.join(" ") - - echo "✔ iOS library created: " & aFile - -task libWakuIOS, "Build the mobile bindings for iOS": - let srcDir = "./library" - let extraParams = "-d:chronicles_log_level=ERROR" - buildMobileIOS srcDir, extraParams - -task liblogosdeliveryStatic, "Build the liblogosdelivery (Logos Messaging Delivery API) static library": - let lib_name = paramStr(paramCount()) - buildLibrary lib_name, "liblogosdelivery/", chroniclesParams, "static", "liblogosdelivery.nim", "liblogosdelivery" - -task liblogosdeliveryDynamic, "Build the liblogosdelivery (Logos Messaging Delivery API) dynamic library": - let lib_name = paramStr(paramCount()) - buildLibrary lib_name, "liblogosdelivery/", chroniclesParams, "dynamic", "liblogosdelivery.nim", "liblogosdelivery" +task liblogosdeliveryStaticMac, "Generate bindings": + buildLibStaticMac("liblogosdelivery", "liblogosdelivery") diff --git a/waku/api/api_conf.nim b/waku/api/api_conf.nim index 30dfd1b2c..3606be596 100644 --- a/waku/api/api_conf.nim +++ b/waku/api/api_conf.nim @@ -13,24 +13,24 @@ import export json_serialization, json_options -type AutoShardingConfig* {.requiresInit.} = object +type AutoShardingConfig* = object numShardsInCluster*: uint16 -type RlnConfig* {.requiresInit.} = object +type RlnConfig* = object contractAddress*: string chainId*: uint epochSizeSec*: uint64 -type NetworkingConfig* {.requiresInit.} = object +type NetworkingConfig* = object listenIpv4*: string p2pTcpPort*: uint16 discv5UdpPort*: uint16 -type MessageValidation* {.requiresInit.} = object +type MessageValidation* = object maxMessageSize*: string # Accepts formats like "150 KiB", "1500 B" rlnConfig*: Option[RlnConfig] -type ProtocolsConfig* {.requiresInit.} = object +type ProtocolsConfig* = object entryNodes: seq[string] staticStoreNodes: seq[string] clusterId: uint16 @@ -526,7 +526,7 @@ proc decodeNodeConfigFromJson*( var val = NodeConfig.init() # default-initialized try: var stream = unsafeMemoryInput(jsonStr) - var reader = JsonReader[DefaultFlavor].init(stream) + var reader = (JsonReader[DefaultFlavor].init(stream)) reader.readValue(val) except IOError as err: raise (ref SerializationError)(msg: err.msg) From 59bd365c16fd40506c167e8dd7e439684ab4bd11 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:33:16 +0200 Subject: [PATCH 116/155] setting num-shards-in-network to 0 by default (#3748) Co-authored-by: darshankabariya --- tests/wakunode2/test_cli_args.nim | 12 ++++++------ tools/confutils/cli_args.nim | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/wakunode2/test_cli_args.nim b/tests/wakunode2/test_cli_args.nim index 9197afe02..d08544c2c 100644 --- a/tests/wakunode2/test_cli_args.nim +++ b/tests/wakunode2/test_cli_args.nim @@ -23,9 +23,8 @@ import suite "Waku external config - default values": test "Default sharding value": ## Setup - let defaultShardingMode = AutoSharding - let defaultNumShardsInCluster = 1.uint16 - let defaultSubscribeShards = @[0.uint16] + let defaultShardingMode = StaticSharding + let defaultSubscribeShards: seq[uint16] = @[] ## Given let preConfig = defaultWakuNodeConf().get() @@ -37,7 +36,6 @@ suite "Waku external config - default values": ## Then let conf = res.get() check conf.shardingConf.kind == defaultShardingMode - check conf.shardingConf.numShardsInCluster == defaultNumShardsInCluster check conf.subscribeShards == defaultSubscribeShards test "Default shards value in static sharding": @@ -212,7 +210,7 @@ suite "Waku external config - Shards": let vRes = wakuConf.validate() assert vRes.isOk(), $vRes.error - test "Imvalid shard is passed without num shards": + test "Any shard is valid without num shards in static sharding mode": ## Setup ## Given @@ -222,7 +220,9 @@ suite "Waku external config - Shards": let res = wakuNodeConf.toWakuConf() ## Then - assert res.isErr(), "Invalid shard was accepted" + let wakuConf = res.get() + let vRes = wakuConf.validate() + assert vRes.isOk(), $vRes.error suite "Waku external config - store retention policy": test "Default retention policy": diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 5c1934712..d63b5880c 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -331,7 +331,7 @@ hence would have reachability issues.""", numShardsInNetwork* {. desc: "Enables autosharding and set number of shards in the cluster, set to `0` to use static sharding", - defaultValue: 1, + defaultValue: 0, name: "num-shards-in-network" .}: uint16 From 5503529531ffc5818c8b4289ed9b55d8a95e725d Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:17:17 +0200 Subject: [PATCH 117/155] chore: Add pre-check of options used in config Json for liblogosdelivery pre-createNode, treat unrecognized options as error (#3801) * Add pre-check of options used in config Json for logos-delivery-api pre-createNode, treat unrecognized options as error * Collect all unrecognized options and report them at once. * Refactor json config parsing and error detection --- .../logos_delivery_api/node_api.nim | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim index cd644abd7..90630717b 100644 --- a/liblogosdelivery/logos_delivery_api/node_api.nim +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -1,4 +1,4 @@ -import std/[json, strutils] +import std/[json, strutils, tables] import chronos, chronicles, results, confutils, confutils/std/net, ffi import waku/factory/waku, @@ -23,22 +23,50 @@ registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): try: jsonNode = parseJson($configJson) except Exception: + let exceptionMsg = getCurrentExceptionMsg() + error "Failed to parse config JSON", + error = exceptionMsg, configJson = $configJson return err( - "Failed to parse config JSON: " & getCurrentExceptionMsg() & - " configJson string: " & $configJson + "Failed to parse config JSON: " & exceptionMsg & " configJson string: " & + $configJson ) + var jsonFields: Table[string, (string, JsonNode)] + for key, value in jsonNode: + let lowerKey = key.toLowerAscii() + + if jsonFields.hasKey(lowerKey): + error "Duplicate configuration option found when normalized to lowercase", + key = key + return err( + "Duplicate configuration option found when normalized to lowercase: '" & key & + "'" + ) + + jsonFields[lowerKey] = (key, value) + for confField, confValue in fieldPairs(conf): - if jsonNode.contains(confField): - let formattedString = ($jsonNode[confField]).strip(chars = {'\"'}) + let lowerField = confField.toLowerAscii() + if jsonFields.hasKey(lowerField): + let (jsonKey, jsonValue) = jsonFields[lowerField] + let formattedString = ($jsonValue).strip(chars = {'\"'}) try: confValue = parseCmdArg(typeof(confValue), formattedString) except Exception: return err( - "Failed to parse field '" & confField & "': " & getCurrentExceptionMsg() & - ". Value: " & formattedString + "Failed to parse field '" & confField & "' from JSON key '" & jsonKey & "': " & + getCurrentExceptionMsg() & ". Value: " & formattedString ) + jsonFields.del(lowerField) + + if jsonFields.len > 0: + var unknownKeys = newSeq[string]() + for _, (jsonKey, _) in pairs(jsonFields): + unknownKeys.add(jsonKey) + error "Unrecognized configuration option(s) found", option = unknownKeys + return err("Unrecognized configuration option(s) found: " & $unknownKeys) + # Create the node ctx.myLib[] = (await api.createNode(conf)).valueOr: let errMsg = $error From 4d314b376d95e210e938894027bb9267e834e81d Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:33:16 +0200 Subject: [PATCH 118/155] setting num-shards-in-network to 0 by default (#3748) Co-authored-by: darshankabariya --- tests/wakunode2/test_cli_args.nim | 12 ++++++------ tools/confutils/cli_args.nim | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/wakunode2/test_cli_args.nim b/tests/wakunode2/test_cli_args.nim index 9197afe02..d08544c2c 100644 --- a/tests/wakunode2/test_cli_args.nim +++ b/tests/wakunode2/test_cli_args.nim @@ -23,9 +23,8 @@ import suite "Waku external config - default values": test "Default sharding value": ## Setup - let defaultShardingMode = AutoSharding - let defaultNumShardsInCluster = 1.uint16 - let defaultSubscribeShards = @[0.uint16] + let defaultShardingMode = StaticSharding + let defaultSubscribeShards: seq[uint16] = @[] ## Given let preConfig = defaultWakuNodeConf().get() @@ -37,7 +36,6 @@ suite "Waku external config - default values": ## Then let conf = res.get() check conf.shardingConf.kind == defaultShardingMode - check conf.shardingConf.numShardsInCluster == defaultNumShardsInCluster check conf.subscribeShards == defaultSubscribeShards test "Default shards value in static sharding": @@ -212,7 +210,7 @@ suite "Waku external config - Shards": let vRes = wakuConf.validate() assert vRes.isOk(), $vRes.error - test "Imvalid shard is passed without num shards": + test "Any shard is valid without num shards in static sharding mode": ## Setup ## Given @@ -222,7 +220,9 @@ suite "Waku external config - Shards": let res = wakuNodeConf.toWakuConf() ## Then - assert res.isErr(), "Invalid shard was accepted" + let wakuConf = res.get() + let vRes = wakuConf.validate() + assert vRes.isOk(), $vRes.error suite "Waku external config - store retention policy": test "Default retention policy": diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index df2bd306c..90a349d9d 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -333,7 +333,7 @@ hence would have reachability issues.""", numShardsInNetwork* {. desc: "Enables autosharding and set number of shards in the cluster, set to `0` to use static sharding", - defaultValue: 1, + defaultValue: 0, name: "num-shards-in-network" .}: uint16 From ca7ec3de056120635ad552db24b2e44a8bc99a1d Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:51:46 +0200 Subject: [PATCH 119/155] add main loop lag monitor (#3803) * add loop lagging as health status --- .../health_monitor/event_loop_monitor.nim | 58 +++++++++++++++++++ waku/node/health_monitor/health_status.nim | 1 + .../health_monitor/node_health_monitor.nim | 20 ++++++- 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 waku/node/health_monitor/event_loop_monitor.nim diff --git a/waku/node/health_monitor/event_loop_monitor.nim b/waku/node/health_monitor/event_loop_monitor.nim new file mode 100644 index 000000000..d4b8d98d2 --- /dev/null +++ b/waku/node/health_monitor/event_loop_monitor.nim @@ -0,0 +1,58 @@ +{.push raises: [].} + +import chronos, chronicles, metrics + +logScope: + topics = "waku event_loop_monitor" + +const CheckInterval = 5.seconds + +declarePublicGauge event_loop_lag_seconds, + "chronos event loop lag in seconds: difference between actual and expected wake-up interval" + +type OnLagChange* = proc(lagTooHigh: bool) {.gcsafe, raises: [].} + +proc eventLoopMonitorLoop*(onLagChange: OnLagChange = nil) {.async.} = + ## Monitors chronos event loop responsiveness. + ## + ## Schedules a task every `CheckInterval`. Because chronos is single-threaded + ## and cooperative, the task can only resume after all previously queued work + ## completes. The actual elapsed time between iterations therefore reflects + ## how saturated the event loop is: + ## + ## actual_elapsed ≈ CheckInterval → loop is healthy + ## actual_elapsed >> CheckInterval → tasks are accumulating / loop is stalling + ## + ## The lag (actual - expected) is exposed via `event_loop_lag_seconds`. + ## When lag transitions above or below `CheckInterval`, `onLagChange` is called. + + var lastWakeup = Moment.now() + var lagWasHigh = false + while true: + await sleepAsync(CheckInterval) + + let now = Moment.now() + let actualElapsed = now - lastWakeup + let lag = actualElapsed - CheckInterval + let lagSecs = lag.nanoseconds.float64 / 1_000_000_000.0 + + event_loop_lag_seconds.set(lagSecs) + + let lagIsHigh = lag > CheckInterval + + if lag > CheckInterval: + warn "chronos event loop severely lagging, many tasks may be accumulating", + expected_secs = CheckInterval.seconds, + actual_secs = actualElapsed.nanoseconds.float64 / 1_000_000_000.0, + lag_secs = lagSecs + elif lag > (CheckInterval div 2): + info "chronos event loop lag detected", + expected_secs = CheckInterval.seconds, + actual_secs = actualElapsed.nanoseconds.float64 / 1_000_000_000.0, + lag_secs = lagSecs + + if not isNil(onLagChange) and lagIsHigh != lagWasHigh: + lagWasHigh = lagIsHigh + onLagChange(lagIsHigh) + + lastWakeup = now diff --git a/waku/node/health_monitor/health_status.nim b/waku/node/health_monitor/health_status.nim index 4dd2bdd9a..91663a507 100644 --- a/waku/node/health_monitor/health_status.nim +++ b/waku/node/health_monitor/health_status.nim @@ -7,6 +7,7 @@ type HealthStatus* {.pure.} = enum NOT_READY NOT_MOUNTED SHUTTING_DOWN + EVENT_LOOP_LAGGING proc init*(t: typedesc[HealthStatus], strRep: string): Result[HealthStatus, string] = try: diff --git a/waku/node/health_monitor/node_health_monitor.nim b/waku/node/health_monitor/node_health_monitor.nim index 066e7776a..c92dc1aaf 100644 --- a/waku/node/health_monitor/node_health_monitor.nim +++ b/waku/node/health_monitor/node_health_monitor.nim @@ -21,6 +21,7 @@ import node/health_monitor/health_report, node/health_monitor/connection_status, node/health_monitor/protocol_health, + node/health_monitor/event_loop_monitor, requests/health_requests, ] @@ -36,6 +37,7 @@ type NodeHealthMonitor* = ref object onlineMonitor*: OnlineMonitor keepAliveFut: Future[void] healthLoopFut: Future[void] + eventLoopMonitorFut: Future[void] healthUpdateEvent: AsyncEvent connectionStatus: ConnectionStatus onConnectionStatusChange*: ConnectionStatusChangeHandler @@ -48,6 +50,9 @@ type NodeHealthMonitor* = ref object relayObserver: PubSubObserver peerEventListener: WakuPeerEventListener shardHealthListener: EventShardTopicHealthChangeListener + eventLoopLagExceeded: bool + ## set to true when the chronos event loop lag exceeds the severe threshold, + ## causing the node health to be reported as EVENT_LOOP_LAGGING until lag recovers. func getHealth*(report: HealthReport, kind: WakuProtocol): ProtocolHealth = for h in report.protocolsHealth: @@ -441,7 +446,8 @@ proc getNodeHealthReport*(hm: NodeHealthMonitor): Future[HealthReport] {.async.} hm.cachedProtocols = await hm.getAllProtocolHealthInfo() hm.connectionStatus = hm.calculateConnectionState() - report.nodeHealth = HealthStatus.READY + report.nodeHealth = + if hm.eventLoopLagExceeded: HealthStatus.EVENT_LOOP_LAGGING else: HealthStatus.READY report.connectionStatus = hm.connectionStatus report.protocolsHealth = hm.cachedProtocols return report @@ -461,7 +467,8 @@ proc getSyncNodeHealthReport*(hm: NodeHealthMonitor): HealthReport = hm.cachedProtocols = hm.getSyncAllProtocolHealthInfo() hm.connectionStatus = hm.calculateConnectionState() - report.nodeHealth = HealthStatus.READY + report.nodeHealth = + if hm.eventLoopLagExceeded: HealthStatus.EVENT_LOOP_LAGGING else: HealthStatus.READY report.connectionStatus = hm.connectionStatus report.protocolsHealth = hm.cachedProtocols return report @@ -694,9 +701,15 @@ proc startHealthMonitor*(hm: NodeHealthMonitor): Result[void, string] = hm.healthUpdateEvent.fire() hm.healthLoopFut = hm.healthLoop() + hm.eventLoopMonitorFut = eventLoopMonitorLoop( + proc(lagTooHigh: bool) {.gcsafe, raises: [].} = + hm.eventLoopLagExceeded = lagTooHigh + hm.healthUpdateEvent.fire() + ) hm.startKeepalive().isOkOr: return err("startHealthMonitor: failed starting keep alive: " & error) + return ok() proc stopHealthMonitor*(hm: NodeHealthMonitor) {.async.} = @@ -709,6 +722,9 @@ proc stopHealthMonitor*(hm: NodeHealthMonitor) {.async.} = if not isNil(hm.healthLoopFut): await hm.healthLoopFut.cancelAndWait() + if not isNil(hm.eventLoopMonitorFut): + await hm.eventLoopMonitorFut.cancelAndWait() + WakuPeerEvent.dropListener(hm.node.brokerCtx, hm.peerEventListener) EventShardTopicHealthChange.dropListener(hm.node.brokerCtx, hm.shardHealthListener) From 494ea94606adb7c0efc60b3ad03eab87eb3832df Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Thu, 9 Apr 2026 14:29:17 -0300 Subject: [PATCH 120/155] fix: recv_service delivers store-recovered messages (#3805) * recv_service now delivers store-recovered messages via MessageReceivedEvent * add regression test_api_receive to prove store recovery actually delivers messages * fix confusing "UNSUCCESSFUL / Missed message" log message * removed some dead/duplicated code Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Co-authored by Zoltan Nagy --- .github/workflows/ci.yml | 6 +- .github/workflows/container-image.yml | 2 +- .github/workflows/windows-build.yml | 2 +- BearSSL.mk | 9 +- Nat.mk | 11 +- tests/api/test_all.nim | 1 + tests/api/test_api_receive.nim | 193 ++++++++++++++++++ waku/events/delivery_events.nim | 16 -- .../delivery_service/delivery_service.nim | 2 +- .../recv_service/recv_service.nim | 134 ++++++------ 10 files changed, 279 insertions(+), 97 deletions(-) create mode 100644 tests/api/test_api_receive.nim diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2de4e50e..1f6fcdfec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: path: | nimbledeps/ nimble.paths - key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock') }} + key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} - name: Install nimble deps if: steps.cache-nimbledeps.outputs.cache-hit != 'true' @@ -136,7 +136,7 @@ jobs: path: | nimbledeps/ nimble.paths - key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock') }} + key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} - name: Install nimble deps if: steps.cache-nimbledeps.outputs.cache-hit != 'true' @@ -215,7 +215,7 @@ jobs: path: | nimbledeps/ nimble.paths - key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock') }} + key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} - name: Install nimble deps if: steps.cache-nimbledeps.outputs.cache-hit != 'true' diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml index ae132a477..c2fb9d4d2 100644 --- a/.github/workflows/container-image.yml +++ b/.github/workflows/container-image.yml @@ -69,7 +69,7 @@ jobs: path: | nimbledeps/ nimble.paths - key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock') }} + key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} - name: Install nimble deps if: ${{ steps.secrets.outcome == 'success' && steps.cache-nimbledeps.outputs.cache-hit != 'true' }} diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 09ef05a5d..5b0894368 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -87,7 +87,7 @@ jobs: path: | nimbledeps/ nimble.paths - key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock') }} + key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} - name: Install nimble deps if: steps.cache-nimbledeps.outputs.cache-hit != 'true' diff --git a/BearSSL.mk b/BearSSL.mk index 98e933ebd..355e46563 100644 --- a/BearSSL.mk +++ b/BearSSL.mk @@ -22,6 +22,13 @@ BEARSSL_NIMBLEDEPS_DIR := $(shell ls -dt $(CURDIR)/nimbledeps/pkgs2/bearssl-* 2>/dev/null | head -1) BEARSSL_CSOURCES_DIR := $(BEARSSL_NIMBLEDEPS_DIR)/bearssl/csources +BEARSSL_UNAME_M := $(shell uname -m) +ifeq ($(BEARSSL_UNAME_M),x86_64) + PORTABLE_BEARSSL_CFLAGS := -W -Wall -Os -fPIC -mssse3 +else + PORTABLE_BEARSSL_CFLAGS := -W -Wall -Os -fPIC +endif + .PHONY: clean-bearssl-nimbledeps rebuild-bearssl-nimbledeps clean-bearssl-nimbledeps: @@ -36,4 +43,4 @@ ifeq ($(BEARSSL_NIMBLEDEPS_DIR),) $(error No bearssl package found under nimbledeps/pkgs2/ — run 'make update' first) endif @echo "Rebuilding bearssl from $(BEARSSL_CSOURCES_DIR)" - + "$(MAKE)" -C "$(BEARSSL_CSOURCES_DIR)" lib \ No newline at end of file + + "$(MAKE)" -C "$(BEARSSL_CSOURCES_DIR)" CFLAGS="$(PORTABLE_BEARSSL_CFLAGS)" lib \ No newline at end of file diff --git a/Nat.mk b/Nat.mk index 31ad4e018..90d0b2ead 100644 --- a/Nat.mk +++ b/Nat.mk @@ -21,6 +21,13 @@ NAT_TRAVERSAL_NIMBLEDEPS_DIR := $(shell ls -dt $(CURDIR)/nimbledeps/pkgs2/nat_traversal-* 2>/dev/null | head -1) +NAT_UNAME_M := $(shell uname -m) +ifeq ($(NAT_UNAME_M),x86_64) + PORTABLE_NAT_MARCH := -mssse3 +else + PORTABLE_NAT_MARCH := +endif + .PHONY: clean-cross-nimbledeps rebuild-nat-libs-nimbledeps clean-cross-nimbledeps: @@ -47,8 +54,8 @@ ifeq ($(OS), Windows_NT) libnatpmp.a $(HANDLE_OUTPUT) else + "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc" \ - CC=$(CC) CFLAGS="-Os -fPIC" build/libminiupnpc.a $(HANDLE_OUTPUT) - + "$(MAKE)" CFLAGS="-Wall -Wno-cpp -Os -fPIC -DENABLE_STRNATPMPERR -DNATPMP_MAX_RETRIES=4 $(CFLAGS)" \ + CC=$(CC) CFLAGS="-Os -fPIC $(PORTABLE_NAT_MARCH)" build/libminiupnpc.a $(HANDLE_OUTPUT) + + "$(MAKE)" CFLAGS="-Wall -Wno-cpp -Os -fPIC $(PORTABLE_NAT_MARCH) -DENABLE_STRNATPMPERR -DNATPMP_MAX_RETRIES=4 $(CFLAGS)" \ -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/libnatpmp-upstream" \ CC=$(CC) libnatpmp.a $(HANDLE_OUTPUT) endif diff --git a/tests/api/test_all.nim b/tests/api/test_all.nim index 4617c8cdb..56be19c27 100644 --- a/tests/api/test_all.nim +++ b/tests/api/test_all.nim @@ -5,4 +5,5 @@ import ./test_node_conf, ./test_api_send, ./test_api_subscription, + ./test_api_receive, ./test_api_health diff --git a/tests/api/test_api_receive.nim b/tests/api/test_api_receive.nim new file mode 100644 index 000000000..52f8713f9 --- /dev/null +++ b/tests/api/test_api_receive.nim @@ -0,0 +1,193 @@ +{.used.} + +import std/[options, sequtils, net, sets] +import chronos, testutils/unittests, stew/byteutils +import libp2p/[peerid, peerinfo, crypto/crypto] +import ../testlib/[common, wakucore, wakunode, testasync] +import ../waku_archive/archive_utils + +import + waku, + waku/[ + waku_node, + waku_core, + common/broker/broker_context, + events/message_events, + waku_relay/protocol, + waku_archive, + waku_archive/common as archive_common, + node/delivery_service/delivery_service, + node/delivery_service/recv_service, + ] +import waku/factory/waku_conf +import tools/confutils/cli_args + +const TestTimeout = chronos.seconds(60) + +type ReceiveEventListenerManager = ref object + brokerCtx: BrokerContext + receivedListener: MessageReceivedEventListener + receivedEvent: AsyncEvent + receivedMessages: seq[WakuMessage] + targetCount: int + +proc newReceiveEventListenerManager( + brokerCtx: BrokerContext, expectedCount: int = 1 +): ReceiveEventListenerManager = + let manager = ReceiveEventListenerManager( + brokerCtx: brokerCtx, receivedMessages: @[], targetCount: expectedCount + ) + manager.receivedEvent = newAsyncEvent() + + manager.receivedListener = MessageReceivedEvent + .listen( + brokerCtx, + proc(event: MessageReceivedEvent) {.async: (raises: []).} = + manager.receivedMessages.add(event.message) + if manager.receivedMessages.len >= manager.targetCount: + manager.receivedEvent.fire() + , + ) + .expect("Failed to listen to MessageReceivedEvent") + + return manager + +proc teardown(manager: ReceiveEventListenerManager) = + MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener) + +proc waitForEvents( + manager: ReceiveEventListenerManager, timeout: Duration +): Future[bool] {.async.} = + return await manager.receivedEvent.wait().withTimeout(timeout) + +proc createApiNodeConf(numShards: uint16 = 1): WakuNodeConf = + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = cli_args.WakuMode.Core + conf.listenAddress = parseIpAddress("0.0.0.0") + conf.tcpPort = Port(0) + conf.discv5UdpPort = Port(0) + conf.clusterId = 3'u16 + conf.numShardsInNetwork = numShards + conf.reliabilityEnabled = true + conf.rest = false + result = conf + +suite "Messaging API, Receive Service (store recovery)": + asyncTest "recv_service delivers store-recovered messages via MessageReceivedEvent": + ## Message gets archived before subscriber exists, checkStore() recovers it. + ## This is a regression test: it proves that messages recovered via store by + ## the RecvService (instead of receiving via a live relay sub) are actually + ## delivered via the MessageReceivedEvent API. + + let numShards: uint16 = 1 + let shards = @[PubsubTopic("/waku/2/rs/3/0")] + let shard = shards[0] + let testTopic = ContentTopic("/waku/2/recv-test/proto") + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + discard + + # store node has archive, store, relay + # it archives messages from relay and serves them to the + # subscriber's store client when it comes up (later) + var storeNode: WakuNode + lockNewGlobalBrokerContext: + storeNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + storeNode.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on storeNode" + ) + (await storeNode.mountRelay()).expect("Failed to mount relay on storeNode") + let archiveDriver = newSqliteArchiveDriver() + storeNode.mountArchive(archiveDriver).expect("Failed to mount archive") + await storeNode.mountStore() + await storeNode.mountLibp2pPing() + await storeNode.start() + + for s in shards: + storeNode.subscribe((kind: PubsubSub, topic: s), dummyHandler).expect( + "Failed to sub storeNode" + ) + + let storeNodePeerInfo = storeNode.peerInfo.toRemotePeerInfo() + + # publisher node (relay) + var publisher: WakuNode + lockNewGlobalBrokerContext: + publisher = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + publisher.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on publisher" + ) + (await publisher.mountRelay()).expect("Failed to mount relay on publisher") + await publisher.mountLibp2pPing() + await publisher.start() + + for s in shards: + publisher.subscribe((kind: PubsubSub, topic: s), dummyHandler).expect( + "Failed to sub publisher" + ) + + # connect publisher to store so messages get archived + await publisher.connectToNodes(@[storeNodePeerInfo]) + + # wait for relay mesh + for _ in 0 ..< 50: + if publisher.wakuRelay.getNumPeersInMesh(shard).valueOr(0) > 0: + break + await sleepAsync(100.milliseconds) + + # publish before subscriber exists, gets archived + let missedPayload = "This message was missed".toBytes() + let missedMsg = WakuMessage( + payload: missedPayload, contentTopic: testTopic, version: 0, timestamp: now() + ) + discard (await publisher.publish(some(shard), missedMsg)).expect( + "Publish missed msg failed" + ) + + # wait for archive + block waitArchive: + for _ in 0 ..< 50: + let query = archive_common.ArchiveQuery( + includeData: false, contentTopics: @[testTopic], pubsubTopic: some(shard) + ) + let res = await storeNode.wakuArchive.findMessages(query) + if res.isOk() and res.get().hashes.len > 0: + break waitArchive + await sleepAsync(100.milliseconds) + raiseAssert "Message was not archived in time" + + # create subscriber + var subscriber: Waku + lockNewGlobalBrokerContext: + subscriber = (await createNode(createApiNodeConf(numShards))).expect( + "Failed to create subscriber" + ) + (await startWaku(addr subscriber)).expect("Failed to start subscriber") + + # connect subscriber to store (not publisher, so msg won't come via relay to it) + await subscriber.node.connectToNodes(@[storeNodePeerInfo]) + + # subscribe to content topic + (await subscriber.subscribe(testTopic)).expect("Failed to subscribe") + + # listen before triggering store check + let eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + # trigger store check, should recover and deliver via MessageReceivedEvent + await subscriber.deliveryService.recvService.checkStore() + + let received = await eventManager.waitForEvents(TestTimeout) + check received + check eventManager.receivedMessages.len == 1 + if eventManager.receivedMessages.len > 0: + check eventManager.receivedMessages[0].payload == missedPayload + + # cleanup + (await subscriber.stop()).expect("Failed to stop subscriber") + await publisher.stop() + await storeNode.stop() diff --git a/waku/events/delivery_events.nim b/waku/events/delivery_events.nim index f8eb0f48d..f27f02721 100644 --- a/waku/events/delivery_events.nim +++ b/waku/events/delivery_events.nim @@ -1,21 +1,5 @@ import waku/waku_core/[message/message, message/digest], waku/common/broker/event_broker -type DeliveryDirection* {.pure.} = enum - PUBLISHING - RECEIVING - -type DeliverySuccess* {.pure.} = enum - SUCCESSFUL - UNSUCCESSFUL - -EventBroker: - type DeliveryFeedbackEvent* = ref object - success*: DeliverySuccess - dir*: DeliveryDirection - comment*: string - msgHash*: WakuMessageHash - msg*: WakuMessage - EventBroker: type OnFilterSubscribeEvent* = object pubsubTopic*: string diff --git a/waku/node/delivery_service/delivery_service.nim b/waku/node/delivery_service/delivery_service.nim index fd728d048..f3d78d98e 100644 --- a/waku/node/delivery_service/delivery_service.nim +++ b/waku/node/delivery_service/delivery_service.nim @@ -12,7 +12,7 @@ import type DeliveryService* = ref object sendService*: SendService - recvService: RecvService + recvService*: RecvService subscriptionManager*: SubscriptionManager proc new*( diff --git a/waku/node/delivery_service/recv_service/recv_service.nim b/waku/node/delivery_service/recv_service/recv_service.nim index 9a85df2f9..9f01ac267 100644 --- a/waku/node/delivery_service/recv_service/recv_service.nim +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -12,7 +12,6 @@ import waku_store/common, waku_filter_v2/client, waku_core/topics, - events/delivery_events, events/message_events, waku_node, common/broker/broker_context, @@ -27,7 +26,8 @@ const PruneOldMsgsPeriod = chronos.minutes(1) const DelayExtra* = chronos.seconds(5) ## Additional security time to overlap the missing messages queries -type TupleHashAndMsg = tuple[hash: WakuMessageHash, msg: WakuMessage] +type TupleHashAndMsg = + tuple[hash: WakuMessageHash, msg: WakuMessage, pubsubTopic: PubsubTopic] type RecvMessage = object msgHash: WakuMessageHash @@ -59,88 +59,78 @@ proc getMissingMsgsFromStore( return err("getMissingMsgsFromStore: " & $error) let otherwiseMsg = WakuMessage() - ## message to be returned if the Option message is none + let otherwiseTopic = PubsubTopic("") return ok( - storeResp.messages.mapIt((hash: it.messageHash, msg: it.message.get(otherwiseMsg))) + storeResp.messages.mapIt( + ( + hash: it.messageHash, + msg: it.message.get(otherwiseMsg), + pubsubTopic: it.pubsubTopic.get(otherwiseTopic), + ) + ) ) -proc performDeliveryFeedback( - self: RecvService, - success: DeliverySuccess, - dir: DeliveryDirection, - comment: string, - msgHash: WakuMessageHash, - msg: WakuMessage, -) {.gcsafe, raises: [].} = - info "recv monitor performDeliveryFeedback", - success, dir, comment, msg_hash = shortLog(msgHash) - - DeliveryFeedbackEvent.emit( - brokerCtx = self.brokerCtx, - success = success, - dir = dir, - comment = comment, - msgHash = msgHash, - msg = msg, - ) - -proc msgChecker(self: RecvService) {.async.} = - ## Continuously checks if a message has been received - while true: - await sleepAsync(StoreCheckPeriod) - self.endTimeToCheck = getNowInNanosecondTime() - - var msgHashesInStore = newSeq[WakuMessageHash](0) - for pubsubTopic, contentTopics in self.subscriptionManager.subscribedTopics: - let storeResp: StoreQueryResponse = ( - await self.node.wakuStoreClient.queryToAny( - StoreQueryRequest( - includeData: false, - pubsubTopic: some(pubsubTopic), - contentTopics: toSeq(contentTopics), - startTime: some(self.startTimeToCheck - DelayExtra.nanos), - endTime: some(self.endTimeToCheck + DelayExtra.nanos), - ) - ) - ).valueOr: - error "msgChecker failed to get remote msgHashes", - pubsubTopic = pubsubTopic, cTopics = toSeq(contentTopics), error = $error - continue - - msgHashesInStore.add(storeResp.messages.mapIt(it.messageHash)) - - ## compare the msgHashes seen from the store vs the ones received directly - let rxMsgHashes = self.recentReceivedMsgs.mapIt(it.msgHash) - let missedHashes: seq[WakuMessageHash] = - msgHashesInStore.filterIt(not rxMsgHashes.contains(it)) - - ## Now retrieve the missed WakuMessages - let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes) - if missingMsgsRet.isOk(): - ## Give feedback so that the api client can perfom any action with the missed messages - for msgTuple in missingMsgsRet.get(): - self.performDeliveryFeedback( - DeliverySuccess.UNSUCCESSFUL, RECEIVING, "Missed message", msgTuple.hash, - msgTuple.msg, - ) - else: - error "failed to retrieve missing messages: ", error = $missingMsgsRet.error - - ## update next check times - self.startTimeToCheck = self.endTimeToCheck - proc processIncomingMessageOfInterest( self: RecvService, pubsubTopic: string, message: WakuMessage -) = - ## Resolve an incoming network message that was already filtered by topic. +): bool = ## Deduplicate (by hash), store (saves in recently-seen messages) and emit ## the MAPI MessageReceivedEvent for every unique incoming message. + ## Returns true if the message was new and the MessageReceivedEvent was properly emitted. let msgHash = computeMessageHash(pubsubTopic, message) if not self.recentReceivedMsgs.anyIt(it.msgHash == msgHash): let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp) self.recentReceivedMsgs.add(rxMsg) MessageReceivedEvent.emit(self.brokerCtx, msgHash.to0xHex(), message) + return true + return false + +proc checkStore*(self: RecvService) {.async.} = + ## Checks the store for messages that were not received directly and + ## delivers them via MessageReceivedEvent. + self.endTimeToCheck = getNowInNanosecondTime() + + ## query store and deliver new recovered messages per subscribed topic + for pubsubTopic, contentTopics in self.subscriptionManager.subscribedTopics: + let storeResp: StoreQueryResponse = ( + await self.node.wakuStoreClient.queryToAny( + StoreQueryRequest( + includeData: false, + pubsubTopic: some(pubsubTopic), + contentTopics: toSeq(contentTopics), + startTime: some(self.startTimeToCheck - DelayExtra.nanos), + endTime: some(self.endTimeToCheck + DelayExtra.nanos), + ) + ) + ).valueOr: + error "msgChecker failed to get remote msgHashes", + pubsubTopic = pubsubTopic, cTopics = toSeq(contentTopics), error = $error + continue + + ## compare the msgHashes seen from the store vs the ones received directly + let msgHashesInStore = storeResp.messages.mapIt(it.messageHash) + let rxMsgHashes = self.recentReceivedMsgs.mapIt(it.msgHash) + let missedHashes: seq[WakuMessageHash] = + msgHashesInStore.filterIt(not rxMsgHashes.contains(it)) + + ## Now retrieve the missing WakuMessages and deliver them + let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes) + if missingMsgsRet.isOk(): + for msgTuple in missingMsgsRet.get(): + if self.processIncomingMessageOfInterest(msgTuple.pubsubTopic, msgTuple.msg): + info "recv service store-recovered message", + msg_hash = shortLog(msgTuple.hash), pubsubTopic = msgTuple.pubsubTopic + else: + error "failed to retrieve missing messages: ", error = $missingMsgsRet.error + + ## update next check times + self.startTimeToCheck = self.endTimeToCheck + +proc msgChecker(self: RecvService) {.async.} = + ## Continuously checks if a message has been received + while true: + await sleepAsync(StoreCheckPeriod) + await self.checkStore() proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T = ## The storeClient will help to acquire any possible missed messages @@ -176,7 +166,7 @@ proc startRecvService*(self: RecvService) = shard = event.topic, contenttopic = event.message.contentTopic return - self.processIncomingMessageOfInterest(event.topic, event.message), + discard self.processIncomingMessageOfInterest(event.topic, event.message), ).valueOr: error "Failed to set MessageSeenEvent listener", error = error quit(QuitFailure) From c04df751db25ab5d5eb3a51f8247a9d03d8b63c7 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Fri, 10 Apr 2026 07:38:02 -0300 Subject: [PATCH 121/155] Fix BearSSL and NAT lib build reproducibility (#3806) * pass -mssse3 on x86_64 to BearSSL and NAT C lib builds * add BearSSL.mk and Nat.mk to nimbledeps cache key From a4db8895e47164fcdcfc014962ddbccd1a023b1f Mon Sep 17 00:00:00 2001 From: darshankabariya Date: Fri, 10 Apr 2026 17:03:25 +0530 Subject: [PATCH 122/155] chore: resolving lint --- tools/confutils/cli_args.nim | 39 +++++++++++++++--------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 90a349d9d..7d531159b 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -50,13 +50,13 @@ type ConfResult*[T] = Result[T, string] type EthRpcUrl* = distinct string type StartUpCommand* = enum - noCommand # default, runs waku + noCommand # default, runs waku generateRlnKeystore # generates a new RLN keystore type WakuMode* {.pure.} = enum noMode # default - use explicit CLI flags as-is - Core # full service node - Edge # client-only node + Core # full service node + Edge # client-only node type WakuNodeConf* = object configFile* {. @@ -183,8 +183,7 @@ type WakuNodeConf* = object name: "agent-string" .}: string - nodekey* {.desc: "P2P node private key as 64 char hex string.", - name: "nodekey".}: + nodekey* {.desc: "P2P node private key as 64 char hex string.", name: "nodekey".}: Option[PrivateKey] listenAddress* {. @@ -193,13 +192,11 @@ type WakuNodeConf* = object name: "listen-address" .}: IpAddress - tcpPort* {.desc: "TCP listening port.", defaultValue: 60000, - name: "tcp-port".}: + tcpPort* {.desc: "TCP listening port.", defaultValue: 60000, name: "tcp-port".}: Port portsShift* {. - desc: "Add a shift to all port numbers.", defaultValue: 0, - name: "ports-shift" + desc: "Add a shift to all port numbers.", defaultValue: 0, name: "ports-shift" .}: uint16 nat* {. @@ -243,13 +240,11 @@ type WakuNodeConf* = object .}: int peerStoreCapacity* {. - desc: "Maximum stored peers in the peerstore.", - name: "peer-store-capacity" + desc: "Maximum stored peers in the peerstore.", name: "peer-store-capacity" .}: Option[int] peerPersistence* {. - desc: "Enable peer persistence.", defaultValue: false, - name: "peer-persistence" + desc: "Enable peer persistence.", defaultValue: false, name: "peer-persistence" .}: bool ## DNS addrs config @@ -406,7 +401,7 @@ hence would have reachability issues.""", storeSyncInterval* {. desc: "Interval between store sync attempts. In seconds.", - defaultValue: 300, # 5 minutes + defaultValue: 300, # 5 minutes name: "store-sync-interval" .}: uint32 @@ -437,7 +432,7 @@ hence would have reachability issues.""", filterSubscriptionTimeout* {. desc: "Timeout for filter subscription without ping or refresh it, in seconds. Only for v2 filter protocol.", - defaultValue: 300, # 5 minutes + defaultValue: 300, # 5 minutes name: "filter-subscription-timeout" .}: uint16 @@ -664,8 +659,7 @@ with the drawback of consuming some more bandwidth.""", .}: bool websocketPort* {. - desc: "WebSocket listening port.", defaultValue: 8000, - name: "websocket-port" + desc: "WebSocket listening port.", defaultValue: 8000, name: "websocket-port" .}: Port websocketSecureSupport* {. @@ -762,8 +756,7 @@ proc parseCmdArg*(T: type ProtectedShard, p: string): T = raise newException(ValueError, "Invalid public key") if isNumber(elements[0]): - return ProtectedShard(shard: uint16.parseCmdArg(elements[0]), - key: publicKey) + return ProtectedShard(shard: uint16.parseCmdArg(elements[0]), key: publicKey) # TODO: Remove when removing protected-topic configuration let shard = RelayShard.parse(elements[0]).valueOr: @@ -891,11 +884,11 @@ proc load*(T: type WakuNodeConf, version = ""): ConfResult[T] = secondarySources = proc( conf: WakuNodeConf, sources: auto ) {.gcsafe, raises: [ConfigurationError].} = - sources.addConfigFile(Envvar, InputFile("wakunode2")) + sources.addConfigFile(Envvar, InputFile("wakunode2")) - if conf.configFile.isSome(): - sources.addConfigFile(Toml, conf.configFile.get()) - , + if conf.configFile.isSome(): + sources.addConfigFile(Toml, conf.configFile.get()) + , ) ok(conf) From 166dc69c390a95efa82f6b439d002eabe712b57f Mon Sep 17 00:00:00 2001 From: Gabriel Cruz <8129788+gmelodie@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:44:30 -0300 Subject: [PATCH 123/155] chore: bump nim-jwt version (#3812) --- waku.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku.nimble b/waku.nimble index a6e824528..3a7956873 100644 --- a/waku.nimble +++ b/waku.nimble @@ -64,7 +64,7 @@ requires "nim >= 2.2.4", requires "https://github.com/logos-messaging/nim-ffi" requires "https://github.com/vacp2p/nim-lsquic" -requires "https://github.com/vacp2p/nim-jwt.git#18f8378de52b241f321c1f9ea905456e89b95c6f" +requires "https://github.com/vacp2p/nim-jwt.git#057ec95eb5af0eea9c49bfe9025b3312c95dc5f2" proc getMyCPU(): string = ## Need to set cpu more explicit manner to avoid arch issues between dependencies From 04b8e8c2a8900e77050fc41677ae7293c7e45790 Mon Sep 17 00:00:00 2001 From: darshankabariya Date: Tue, 14 Apr 2026 18:07:02 +0530 Subject: [PATCH 124/155] chore: update remaining changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3fcaa414..37220a235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,8 @@ - Add gasprice overflow check ([#3636](https://github.com/logos-messaging/logos-delivery/pull/3636)) ([a8590a0a](https://github.com/logos-messaging/logos-delivery/commit/a8590a0a)) - Pin RLN dependencies to specific version ([#3649](https://github.com/logos-messaging/logos-delivery/pull/3649)) ([834eea94](https://github.com/logos-messaging/logos-delivery/commit/834eea94)) - Update CI/README references after repository rename to logos-delivery ([#3729](https://github.com/logos-messaging/logos-delivery/pull/3729)) ([895f3e2d](https://github.com/logos-messaging/logos-delivery/commit/895f3e2d)) +- Simplify on chain group manager error handling ([#3678](https://github.com/logos-messaging/logos-delivery/pull/3678)) ([bc9454db](https://github.com/logos-messaging/logos-delivery/commit/bc9454db)) +- Extend RequestBroker with support for native/external types and sync requests ([#3665](https://github.com/logos-messaging/logos-delivery/pull/3665)) ([33233255](https://github.com/logos-messaging/logos-delivery/commit/33233255)) ### This release supports the following [libp2p protocols](https://docs.libp2p.io/concepts/protocols/): From 509c8755336948b5759310555fc0db9ffc895ada Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:12:52 +0200 Subject: [PATCH 125/155] chore: enable postgres support in nix liblogosdelivery build (#3813) Add -d:postgres and -d:nimDebugDlOpen to both the dynamic and static nim c invocations in nix/default.nix, matching the POSTGRES=1 flag already used in the Make-based build path. --- nix/default.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/default.nix b/nix/default.nix index f90b8185e..0d1de2ece 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -62,6 +62,8 @@ pkgs.stdenv.mkDerivation { --path:$NAT_TRAV/src \ --passL:"-L${zerokitRln}/lib -lrln" \ --define:disable_libbacktrace \ + --define:postgres \ + --define:nimDebugDlOpen \ --out:build/liblogosdelivery.${libExt} \ --app:lib \ --threads:on \ @@ -81,6 +83,8 @@ pkgs.stdenv.mkDerivation { --path:$NAT_TRAV/src \ --passL:"-L${zerokitRln}/lib -lrln" \ --define:disable_libbacktrace \ + --define:postgres \ + --define:nimDebugDlOpen \ --out:build/liblogosdelivery.a \ --app:staticlib \ --threads:on \ From ca4dbb19e0cb61a496921c286ffcef76e62ee9b9 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 20 Apr 2026 08:05:54 -0300 Subject: [PATCH 126/155] Improve logging of content topic on server (#3818) --- waku/waku_filter_v2/protocol.nim | 3 ++- waku/waku_lightpush/protocol.nim | 1 + waku/waku_lightpush_legacy/protocol.nim | 1 + waku/waku_relay/protocol.nim | 5 ++++- waku/waku_rln_relay/rln_relay.nim | 23 ++++++++++++++++------- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/waku/waku_filter_v2/protocol.nim b/waku/waku_filter_v2/protocol.nim index 451bf5cb2..35620b6cd 100644 --- a/waku/waku_filter_v2/protocol.nim +++ b/waku/waku_filter_v2/protocol.nim @@ -244,7 +244,8 @@ proc handleMessage*( ) {.async.} = let msgHash = computeMessageHash(pubsubTopic, message).to0xHex() - info "handling message", pubsubTopic = pubsubTopic, msg_hash = msgHash + info "handling message", + pubsubTopic = pubsubTopic, contentTopic = message.contentTopic, msg_hash = msgHash let handleMessageStartTime = Moment.now() diff --git a/waku/waku_lightpush/protocol.nim b/waku/waku_lightpush/protocol.nim index ecbff8461..8336f4dfc 100644 --- a/waku/waku_lightpush/protocol.nim +++ b/waku/waku_lightpush/protocol.nim @@ -68,6 +68,7 @@ proc handleRequest( peer_id = peerId, requestId = pushRequest.requestId, pubsubTopic = pushRequest.pubsubTopic, + contentTopic = pushRequest.message.contentTopic, msg_hash = msg_hash, receivedTime = getNowInNanosecondTime() diff --git a/waku/waku_lightpush_legacy/protocol.nim b/waku/waku_lightpush_legacy/protocol.nim index 72fc963ee..f5ed60134 100644 --- a/waku/waku_lightpush_legacy/protocol.nim +++ b/waku/waku_lightpush_legacy/protocol.nim @@ -50,6 +50,7 @@ proc handleRequest*( peer_id = peerId, requestId = requestId, pubsubTopic = pubsubTopic, + contentTopic = message.contentTopic, msg_hash = msg_hash, receivedTime = getNowInNanosecondTime() diff --git a/waku/waku_relay/protocol.nim b/waku/waku_relay/protocol.nim index 490feae87..79d3702eb 100644 --- a/waku/waku_relay/protocol.nim +++ b/waku/waku_relay/protocol.nim @@ -223,6 +223,7 @@ proc logMessageInfo*( msg_id = msg_id_short, from_peer_id = remotePeerId, topic = topic, + contentTopic = msg.contentTopic, receivedTime = getNowInNanosecondTime(), payloadSizeBytes = payloadSize else: @@ -232,6 +233,7 @@ proc logMessageInfo*( msg_id = msg_id_short, to_peer_id = remotePeerId, topic = topic, + contentTopic = msg.contentTopic, sentTime = getNowInNanosecondTime(), payloadSizeBytes = payloadSize @@ -680,7 +682,8 @@ proc publish*( let data = message.encode().buffer let msgHash = computeMessageHash(pubsubTopic, message).to0xHex() - notice "start publish Waku message", msg_hash = msgHash, pubsubTopic = pubsubTopic + notice "start publish Waku message", + msg_hash = msgHash, pubsubTopic = pubsubTopic, contentTopic = message.contentTopic let relayedPeerCount = await procCall GossipSub(w).publish(pubsubTopic, data) diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index 8559dcd66..ac128b5bc 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -201,14 +201,18 @@ proc validateMessage*( if timeDiff > rlnPeer.rlnMaxTimestampGap: warn "invalid message: timestamp difference exceeds threshold", - timeDiff = timeDiff, maxTimestampGap = rlnPeer.rlnMaxTimestampGap + timeDiff = timeDiff, + maxTimestampGap = rlnPeer.rlnMaxTimestampGap, + contentTopic = msg.contentTopic waku_rln_invalid_messages_total.inc(labelValues = ["invalid_timestamp"]) return MessageValidationResult.Invalid let computedEpoch = rlnPeer.calcEpoch(messageTime) if proof.epoch != computedEpoch: warn "invalid message: timestamp mismatches epoch", - proofEpoch = fromEpoch(proof.epoch), computedEpoch = fromEpoch(computedEpoch) + proofEpoch = fromEpoch(proof.epoch), + computedEpoch = fromEpoch(computedEpoch), + contentTopic = msg.contentTopic waku_rln_invalid_messages_total.inc(labelValues = ["timestamp_mismatch"]) return MessageValidationResult.Invalid @@ -216,7 +220,8 @@ proc validateMessage*( if not rootValidationRes: warn "invalid message: provided root does not belong to acceptable window of roots", provided = proof.merkleRoot.inHex(), - validRoots = rlnPeer.groupManager.validRoots.mapIt(it.inHex()) + validRoots = rlnPeer.groupManager.validRoots.mapIt(it.inHex()), + contentTopic = msg.contentTopic waku_rln_invalid_messages_total.inc(labelValues = ["invalid_root"]) return MessageValidationResult.Invalid @@ -233,12 +238,14 @@ proc validateMessage*( proofVerificationRes.isOkOr: waku_rln_errors_total.inc(labelValues = ["proof_verification"]) - warn "invalid message: proof verification failed", payloadLen = msg.payload.len + warn "invalid message: proof verification failed", + payloadLen = msg.payload.len, contentTopic = msg.contentTopic return MessageValidationResult.Invalid if not proofVerificationRes.value(): # invalid proof - warn "invalid message: invalid proof", payloadLen = msg.payload.len + warn "invalid message: invalid proof", + payloadLen = msg.payload.len, contentTopic = msg.contentTopic waku_rln_invalid_messages_total.inc(labelValues = ["invalid_proof"]) return MessageValidationResult.Invalid @@ -252,11 +259,13 @@ proc validateMessage*( if hasDup.isErr(): waku_rln_errors_total.inc(labelValues = ["duplicate_check"]) elif hasDup.value == true: - trace "invalid message: message is spam", payloadLen = msg.payload.len + trace "invalid message: message is spam", + payloadLen = msg.payload.len, contentTopic = msg.contentTopic waku_rln_spam_messages_total.inc() return MessageValidationResult.Spam - trace "message is valid", payloadLen = msg.payload.len + trace "message is valid", + payloadLen = msg.payload.len, contentTopic = msg.contentTopic # Metric increment moved to validator to include shard label return MessageValidationResult.Valid From 9ae108b4a7c4f15218e40d5df04ee01fc7327c3e Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 20 Apr 2026 08:16:01 -0300 Subject: [PATCH 127/155] Fix peer stats endpoint (#3815) --- waku/rest_api/endpoint/admin/handlers.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/waku/rest_api/endpoint/admin/handlers.nim b/waku/rest_api/endpoint/admin/handlers.nim index 1904d43f9..304fdabf8 100644 --- a/waku/rest_api/endpoint/admin/handlers.nim +++ b/waku/rest_api/endpoint/admin/handlers.nim @@ -344,7 +344,7 @@ proc installAdminV1GetPeersHandler(router: var RestRouter, node: WakuNode) = for ps in relayPeers: totalRelayPeers += ps.peers.len stat[$ps.shard] = ps.peers.len - stat["Total relay peers"] = relayPeers.len + stat["Total relay peers"] = totalRelayPeers stat # stats of mesh peers @@ -355,7 +355,7 @@ proc installAdminV1GetPeersHandler(router: var RestRouter, node: WakuNode) = for ps in meshPeers: totalMeshPeers += ps.peers.len stat[$ps.shard] = ps.peers.len - stat["Total mesh peers"] = meshPeers.len + stat["Total mesh peers"] = totalMeshPeers stat var protoStats = initOrderedTable[string, int]() From 9cbb4e7338c62abe9345fe70cc81694373be87e2 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 20 Apr 2026 08:48:27 -0300 Subject: [PATCH 128/155] fix: prefer --num-shards-in-network over preset (#3816) * fill numShardsInCluster from preset when builder slot is none * add regression tests --- tests/factory/test_waku_conf.nim | 48 +++++++++++++++++++ .../conf_builder/waku_conf_builder.nim | 13 +++-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/tests/factory/test_waku_conf.nim b/tests/factory/test_waku_conf.nim index 9d05f7fb5..eeacf791b 100644 --- a/tests/factory/test_waku_conf.nim +++ b/tests/factory/test_waku_conf.nim @@ -213,6 +213,54 @@ suite "Waku Conf - build with cluster conf": check rlnRelayConf.epochSizeSec == networkConf.rlnEpochSizeSec check rlnRelayConf.userMessageLimit == userMessageLimit.uint + test "num-shards-in-network > 0 overrides preset": + ## Setup + let networkConf = NetworkConf.LogosDevConf() + var builder = WakuConfBuilder.init() + + # Sanity check + check networkConf.shardingConf.kind == AutoSharding + check networkConf.shardingConf.numShardsInCluster > 1 + + ## Given: preset says >1 shards but user explicitly sets 1 + builder.withNetworkConf(networkConf) + builder.withNumShardsInCluster(1) + builder.withShardingConf(AutoSharding) + + ## When + let conf = builder.build().expect("build should succeed") + + ## Then: user value wins, not preset + conf.validate().expect("conf should validate") + check conf.shardingConf.kind == AutoSharding + check conf.shardingConf.numShardsInCluster == 1 + + test "num-shards-in-network == 0 does not override preset": + ## Passing an AutoSharding preset and trying to override with + ## --num-shards-in-network=0 (which is StaticSharding) doesn't work. + ## Note that --num-shards-in-network=0 and omitting the switch are + ## internally the same. Promoting the config to an Option[uint16] is + ## probably not worth it since overriding an AutoSharding preset with + ## StaticSharding shouldn't make any sense (that is, no use case). + + ## Given: emulate --preset=logos.dev --num-shards-in-network=0 + let networkConf = NetworkConf.LogosDevConf() + var builder = WakuConfBuilder.init() + builder.withNetworkConf(networkConf) + # Note: builder.withNumShardsInCluster() is not called when the + # value that comes from the CLI path is 0 (which means it was + # either set to 0 or was left unset). + builder.withShardingConf(StaticSharding) + + ## When + let conf = builder.build().expect("build should succeed") + + ## Then: preset wins and StaticSharding user intent is lost + conf.validate().expect("conf should validate") + check conf.shardingConf.kind == networkConf.shardingConf.kind + check conf.shardingConf.numShardsInCluster == + networkConf.shardingConf.numShardsInCluster + suite "Waku Conf - node key": test "Node key is generated": ## Setup diff --git a/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index 956d733d3..78dbd9eb9 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -299,7 +299,6 @@ proc buildShardingConf( bNumShardsInCluster: Option[uint16], bSubscribeShards: Option[seq[uint16]], ): (ShardingConf, seq[uint16]) = - echo "bSubscribeShards: ", bSubscribeShards case bShardingConfKind.get(AutoSharding) of StaticSharding: (ShardingConf(kind: StaticSharding), bSubscribeShards.get(@[])) @@ -374,17 +373,17 @@ proc applyNetworkConf(builder: var WakuConfBuilder) = warn "Sharding Conf was provided alongside a network conf", used = networkConf.shardingConf.kind, discarded = builder.shardingConf - if builder.numShardsInCluster.isSome(): - warn "Num Shards In Cluster was provided alongside a network conf", - used = networkConf.shardingConf.numShardsInCluster, - discarded = builder.numShardsInCluster - case networkConf.shardingConf.kind of StaticSharding: builder.shardingConf = some(StaticSharding) of AutoSharding: builder.shardingConf = some(AutoSharding) - builder.numShardsInCluster = some(networkConf.shardingConf.numShardsInCluster) + if builder.numShardsInCluster.isSome(): + warn "Num Shards In Cluster overrides network conf preset", + used = builder.numShardsInCluster.get(), + ignored = networkConf.shardingConf.numShardsInCluster + else: + builder.numShardsInCluster = some(networkConf.shardingConf.numShardsInCluster) if networkConf.discv5Discovery: if builder.discv5Conf.enabled.isNone: From cda0197168bed3ebf2f3002338a9135a4dc9706c Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:54:34 +0200 Subject: [PATCH 129/155] use nimble 0.22.3 and more appropriate nimble.lock (#3809) --- .github/workflows/ci.yml | 8 +- .github/workflows/container-image.yml | 4 +- .github/workflows/windows-build.yml | 19 +- Makefile | 54 +- config.nims | 3 +- flake.nix | 14 +- nimble.lock | 750 ++++++++++--------- nix/deps.nix | 314 ++++---- nix/shell.nix | 17 +- scripts/install_nim.sh | 72 ++ waku.nimble | 12 +- waku/incentivization/eligibility_manager.nim | 7 +- 12 files changed, 696 insertions(+), 578 deletions(-) create mode 100755 scripts/install_nim.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f6fcdfec..b45853e21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ env: MAKEFLAGS: "-j${NPROC}" NIMFLAGS: "--parallelBuild:${NPROC} --colors:off -d:chronicles_colors:none" NIM_VERSION: '2.2.4' - NIMBLE_VERSION: '0.18.2' + NIMBLE_VERSION: '0.22.3' jobs: changes: # changes detection @@ -83,7 +83,7 @@ jobs: path: | nimbledeps/ nimble.paths - key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} + key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} - name: Install nimble deps if: steps.cache-nimbledeps.outputs.cache-hit != 'true' @@ -136,7 +136,7 @@ jobs: path: | nimbledeps/ nimble.paths - key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} + key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} - name: Install nimble deps if: steps.cache-nimbledeps.outputs.cache-hit != 'true' @@ -215,7 +215,7 @@ jobs: path: | nimbledeps/ nimble.paths - key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} + key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} - name: Install nimble deps if: steps.cache-nimbledeps.outputs.cache-hit != 'true' diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml index c2fb9d4d2..0783c1f66 100644 --- a/.github/workflows/container-image.yml +++ b/.github/workflows/container-image.yml @@ -16,7 +16,7 @@ env: MAKEFLAGS: "-j${NPROC}" NIMFLAGS: "--parallelBuild:${NPROC}" NIM_VERSION: '2.2.4' - NIMBLE_VERSION: '0.18.2' + NIMBLE_VERSION: '0.22.3' # This workflow should not run for outside contributors # If org secrets are not available, we'll avoid building and publishing the docker image and we'll pass the workflow @@ -69,7 +69,7 @@ jobs: path: | nimbledeps/ nimble.paths - key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} + key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} - name: Install nimble deps if: ${{ steps.secrets.outcome == 'success' && steps.cache-nimbledeps.outputs.cache-hit != 'true' }} diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 5b0894368..50f1602cd 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -10,7 +10,7 @@ on: env: NPROC: 4 NIM_VERSION: '2.2.4' - NIMBLE_VERSION: '0.18.2' + NIMBLE_VERSION: '0.22.3' jobs: build: @@ -80,14 +80,13 @@ jobs: cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y echo "$HOME/.nimble/bin" >> $GITHUB_PATH - - name: Cache nimble deps - id: cache-nimbledeps - uses: actions/cache@v4 - with: - path: | - nimbledeps/ - nimble.paths - key: ${{ runner.os }}-nimbledeps-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} + - name: Patch nimble.lock for Windows nim checksum + # nimble.exe uses Windows Git (core.autocrlf=true by default), which converts LF→CRLF + # on checkout. This changes the SHA1 of the nim package source tree relative to the + # Linux-computed checksum stored in nimble.lock. Patch the lock file with the + # Windows-computed checksum before nimble reads it. + run: | + sed -i 's/68bb85cbfb1832ce4db43943911b046c3af3caab/a092a045d3a427d127a5334a6e59c76faff54686/g' nimble.lock - name: Install nimble deps if: steps.cache-nimbledeps.outputs.cache-hit != 'true' @@ -124,4 +123,4 @@ jobs: else echo "Build failed: libwaku.dll not found" exit 1 - fi + fi \ No newline at end of file diff --git a/Makefile b/Makefile index cabeec80f..7ba417527 100644 --- a/Makefile +++ b/Makefile @@ -18,8 +18,11 @@ ifneq (,$(findstring MINGW,$(detected_OS))) detected_OS := Windows endif +# Ensure the nim/nimble installed by install-nim/install-nimble are found first +export PATH := $(HOME)/.nimble/bin:$(PATH) + # NIM binary location -NIM_BINARY := $(shell which nim) +NIM_BINARY := $(shell which nim 2>/dev/null) NPH := $(HOME)/.nimble/bin/nph NIMBLEDEPS_STAMP := nimbledeps/.nimble-setup @@ -39,7 +42,7 @@ endif ########## ## Main ## ########## -.PHONY: all test update clean examples deps nimble +.PHONY: all test update clean examples deps nimble install-nim install-nimble # default target all: | wakunode2 libwaku liblogosdelivery @@ -67,7 +70,7 @@ waku.nims: ln -s waku.nimble $@ $(NIMBLEDEPS_STAMP): nimble.lock | waku.nims - @if ! command -v nimble > /dev/null 2>&1; then $(MAKE) install-nimble; fi + $(MAKE) install-nimble nimble setup --localdeps $(MAKE) build-nph $(MAKE) rebuild-bearssl-nimbledeps @@ -81,59 +84,28 @@ update: clean: rm -rf build 2> /dev/null || true rm -rf nimbledeps 2> /dev/null || true - rm nimble.lock 2> /dev/null || true rm -fr nimcache 2> /dev/null || true rm nimble.paths 2> /dev/null || true nimble clean -REQUIRED_NIM_VERSION := $(shell grep -E '^const NimVersion\s*=' waku.nimble | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') -REQUIRED_NIMBLE_VERSION := $(shell grep -E '^const NimbleVersion\s*=' waku.nimble | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') +REQUIRED_NIM_VERSION := $(shell grep -E '^const RequiredNimVersion\s*=' waku.nimble | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') +REQUIRED_NIMBLE_VERSION := $(shell grep -E '^const RequiredNimbleVersion\s*=' waku.nimble | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') install-nim: - $(eval NIM_OS := $(shell uname -s | tr 'A-Z' 'a-z' | sed 's/darwin/macosx/')) - $(eval NIM_ARCH := $(shell uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')) - $(eval NIM_INSTALL_DIR := $(HOME)/.nim_runtime) - @nim_ver=$$(nim --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ - if [ "$$nim_ver" = "$(REQUIRED_NIM_VERSION)" ]; then \ - echo "nim $(REQUIRED_NIM_VERSION) already installed, skipping."; \ - else \ - curl -L "https://github.com/nim-lang/Nim/releases/download/v$(REQUIRED_NIM_VERSION)/nim-$(REQUIRED_NIM_VERSION)-$(NIM_OS)_$(NIM_ARCH).tar.xz" \ - -o /tmp/nim-$(REQUIRED_NIM_VERSION).tar.xz && \ - tar -xJf /tmp/nim-$(REQUIRED_NIM_VERSION).tar.xz -C /tmp && \ - mkdir -p $(NIM_INSTALL_DIR) && \ - cd /tmp/nim-$(REQUIRED_NIM_VERSION) && ./install.sh $(NIM_INSTALL_DIR); \ - fi + scripts/install_nim.sh $(REQUIRED_NIM_VERSION) install-nimble: install-nim @nimble_ver=$$(nimble --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ if [ "$$nimble_ver" = "$(REQUIRED_NIMBLE_VERSION)" ]; then \ echo "nimble $(REQUIRED_NIMBLE_VERSION) already installed, skipping."; \ else \ - cd /tmp && PATH="$(HOME)/.nim_runtime/bin:$$PATH" \ - nimble install "nimble@$(REQUIRED_NIMBLE_VERSION)" -y; \ + cd $$(mktemp -d) && nimble install "nimble@$(REQUIRED_NIMBLE_VERSION)" -y; \ fi build: - @nim_ver=$$(nim --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ - if [ "$$nim_ver" != "$(REQUIRED_NIM_VERSION)" ]; then \ - echo "Error: Nim $(REQUIRED_NIM_VERSION) is required, but found '$$nim_ver'"; \ - exit 1; \ - fi - @nimble_ver=$$(nimble --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ - if [ "$$nimble_ver" != "$(REQUIRED_NIMBLE_VERSION)" ]; then \ - echo "Error: Nimble $(REQUIRED_NIMBLE_VERSION) is required, but found '$$nimble_ver'"; \ - exit 1; \ - fi mkdir -p build -nimble: - echo "Inside nimble target, checking for nimble..." && \ - command -v nimble >/dev/null 2>&1 || { \ - mv nimbledeps nimbledeps_backup 2>/dev/null || true; \ - echo "choosenim not found, installing ..."; \ - curl -sSf https://nim-lang.org/choosenim/init.sh | sh; \ - mv nimbledeps_backup nimbledeps 2>/dev/null || true; \ - } +nimble: install-nimble ## Possible values: prod; debug TARGET ?= prod @@ -230,7 +202,7 @@ clean: | clean-librln ################# .PHONY: testcommon -testcommon: | build +testcommon: | $(NIMBLEDEPS_STAMP) build echo -e $(BUILD_MSG) "build/$@" && \ nimble testcommon @@ -239,7 +211,7 @@ testcommon: | build ########## .PHONY: testwaku wakunode2 testwakunode2 example2 chat2 chat2bridge liteprotocoltester -testwaku: | build rln-deps librln +testwaku: | $(NIMBLEDEPS_STAMP) build rln-deps librln echo -e $(BUILD_MSG) "build/$@" && \ nimble test diff --git a/config.nims b/config.nims index 329384ac4..0f6052c9b 100644 --- a/config.nims +++ b/config.nims @@ -83,8 +83,9 @@ if not defined(macosx) and not defined(android): # add debugging symbols and original files and line numbers --debugger: native - if not (defined(windows) and defined(i386)) and not defined(disable_libbacktrace): + when defined(enable_libbacktrace): # light-weight stack traces using libbacktrace and libunwind + # opt-in: pass -d:enable_libbacktrace (requires libbacktrace in project deps) --define: nimStackTraceOverride switch("import", "libbacktrace") diff --git a/flake.nix b/flake.nix index 57592722b..31d5a120c 100644 --- a/flake.nix +++ b/flake.nix @@ -36,9 +36,21 @@ forAllSystems = nixpkgs.lib.genAttrs systems; + nimbleOverlay = final: prev: { + nimble = prev.nimble.overrideAttrs (_: { + version = "0.22.3"; + src = prev.fetchFromGitHub { + owner = "nim-lang"; + repo = "nimble"; + rev = "v0.22.3"; + sha256 = "sha256-f7DYpRGVUeSi6basK1lfu5AxZpMFOSJ3oYsy+urYErg="; + }; + }); + }; + pkgsFor = system: import nixpkgs { inherit system; - overlays = [ (import rust-overlay) ]; + overlays = [ (import rust-overlay) nimbleOverlay ]; }; in { packages = forAllSystems (system: diff --git a/nimble.lock b/nimble.lock index 96f64baf3..7c76f7fa9 100644 --- a/nimble.lock +++ b/nimble.lock @@ -1,103 +1,66 @@ { "version": 2, "packages": { + "nim": { + "version": "2.2.4", + "vcsRevision": "911e0dbb1f76de61fa0215ab1bb85af5334cc9a8", + "url": "https://github.com/nim-lang/Nim.git", + "downloadMethod": "git", + "dependencies": [], + "checksums": { + "sha1": "68bb85cbfb1832ce4db43943911b046c3af3caab" + } + }, "unittest2": { "version": "0.2.5", "vcsRevision": "26f2ef3ae0ec72a2a75bfe557e02e88f6a31c189", "url": "https://github.com/status-im/nim-unittest2", "downloadMethod": "git", - "dependencies": [], + "dependencies": [ + "nim" + ], "checksums": { "sha1": "02bb3751ba9ddc3c17bfd89f2e41cb6bfb8fc0c9" } }, "bearssl": { - "version": "0.2.7", - "vcsRevision": "3b341f30d8c619b9a75c154243f9a55468a404e2", + "version": "0.2.8", + "vcsRevision": "22c6a76ce015bc07e011562bdcfc51d9446c1e82", "url": "https://github.com/status-im/nim-bearssl", "downloadMethod": "git", "dependencies": [ + "nim", "unittest2" ], "checksums": { - "sha1": "a85aab15b1b9a8b2438e9a128ac2eba41227da79" + "sha1": "da4dd7ae96d536bdaf42dca9c85d7aed024b6a86" } }, "bearssl_pkey_decoder": { - "version": "0.1.0", + "version": "#21dd3710df9345ed2ad8bf8f882761e07863b8e0", "vcsRevision": "21dd3710df9345ed2ad8bf8f882761e07863b8e0", "url": "https://github.com/vacp2p/bearssl_pkey_decoder", "downloadMethod": "git", "dependencies": [ + "nim", "bearssl" ], "checksums": { "sha1": "21b42e2e6ddca6c875d3fc50f36a5115abf51714" } }, - "results": { - "version": "0.5.1", - "vcsRevision": "df8113dda4c2d74d460a8fa98252b0b771bf1f27", - "url": "https://github.com/arnetheduck/nim-results", - "downloadMethod": "git", - "dependencies": [], - "checksums": { - "sha1": "a9c011f74bc9ed5c91103917b9f382b12e82a9e7" - } - }, - "stew": { - "version": "0.5.0", - "vcsRevision": "4382b18f04b3c43c8409bfcd6b62063773b2bbaa", - "url": "https://github.com/status-im/nim-stew", + "jwt": { + "version": "#18f8378de52b241f321c1f9ea905456e89b95c6f", + "vcsRevision": "18f8378de52b241f321c1f9ea905456e89b95c6f", + "url": "https://github.com/vacp2p/nim-jwt.git", "downloadMethod": "git", "dependencies": [ - "results", - "unittest2" + "nim", + "bearssl", + "bearssl_pkey_decoder" ], "checksums": { - "sha1": "db22942939773ab7d5a0f2b2668c237240c67dd6" - } - }, - "faststreams": { - "version": "0.5.0", - "vcsRevision": "ce27581a3e881f782f482cb66dc5b07a02bd615e", - "url": "https://github.com/status-im/nim-faststreams", - "downloadMethod": "git", - "dependencies": [ - "stew", - "unittest2" - ], - "checksums": { - "sha1": "ee61e507b805ae1df7ec936f03f2d101b0d72383" - } - }, - "serialization": { - "version": "0.5.2", - "vcsRevision": "b0f2fa32960ea532a184394b0f27be37bd80248b", - "url": "https://github.com/status-im/nim-serialization", - "downloadMethod": "git", - "dependencies": [ - "faststreams", - "unittest2", - "stew" - ], - "checksums": { - "sha1": "fa35c1bb76a0a02a2379fe86eaae0957c7527cb8" - } - }, - "json_serialization": { - "version": "0.4.4", - "vcsRevision": "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44", - "url": "https://github.com/status-im/nim-json-serialization", - "downloadMethod": "git", - "dependencies": [ - "faststreams", - "serialization", - "stew", - "results" - ], - "checksums": { - "sha1": "8b3115354104858a0ac9019356fb29720529c2bd" + "sha1": "bcfd6fc9c5e10a52b87117219b7ab5c98136bc8e" } }, "testutils": { @@ -106,25 +69,76 @@ "url": "https://github.com/status-im/nim-testutils", "downloadMethod": "git", "dependencies": [ + "nim", "unittest2" ], "checksums": { "sha1": "96a11cf8b84fa9bd12d4a553afa1cc4b7f9df4e3" } }, - "chronicles": { - "version": "0.12.2", - "vcsRevision": "27ec507429a4eb81edc20f28292ee8ec420be05b", - "url": "https://github.com/status-im/nim-chronicles", + "db_connector": { + "version": "0.1.0", + "vcsRevision": "29450a2063970712422e1ab857695c12d80112a6", + "url": "https://github.com/nim-lang/db_connector", "downloadMethod": "git", "dependencies": [ - "faststreams", - "serialization", - "json_serialization", - "testutils" + "nim" ], "checksums": { - "sha1": "02febb20d088120b2836d3306cfa21f434f88f65" + "sha1": "4f2e67d0e4b61af9ac5575509305660b473f01a4" + } + }, + "results": { + "version": "0.5.1", + "vcsRevision": "df8113dda4c2d74d460a8fa98252b0b771bf1f27", + "url": "https://github.com/arnetheduck/nim-results", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "a9c011f74bc9ed5c91103917b9f382b12e82a9e7" + } + }, + "nat_traversal": { + "version": "0.0.1", + "vcsRevision": "860e18c37667b5dd005b94c63264560c35d88004", + "url": "https://github.com/status-im/nim-nat-traversal", + "downloadMethod": "git", + "dependencies": [ + "nim", + "results" + ], + "checksums": { + "sha1": "1a376d3e710590ef2c48748a546369755f0a7c97" + } + }, + "stew": { + "version": "0.5.0", + "vcsRevision": "4382b18f04b3c43c8409bfcd6b62063773b2bbaa", + "url": "https://github.com/status-im/nim-stew", + "downloadMethod": "git", + "dependencies": [ + "nim", + "results", + "unittest2" + ], + "checksums": { + "sha1": "db22942939773ab7d5a0f2b2668c237240c67dd6" + } + }, + "zlib": { + "version": "0.1.0", + "vcsRevision": "e680f269fb01af2c34a2ba879ff281795a5258fe", + "url": "https://github.com/status-im/nim-zlib", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "results" + ], + "checksums": { + "sha1": "bbde4f5a97a84b450fef7d107461e5f35cf2b47f" } }, "httputils": { @@ -133,6 +147,7 @@ "url": "https://github.com/status-im/nim-http-utils", "downloadMethod": "git", "dependencies": [ + "nim", "stew", "results", "unittest2" @@ -147,6 +162,7 @@ "url": "https://github.com/status-im/nim-chronos", "downloadMethod": "git", "dependencies": [ + "nim", "results", "stew", "bearssl", @@ -157,95 +173,13 @@ "sha1": "3a4c9477df8cef20a04e4f1b54a2d74fdfc2a3d0" } }, - "confutils": { - "version": "0.1.0", - "vcsRevision": "7728f6bd81a1eedcfe277d02ea85fdb805bcc05a", - "url": "https://github.com/status-im/nim-confutils", - "downloadMethod": "git", - "dependencies": [ - "stew", - "serialization", - "results" - ], - "checksums": { - "sha1": "8bc8c30b107fdba73b677e5f257c6c42ae1cdc8e" - } - }, - "db_connector": { - "version": "0.1.0", - "vcsRevision": "29450a2063970712422e1ab857695c12d80112a6", - "url": "https://github.com/nim-lang/db_connector", - "downloadMethod": "git", - "dependencies": [], - "checksums": { - "sha1": "4f2e67d0e4b61af9ac5575509305660b473f01a4" - } - }, - "dnsclient": { - "version": "0.3.4", - "vcsRevision": "23214235d4784d24aceed99bbfe153379ea557c8", - "url": "https://github.com/ba0f3/dnsclient.nim", - "downloadMethod": "git", - "dependencies": [], - "checksums": { - "sha1": "65262c7e533ff49d6aca5539da4bc6c6ce132f40" - } - }, - "nimcrypto": { - "version": "0.6.4", - "vcsRevision": "721fb99ee099b632eb86dfad1f0d96ee87583774", - "url": "https://github.com/cheatfate/nimcrypto", - "downloadMethod": "git", - "dependencies": [], - "checksums": { - "sha1": "f9ab24fa940ed03d0fb09729a7303feb50b7eaec" - } - }, - "stint": { - "version": "0.8.2", - "vcsRevision": "470b7892561b5179ab20bd389a69217d6213fe58", - "url": "https://github.com/status-im/nim-stint", - "downloadMethod": "git", - "dependencies": [ - "stew", - "unittest2" - ], - "checksums": { - "sha1": "d8f871fd617e7857192d4609fe003b48942a8ae5" - } - }, - "secp256k1": { - "version": "0.6.0.3.2", - "vcsRevision": "d8f1288b7c72f00be5fc2c5ea72bf5cae1eafb15", - "url": "https://github.com/status-im/nim-secp256k1", - "downloadMethod": "git", - "dependencies": [ - "stew", - "results", - "nimcrypto" - ], - "checksums": { - "sha1": "6618ef9de17121846a8c1d0317026b0ce8584e10" - } - }, - "nat_traversal": { - "version": "0.0.1", - "vcsRevision": "860e18c37667b5dd005b94c63264560c35d88004", - "url": "https://github.com/status-im/nim-nat-traversal", - "downloadMethod": "git", - "dependencies": [ - "results" - ], - "checksums": { - "sha1": "1a376d3e710590ef2c48748a546369755f0a7c97" - } - }, "metrics": { "version": "0.2.1", "vcsRevision": "a1296caf3ebb5f30f51a5feae7749a30df2824c2", "url": "https://github.com/status-im/nim-metrics", "downloadMethod": "git", "dependencies": [ + "nim", "chronos", "results", "stew" @@ -254,27 +188,18 @@ "sha1": "84bb09873d7677c06046f391c7b473cd2fcff8a2" } }, - "sqlite3_abi": { - "version": "3.52.0.0", - "vcsRevision": "4b79c5e1882b7fc6c00aec311daf1ed50ad653d5", - "url": "https://github.com/arnetheduck/nim-sqlite3-abi", - "downloadMethod": "git", - "dependencies": [], - "checksums": { - "sha1": "b56b489a7cb01eef8821d66d38d411923a14316d" - } - }, - "minilru": { - "version": "0.1.0", - "vcsRevision": "6dd93feb60f4cded3c05e7af7209cf63fb677893", - "url": "https://github.com/status-im/nim-minilru", + "faststreams": { + "version": "0.5.0", + "vcsRevision": "ce27581a3e881f782f482cb66dc5b07a02bd615e", + "url": "https://github.com/status-im/nim-faststreams", "downloadMethod": "git", "dependencies": [ - "results", + "nim", + "stew", "unittest2" ], "checksums": { - "sha1": "0be03a5da29fdd4409ea74a60fd0ccce882601b4" + "sha1": "ee61e507b805ae1df7ec936f03f2d101b0d72383" } }, "snappy": { @@ -283,6 +208,7 @@ "url": "https://github.com/status-im/nim-snappy", "downloadMethod": "git", "dependencies": [ + "nim", "faststreams", "unittest2", "results", @@ -292,12 +218,271 @@ "sha1": "e572d60d6a3178c5b1cde2400c51ad771812cd3d" } }, + "serialization": { + "version": "0.5.2", + "vcsRevision": "b0f2fa32960ea532a184394b0f27be37bd80248b", + "url": "https://github.com/status-im/nim-serialization", + "downloadMethod": "git", + "dependencies": [ + "nim", + "faststreams", + "unittest2", + "stew" + ], + "checksums": { + "sha1": "fa35c1bb76a0a02a2379fe86eaae0957c7527cb8" + } + }, + "toml_serialization": { + "version": "0.2.18", + "vcsRevision": "b5b387e6fb2a7cc75d54a269b07cc6218361bd46", + "url": "https://github.com/status-im/nim-toml-serialization", + "downloadMethod": "git", + "dependencies": [ + "nim", + "faststreams", + "serialization", + "stew" + ], + "checksums": { + "sha1": "76ae1c2af5dd092849b41750ff29217980dc9ca3" + } + }, + "confutils": { + "version": "0.1.0", + "vcsRevision": "7728f6bd81a1eedcfe277d02ea85fdb805bcc05a", + "url": "https://github.com/status-im/nim-confutils", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "serialization", + "results" + ], + "checksums": { + "sha1": "8bc8c30b107fdba73b677e5f257c6c42ae1cdc8e" + } + }, + "json_serialization": { + "version": "0.4.4", + "vcsRevision": "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44", + "url": "https://github.com/status-im/nim-json-serialization", + "downloadMethod": "git", + "dependencies": [ + "nim", + "faststreams", + "serialization", + "stew", + "results" + ], + "checksums": { + "sha1": "8b3115354104858a0ac9019356fb29720529c2bd" + } + }, + "chronicles": { + "version": "0.12.2", + "vcsRevision": "27ec507429a4eb81edc20f28292ee8ec420be05b", + "url": "https://github.com/status-im/nim-chronicles", + "downloadMethod": "git", + "dependencies": [ + "nim", + "faststreams", + "serialization", + "json_serialization", + "testutils" + ], + "checksums": { + "sha1": "02febb20d088120b2836d3306cfa21f434f88f65" + } + }, + "presto": { + "version": "0.1.1", + "vcsRevision": "d66043dd7ede146442e6c39720c76a20bde5225f", + "url": "https://github.com/status-im/nim-presto", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronos", + "chronicles", + "metrics", + "results", + "stew" + ], + "checksums": { + "sha1": "8df97c45683abe2337bdff43b844c4fbcc124ca2" + } + }, + "stint": { + "version": "0.8.2", + "vcsRevision": "470b7892561b5179ab20bd389a69217d6213fe58", + "url": "https://github.com/status-im/nim-stint", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "unittest2" + ], + "checksums": { + "sha1": "d8f871fd617e7857192d4609fe003b48942a8ae5" + } + }, + "minilru": { + "version": "0.1.0", + "vcsRevision": "6dd93feb60f4cded3c05e7af7209cf63fb677893", + "url": "https://github.com/status-im/nim-minilru", + "downloadMethod": "git", + "dependencies": [ + "nim", + "results", + "unittest2" + ], + "checksums": { + "sha1": "0be03a5da29fdd4409ea74a60fd0ccce882601b4" + } + }, + "sqlite3_abi": { + "version": "3.53.0.0", + "vcsRevision": "8240e8e2819dfce1b67fa2733135d01b5cc80ae0", + "url": "https://github.com/arnetheduck/nim-sqlite3-abi", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "fb7a6e6f36fc4eb4dfa6634dbcbf5cd0dfd0ebf0" + } + }, + "dnsclient": { + "version": "0.3.4", + "vcsRevision": "23214235d4784d24aceed99bbfe153379ea557c8", + "url": "https://github.com/ba0f3/dnsclient.nim", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "65262c7e533ff49d6aca5539da4bc6c6ce132f40" + } + }, + "unicodedb": { + "version": "0.13.2", + "vcsRevision": "66f2458710dc641dd4640368f9483c8a0ec70561", + "url": "https://github.com/nitely/nim-unicodedb", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "739102d885d99bb4571b1955f5f12aee423c935b" + } + }, + "regex": { + "version": "0.26.3", + "vcsRevision": "4593305ed1e49731fc75af1dc572dd2559aad19c", + "url": "https://github.com/nitely/nim-regex", + "downloadMethod": "git", + "dependencies": [ + "nim", + "unicodedb" + ], + "checksums": { + "sha1": "4d24e7d7441137cd202e16f2359a5807ddbdc31f" + } + }, + "nimcrypto": { + "version": "0.6.4", + "vcsRevision": "721fb99ee099b632eb86dfad1f0d96ee87583774", + "url": "https://github.com/cheatfate/nimcrypto", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "f9ab24fa940ed03d0fb09729a7303feb50b7eaec" + } + }, + "websock": { + "version": "0.3.0", + "vcsRevision": "c105d98e6522e0e2cbe3dfa11b07a273e9fd0e7b", + "url": "https://github.com/status-im/nim-websock", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronos", + "httputils", + "chronicles", + "stew", + "nimcrypto", + "bearssl", + "results", + "zlib" + ], + "checksums": { + "sha1": "1294a66520fa4541e261dec8a6a84f774fb8c0ac" + } + }, + "json_rpc": { + "version": "#43bbf499143eb45046c83ac9794c9e3280a2b8e7", + "vcsRevision": "43bbf499143eb45046c83ac9794c9e3280a2b8e7", + "url": "https://github.com/status-im/nim-json-rpc.git", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "nimcrypto", + "stint", + "chronos", + "httputils", + "chronicles", + "websock", + "serialization", + "json_serialization", + "unittest2" + ], + "checksums": { + "sha1": "30ff6ead115b88c79862c5c7e37b1c9852eea59f" + } + }, + "lsquic": { + "version": "0.0.1", + "vcsRevision": "4fb03ee7bfb39aecb3316889fdcb60bec3d0936f", + "url": "https://github.com/vacp2p/nim-lsquic", + "downloadMethod": "git", + "dependencies": [ + "nim", + "zlib", + "stew", + "chronos", + "nimcrypto", + "unittest2", + "chronicles" + ], + "checksums": { + "sha1": "f465fa994346490d0924d162f53d9b5aec62f948" + } + }, + "secp256k1": { + "version": "0.6.0.3.2", + "vcsRevision": "d8f1288b7c72f00be5fc2c5ea72bf5cae1eafb15", + "url": "https://github.com/status-im/nim-secp256k1", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "results", + "nimcrypto" + ], + "checksums": { + "sha1": "6618ef9de17121846a8c1d0317026b0ce8584e10" + } + }, "eth": { "version": "0.9.0", "vcsRevision": "d9135e6c3c5d6d819afdfb566aa8d958756b73a8", "url": "https://github.com/status-im/nim-eth", "downloadMethod": "git", "dependencies": [ + "nim", "nimcrypto", "stint", "secp256k1", @@ -318,12 +503,37 @@ "sha1": "2e01b0cfff9523d110562af70d19948280f8013e" } }, + "web3": { + "version": "0.8.0", + "vcsRevision": "cdfe5601d2812a58e54faf53ee634452d01e5918", + "url": "https://github.com/status-im/nim-web3", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronicles", + "chronos", + "bearssl", + "eth", + "faststreams", + "json_rpc", + "serialization", + "json_serialization", + "nimcrypto", + "stew", + "stint", + "results" + ], + "checksums": { + "sha1": "26a112af032ef1536f97da2ca7364af618a11b80" + } + }, "dnsdisc": { "version": "0.1.0", "vcsRevision": "38f2e0f52c0a8f032ef4530835e519d550706d9e", "url": "https://github.com/status-im/nim-dnsdisc", "downloadMethod": "git", "dependencies": [ + "nim", "bearssl", "chronicles", "chronos", @@ -339,119 +549,13 @@ "sha1": "055b882a0f6b1d1e57a25a7af99d2e5ac6268154" } }, - "taskpools": { - "version": "0.1.0", - "vcsRevision": "9e8ccc754631ac55ac2fd495e167e74e86293edb", - "url": "https://github.com/status-im/nim-taskpools", - "downloadMethod": "git", - "dependencies": [], - "checksums": { - "sha1": "09e1b2fdad55b973724d61227971afc0df0b7a81" - } - }, - "ffi": { - "version": "0.1.3", - "vcsRevision": "06111de155253b34e47ed2aaed1d61d08d62cc1b", - "url": "https://github.com/logos-messaging/nim-ffi", - "downloadMethod": "git", - "dependencies": [ - "chronos", - "chronicles", - "taskpools" - ], - "checksums": { - "sha1": "6f9d49375ea1dc71add55c72ac80a808f238e5b0" - } - }, - "zlib": { - "version": "0.1.0", - "vcsRevision": "e680f269fb01af2c34a2ba879ff281795a5258fe", - "url": "https://github.com/status-im/nim-zlib", - "downloadMethod": "git", - "dependencies": [ - "stew", - "results" - ], - "checksums": { - "sha1": "bbde4f5a97a84b450fef7d107461e5f35cf2b47f" - } - }, - "websock": { - "version": "0.2.2", - "vcsRevision": "3918ce3900c83e1cc7496232a307709f195f7acd", - "url": "https://github.com/status-im/nim-websock", - "downloadMethod": "git", - "dependencies": [ - "chronos", - "httputils", - "chronicles", - "stew", - "nimcrypto", - "bearssl", - "results", - "zlib" - ], - "checksums": { - "sha1": "3c424661eff56c925b01e1cd1a911ff744e72962" - } - }, - "json_rpc": { - "version": "0.5.4", - "vcsRevision": "b6e40a776fa2d00b97a9366761fb7da18f31ae5c", - "url": "https://github.com/status-im/nim-json-rpc", - "downloadMethod": "git", - "dependencies": [ - "stew", - "nimcrypto", - "stint", - "chronos", - "httputils", - "chronicles", - "websock", - "serialization", - "json_serialization", - "unittest2" - ], - "checksums": { - "sha1": "d8e8be795fcf098f4ce03b5826f6b3153f6a6e07" - } - }, - "jwt": { - "version": "0.2", - "vcsRevision": "18f8378de52b241f321c1f9ea905456e89b95c6f", - "url": "https://github.com/vacp2p/nim-jwt.git", - "downloadMethod": "git", - "dependencies": [ - "bearssl", - "bearssl_pkey_decoder" - ], - "checksums": { - "sha1": "bcfd6fc9c5e10a52b87117219b7ab5c98136bc8e" - } - }, - "lsquic": { - "version": "0.0.1", - "vcsRevision": "4fb03ee7bfb39aecb3316889fdcb60bec3d0936f", - "url": "https://github.com/vacp2p/nim-lsquic", - "downloadMethod": "git", - "dependencies": [ - "zlib", - "stew", - "chronos", - "nimcrypto", - "unittest2", - "chronicles" - ], - "checksums": { - "sha1": "f465fa994346490d0924d162f53d9b5aec62f948" - } - }, "libp2p": { - "version": "1.15.2", + "version": "#ff8d51857b4b79a68468e7bcc27b2026cca02996", "vcsRevision": "ff8d51857b4b79a68468e7bcc27b2026cca02996", "url": "https://github.com/vacp2p/nim-libp2p.git", "downloadMethod": "git", "dependencies": [ + "nim", "nimcrypto", "dnsclient", "bearssl", @@ -471,79 +575,31 @@ "sha1": "fa2a7552c6ec860717b77ce34cf0b7afe4570234" } }, - "presto": { - "version": "0.1.1", - "vcsRevision": "d66043dd7ede146442e6c39720c76a20bde5225f", - "url": "https://github.com/status-im/nim-presto", + "taskpools": { + "version": "0.1.0", + "vcsRevision": "9e8ccc754631ac55ac2fd495e167e74e86293edb", + "url": "https://github.com/status-im/nim-taskpools", "downloadMethod": "git", "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "09e1b2fdad55b973724d61227971afc0df0b7a81" + } + }, + "ffi": { + "version": "0.1.3", + "vcsRevision": "06111de155253b34e47ed2aaed1d61d08d62cc1b", + "url": "https://github.com/logos-messaging/nim-ffi", + "downloadMethod": "git", + "dependencies": [ + "nim", "chronos", "chronicles", - "metrics", - "results", - "stew" + "taskpools" ], "checksums": { - "sha1": "8df97c45683abe2337bdff43b844c4fbcc124ca2" - } - }, - "unicodedb": { - "version": "0.13.2", - "vcsRevision": "66f2458710dc641dd4640368f9483c8a0ec70561", - "url": "https://github.com/nitely/nim-unicodedb", - "downloadMethod": "git", - "dependencies": [], - "checksums": { - "sha1": "739102d885d99bb4571b1955f5f12aee423c935b" - } - }, - "regex": { - "version": "0.26.3", - "vcsRevision": "4593305ed1e49731fc75af1dc572dd2559aad19c", - "url": "https://github.com/nitely/nim-regex", - "downloadMethod": "git", - "dependencies": [ - "unicodedb" - ], - "checksums": { - "sha1": "4d24e7d7441137cd202e16f2359a5807ddbdc31f" - } - }, - "toml_serialization": { - "version": "0.2.18", - "vcsRevision": "b5b387e6fb2a7cc75d54a269b07cc6218361bd46", - "url": "https://github.com/status-im/nim-toml-serialization", - "downloadMethod": "git", - "dependencies": [ - "faststreams", - "serialization", - "stew" - ], - "checksums": { - "sha1": "76ae1c2af5dd092849b41750ff29217980dc9ca3" - } - }, - "web3": { - "version": "0.8.0", - "vcsRevision": "cdfe5601d2812a58e54faf53ee634452d01e5918", - "url": "https://github.com/status-im/nim-web3", - "downloadMethod": "git", - "dependencies": [ - "chronicles", - "chronos", - "bearssl", - "eth", - "faststreams", - "json_rpc", - "serialization", - "json_serialization", - "nimcrypto", - "stew", - "stint", - "results" - ], - "checksums": { - "sha1": "26a112af032ef1536f97da2ca7364af618a11b80" + "sha1": "6f9d49375ea1dc71add55c72ac80a808f238e5b0" } } }, diff --git a/nix/deps.nix b/nix/deps.nix index 2f30a572c..0d9986528 100644 --- a/nix/deps.nix +++ b/nix/deps.nix @@ -12,8 +12,8 @@ bearssl = pkgs.fetchgit { url = "https://github.com/status-im/nim-bearssl"; - rev = "3b341f30d8c619b9a75c154243f9a55468a404e2"; - sha256 = "059avc2dh39vv9c3a1qayah98fjm5pw04r3dn2bqrgs6vf7licmv"; + rev = "22c6a76ce015bc07e011562bdcfc51d9446c1e82"; + sha256 = "1cvdd7lfrpa6asmc39al3g4py5nqhpqmvypc36r5qyv7p5arc8a3"; fetchSubmodules = true; }; @@ -24,38 +24,10 @@ fetchSubmodules = true; }; - results = pkgs.fetchgit { - url = "https://github.com/arnetheduck/nim-results"; - rev = "df8113dda4c2d74d460a8fa98252b0b771bf1f27"; - sha256 = "1h7amas16sbhlr7zb7n3jb5434k98ji375vzw72k1fsc86vnmcr9"; - fetchSubmodules = true; - }; - - stew = pkgs.fetchgit { - url = "https://github.com/status-im/nim-stew"; - rev = "4382b18f04b3c43c8409bfcd6b62063773b2bbaa"; - sha256 = "0mx9g5m636h3sk5pllcpylk51brf7lx91izx3gc23k3ih3hrxyk2"; - fetchSubmodules = true; - }; - - faststreams = pkgs.fetchgit { - url = "https://github.com/status-im/nim-faststreams"; - rev = "ce27581a3e881f782f482cb66dc5b07a02bd615e"; - sha256 = "0y6bw2scnmr8cxj4fg18w7f34l2bh9qwg5nhlgd84m9fpr5bqarn"; - fetchSubmodules = true; - }; - - serialization = pkgs.fetchgit { - url = "https://github.com/status-im/nim-serialization"; - rev = "b0f2fa32960ea532a184394b0f27be37bd80248b"; - sha256 = "0wip1fjx7ka39ck1g1xvmyarzq1p5dlngpqil6zff8k8z5skiz27"; - fetchSubmodules = true; - }; - - json_serialization = pkgs.fetchgit { - url = "https://github.com/status-im/nim-json-serialization"; - rev = "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44"; - sha256 = "0i8sq51nqj8lshf6bfixaz9a7sq0ahsbvq3chkxdvv4khsqvam91"; + jwt = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-jwt.git"; + rev = "18f8378de52b241f321c1f9ea905456e89b95c6f"; + sha256 = "1986czmszdxj6g9yr7xn1fx8y2y9mwpb3f1bn9nc6973qawsdm0p"; fetchSubmodules = true; }; @@ -66,10 +38,38 @@ fetchSubmodules = true; }; - chronicles = pkgs.fetchgit { - url = "https://github.com/status-im/nim-chronicles"; - rev = "27ec507429a4eb81edc20f28292ee8ec420be05b"; - sha256 = "1xx9fcfwgcaizq3s7i3s03mclz253r5j8va38l9ycl19fcbc96z9"; + db_connector = pkgs.fetchgit { + url = "https://github.com/nim-lang/db_connector"; + rev = "29450a2063970712422e1ab857695c12d80112a6"; + sha256 = "11dna09ccdhj3pzpqa04j7a95ibx907z6n1ff33yf0n92qa4x59z"; + fetchSubmodules = true; + }; + + results = pkgs.fetchgit { + url = "https://github.com/arnetheduck/nim-results"; + rev = "df8113dda4c2d74d460a8fa98252b0b771bf1f27"; + sha256 = "1h7amas16sbhlr7zb7n3jb5434k98ji375vzw72k1fsc86vnmcr9"; + fetchSubmodules = true; + }; + + nat_traversal = pkgs.fetchgit { + url = "https://github.com/status-im/nim-nat-traversal"; + rev = "860e18c37667b5dd005b94c63264560c35d88004"; + sha256 = "0319k5bbl468phwfnvlrh7725sc80rnf7m9gyj0i3cb5hb9q78bs"; + fetchSubmodules = true; + }; + + stew = pkgs.fetchgit { + url = "https://github.com/status-im/nim-stew"; + rev = "4382b18f04b3c43c8409bfcd6b62063773b2bbaa"; + sha256 = "0mx9g5m636h3sk5pllcpylk51brf7lx91izx3gc23k3ih3hrxyk2"; + fetchSubmodules = true; + }; + + zlib = pkgs.fetchgit { + url = "https://github.com/status-im/nim-zlib"; + rev = "e680f269fb01af2c34a2ba879ff281795a5258fe"; + sha256 = "1xw9f1gjsgqihdg7kdkbaq1wankgnx2vn9l3ihc6nqk2jzv5bvk5"; fetchSubmodules = true; }; @@ -87,55 +87,6 @@ fetchSubmodules = true; }; - confutils = pkgs.fetchgit { - url = "https://github.com/status-im/nim-confutils"; - rev = "7728f6bd81a1eedcfe277d02ea85fdb805bcc05a"; - sha256 = "18bj1ilx10jm2vmqx2wy2xl9rzy7alymi2m4n9jgpa4sbxnfh0x3"; - fetchSubmodules = true; - }; - - db_connector = pkgs.fetchgit { - url = "https://github.com/nim-lang/db_connector"; - rev = "29450a2063970712422e1ab857695c12d80112a6"; - sha256 = "11dna09ccdhj3pzpqa04j7a95ibx907z6n1ff33yf0n92qa4x59z"; - fetchSubmodules = true; - }; - - dnsclient = pkgs.fetchgit { - url = "https://github.com/ba0f3/dnsclient.nim"; - rev = "23214235d4784d24aceed99bbfe153379ea557c8"; - sha256 = "03mf3lw5c0m5nq9ppa49nylrl8ibkv2zzlc0wyhqg7w09kz6hks6"; - fetchSubmodules = true; - }; - - nimcrypto = pkgs.fetchgit { - url = "https://github.com/cheatfate/nimcrypto"; - rev = "721fb99ee099b632eb86dfad1f0d96ee87583774"; - sha256 = "178vzb3q8wzjq295ik2pd25rrqf32w381ck76hm5x2d8qnzfmkkc"; - fetchSubmodules = true; - }; - - stint = pkgs.fetchgit { - url = "https://github.com/status-im/nim-stint"; - rev = "470b7892561b5179ab20bd389a69217d6213fe58"; - sha256 = "1isfwmbj98qfi5pm9acy0yyvq0vlz38nxp30xl43jx2mmaga2w22"; - fetchSubmodules = true; - }; - - secp256k1 = pkgs.fetchgit { - url = "https://github.com/status-im/nim-secp256k1"; - rev = "d8f1288b7c72f00be5fc2c5ea72bf5cae1eafb15"; - sha256 = "1qjrmwbngb73f6r1fznvig53nyal7wj41d1cmqfksrmivk2sgrn2"; - fetchSubmodules = true; - }; - - nat_traversal = pkgs.fetchgit { - url = "https://github.com/status-im/nim-nat-traversal"; - rev = "860e18c37667b5dd005b94c63264560c35d88004"; - sha256 = "0319k5bbl468phwfnvlrh7725sc80rnf7m9gyj0i3cb5hb9q78bs"; - fetchSubmodules = true; - }; - metrics = pkgs.fetchgit { url = "https://github.com/status-im/nim-metrics"; rev = "a1296caf3ebb5f30f51a5feae7749a30df2824c2"; @@ -143,17 +94,10 @@ fetchSubmodules = true; }; - sqlite3_abi = pkgs.fetchgit { - url = "https://github.com/arnetheduck/nim-sqlite3-abi"; - rev = "4b79c5e1882b7fc6c00aec311daf1ed50ad653d5"; - sha256 = "0qa6p2vnxmf6r2w19mfydr5rzv7bg1lfxccnpdhk0akzxnc7i5gy"; - fetchSubmodules = true; - }; - - minilru = pkgs.fetchgit { - url = "https://github.com/status-im/nim-minilru"; - rev = "6dd93feb60f4cded3c05e7af7209cf63fb677893"; - sha256 = "1xgx4j56ais3hk8b51zhnfs9q85g2afkp3y1j9ky5iziqvcs2sml"; + faststreams = pkgs.fetchgit { + url = "https://github.com/status-im/nim-faststreams"; + rev = "ce27581a3e881f782f482cb66dc5b07a02bd615e"; + sha256 = "0y6bw2scnmr8cxj4fg18w7f34l2bh9qwg5nhlgd84m9fpr5bqarn"; fetchSubmodules = true; }; @@ -164,73 +108,38 @@ fetchSubmodules = true; }; - eth = pkgs.fetchgit { - url = "https://github.com/status-im/nim-eth"; - rev = "d9135e6c3c5d6d819afdfb566aa8d958756b73a8"; - sha256 = "15r6aszalnbk6mkyfbv5rnz5vcf1mmgj6yg332wry53xsd2ipg7r"; + serialization = pkgs.fetchgit { + url = "https://github.com/status-im/nim-serialization"; + rev = "b0f2fa32960ea532a184394b0f27be37bd80248b"; + sha256 = "0wip1fjx7ka39ck1g1xvmyarzq1p5dlngpqil6zff8k8z5skiz27"; fetchSubmodules = true; }; - dnsdisc = pkgs.fetchgit { - url = "https://github.com/status-im/nim-dnsdisc"; - rev = "38f2e0f52c0a8f032ef4530835e519d550706d9e"; - sha256 = "0dk787ny49n41bmzhlrvm87giwajr01gwdw9nlmphch89rdqpxxn"; + toml_serialization = pkgs.fetchgit { + url = "https://github.com/status-im/nim-toml-serialization"; + rev = "b5b387e6fb2a7cc75d54a269b07cc6218361bd46"; + sha256 = "175swdj01rz57h1hvflkyaz4x76qbfn0174ysrk3qk385i1zlg5z"; fetchSubmodules = true; }; - taskpools = pkgs.fetchgit { - url = "https://github.com/status-im/nim-taskpools"; - rev = "9e8ccc754631ac55ac2fd495e167e74e86293edb"; - sha256 = "1y78l33vdjxmb9dkr455pbphxa73rgdsh8m9gpkf4d9b1wm1yivy"; + confutils = pkgs.fetchgit { + url = "https://github.com/status-im/nim-confutils"; + rev = "7728f6bd81a1eedcfe277d02ea85fdb805bcc05a"; + sha256 = "18bj1ilx10jm2vmqx2wy2xl9rzy7alymi2m4n9jgpa4sbxnfh0x3"; fetchSubmodules = true; }; - ffi = pkgs.fetchgit { - url = "https://github.com/logos-messaging/nim-ffi"; - rev = "06111de155253b34e47ed2aaed1d61d08d62cc1b"; - sha256 = "0rb0d2i519amgsp7q0bn6m5465z1vwj4rab89529pyiivh3fgh8j"; + json_serialization = pkgs.fetchgit { + url = "https://github.com/status-im/nim-json-serialization"; + rev = "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44"; + sha256 = "0i8sq51nqj8lshf6bfixaz9a7sq0ahsbvq3chkxdvv4khsqvam91"; fetchSubmodules = true; }; - zlib = pkgs.fetchgit { - url = "https://github.com/status-im/nim-zlib"; - rev = "e680f269fb01af2c34a2ba879ff281795a5258fe"; - sha256 = "1xw9f1gjsgqihdg7kdkbaq1wankgnx2vn9l3ihc6nqk2jzv5bvk5"; - fetchSubmodules = true; - }; - - websock = pkgs.fetchgit { - url = "https://github.com/status-im/nim-websock"; - rev = "3918ce3900c83e1cc7496232a307709f195f7acd"; - sha256 = "16zvdjyasfpb04708d072rpvg12pyz3gmszi3md5brmlhbc3x8jp"; - fetchSubmodules = true; - }; - - json_rpc = pkgs.fetchgit { - url = "https://github.com/status-im/nim-json-rpc"; - rev = "b6e40a776fa2d00b97a9366761fb7da18f31ae5c"; - sha256 = "0c86glijpzcxdb5fagdk98hm9dmsrgw179nn3ixbapl48pvly9nr"; - fetchSubmodules = true; - }; - - jwt = pkgs.fetchgit { - url = "https://github.com/vacp2p/nim-jwt.git"; - rev = "18f8378de52b241f321c1f9ea905456e89b95c6f"; - sha256 = "1986czmszdxj6g9yr7xn1fx8y2y9mwpb3f1bn9nc6973qawsdm0p"; - fetchSubmodules = true; - }; - - lsquic = pkgs.fetchgit { - url = "https://github.com/vacp2p/nim-lsquic"; - rev = "4fb03ee7bfb39aecb3316889fdcb60bec3d0936f"; - sha256 = "0qdhcd4hyp185szc9sv3jvwdwc9zp3j0syy7glxv13k9bchfmkfg"; - fetchSubmodules = true; - }; - - libp2p = pkgs.fetchgit { - url = "https://github.com/vacp2p/nim-libp2p.git"; - rev = "ff8d51857b4b79a68468e7bcc27b2026cca02996"; - sha256 = "08y4s0zhqzsd780bwaixfqbi79km0mcq5g8nyw7awfvcbjqsa53l"; + chronicles = pkgs.fetchgit { + url = "https://github.com/status-im/nim-chronicles"; + rev = "27ec507429a4eb81edc20f28292ee8ec420be05b"; + sha256 = "1xx9fcfwgcaizq3s7i3s03mclz253r5j8va38l9ycl19fcbc96z9"; fetchSubmodules = true; }; @@ -241,6 +150,34 @@ fetchSubmodules = true; }; + stint = pkgs.fetchgit { + url = "https://github.com/status-im/nim-stint"; + rev = "470b7892561b5179ab20bd389a69217d6213fe58"; + sha256 = "1isfwmbj98qfi5pm9acy0yyvq0vlz38nxp30xl43jx2mmaga2w22"; + fetchSubmodules = true; + }; + + minilru = pkgs.fetchgit { + url = "https://github.com/status-im/nim-minilru"; + rev = "6dd93feb60f4cded3c05e7af7209cf63fb677893"; + sha256 = "1xgx4j56ais3hk8b51zhnfs9q85g2afkp3y1j9ky5iziqvcs2sml"; + fetchSubmodules = true; + }; + + sqlite3_abi = pkgs.fetchgit { + url = "https://github.com/arnetheduck/nim-sqlite3-abi"; + rev = "8240e8e2819dfce1b67fa2733135d01b5cc80ae0"; + sha256 = "0g8bc0kiwxxh3h5w06ksa23cw81hnx87rdn93v64m2f053nb6bcm"; + fetchSubmodules = true; + }; + + dnsclient = pkgs.fetchgit { + url = "https://github.com/ba0f3/dnsclient.nim"; + rev = "23214235d4784d24aceed99bbfe153379ea557c8"; + sha256 = "03mf3lw5c0m5nq9ppa49nylrl8ibkv2zzlc0wyhqg7w09kz6hks6"; + fetchSubmodules = true; + }; + unicodedb = pkgs.fetchgit { url = "https://github.com/nitely/nim-unicodedb"; rev = "66f2458710dc641dd4640368f9483c8a0ec70561"; @@ -255,10 +192,45 @@ fetchSubmodules = true; }; - toml_serialization = pkgs.fetchgit { - url = "https://github.com/status-im/nim-toml-serialization"; - rev = "b5b387e6fb2a7cc75d54a269b07cc6218361bd46"; - sha256 = "175swdj01rz57h1hvflkyaz4x76qbfn0174ysrk3qk385i1zlg5z"; + nimcrypto = pkgs.fetchgit { + url = "https://github.com/cheatfate/nimcrypto"; + rev = "721fb99ee099b632eb86dfad1f0d96ee87583774"; + sha256 = "178vzb3q8wzjq295ik2pd25rrqf32w381ck76hm5x2d8qnzfmkkc"; + fetchSubmodules = true; + }; + + websock = pkgs.fetchgit { + url = "https://github.com/status-im/nim-websock"; + rev = "c105d98e6522e0e2cbe3dfa11b07a273e9fd0e7b"; + sha256 = "1zrigw27nwcmg7mw9867581ipcp3ckrqq3cwl2snabcjhkp5dm2c"; + fetchSubmodules = true; + }; + + json_rpc = pkgs.fetchgit { + url = "https://github.com/status-im/nim-json-rpc.git"; + rev = "43bbf499143eb45046c83ac9794c9e3280a2b8e7"; + sha256 = "1c1msxg958jm2ggvs875b6wh6n829d3lh7x4ch6dcxawda16qf95"; + fetchSubmodules = true; + }; + + lsquic = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-lsquic"; + rev = "4fb03ee7bfb39aecb3316889fdcb60bec3d0936f"; + sha256 = "0qdhcd4hyp185szc9sv3jvwdwc9zp3j0syy7glxv13k9bchfmkfg"; + fetchSubmodules = true; + }; + + secp256k1 = pkgs.fetchgit { + url = "https://github.com/status-im/nim-secp256k1"; + rev = "d8f1288b7c72f00be5fc2c5ea72bf5cae1eafb15"; + sha256 = "1qjrmwbngb73f6r1fznvig53nyal7wj41d1cmqfksrmivk2sgrn2"; + fetchSubmodules = true; + }; + + eth = pkgs.fetchgit { + url = "https://github.com/status-im/nim-eth"; + rev = "d9135e6c3c5d6d819afdfb566aa8d958756b73a8"; + sha256 = "15r6aszalnbk6mkyfbv5rnz5vcf1mmgj6yg332wry53xsd2ipg7r"; fetchSubmodules = true; }; @@ -269,4 +241,32 @@ fetchSubmodules = true; }; + dnsdisc = pkgs.fetchgit { + url = "https://github.com/status-im/nim-dnsdisc"; + rev = "38f2e0f52c0a8f032ef4530835e519d550706d9e"; + sha256 = "0dk787ny49n41bmzhlrvm87giwajr01gwdw9nlmphch89rdqpxxn"; + fetchSubmodules = true; + }; + + libp2p = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-libp2p.git"; + rev = "ff8d51857b4b79a68468e7bcc27b2026cca02996"; + sha256 = "08y4s0zhqzsd780bwaixfqbi79km0mcq5g8nyw7awfvcbjqsa53l"; + fetchSubmodules = true; + }; + + taskpools = pkgs.fetchgit { + url = "https://github.com/status-im/nim-taskpools"; + rev = "9e8ccc754631ac55ac2fd495e167e74e86293edb"; + sha256 = "1y78l33vdjxmb9dkr455pbphxa73rgdsh8m9gpkf4d9b1wm1yivy"; + fetchSubmodules = true; + }; + + ffi = pkgs.fetchgit { + url = "https://github.com/logos-messaging/nim-ffi"; + rev = "06111de155253b34e47ed2aaed1d61d08d62cc1b"; + sha256 = "0rb0d2i519amgsp7q0bn6m5465z1vwj4rab89529pyiivh3fgh8j"; + fetchSubmodules = true; + }; + } diff --git a/nix/shell.nix b/nix/shell.nix index 80e3b7930..edff468ae 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,5 +1,17 @@ { pkgs }: +let + nimble = pkgs.nimble.overrideAttrs (_: { + version = "0.22.3"; + src = pkgs.fetchFromGitHub { + owner = "nim-lang"; + repo = "nimble"; + rev = "v0.22.3"; + sha256 = "sha256-f7DYpRGVUeSi6basK1lfu5AxZpMFOSJ3oYsy+urYErg="; + }; + }); +in + pkgs.mkShell { inputsFrom = [ pkgs.androidShell @@ -8,13 +20,12 @@ pkgs.mkShell { pkgs.darwin.apple_sdk.frameworks.Security ]; - buildInputs = with pkgs; [ + buildInputs = (with pkgs; [ git cargo rustup rustc cmake nim-2_2 - nimble - ]; + ]) ++ [ nimble ]; # nimble pinned to 0.22.3 via let binding above } diff --git a/scripts/install_nim.sh b/scripts/install_nim.sh new file mode 100755 index 000000000..c8d0f439d --- /dev/null +++ b/scripts/install_nim.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Installs a specific Nim version. +# Usage: install_nim.sh +# +# Installs to ~/.nim/nim-/ and symlinks binaries into ~/.nimble/bin/, +# which is the idiomatic Nim location already on PATH. +# +# Pre-built binaries are downloaded from nim-lang.org when available. +# Falls back to building from source otherwise (e.g. macOS on older releases). + +set -e + +NIM_VERSION="${1:-}" + +if [ -z "${NIM_VERSION}" ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +# Check if the right version is already installed +nim_ver=$(nim --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true) +if [ "${nim_ver}" = "${NIM_VERSION}" ]; then + echo "Nim ${NIM_VERSION} already installed, skipping." + exit 0 +fi + +if [ -n "${nim_ver}" ]; then + newer=$(printf '%s\n%s\n' "${NIM_VERSION}" "${nim_ver}" | sort -V | tail -1) + if [ "${newer}" = "${nim_ver}" ]; then + echo "WARNING: Nim ${nim_ver} is installed; this repo is validated against ${NIM_VERSION}." >&2 + echo "WARNING: The build will proceed but may behave differently." >&2 + exit 0 + fi +fi + +OS=$(uname -s | tr 'A-Z' 'a-z' | sed 's/darwin/macosx/') +ARCH=$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/') + +NIM_DEST="${HOME}/.nim/nim-${NIM_VERSION}" +BINARY_URL="https://nim-lang.org/download/nim-${NIM_VERSION}-${OS}_${ARCH}.tar.xz" +WORK_DIR=$(mktemp -d) +trap 'rm -rf "${WORK_DIR}"' EXIT + +echo "Checking for pre-built Nim ${NIM_VERSION} (${OS}_${ARCH})..." +HTTP_STATUS=$(curl -sI "${BINARY_URL}" | head -1 | grep -oE '[0-9]{3}' || true) + +if [ "${HTTP_STATUS}" = "200" ]; then + echo "Downloading pre-built binary from ${BINARY_URL}..." + curl -fL "${BINARY_URL}" -o "${WORK_DIR}/nim.tar.xz" + tar -xJf "${WORK_DIR}/nim.tar.xz" -C "${WORK_DIR}" + rm -rf "${NIM_DEST}" + mkdir -p "${HOME}/.nim" + cp -r "${WORK_DIR}/nim-${NIM_VERSION}" "${NIM_DEST}" +else + echo "No pre-built binary found for ${OS}_${ARCH}. Building from source..." + SRC_URL="https://github.com/nim-lang/Nim/archive/refs/tags/v${NIM_VERSION}.tar.gz" + curl -fL "${SRC_URL}" -o "${WORK_DIR}/nim-src.tar.gz" + tar -xzf "${WORK_DIR}/nim-src.tar.gz" -C "${WORK_DIR}" + cd "${WORK_DIR}/Nim-${NIM_VERSION}" + sh build_all.sh + rm -rf "${NIM_DEST}" + mkdir -p "${HOME}/.nim" + cp -r "${WORK_DIR}/Nim-${NIM_VERSION}" "${NIM_DEST}" +fi + +mkdir -p "${HOME}/.nimble/bin" +for bin_path in "${NIM_DEST}/bin/"*; do + ln -sf "${bin_path}" "${HOME}/.nimble/bin/$(basename "${bin_path}")" +done + +echo "Nim ${NIM_VERSION} installed to ${NIM_DEST}" +echo "Binaries symlinked in ~/.nimble/bin — ensure it is in your PATH." diff --git a/waku.nimble b/waku.nimble index 3a7956873..d99f05e84 100644 --- a/waku.nimble +++ b/waku.nimble @@ -8,13 +8,11 @@ version = "0.37.4" author = "Status Research & Development GmbH" description = "Waku, Private P2P Messaging for Resource-Restricted Devices" license = "MIT or Apache License 2.0" -#bin = @["build/waku"] -## This indicates the nim compiler version we are currently working on. It may compile with others -## but we haven't tested. -const NimVersion = "2.2.4" -## This is the underlying nimble version that gets installed after doing `choosenim 2.2.4`. -const NimbleVersion = "0.18.2" +const RequiredNimVersion = "2.2.4" + ## This is the nim compiler version that we are working on. Other versions may behave differently. +const RequiredNimbleVersion = "0.22.3" + ## Enforced nimble version to ensure a reproducible flow ### Dependencies requires "nim >= 2.2.4", @@ -41,7 +39,7 @@ requires "nim >= 2.2.4", "secp256k1", "bearssl", # RPC & APIs - "json_rpc", + "https://github.com/status-im/nim-json-rpc.git#43bbf499143eb45046c83ac9794c9e3280a2b8e7", "presto", "web3", # Database diff --git a/waku/incentivization/eligibility_manager.nim b/waku/incentivization/eligibility_manager.nim index 29443536a..cbbf4774c 100644 --- a/waku/incentivization/eligibility_manager.nim +++ b/waku/incentivization/eligibility_manager.nim @@ -38,11 +38,8 @@ proc getMinedTransactionReceipt( proc getTxAndTxReceipt( eligibilityManager: EligibilityManager, txHash: TxHash ): Future[Result[(TransactionObject, ReceiptObject), string]] {.async.} = - let txFuture = eligibilityManager.getTransactionByHash(txHash) - let receiptFuture = eligibilityManager.getMinedTransactionReceipt(txHash) - await allFutures(txFuture, receiptFuture) - let tx = txFuture.read() - let txReceipt = receiptFuture.read().valueOr: + let tx = await eligibilityManager.getTransactionByHash(txHash) + let txReceipt = (await eligibilityManager.getMinedTransactionReceipt(txHash)).valueOr: return err("Cannot get tx receipt: " & error) return ok((tx, txReceipt)) From 260def68add27840b36bf80bc622ce16aff6c1c0 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:05:44 +0200 Subject: [PATCH 130/155] use EWMA to show main loop lag information (#3808) --- .../health_monitor/event_loop_monitor.nim | 76 ++++++++++++------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/waku/node/health_monitor/event_loop_monitor.nim b/waku/node/health_monitor/event_loop_monitor.nim index d4b8d98d2..49a2bcc6e 100644 --- a/waku/node/health_monitor/event_loop_monitor.nim +++ b/waku/node/health_monitor/event_loop_monitor.nim @@ -1,58 +1,78 @@ {.push raises: [].} +import std/math import chronos, chronicles, metrics logScope: topics = "waku event_loop_monitor" -const CheckInterval = 5.seconds - -declarePublicGauge event_loop_lag_seconds, - "chronos event loop lag in seconds: difference between actual and expected wake-up interval" +declarePublicGauge event_loop_load, + "chronos event loop load EWMA by window (1.0 = sustained lag at MaxAcceptedLag)", + labels = ["window"] type OnLagChange* = proc(lagTooHigh: bool) {.gcsafe, raises: [].} proc eventLoopMonitorLoop*(onLagChange: OnLagChange = nil) {.async.} = - ## Monitors chronos event loop responsiveness. + ## Monitors chronos event loop responsiveness by measuring how much each + ## iteration oversleeps its `CheckInterval`. ## - ## Schedules a task every `CheckInterval`. Because chronos is single-threaded - ## and cooperative, the task can only resume after all previously queued work - ## completes. The actual elapsed time between iterations therefore reflects - ## how saturated the event loop is: + ## The lag is normalised against `MaxAcceptedLag` and tracked as an EWMA + ## over 1, 5, and 15-minute windows (Unix load-average decay model), + ## exposed via the `event_loop_load` gauge (labelled by window: 1m/5m/15m): ## - ## actual_elapsed ≈ CheckInterval → loop is healthy - ## actual_elapsed >> CheckInterval → tasks are accumulating / loop is stalling + ## load < 1.0 → within budget + ## load = 1.0 → sustained lag at MaxAcceptedLag (fully loaded) + ## load > 1.0 → over budget; e.g. 2.0 means twice the accepted lag ## - ## The lag (actual - expected) is exposed via `event_loop_lag_seconds`. - ## When lag transitions above or below `CheckInterval`, `onLagChange` is called. + ## `onLagChange` is called when instantaneous lag crosses `MaxAcceptedLag`. - var lastWakeup = Moment.now() + const CheckInterval = 5.seconds + const MaxAcceptedLag = 50.milliseconds + + # Decay factors: α = 1 − e^(−CheckInterval_secs / window_secs) + # Mirrors the Unix load-average convention so each EWMA has a half-life equal + # to its named window. + const alpha1m = 1.0 - exp(-5.0 / 60.0) # ≈ 0.0821 + const alpha5m = 1.0 - exp(-5.0 / 300.0) # ≈ 0.0165 + const alpha15m = 1.0 - exp(-5.0 / 900.0) # ≈ 0.0055 + + var ewma1m = 0.0 + var ewma5m = 0.0 + var ewma15m = 0.0 + + var now = Moment.now() var lagWasHigh = false + while true: + let lastWakeup = now await sleepAsync(CheckInterval) + now = Moment.now() - let now = Moment.now() let actualElapsed = now - lastWakeup - let lag = actualElapsed - CheckInterval + let lag = max(ZeroDuration, actualElapsed - CheckInterval) + const maxAcceptedLagSecs = MaxAcceptedLag.nanoseconds.float64 / 1_000_000_000.0 + let lagSecs = lag.nanoseconds.float64 / 1_000_000_000.0 + let load = lagSecs / maxAcceptedLagSecs - event_loop_lag_seconds.set(lagSecs) + ewma1m = alpha1m * load + (1.0 - alpha1m) * ewma1m + ewma5m = alpha5m * load + (1.0 - alpha5m) * ewma5m + ewma15m = alpha15m * load + (1.0 - alpha15m) * ewma15m - let lagIsHigh = lag > CheckInterval + event_loop_load.set(round(ewma1m, 4), labelValues = ["1m"]) + event_loop_load.set(round(ewma5m, 4), labelValues = ["5m"]) + event_loop_load.set(round(ewma15m, 4), labelValues = ["15m"]) + + let lagIsHigh = lag > MaxAcceptedLag if lag > CheckInterval: warn "chronos event loop severely lagging, many tasks may be accumulating", expected_secs = CheckInterval.seconds, - actual_secs = actualElapsed.nanoseconds.float64 / 1_000_000_000.0, - lag_secs = lagSecs - elif lag > (CheckInterval div 2): - info "chronos event loop lag detected", - expected_secs = CheckInterval.seconds, - actual_secs = actualElapsed.nanoseconds.float64 / 1_000_000_000.0, - lag_secs = lagSecs + lag_secs = round(lagSecs, 4), + load_1m = round(ewma1m, 4), + load_5m = round(ewma5m, 4), + load_15m = round(ewma15m, 4) - if not isNil(onLagChange) and lagIsHigh != lagWasHigh: + if not onLagChange.isNil() and lagIsHigh != lagWasHigh: lagWasHigh = lagIsHigh onLagChange(lagIsHigh) - - lastWakeup = now From 43948432998064342e9d691381291224724782d6 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Tue, 21 Apr 2026 22:20:53 +0100 Subject: [PATCH 131/155] fix: make update and wakunode2 build on arm64 after Nimble migration (#3814) Rebuild nat libs (miniupnpc, libnatpmp) for the host architecture during nimble deps setup. The prebuilt libs from the nimble cache are x86_64 and fail to link on arm64 (Apple Silicon). Co-authored-by: Claude Sonnet 4.6 --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 7ba417527..be9e14027 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,7 @@ $(NIMBLEDEPS_STAMP): nimble.lock | waku.nims nimble setup --localdeps $(MAKE) build-nph $(MAKE) rebuild-bearssl-nimbledeps + $(MAKE) rebuild-nat-libs-nimbledeps touch $@ update: From bb8a7e878233e8f51fd67cf266ba4df4b7bae08c Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Wed, 22 Apr 2026 09:52:57 -0300 Subject: [PATCH 132/155] Fix redundant start/stop calls (#3817) * remove redundant proto start/stop calls from node start/stop * fix WakuRelay start/stop not overriding GossipSub start/stop * replace startRelay with reconnectRelayPeers --- tests/waku_store_sync/sync_utils.nim | 6 +-- tests/waku_store_sync/test_protocol.nim | 36 ++++++------- waku/node/kernel_api/relay.nim | 3 +- waku/node/waku_node.nim | 68 +++++++------------------ waku/waku_metadata/protocol.nim | 6 --- waku/waku_mix/protocol.nim | 6 --- waku/waku_relay/protocol.nim | 4 +- waku/waku_rendezvous/protocol.nim | 15 ++---- waku/waku_store_sync/reconciliation.nim | 15 +++--- waku/waku_store_sync/transfer.nim | 11 ++-- 10 files changed, 61 insertions(+), 109 deletions(-) diff --git a/tests/waku_store_sync/sync_utils.nim b/tests/waku_store_sync/sync_utils.nim index fe62e02a1..888b10a83 100644 --- a/tests/waku_store_sync/sync_utils.nim +++ b/tests/waku_store_sync/sync_utils.nim @@ -45,7 +45,7 @@ proc newTestWakuRecon*( let proto = res.get() - proto.start() + await proto.start() switch.mount(proto) return proto @@ -55,7 +55,7 @@ proc newTestWakuTransfer*( idsTx: AsyncQueue[(SyncID, PubsubTopic, ContentTopic)], wantsRx: AsyncQueue[PeerId], needsRx: AsyncQueue[(PeerId, WakuMessageHash)], -): SyncTransfer = +): Future[SyncTransfer] {.async.} = let peerManager = PeerManager.new(switch) let proto = SyncTransfer.new( @@ -66,7 +66,7 @@ proc newTestWakuTransfer*( remoteNeedsRx = needsRx, ) - proto.start() + await proto.start() switch.mount(proto) return proto diff --git a/tests/waku_store_sync/test_protocol.nim b/tests/waku_store_sync/test_protocol.nim index d051eebd7..d1f5a102a 100644 --- a/tests/waku_store_sync/test_protocol.nim +++ b/tests/waku_store_sync/test_protocol.nim @@ -63,8 +63,8 @@ suite "Waku Sync: reconciliation": clientPeerInfo = clientSwitch.peerInfo.toRemotePeerInfo() asyncTeardown: - server.stop() - client.stop() + await server.stop() + await client.stop() await allFutures(serverSwitch.stop(), clientSwitch.stop()) @@ -561,8 +561,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -610,8 +610,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -657,8 +657,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -701,8 +701,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -736,8 +736,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -773,8 +773,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -848,8 +848,8 @@ suite "Waku Sync: transfer": remoteNeedsRx = clientRemoteNeeds, ) - server.start() - client.start() + await server.start() + await client.start() serverSwitch.mount(server) clientSwitch.mount(client) @@ -861,8 +861,8 @@ suite "Waku Sync: transfer": clientPeermanager.addPeer(serverPeerInfo) asyncTeardown: - server.stop() - client.stop() + await server.stop() + await client.stop() await allFutures(serverSwitch.stop(), clientSwitch.stop()) diff --git a/waku/node/kernel_api/relay.nim b/waku/node/kernel_api/relay.nim index c5a11ff02..fe46f5bd2 100644 --- a/waku/node/kernel_api/relay.nim +++ b/waku/node/kernel_api/relay.nim @@ -263,7 +263,8 @@ proc mountRelay*( node.wakuRelay.routingRecordsHandler.add(peerExchangeHandler.get()) if node.started: - await node.startRelay() + await node.wakuRelay.start() + await node.reconnectRelayPeers() node.switch.mount(node.wakuRelay, protocolMatcher(WakuRelayCodec)) diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 506a3e592..45080d9d0 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -369,30 +369,16 @@ proc mountStoreSync*( return ok() -proc startRelay*(node: WakuNode) {.async.} = - ## Setup and start relay protocol - info "starting relay protocol" - +proc reconnectRelayPeers*(node: WakuNode) {.async.} = + ## Reconnect to previously-seen WakuRelay peers. if node.wakuRelay.isNil(): - error "Failed to start relay. Not mounted." return - - ## Setup relay protocol - - # Resume previous relay connections - if node.peerManager.switch.peerStore.hasPeers(protocolMatcher(WakuRelayCodec)): - info "Found previous WakuRelay peers. Reconnecting." - - # Reconnect to previous relay peers. This will respect a backoff period, if necessary - let backoffPeriod = - node.wakuRelay.parameters.pruneBackoff + chronos.seconds(BackoffSlackTime) - - await node.peerManager.reconnectPeers(WakuRelayCodec, backoffPeriod) - - # Start the WakuRelay protocol - await node.wakuRelay.start() - - info "relay started successfully" + if not node.peerManager.switch.peerStore.hasPeers(protocolMatcher(WakuRelayCodec)): + return + info "Found previous WakuRelay peers. Reconnecting." + let backoffPeriod = + node.wakuRelay.parameters.pruneBackoff + chronos.seconds(BackoffSlackTime) + await node.peerManager.reconnectPeers(WakuRelayCodec, backoffPeriod) proc selectRandomPeers*(peers: seq[PeerId], numRandomPeers: int): seq[PeerId] = var randomPeers = peers @@ -430,7 +416,10 @@ proc mountRendezvous*( return if node.started: - await node.wakuRendezvous.start() + try: + await node.wakuRendezvous.start() + except CancelledError as exc: + error "failed to start wakuRendezvous", error = exc.msg try: node.switch.mount(node.wakuRendezvous, protocolMatcher(WakuRendezVousCodec)) @@ -578,31 +567,12 @@ proc start*(node: WakuNode) {.async.} = if isBindIpWithZeroPort(address): zeroPortPresent = true - # Perform relay-specific startup tasks TODO: this should be rethought - if not node.wakuRelay.isNil(): - await node.startRelay() - - if not node.wakuMix.isNil(): - node.wakuMix.start() - - if not node.wakuMetadata.isNil(): - node.wakuMetadata.start() - if not node.wakuStoreResume.isNil(): await node.wakuStoreResume.start() - if not node.wakuRendezvous.isNil(): - await node.wakuRendezvous.start() - if not node.wakuRendezvousClient.isNil(): await node.wakuRendezvousClient.start() - if not node.wakuStoreReconciliation.isNil(): - node.wakuStoreReconciliation.start() - - if not node.wakuStoreTransfer.isNil(): - node.wakuStoreTransfer.start() - ## The switch uses this mapper to update peer info addrs ## with announced addrs after start let addressMapper = proc( @@ -612,8 +582,12 @@ proc start*(node: WakuNode) {.async.} = node.switch.peerInfo.addressMappers.add(addressMapper) ## The switch will update addresses after start using the addressMapper + ## NOTE: This will dispatch gossipsub start to the WakuRelay.start method override await node.switch.start() + # After switch.start, run custom Logos Delivery relay start logic + await node.reconnectRelayPeers() + node.started = true if not node.wakuFilterClient.isNil(): @@ -637,6 +611,7 @@ proc stop*(node: WakuNode) {.async.} = node.stopProvidersAndListeners() + ## NOTE: This will dispatch gossipsub stop to the WakuRelay.stop method override await node.switch.stop() node.peerManager.stop() @@ -653,12 +628,6 @@ proc stop*(node: WakuNode) {.async.} = if not node.wakuStoreResume.isNil(): await node.wakuStoreResume.stopWait() - if not node.wakuStoreReconciliation.isNil(): - node.wakuStoreReconciliation.stop() - - if not node.wakuStoreTransfer.isNil(): - node.wakuStoreTransfer.stop() - if not node.wakuPeerExchangeClient.isNil() and not node.wakuPeerExchangeClient.pxLoopHandle.isNil(): await node.wakuPeerExchangeClient.pxLoopHandle.cancelAndWait() @@ -666,9 +635,6 @@ proc stop*(node: WakuNode) {.async.} = if not node.wakuKademlia.isNil(): await node.wakuKademlia.stop() - if not node.wakuRendezvous.isNil(): - await node.wakuRendezvous.stopWait() - if not node.wakuRendezvousClient.isNil(): await node.wakuRendezvousClient.stopWait() diff --git a/waku/waku_metadata/protocol.nim b/waku/waku_metadata/protocol.nim index 623cbb6c3..7c72a6934 100644 --- a/waku/waku_metadata/protocol.nim +++ b/waku/waku_metadata/protocol.nim @@ -108,9 +108,3 @@ proc new*(T: type WakuMetadata, clusterId: uint32, getShards: GetShards): T = clusterId = wm.clusterId, shards = wm.getShards() return wm - -proc start*(wm: WakuMetadata) = - wm.started = true - -proc stop*(wm: WakuMetadata) = - wm.started = false diff --git a/waku/waku_mix/protocol.nim b/waku/waku_mix/protocol.nim index e31929b71..ac8b69eaf 100644 --- a/waku/waku_mix/protocol.nim +++ b/waku/waku_mix/protocol.nim @@ -104,10 +104,4 @@ proc new*( proc poolSize*(mix: WakuMix): int = mix.nodePool.len -method start*(mix: WakuMix) = - info "starting waku mix protocol" - -method stop*(mix: WakuMix) {.async.} = - discard - # Mix Protocol diff --git a/waku/waku_relay/protocol.nim b/waku/waku_relay/protocol.nim index 79d3702eb..b19173d36 100644 --- a/waku/waku_relay/protocol.nim +++ b/waku/waku_relay/protocol.nim @@ -517,12 +517,12 @@ proc topicsHealthLoop(w: WakuRelay) {.async.} = # safety cooldown to protect from edge cases await sleepAsync(100.milliseconds) -method start*(w: WakuRelay) {.async, base.} = +method start*(w: WakuRelay) {.async: (raises: [CancelledError]).} = info "start" await procCall GossipSub(w).start() w.topicHealthLoopHandle = w.topicsHealthLoop() -method stop*(w: WakuRelay) {.async, base.} = +method stop*(w: WakuRelay) {.async: (raises: []).} = info "stop" await procCall GossipSub(w).stop() diff --git a/waku/waku_rendezvous/protocol.nim b/waku/waku_rendezvous/protocol.nim index 00b5f1a5c..89433f533 100644 --- a/waku/waku_rendezvous/protocol.nim +++ b/waku/waku_rendezvous/protocol.nim @@ -211,29 +211,22 @@ proc new*( return ok(wrv) -proc start*(self: WakuRendezVous) {.async: (raises: []).} = +method start*(self: WakuRendezVous) {.async: (raises: [CancelledError]).} = # Start the parent GenericRendezVous (starts the register deletion loop) if self.started: warn "waku rendezvous already started" return - try: - await procCall GenericRendezVous[WakuPeerRecord](self).start() - except CancelledError as exc: - error "failed to start GenericRendezVous", cause = exc.msg - return + await procCall GenericRendezVous[WakuPeerRecord](self).start() # start registering forever self.periodicRegistrationFut = self.periodicRegistration() info "waku rendezvous discovery started" -proc stopWait*(self: WakuRendezVous) {.async: (raises: []).} = +method stop*(self: WakuRendezVous) {.async: (raises: []).} = if not self.periodicRegistrationFut.isNil(): await self.periodicRegistrationFut.cancelAndWait() # Stop the parent GenericRendezVous (stops the register deletion loop) - await GenericRendezVous[WakuPeerRecord](self).stop() - - # Stop the parent GenericRendezVous (stops the register deletion loop) - await GenericRendezVous[WakuPeerRecord](self).stop() + await procCall GenericRendezVous[WakuPeerRecord](self).stop() info "waku rendezvous discovery stopped" diff --git a/waku/waku_store_sync/reconciliation.nim b/waku/waku_store_sync/reconciliation.nim index 23f513322..9dd308255 100644 --- a/waku/waku_store_sync/reconciliation.nim +++ b/waku/waku_store_sync/reconciliation.nim @@ -468,7 +468,7 @@ proc idsReceiverLoop(self: SyncReconciliation) {.async.} = self.messageIngress(id, pubsub, content) -proc start*(self: SyncReconciliation) = +method start*(self: SyncReconciliation) {.async: (raises: [CancelledError]).} = if self.started: return @@ -484,13 +484,16 @@ proc start*(self: SyncReconciliation) = info "Store Sync Reconciliation protocol started" -proc stop*(self: SyncReconciliation) = - if self.syncInterval > ZeroDuration: - self.periodicSyncFut.cancelSoon() +method stop*(self: SyncReconciliation) {.async: (raises: []).} = + defer: + self.started = false if self.syncInterval > ZeroDuration: - self.periodicPruneFut.cancelSoon() + await self.periodicSyncFut.cancelAndWait() - self.idsReceiverFut.cancelSoon() + if self.syncInterval > ZeroDuration: + await self.periodicPruneFut.cancelAndWait() + + await self.idsReceiverFut.cancelAndWait() info "Store Sync Reconciliation protocol stopped" diff --git a/waku/waku_store_sync/transfer.nim b/waku/waku_store_sync/transfer.nim index 6a600b4e3..5d20afb18 100644 --- a/waku/waku_store_sync/transfer.nim +++ b/waku/waku_store_sync/transfer.nim @@ -217,7 +217,7 @@ proc new*( return transfer -proc start*(self: SyncTransfer) = +method start*(self: SyncTransfer) {.async: (raises: [CancelledError]).} = if self.started: return @@ -228,10 +228,11 @@ proc start*(self: SyncTransfer) = info "Store Sync Transfer protocol started" -proc stop*(self: SyncTransfer) = - self.started = false +method stop*(self: SyncTransfer) {.async: (raises: []).} = + defer: + self.started = false - self.localWantsRxFut.cancelSoon() - self.remoteNeedsRxFut.cancelSoon() + await self.localWantsRxFut.cancelAndWait() + await self.remoteNeedsRxFut.cancelAndWait() info "Store Sync Transfer protocol stopped" From 820ccc6e1012ba6d5dfeef27f8bf4d126c2e7940 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:24:55 +0200 Subject: [PATCH 133/155] Add ci support for liblogosdeliery, build and artifacts (#3746) --- .github/workflows/ci.yml | 1 + .github/workflows/release-assets.yml | 48 +++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b45853e21..52d20157a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,7 @@ jobs: - 'waku.nimble' - 'Makefile' - 'library/**' + - 'liblogosdelivery/**' v2: - 'waku/**' - 'apps/**' diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml index 50e3c4c3d..274eb564c 100644 --- a/.github/workflows/release-assets.yml +++ b/.github/workflows/release-assets.yml @@ -4,7 +4,7 @@ on: push: tags: - 'v*' # "e.g. v0.4" - + workflow_dispatch: env: @@ -65,6 +65,16 @@ jobs: echo "libwaku=${LIBWAKU_ARTIFACT_NAME}" >> $GITHUB_OUTPUT + if [[ "${{ runner.os }}" == "Linux" ]]; then + LIBLOGOSDELIVERY_ARTIFACT_NAME=$(echo "liblogosdelivery-${VERSION}-${{matrix.arch}}-${{runner.os}}-linux.deb" | tr "[:upper:]" "[:lower:]") + fi + + if [[ "${{ runner.os }}" == "macOS" ]]; then + LIBLOGOSDELIVERY_ARTIFACT_NAME=$(echo "liblogosdelivery-${VERSION}-${{matrix.arch}}-macos.tar.gz" | tr "[:upper:]" "[:lower:]") + fi + + echo "liblogosdelivery=${LIBLOGOSDELIVERY_ARTIFACT_NAME}" >> $GITHUB_OUTPUT + - name: Install build dependencies run: | if [[ "${{ runner.os }}" == "Linux" ]]; then @@ -83,6 +93,9 @@ jobs: 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 + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}} -d:postgres" CI=false liblogosdelivery + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}} -d:postgres" CI=false STATIC=1 liblogosdelivery + - name: Create distributable libwaku package run: | VERSION=${{ steps.version.outputs.version }} @@ -109,6 +122,32 @@ jobs: tar -cvzf ${{steps.vars.outputs.libwaku}} ./build/libwaku.dylib ./build/libwaku.a ./library/libwaku.h fi + - name: Create distributable liblogosdelivery package + run: | + VERSION=${{ steps.version.outputs.version }} + + if [[ "${{ runner.os }}" == "Linux" ]]; then + rm -rf pkg + mkdir -p pkg/DEBIAN pkg/usr/local/lib pkg/usr/local/include + cp build/liblogosdelivery.so pkg/usr/local/lib/ + cp build/liblogosdelivery.a pkg/usr/local/lib/ + cp liblogosdelivery/liblogosdelivery.h pkg/usr/local/include/ + + echo "Package: logosdelivery" >> pkg/DEBIAN/control + echo "Version: ${VERSION}" >> pkg/DEBIAN/control + echo "Priority: optional" >> pkg/DEBIAN/control + echo "Section: libs" >> pkg/DEBIAN/control + echo "Architecture: ${{matrix.arch}}" >> pkg/DEBIAN/control + echo "Maintainer: Logos Messaging Team" >> pkg/DEBIAN/control + echo "Description: Logos Delivery library" >> pkg/DEBIAN/control + + dpkg-deb --build pkg ${{steps.vars.outputs.liblogosdelivery}} + fi + + if [[ "${{ runner.os }}" == "macOS" ]]; then + tar -cvzf ${{steps.vars.outputs.liblogosdelivery}} ./build/liblogosdelivery.dylib ./build/liblogosdelivery.a ./liblogosdelivery/liblogosdelivery.h + fi + - name: Upload waku artifact uses: actions/upload-artifact@v4.4.0 with: @@ -122,3 +161,10 @@ jobs: name: libwaku-${{ steps.version.outputs.version }}-${{ matrix.arch }}-${{ runner.os }} path: ${{ steps.vars.outputs.libwaku }} if-no-files-found: error + + - name: Upload liblogosdelivery artifact + uses: actions/upload-artifact@v4.4.0 + with: + name: liblogosdelivery-${{ steps.version.outputs.version }}-${{ matrix.arch }}-${{ runner.os }} + path: ${{ steps.vars.outputs.liblogosdelivery }} + if-no-files-found: error From ff98d853138c3f07c8c5074aa7a9d5ab6cf316b8 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Thu, 23 Apr 2026 16:02:34 -0300 Subject: [PATCH 134/155] fix: relay validator registration and sync filter (#3823) * reuse stored validator in relay * fix skip check in store sync * increase sync tolerance in test (matches similar test) --- tests/waku_store_sync/test_protocol.nim | 2 +- waku/waku_relay/protocol.nim | 2 +- waku/waku_store_sync/reconciliation.nim | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/waku_store_sync/test_protocol.nim b/tests/waku_store_sync/test_protocol.nim index d1f5a102a..3ffa7ad4a 100644 --- a/tests/waku_store_sync/test_protocol.nim +++ b/tests/waku_store_sync/test_protocol.nim @@ -372,7 +372,7 @@ suite "Waku Sync: reconciliation": const msgCount = 400_000 diffCount = 100_000 - tol = 1000 + tol = 10_000 var diffMsgHashes: HashSet[WakuMessageHash] var missingIdx: HashSet[int] diff --git a/waku/waku_relay/protocol.nim b/waku/waku_relay/protocol.nim index b19173d36..e7b2c99cb 100644 --- a/waku/waku_relay/protocol.nim +++ b/waku/waku_relay/protocol.nim @@ -618,7 +618,7 @@ proc subscribe*(w: WakuRelay, pubsubTopic: PubsubTopic, handler: WakuRelayHandle # Otherwise this might lead to unintended behaviour. if not w.topicValidator.hasKey(pubSubTopic): let newValidator = w.generateOrderedValidator() - procCall GossipSub(w).addValidator(pubSubTopic, w.generateOrderedValidator()) + procCall GossipSub(w).addValidator(pubSubTopic, newValidator) w.topicValidator[pubSubTopic] = newValidator # set this topic parameters for scoring diff --git a/waku/waku_store_sync/reconciliation.nim b/waku/waku_store_sync/reconciliation.nim index 9dd308255..b18251fff 100644 --- a/waku/waku_store_sync/reconciliation.nim +++ b/waku/waku_store_sync/reconciliation.nim @@ -145,7 +145,7 @@ proc preProcessPayload( # convert to skip range before processing for i in 0 ..< payload.ranges.len: let rangeType = payload.ranges[i][1] - if rangeType != RangeType.Skip: + if rangeType == RangeType.Skip: continue let upperBound = payload.ranges[i][0].b.time From 324048430bf1b84ffa0376041b79ce46f3df96de Mon Sep 17 00:00:00 2001 From: Darshan <35736874+darshankabariya@users.noreply.github.com> Date: Sat, 25 Apr 2026 00:03:46 +0530 Subject: [PATCH 135/155] fix: restore -d:postgres in nimble task and propagate NIMFLAGS (#3830) --- .github/workflows/container-image.yml | 2 +- .github/workflows/pre-release.yml | 4 ++-- .github/workflows/release-assets.yml | 10 +++++----- Dockerfile | 3 ++- ci/Jenkinsfile.release | 6 ++++-- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml index 0783c1f66..0ff427d87 100644 --- a/.github/workflows/container-image.yml +++ b/.github/workflows/container-image.yml @@ -83,7 +83,7 @@ jobs: id: build if: ${{ steps.secrets.outcome == 'success' }} run: | - make -j${NPROC} V=1 NIMFLAGS="-d:disableMarchNative -d:postgres -d:chronicles_colors:none" wakunode2 + make -j${NPROC} V=1 POSTGRES=1 NIMFLAGS="-d:disableMarchNative -d:chronicles_colors:none" wakunode2 SHORT_REF=$(git rev-parse --short HEAD) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index e3c8bb575..52a50adc8 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -66,8 +66,8 @@ jobs: make V=1 CI=false NIMFLAGS="-d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" \ update - make V=1 CI=false\ - NIMFLAGS="-d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}} -d:postgres" \ + make V=1 CI=false POSTGRES=1\ + NIMFLAGS="-d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" \ wakunode2\ chat2\ tools diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml index 274eb564c..fc1f819d9 100644 --- a/.github/workflows/release-assets.yml +++ b/.github/workflows/release-assets.yml @@ -86,15 +86,15 @@ jobs: OS=$([[ "${{runner.os}}" == "macOS" ]] && echo "macosx" || echo "linux") make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" V=1 update - make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}} -d:postgres" CI=false wakunode2 + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" POSTGRES=1 CI=false wakunode2 make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" CI=false chat2 tar -cvzf ${{steps.vars.outputs.waku}} ./build/ - 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 + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" POSTGRES=1 CI=false libwaku + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" POSTGRES=1 CI=false STATIC=1 libwaku - make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}} -d:postgres" CI=false liblogosdelivery - make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}} -d:postgres" CI=false STATIC=1 liblogosdelivery + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" POSTGRES=1 CI=false liblogosdelivery + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" POSTGRES=1 CI=false STATIC=1 liblogosdelivery - name: Create distributable libwaku package run: | diff --git a/Dockerfile b/Dockerfile index 412d0977a..05525774b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ ARG NIMFLAGS ARG MAKE_TARGET=wakunode2 ARG NIM_COMMIT ARG HEAPTRACK_BUILD=0 +ARG POSTGRES=0 # Get build tools and required header files RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq libbsd-dev @@ -26,7 +27,7 @@ RUN if [ "$HEAPTRACK_BUILD" = "1" ]; then \ RUN make -j$(nproc) deps QUICK_AND_DIRTY_COMPILER=1 ${NIM_COMMIT} # Build the final node binary -RUN make -j$(nproc) ${NIM_COMMIT} $MAKE_TARGET NIMFLAGS="${NIMFLAGS}" +RUN make -j$(nproc) ${NIM_COMMIT} $MAKE_TARGET NIMFLAGS="${NIMFLAGS}" POSTGRES=${POSTGRES} # PRODUCTION IMAGE ------------------------------------------------------------- diff --git a/ci/Jenkinsfile.release b/ci/Jenkinsfile.release index 570a37d5f..d8237f009 100644 --- a/ci/Jenkinsfile.release +++ b/ci/Jenkinsfile.release @@ -85,7 +85,8 @@ pipeline { "--label=commit='${git.commit()}' " + "--label=version='${git.describe('--tags')}' " + "--build-arg=MAKE_TARGET='${params.MAKE_TARGET}' " + - "--build-arg=NIMFLAGS='${params.NIMFLAGS} -d:postgres -d:heaptracker ' " + + "--build-arg=NIMFLAGS='${params.NIMFLAGS} -d:heaptracker ' " + + "--build-arg=POSTGRES='1' " + "--build-arg=LOG_LEVEL='${params.LOWEST_LOG_LEVEL_ALLOWED}' " + "--build-arg=DEBUG='${params.DEBUG ? "1" : "0"} ' " + "--build-arg=NIM_COMMIT='NIM_COMMIT=heaptrack_support_v2.0.12' " + @@ -98,7 +99,8 @@ pipeline { "--label=commit='${git.commit()}' " + "--label=version='${git.describe('--tags')}' " + "--build-arg=MAKE_TARGET='${params.MAKE_TARGET}' " + - "--build-arg=NIMFLAGS='${params.NIMFLAGS} -d:postgres ' " + + "--build-arg=NIMFLAGS='${params.NIMFLAGS}' " + + "--build-arg=POSTGRES='1' " + "--build-arg=LOG_LEVEL='${params.LOWEST_LOG_LEVEL_ALLOWED}' " + "--build-arg=DEBUG='${params.DEBUG ? "1" : "0"} ' " + "--target='prod' ." From 5034086fefe2f32bf95319cdd39aa62fc622e4bc Mon Sep 17 00:00:00 2001 From: osmaczko <33099791+osmaczko@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:51:39 +0200 Subject: [PATCH 136/155] Chore/make nix build phase configurable (#3826) * nix: parameterize build flags with named args Expose `enablePostgres`, `enableNimDebugDlOpen`, and `chroniclesLogLevel` as arguments on `nix/default.nix`. Defaults preserve today's hardcoded behavior, so `nix build .#liblogosdelivery` with no overrides is a no-op change. Consume the package via `callPackage` in `flake.nix` so consumers can use `.override { ... }` without extra wrapping. * nix: link libstdc++ on Linux so consumers don't need patchelf Append `stdenv.cc.cc.lib` to `buildInputs` on Linux and add `-lstdc++` to the Nim `--passL` flags. Nix stdenv's fixupPhase will auto-inject `${stdenv.cc.cc.lib}/lib` into the output's RUNPATH, so downstream consumers can drop their patchelf step. macOS resolves the C++ stdlib via dyld/libc++ and is unaffected. * nix: bundle librln into the output for a self-contained package Copy the librln shared library (`librln.so` / `librln.dylib`) from the zerokit input into `$out/lib` and rewrite the internal reference in `liblogosdelivery`: - Darwin: set librln's install name to `@rpath/librln.dylib`, change the consumer's reference to match, and add `@loader_path` as an rpath. - Linux: add `$ORIGIN` to the rpath so `librln.so` resolves from the sibling directory, preserving the gcc-lib entry injected by the stdenv fixupPhase for libstdc++. The installed `liblogosdelivery` no longer carries a `/nix/store/...` absolute path to zerokit, so downstream consumers can ship the bundle as-is. --- flake.nix | 11 ++++++----- nix/default.nix | 51 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/flake.nix b/flake.nix index 31d5a120c..50b6dc0b5 100644 --- a/flake.nix +++ b/flake.nix @@ -56,13 +56,14 @@ packages = forAllSystems (system: let pkgs = pkgsFor system; - mkPkg = zerokitRln: import ./nix/default.nix { - inherit pkgs zerokitRln; + liblogosdelivery = pkgs.callPackage ./nix/default.nix { + inherit pkgs; src = ./.; + zerokitRln = zerokit.packages.${system}.rln; }; - in rec { - liblogosdelivery = mkPkg zerokit.packages.${system}.rln; - default = liblogosdelivery; + in { + inherit liblogosdelivery; + default = liblogosdelivery; } ); diff --git a/nix/default.nix b/nix/default.nix index 0d1de2ece..a9ea0f598 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,8 +1,22 @@ -{ pkgs, src, zerokitRln }: +{ pkgs +, src +, zerokitRln +, enablePostgres ? true +, enableNimDebugDlOpen ? true +, chroniclesLogLevel ? null +}: let deps = import ./deps.nix { inherit pkgs; }; + nimDefineArgs = pkgs.lib.concatStringsSep " \\\n " ( + [ "--define:disable_libbacktrace" ] + ++ pkgs.lib.optional enablePostgres "--define:postgres" + ++ pkgs.lib.optional enableNimDebugDlOpen "--define:nimDebugDlOpen" + ++ pkgs.lib.optional (chroniclesLogLevel != null) + "--define:chronicles_log_level=${toString chroniclesLogLevel}" + ); + # nat_traversal is excluded from the static pathArgs; it is handled # separately in buildPhase (its bundled C libs must be compiled first). otherDeps = builtins.removeAttrs deps [ "nat_traversal" ]; @@ -32,7 +46,8 @@ pkgs.stdenv.mkDerivation { which ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ pkgs.darwin.cctools ]; - buildInputs = [ zerokitRln ]; + buildInputs = [ zerokitRln ] + ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.stdenv.cc.cc.lib ]; buildPhase = '' export HOME=$TMPDIR @@ -60,10 +75,8 @@ pkgs.stdenv.mkDerivation { ${pathArgs} \ --path:$NAT_TRAV \ --path:$NAT_TRAV/src \ - --passL:"-L${zerokitRln}/lib -lrln" \ - --define:disable_libbacktrace \ - --define:postgres \ - --define:nimDebugDlOpen \ + --passL:"-L${zerokitRln}/lib -lrln${pkgs.lib.optionalString pkgs.stdenv.isLinux " -lstdc++"}" \ + ${nimDefineArgs} \ --out:build/liblogosdelivery.${libExt} \ --app:lib \ --threads:on \ @@ -81,10 +94,8 @@ pkgs.stdenv.mkDerivation { ${pathArgs} \ --path:$NAT_TRAV \ --path:$NAT_TRAV/src \ - --passL:"-L${zerokitRln}/lib -lrln" \ - --define:disable_libbacktrace \ - --define:postgres \ - --define:nimDebugDlOpen \ + --passL:"-L${zerokitRln}/lib -lrln${pkgs.lib.optionalString pkgs.stdenv.isLinux " -lstdc++"}" \ + ${nimDefineArgs} \ --out:build/liblogosdelivery.a \ --app:staticlib \ --threads:on \ @@ -97,9 +108,29 @@ pkgs.stdenv.mkDerivation { ''; installPhase = '' + runHook preInstall mkdir -p $out/lib $out/include cp build/liblogosdelivery.${libExt} $out/lib/ 2>/dev/null || true cp build/liblogosdelivery.a $out/lib/ 2>/dev/null || true cp liblogosdelivery/liblogosdelivery.h $out/include/ 2>/dev/null || true + runHook postInstall ''; + + # Bundle librln alongside liblogosdelivery so the output is self-contained. + # Use --add-rpath (not --set-rpath) so fixupPhase's stdenv RUNPATH injection + # for libstdc++ is preserved. + postInstall = + pkgs.lib.optionalString pkgs.stdenv.isDarwin '' + cp ${zerokitRln}/lib/librln.dylib $out/lib/ + chmod +w $out/lib/librln.dylib $out/lib/liblogosdelivery.dylib + install_name_tool -id @rpath/liblogosdelivery.dylib $out/lib/liblogosdelivery.dylib + install_name_tool -id @rpath/librln.dylib $out/lib/librln.dylib + old=$(otool -L $out/lib/liblogosdelivery.dylib | awk 'NR>1{print $1}' | grep librln) + install_name_tool -change "$old" @rpath/librln.dylib $out/lib/liblogosdelivery.dylib + install_name_tool -add_rpath @loader_path $out/lib/liblogosdelivery.dylib + '' + + pkgs.lib.optionalString pkgs.stdenv.isLinux '' + cp ${zerokitRln}/lib/librln.so $out/lib/ + patchelf --add-rpath '$ORIGIN' $out/lib/liblogosdelivery.so + ''; } From 300f584efcb69c3d562762ec27515bb94a4d06b2 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:10:21 +0200 Subject: [PATCH 137/155] Removed duplicates of announcedAddresses, extMultiaddresses (#3831) Removing duplicates of multiaddresses for Enr. Safe building Enr Record. Co-authored-by: Copilot --- tests/factory/test_node_factory.nim | 49 ++++++++++++++++++++++++++++- tests/test_waku_enr.nim | 38 ++++++++++++++++++++++ tests/test_waku_netconfig.nim | 25 +++++++++++++++ waku/factory/internal_config.nim | 33 ++++++++++++++++--- waku/node/net_config.nim | 10 ++++-- waku/waku_enr/multiaddr.nim | 2 +- 6 files changed, 148 insertions(+), 9 deletions(-) diff --git a/tests/factory/test_node_factory.nim b/tests/factory/test_node_factory.nim index f30e079b5..4b2085e82 100644 --- a/tests/factory/test_node_factory.nim +++ b/tests/factory/test_node_factory.nim @@ -1,11 +1,19 @@ {.used.} -import testutils/unittests, chronos, libp2p/protocols/connectivity/relay/relay +import + std/[options, sequtils], + testutils/unittests, + chronos, + libp2p/multiaddress, + libp2p/protocols/connectivity/relay/relay +import eth/p2p/discoveryv5/enr import ../testlib/wakunode, waku/waku_node, + waku/waku_enr, waku/factory/node_factory, + waku/factory/internal_config, waku/factory/conf_builder/conf_builder, waku/factory/conf_builder/web_socket_conf_builder @@ -38,6 +46,45 @@ suite "Node Factory": not node.wakuStore.isNil() not node.wakuArchive.isNil() + test "ENR configuration trims multiaddrs until record fits": + var conf = defaultTestWakuConf() + let bindIp = conf.endpointConf.p2pListenAddress + let bindPort = Port(30303) + + let oversizedMultiaddrs = (0 .. 11).mapIt( + MultiAddress + .init( + "/dns4/very-long-logical-hostname-" & $it & + ".example.logos.dev.status.im/tcp/30303/wss" + ) + .get() + ) + + let netConfig = NetConfig.init( + clusterId = conf.clusterId, + bindIp = bindIp, + bindPort = bindPort, + extMultiAddrs = oversizedMultiaddrs, + extMultiAddrsOnly = true, + wakuFlags = some(conf.wakuFlags), + ).valueOr: + raiseAssert error + + let record = enrConfiguration(conf, netConfig).valueOr: + raiseAssert error + + let typedRecord = record.toTyped() + require typedRecord.isOk() + + let multiaddrsOpt = typedRecord.value.multiaddrs + require multiaddrsOpt.isSome() + + let retainedMultiaddrs = multiaddrsOpt.get() + check: + retainedMultiaddrs.len < oversizedMultiaddrs.len + retainedMultiaddrs.len > 0 + retainedMultiaddrs == oversizedMultiaddrs[0 ..< retainedMultiaddrs.len] + asynctest "Set up a node with Filter enabled": var confBuilder = defaultTestWakuConfBuilder() confBuilder.filterServiceConf.withEnabled(true) diff --git a/tests/test_waku_enr.nim b/tests/test_waku_enr.nim index 2ffff5e57..10183adf5 100644 --- a/tests/test_waku_enr.nim +++ b/tests/test_waku_enr.nim @@ -271,6 +271,44 @@ suite "Waku ENR - Multiaddresses": multiaddrs.contains(expectedAddr1) multiaddrs.contains(addr2) + test "encode and decode record with multiaddrs field deduplicates duplicate entries": + ## Given + let + enrSeqNum = 1u64 + enrPrivKey = generatesecp256k1key() + + let + addr1 = MultiAddress + .init( + "/ip4/127.0.0.1/tcp/80/ws/p2p/16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr31iDQpSN5Qa882BCjjwgrD" + ) + .get() + addr1NoPeerId = MultiAddress.init("/ip4/127.0.0.1/tcp/80/ws").get() + addr2 = MultiAddress.init("/ip4/127.0.0.1/tcp/443/wss").get() + + ## When + var builder = EnrBuilder.init(enrPrivKey, seqNum = enrSeqNum) + builder.withMultiaddrs(@[addr1, addr1NoPeerId, addr2, addr2]) + + let recordRes = builder.build() + + require recordRes.isOk() + let record = recordRes.tryGet() + + let typedRecord = record.toTyped() + require typedRecord.isOk() + + let multiaddrsOpt = typedRecord.value.multiaddrs + + ## Then + check multiaddrsOpt.isSome() + + let multiaddrs = multiaddrsOpt.get() + check: + multiaddrs.len == 2 + multiaddrs.contains(addr1NoPeerId) + multiaddrs.contains(addr2) + suite "Waku ENR - Relay static sharding": test "new relay shards object with single invalid shard id": ## Given diff --git a/tests/test_waku_netconfig.nim b/tests/test_waku_netconfig.nim index 5f9ff4b46..20d95d59b 100644 --- a/tests/test_waku_netconfig.nim +++ b/tests/test_waku_netconfig.nim @@ -152,6 +152,31 @@ suite "Waku NetConfig": netConfig.announcedAddresses.len == 1 # DNS address netConfig.announcedAddresses[0] == dns4TcpEndPoint(dns4DomainName, extPort) + asyncTest "AnnouncedAddresses and enrMultiaddrs deduplicate dns4DomainName and extMultiAddrs overlap": + let + conf = defaultTestWakuConf() + dns4DomainName = "example.com" + extPort = Port(1234) + dns4Address = dns4TcpEndPoint(dns4DomainName, extPort) + + let netConfigRes = NetConfig.init( + bindIp = conf.endpointConf.p2pListenAddress, + bindPort = conf.endpointConf.p2pTcpPort, + dns4DomainName = some(dns4DomainName), + extPort = some(extPort), + extMultiAddrs = @[dns4Address], + ) + + assert netConfigRes.isOk(), $netConfigRes.error + + let netConfig = netConfigRes.get() + + check: + netConfig.announcedAddresses.len == 1 + netConfig.announcedAddresses[0] == dns4Address + netConfig.enrMultiAddrs.len == 1 + netConfig.enrMultiAddrs[0] == dns4Address + asyncTest "AnnouncedAddresses includes WebSocket addresses when enabled": var confBuilder = defaultTestWakuConfBuilder() diff --git a/waku/factory/internal_config.nim b/waku/factory/internal_config.nim index 7aad6e615..fd85c26a5 100644 --- a/waku/factory/internal_config.nim +++ b/waku/factory/internal_config.nim @@ -10,8 +10,8 @@ import import ../common/utils/nat, ../node/net_config, ../waku_enr, ../waku_core, ./waku_conf -proc enrConfiguration*( - conf: WakuConf, netConfig: NetConfig +proc tryBuildEnrRecord( + conf: WakuConf, netConfig: NetConfig, multiaddrs: seq[MultiAddress] ): Result[enr.Record, string] = var enrBuilder = EnrBuilder.init(conf.nodeKey) @@ -22,7 +22,8 @@ proc enrConfiguration*( if netConfig.wakuFlags.isSome(): enrBuilder.withWakuCapabilities(netConfig.wakuFlags.get()) - enrBuilder.withMultiaddrs(netConfig.enrMultiaddrs) + if multiaddrs.len > 0: + enrBuilder.withMultiaddrs(multiaddrs) enrBuilder.withWakuRelaySharding( RelayShards(clusterId: conf.clusterId, shardIds: conf.subscribeShards) @@ -30,11 +31,35 @@ proc enrConfiguration*( return err("could not initialize ENR with shards") let record = enrBuilder.build().valueOr: - error "failed to create enr record", error = error return err($error) return ok(record) +proc enrConfiguration*( + conf: WakuConf, netConfig: NetConfig +): Result[enr.Record, string] = + for retained in countdown(netConfig.enrMultiaddrs.len, 0): + let multiaddrs = netConfig.enrMultiaddrs[0 ..< retained] + let record = tryBuildEnrRecord(conf, netConfig, multiaddrs).valueOr: + if retained > 0: + warn "failed to create enr record, retrying with fewer multiaddrs", + error = error, + totalMultiaddrs = netConfig.enrMultiaddrs.len, + retainedMultiaddrs = retained - 1, + removedMultiaddr = multiaddrs[^1] + continue + + error "failed to create enr record", error = error + return err($error) + + if retained < netConfig.enrMultiaddrs.len: + warn "created enr record after trimming multiaddrs", + totalMultiaddrs = netConfig.enrMultiaddrs.len, retainedMultiaddrs = retained + + return ok(record) + + return err("failed to create enr record") + proc dnsResolve*( domain: string, dnsAddrsNameServers: seq[IpAddress] ): Future[Result[string, string]] {.async.} = diff --git a/waku/node/net_config.nim b/waku/node/net_config.nim index 4802694c4..fc4b42fe6 100644 --- a/waku/node/net_config.nim +++ b/waku/node/net_config.nim @@ -156,12 +156,16 @@ proc init*( if extMultiAddrs.len > 0: announcedAddresses.add(extMultiAddrs) + announcedAddresses = announcedAddresses.deduplicate() + let # enrMultiaddrs are just addresses which cannot be represented in ENR, as described in # https://rfc.vac.dev/spec/31/#many-connection-types - enrMultiaddrs = announcedAddresses.filterIt( - it.hasProtocol("dns4") or it.hasProtocol("dns6") or it.hasProtocol("ws") or - it.hasProtocol("wss") + enrMultiaddrs = deduplicate( + announcedAddresses.filterIt( + it.hasProtocol("dns4") or it.hasProtocol("dns6") or it.hasProtocol("ws") or + it.hasProtocol("wss") + ) ) ok( diff --git a/waku/waku_enr/multiaddr.nim b/waku/waku_enr/multiaddr.nim index c343fff51..4d6e9baa7 100644 --- a/waku/waku_enr/multiaddr.nim +++ b/waku/waku_enr/multiaddr.nim @@ -74,7 +74,7 @@ func stripPeerId(multiaddr: MultiAddress): MultiAddress = return cleanAddr func withMultiaddrs*(builder: var EnrBuilder, multiaddrs: seq[MultiAddress]) = - let multiaddrs = multiaddrs.map(stripPeerId) + let multiaddrs = deduplicate(multiaddrs.map(stripPeerId)) let value = encodeMultiaddrs(multiaddrs) builder.addFieldPair(MultiaddrEnrField, value) From 587014e34fbfaf15fa151616bf636aeede58d30a Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 30 Apr 2026 00:27:38 +0200 Subject: [PATCH 138/155] add event_loop_accumulates_lag_secs (#3833) --- waku/node/health_monitor/event_loop_monitor.nim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/waku/node/health_monitor/event_loop_monitor.nim b/waku/node/health_monitor/event_loop_monitor.nim index 49a2bcc6e..bd1a33e4e 100644 --- a/waku/node/health_monitor/event_loop_monitor.nim +++ b/waku/node/health_monitor/event_loop_monitor.nim @@ -10,6 +10,9 @@ declarePublicGauge event_loop_load, "chronos event loop load EWMA by window (1.0 = sustained lag at MaxAcceptedLag)", labels = ["window"] +declarePublicCounter event_loop_accumulated_lag_secs, + "chronos event loop total accumulated lag in seconds since node start" + type OnLagChange* = proc(lagTooHigh: bool) {.gcsafe, raises: [].} proc eventLoopMonitorLoop*(onLagChange: OnLagChange = nil) {.async.} = @@ -55,6 +58,8 @@ proc eventLoopMonitorLoop*(onLagChange: OnLagChange = nil) {.async.} = let lagSecs = lag.nanoseconds.float64 / 1_000_000_000.0 let load = lagSecs / maxAcceptedLagSecs + event_loop_accumulated_lag_secs.inc(lagSecs) + ewma1m = alpha1m * load + (1.0 - alpha1m) * ewma1m ewma5m = alpha5m * load + (1.0 - alpha5m) * ewma5m ewma15m = alpha15m * load + (1.0 - alpha15m) * ewma15m From 75864a705ea0b913d517a5f3640747f8709e9e53 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:20:11 +0200 Subject: [PATCH 139/155] Fix websock nimble dependency version restriction to match lock file. (#3829) --- waku.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku.nimble b/waku.nimble index d99f05e84..591307c23 100644 --- a/waku.nimble +++ b/waku.nimble @@ -33,7 +33,7 @@ requires "nim >= 2.2.4", "dnsdisc", "dnsclient", "httputils >= 0.4.1", - "websock >= 0.2.1", + "websock >= 0.3.0", # Cryptography "nimcrypto == 0.6.4", # 0.6.4 used in libp2p. Version 0.7.3 makes test to crash on Ubuntu. "secp256k1", From 27ae07adaaea7beeae02cea9f8647b18cd9fb482 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Wed, 6 May 2026 19:58:19 +0200 Subject: [PATCH 140/155] receive_service: ensure fetch msgs query is performed when missing msg (#3849) --- .../recv_service/recv_service.nim | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/waku/node/delivery_service/recv_service/recv_service.nim b/waku/node/delivery_service/recv_service/recv_service.nim index 9f01ac267..64f4d683d 100644 --- a/waku/node/delivery_service/recv_service/recv_service.nim +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -113,15 +113,19 @@ proc checkStore*(self: RecvService) {.async.} = let missedHashes: seq[WakuMessageHash] = msgHashesInStore.filterIt(not rxMsgHashes.contains(it)) - ## Now retrieve the missing WakuMessages and deliver them - let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes) - if missingMsgsRet.isOk(): - for msgTuple in missingMsgsRet.get(): - if self.processIncomingMessageOfInterest(msgTuple.pubsubTopic, msgTuple.msg): - info "recv service store-recovered message", - msg_hash = shortLog(msgTuple.hash), pubsubTopic = msgTuple.pubsubTopic - else: - error "failed to retrieve missing messages: ", error = $missingMsgsRet.error + if missedHashes.len > 0: + info "missed messages detected, checking store for missed messages", + pubsubTopic = pubsubTopic, missedCount = missedHashes.len + + ## Now retrieve the missing WakuMessages and deliver them + let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes) + if missingMsgsRet.isOk(): + for msgTuple in missingMsgsRet.get(): + if self.processIncomingMessageOfInterest(msgTuple.pubsubTopic, msgTuple.msg): + info "recv service store-recovered message", + msg_hash = shortLog(msgTuple.hash), pubsubTopic = msgTuple.pubsubTopic + else: + error "failed to retrieve missing messages: ", error = $missingMsgsRet.error ## update next check times self.startTimeToCheck = self.endTimeToCheck From 35da224d5da630924694202f9c14e656402e1c62 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Thu, 7 May 2026 17:28:30 +0200 Subject: [PATCH 141/155] Evict peer instead of abrupt disconnect and avoid sending unnecessary store requests (#3857) * peer manager not disconnect abruptly ongoing service peers streams * fix: recv_service delivers store-recovered messages (#3805) * recv_service now delivers store-recovered messages via MessageReceivedEvent --- tests/api/test_all.nim | 1 + tests/api/test_api_receive.nim | 193 ++++++++++++++++++ tests/test_peer_manager.nim | 38 ++++ waku/events/delivery_events.nim | 16 -- .../delivery_service/delivery_service.nim | 2 +- .../recv_service/recv_service.nim | 138 ++++++------- waku/node/peer_manager/peer_manager.nim | 36 +++- waku/waku_store/client.nim | 2 + waku/waku_store/protocol.nim | 2 + 9 files changed, 335 insertions(+), 93 deletions(-) create mode 100644 tests/api/test_api_receive.nim diff --git a/tests/api/test_all.nim b/tests/api/test_all.nim index 4617c8cdb..56be19c27 100644 --- a/tests/api/test_all.nim +++ b/tests/api/test_all.nim @@ -5,4 +5,5 @@ import ./test_node_conf, ./test_api_send, ./test_api_subscription, + ./test_api_receive, ./test_api_health diff --git a/tests/api/test_api_receive.nim b/tests/api/test_api_receive.nim new file mode 100644 index 000000000..52f8713f9 --- /dev/null +++ b/tests/api/test_api_receive.nim @@ -0,0 +1,193 @@ +{.used.} + +import std/[options, sequtils, net, sets] +import chronos, testutils/unittests, stew/byteutils +import libp2p/[peerid, peerinfo, crypto/crypto] +import ../testlib/[common, wakucore, wakunode, testasync] +import ../waku_archive/archive_utils + +import + waku, + waku/[ + waku_node, + waku_core, + common/broker/broker_context, + events/message_events, + waku_relay/protocol, + waku_archive, + waku_archive/common as archive_common, + node/delivery_service/delivery_service, + node/delivery_service/recv_service, + ] +import waku/factory/waku_conf +import tools/confutils/cli_args + +const TestTimeout = chronos.seconds(60) + +type ReceiveEventListenerManager = ref object + brokerCtx: BrokerContext + receivedListener: MessageReceivedEventListener + receivedEvent: AsyncEvent + receivedMessages: seq[WakuMessage] + targetCount: int + +proc newReceiveEventListenerManager( + brokerCtx: BrokerContext, expectedCount: int = 1 +): ReceiveEventListenerManager = + let manager = ReceiveEventListenerManager( + brokerCtx: brokerCtx, receivedMessages: @[], targetCount: expectedCount + ) + manager.receivedEvent = newAsyncEvent() + + manager.receivedListener = MessageReceivedEvent + .listen( + brokerCtx, + proc(event: MessageReceivedEvent) {.async: (raises: []).} = + manager.receivedMessages.add(event.message) + if manager.receivedMessages.len >= manager.targetCount: + manager.receivedEvent.fire() + , + ) + .expect("Failed to listen to MessageReceivedEvent") + + return manager + +proc teardown(manager: ReceiveEventListenerManager) = + MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener) + +proc waitForEvents( + manager: ReceiveEventListenerManager, timeout: Duration +): Future[bool] {.async.} = + return await manager.receivedEvent.wait().withTimeout(timeout) + +proc createApiNodeConf(numShards: uint16 = 1): WakuNodeConf = + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = cli_args.WakuMode.Core + conf.listenAddress = parseIpAddress("0.0.0.0") + conf.tcpPort = Port(0) + conf.discv5UdpPort = Port(0) + conf.clusterId = 3'u16 + conf.numShardsInNetwork = numShards + conf.reliabilityEnabled = true + conf.rest = false + result = conf + +suite "Messaging API, Receive Service (store recovery)": + asyncTest "recv_service delivers store-recovered messages via MessageReceivedEvent": + ## Message gets archived before subscriber exists, checkStore() recovers it. + ## This is a regression test: it proves that messages recovered via store by + ## the RecvService (instead of receiving via a live relay sub) are actually + ## delivered via the MessageReceivedEvent API. + + let numShards: uint16 = 1 + let shards = @[PubsubTopic("/waku/2/rs/3/0")] + let shard = shards[0] + let testTopic = ContentTopic("/waku/2/recv-test/proto") + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + discard + + # store node has archive, store, relay + # it archives messages from relay and serves them to the + # subscriber's store client when it comes up (later) + var storeNode: WakuNode + lockNewGlobalBrokerContext: + storeNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + storeNode.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on storeNode" + ) + (await storeNode.mountRelay()).expect("Failed to mount relay on storeNode") + let archiveDriver = newSqliteArchiveDriver() + storeNode.mountArchive(archiveDriver).expect("Failed to mount archive") + await storeNode.mountStore() + await storeNode.mountLibp2pPing() + await storeNode.start() + + for s in shards: + storeNode.subscribe((kind: PubsubSub, topic: s), dummyHandler).expect( + "Failed to sub storeNode" + ) + + let storeNodePeerInfo = storeNode.peerInfo.toRemotePeerInfo() + + # publisher node (relay) + var publisher: WakuNode + lockNewGlobalBrokerContext: + publisher = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + publisher.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on publisher" + ) + (await publisher.mountRelay()).expect("Failed to mount relay on publisher") + await publisher.mountLibp2pPing() + await publisher.start() + + for s in shards: + publisher.subscribe((kind: PubsubSub, topic: s), dummyHandler).expect( + "Failed to sub publisher" + ) + + # connect publisher to store so messages get archived + await publisher.connectToNodes(@[storeNodePeerInfo]) + + # wait for relay mesh + for _ in 0 ..< 50: + if publisher.wakuRelay.getNumPeersInMesh(shard).valueOr(0) > 0: + break + await sleepAsync(100.milliseconds) + + # publish before subscriber exists, gets archived + let missedPayload = "This message was missed".toBytes() + let missedMsg = WakuMessage( + payload: missedPayload, contentTopic: testTopic, version: 0, timestamp: now() + ) + discard (await publisher.publish(some(shard), missedMsg)).expect( + "Publish missed msg failed" + ) + + # wait for archive + block waitArchive: + for _ in 0 ..< 50: + let query = archive_common.ArchiveQuery( + includeData: false, contentTopics: @[testTopic], pubsubTopic: some(shard) + ) + let res = await storeNode.wakuArchive.findMessages(query) + if res.isOk() and res.get().hashes.len > 0: + break waitArchive + await sleepAsync(100.milliseconds) + raiseAssert "Message was not archived in time" + + # create subscriber + var subscriber: Waku + lockNewGlobalBrokerContext: + subscriber = (await createNode(createApiNodeConf(numShards))).expect( + "Failed to create subscriber" + ) + (await startWaku(addr subscriber)).expect("Failed to start subscriber") + + # connect subscriber to store (not publisher, so msg won't come via relay to it) + await subscriber.node.connectToNodes(@[storeNodePeerInfo]) + + # subscribe to content topic + (await subscriber.subscribe(testTopic)).expect("Failed to subscribe") + + # listen before triggering store check + let eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) + defer: + eventManager.teardown() + + # trigger store check, should recover and deliver via MessageReceivedEvent + await subscriber.deliveryService.recvService.checkStore() + + let received = await eventManager.waitForEvents(TestTimeout) + check received + check eventManager.receivedMessages.len == 1 + if eventManager.receivedMessages.len > 0: + check eventManager.receivedMessages[0].payload == missedPayload + + # cleanup + (await subscriber.stop()).expect("Failed to stop subscriber") + await publisher.stop() + await storeNode.stop() diff --git a/tests/test_peer_manager.nim b/tests/test_peer_manager.nim index f78c3831f..608889d32 100644 --- a/tests/test_peer_manager.nim +++ b/tests/test_peer_manager.nim @@ -54,6 +54,44 @@ procSuite "Peer Manager": nodes[0].peerManager.switch.peerStore.connectedness(nodes[1].peerInfo.peerId) == Connectedness.Connected + asyncTest "Peer manager tracks active store request state": + let nodes = toSeq(0 ..< 2).mapIt( + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + ) + + await allFutures(nodes.mapIt(it.start())) + await allFutures(nodes.mapIt(it.mountRelay())) + + let peerId = nodes[1].peerInfo.peerId + require ( + await nodes[0].peerManager.connectPeer(nodes[1].peerInfo.toRemotePeerInfo()) + ) + await sleepAsync(chronos.milliseconds(500)) + + nodes[0].peerManager.addActiveStoreRequest(peerId) + check: + nodes[0].peerManager.hasActiveStoreRequest(peerId) + + await nodes[0].peerManager.evictPeer(peerId) + await sleepAsync(chronos.milliseconds(100)) + + check: + nodes[0].peerManager.switch.peerStore.connectedness(peerId) == + Connectedness.Connected + + nodes[0].peerManager.removeActiveStoreRequest(peerId) + check: + not nodes[0].peerManager.hasActiveStoreRequest(peerId) + + await nodes[0].peerManager.evictPeer(peerId) + await sleepAsync(chronos.milliseconds(100)) + + check: + nodes[0].peerManager.switch.peerStore.connectedness(peerId) != + Connectedness.Connected + + await allFutures(nodes.mapIt(it.stop())) + asyncTest "dialPeer() works": # Create 2 nodes let nodes = toSeq(0 ..< 2).mapIt( diff --git a/waku/events/delivery_events.nim b/waku/events/delivery_events.nim index f8eb0f48d..f27f02721 100644 --- a/waku/events/delivery_events.nim +++ b/waku/events/delivery_events.nim @@ -1,21 +1,5 @@ import waku/waku_core/[message/message, message/digest], waku/common/broker/event_broker -type DeliveryDirection* {.pure.} = enum - PUBLISHING - RECEIVING - -type DeliverySuccess* {.pure.} = enum - SUCCESSFUL - UNSUCCESSFUL - -EventBroker: - type DeliveryFeedbackEvent* = ref object - success*: DeliverySuccess - dir*: DeliveryDirection - comment*: string - msgHash*: WakuMessageHash - msg*: WakuMessage - EventBroker: type OnFilterSubscribeEvent* = object pubsubTopic*: string diff --git a/waku/node/delivery_service/delivery_service.nim b/waku/node/delivery_service/delivery_service.nim index fd728d048..f3d78d98e 100644 --- a/waku/node/delivery_service/delivery_service.nim +++ b/waku/node/delivery_service/delivery_service.nim @@ -12,7 +12,7 @@ import type DeliveryService* = ref object sendService*: SendService - recvService: RecvService + recvService*: RecvService subscriptionManager*: SubscriptionManager proc new*( diff --git a/waku/node/delivery_service/recv_service/recv_service.nim b/waku/node/delivery_service/recv_service/recv_service.nim index 9a85df2f9..64f4d683d 100644 --- a/waku/node/delivery_service/recv_service/recv_service.nim +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -12,7 +12,6 @@ import waku_store/common, waku_filter_v2/client, waku_core/topics, - events/delivery_events, events/message_events, waku_node, common/broker/broker_context, @@ -27,7 +26,8 @@ const PruneOldMsgsPeriod = chronos.minutes(1) const DelayExtra* = chronos.seconds(5) ## Additional security time to overlap the missing messages queries -type TupleHashAndMsg = tuple[hash: WakuMessageHash, msg: WakuMessage] +type TupleHashAndMsg = + tuple[hash: WakuMessageHash, msg: WakuMessage, pubsubTopic: PubsubTopic] type RecvMessage = object msgHash: WakuMessageHash @@ -59,88 +59,82 @@ proc getMissingMsgsFromStore( return err("getMissingMsgsFromStore: " & $error) let otherwiseMsg = WakuMessage() - ## message to be returned if the Option message is none + let otherwiseTopic = PubsubTopic("") return ok( - storeResp.messages.mapIt((hash: it.messageHash, msg: it.message.get(otherwiseMsg))) + storeResp.messages.mapIt( + ( + hash: it.messageHash, + msg: it.message.get(otherwiseMsg), + pubsubTopic: it.pubsubTopic.get(otherwiseTopic), + ) + ) ) -proc performDeliveryFeedback( - self: RecvService, - success: DeliverySuccess, - dir: DeliveryDirection, - comment: string, - msgHash: WakuMessageHash, - msg: WakuMessage, -) {.gcsafe, raises: [].} = - info "recv monitor performDeliveryFeedback", - success, dir, comment, msg_hash = shortLog(msgHash) - - DeliveryFeedbackEvent.emit( - brokerCtx = self.brokerCtx, - success = success, - dir = dir, - comment = comment, - msgHash = msgHash, - msg = msg, - ) - -proc msgChecker(self: RecvService) {.async.} = - ## Continuously checks if a message has been received - while true: - await sleepAsync(StoreCheckPeriod) - self.endTimeToCheck = getNowInNanosecondTime() - - var msgHashesInStore = newSeq[WakuMessageHash](0) - for pubsubTopic, contentTopics in self.subscriptionManager.subscribedTopics: - let storeResp: StoreQueryResponse = ( - await self.node.wakuStoreClient.queryToAny( - StoreQueryRequest( - includeData: false, - pubsubTopic: some(pubsubTopic), - contentTopics: toSeq(contentTopics), - startTime: some(self.startTimeToCheck - DelayExtra.nanos), - endTime: some(self.endTimeToCheck + DelayExtra.nanos), - ) - ) - ).valueOr: - error "msgChecker failed to get remote msgHashes", - pubsubTopic = pubsubTopic, cTopics = toSeq(contentTopics), error = $error - continue - - msgHashesInStore.add(storeResp.messages.mapIt(it.messageHash)) - - ## compare the msgHashes seen from the store vs the ones received directly - let rxMsgHashes = self.recentReceivedMsgs.mapIt(it.msgHash) - let missedHashes: seq[WakuMessageHash] = - msgHashesInStore.filterIt(not rxMsgHashes.contains(it)) - - ## Now retrieve the missed WakuMessages - let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes) - if missingMsgsRet.isOk(): - ## Give feedback so that the api client can perfom any action with the missed messages - for msgTuple in missingMsgsRet.get(): - self.performDeliveryFeedback( - DeliverySuccess.UNSUCCESSFUL, RECEIVING, "Missed message", msgTuple.hash, - msgTuple.msg, - ) - else: - error "failed to retrieve missing messages: ", error = $missingMsgsRet.error - - ## update next check times - self.startTimeToCheck = self.endTimeToCheck - proc processIncomingMessageOfInterest( self: RecvService, pubsubTopic: string, message: WakuMessage -) = - ## Resolve an incoming network message that was already filtered by topic. +): bool = ## Deduplicate (by hash), store (saves in recently-seen messages) and emit ## the MAPI MessageReceivedEvent for every unique incoming message. + ## Returns true if the message was new and the MessageReceivedEvent was properly emitted. let msgHash = computeMessageHash(pubsubTopic, message) if not self.recentReceivedMsgs.anyIt(it.msgHash == msgHash): let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp) self.recentReceivedMsgs.add(rxMsg) MessageReceivedEvent.emit(self.brokerCtx, msgHash.to0xHex(), message) + return true + return false + +proc checkStore*(self: RecvService) {.async.} = + ## Checks the store for messages that were not received directly and + ## delivers them via MessageReceivedEvent. + self.endTimeToCheck = getNowInNanosecondTime() + + ## query store and deliver new recovered messages per subscribed topic + for pubsubTopic, contentTopics in self.subscriptionManager.subscribedTopics: + let storeResp: StoreQueryResponse = ( + await self.node.wakuStoreClient.queryToAny( + StoreQueryRequest( + includeData: false, + pubsubTopic: some(pubsubTopic), + contentTopics: toSeq(contentTopics), + startTime: some(self.startTimeToCheck - DelayExtra.nanos), + endTime: some(self.endTimeToCheck + DelayExtra.nanos), + ) + ) + ).valueOr: + error "msgChecker failed to get remote msgHashes", + pubsubTopic = pubsubTopic, cTopics = toSeq(contentTopics), error = $error + continue + + ## compare the msgHashes seen from the store vs the ones received directly + let msgHashesInStore = storeResp.messages.mapIt(it.messageHash) + let rxMsgHashes = self.recentReceivedMsgs.mapIt(it.msgHash) + let missedHashes: seq[WakuMessageHash] = + msgHashesInStore.filterIt(not rxMsgHashes.contains(it)) + + if missedHashes.len > 0: + info "missed messages detected, checking store for missed messages", + pubsubTopic = pubsubTopic, missedCount = missedHashes.len + + ## Now retrieve the missing WakuMessages and deliver them + let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes) + if missingMsgsRet.isOk(): + for msgTuple in missingMsgsRet.get(): + if self.processIncomingMessageOfInterest(msgTuple.pubsubTopic, msgTuple.msg): + info "recv service store-recovered message", + msg_hash = shortLog(msgTuple.hash), pubsubTopic = msgTuple.pubsubTopic + else: + error "failed to retrieve missing messages: ", error = $missingMsgsRet.error + + ## update next check times + self.startTimeToCheck = self.endTimeToCheck + +proc msgChecker(self: RecvService) {.async.} = + ## Continuously checks if a message has been received + while true: + await sleepAsync(StoreCheckPeriod) + await self.checkStore() proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T = ## The storeClient will help to acquire any possible missed messages @@ -176,7 +170,7 @@ proc startRecvService*(self: RecvService) = shard = event.topic, contenttopic = event.message.contentTopic return - self.processIncomingMessageOfInterest(event.topic, event.message), + discard self.processIncomingMessageOfInterest(event.topic, event.message), ).valueOr: error "Failed to set MessageSeenEvent listener", error = error quit(QuitFailure) diff --git a/waku/node/peer_manager/peer_manager.nim b/waku/node/peer_manager/peer_manager.nim index e3eb8d75b..1d8b55bb5 100644 --- a/waku/node/peer_manager/peer_manager.nim +++ b/waku/node/peer_manager/peer_manager.nim @@ -107,6 +107,7 @@ type PeerManager* = ref object of RootObj online: bool ## state managed by online_monitor module getShards: GetShards maxConnections: int + activeStoreRequests*: Table[PeerId, int] #~~~~~~~~~~~~~~~~~~~# # Helper Functions # @@ -169,6 +170,23 @@ proc addPeer*( proc getPeer*(pm: PeerManager, peerId: PeerId): RemotePeerInfo = return pm.switch.peerStore.getPeer(peerId) +proc addActiveStoreRequest*(pm: PeerManager, peerId: PeerId) {.gcsafe.} = + pm.activeStoreRequests.mgetOrPut(peerId, 0).inc() + +proc removeActiveStoreRequest*(pm: PeerManager, peerId: PeerId) {.gcsafe.} = + let count = pm.activeStoreRequests.getOrDefault(peerId, 0) + if count == 0: + return + + let newCount = count - 1 + if newCount <= 0: + pm.activeStoreRequests.del(peerId) + else: + pm.activeStoreRequests[peerId] = newCount + +proc hasActiveStoreRequest*(pm: PeerManager, peerId: PeerId): bool {.gcsafe.} = + pm.activeStoreRequests.contains(peerId) + proc loadFromStorage(pm: PeerManager) {.gcsafe.} = ## Load peers from storage, if available @@ -519,6 +537,15 @@ proc connectedPeers*( return (inPeers, outPeers) +proc evictPeer*(pm: PeerManager, peerId: PeerId) {.async.} = + ## Policy-based eviction (relay-peer limit, IP colocation, pruning). + ## Skips the disconnect when the peer has an in-flight store request to + ## avoid aborting active store requests. + if pm.hasActiveStoreRequest(peerId): + trace "skipping peer eviction: active store request", peerId = peerId + return + await pm.switch.disconnect(peerId) + proc capablePeers*(pm: PeerManager, protocol: string): (seq[PeerId], seq[PeerId]) = ## Returns the PeerIds of peers with an active socket connection. ## If a protocol is specified, it returns peers that have identified @@ -770,11 +797,11 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = let inRelayPeers = pm.connectedPeers(WakuRelayCodec)[0] if inRelayPeers.len > pm.inRelayPeersTarget and peerStore.hasPeer(peerId, WakuRelayCodec): - info "disconnecting relay peer because reached max num in-relay peers", + info "relay peer limit reached, evicting peer", peerId = peerId, inRelayPeers = inRelayPeers.len, inRelayPeersTarget = pm.inRelayPeersTarget - await pm.switch.disconnect(peerId) + await pm.evictPeer(peerId) ## Apply max ip colocation limit if (let ip = pm.getPeerIp(peerId); ip.isSome()): @@ -787,7 +814,7 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = if pm.colocationLimit != 0 and peersBehindIp.len > pm.colocationLimit: for peerId in peersBehindIp[0 ..< (peersBehindIp.len - pm.colocationLimit)]: info "Pruning connection due to ip colocation", peerId = peerId, ip = ip - asyncSpawn(pm.switch.disconnect(peerId)) + asyncSpawn(pm.evictPeer(peerId)) peerStore.delete(peerId) WakuPeerEvent.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventConnected) @@ -1100,7 +1127,7 @@ proc pruneInRelayConns(pm: PeerManager, amount: int) {.async.} = for p in inRelayPeers[0 ..< connsToPrune]: trace "Pruning Peer", Peer = $p - asyncSpawn(pm.switch.disconnect(p)) + asyncSpawn(pm.evictPeer(p)) proc addExtPeerEventHandler*( pm: PeerManager, eventHandler: PeerEventHandler, eventKind: PeerEventKind @@ -1214,6 +1241,7 @@ proc new*( pm.serviceSlots = initTable[string, RemotePeerInfo]() pm.ipTable = initTable[string, seq[PeerId]]() + pm.activeStoreRequests = initTable[PeerId, int]() if not storage.isNil(): trace "found persistent peer storage" diff --git a/waku/waku_store/client.nim b/waku/waku_store/client.nim index 5b261af47..9b26d44a8 100644 --- a/waku/waku_store/client.nim +++ b/waku/waku_store/client.nim @@ -33,7 +33,9 @@ proc sendStoreRequest( ): Future[StoreQueryResult] {.async, gcsafe.} = var req = request + self.peerManager.addActiveStoreRequest(connection.peerId) defer: + self.peerManager.removeActiveStoreRequest(connection.peerId) await connection.closeWithEof() if req.requestId == "": diff --git a/waku/waku_store/protocol.nim b/waku/waku_store/protocol.nim index 891c6a93c..17b7fb214 100644 --- a/waku/waku_store/protocol.nim +++ b/waku/waku_store/protocol.nim @@ -93,7 +93,9 @@ proc initProtocolHandler(self: WakuStore) = var resBuf: StoreResp var queryDuration: float + self.peerManager.addActiveStoreRequest(conn.peerId) defer: + self.peerManager.removeActiveStoreRequest(conn.peerId) await conn.closeWithEof() self.requestRateLimiter.checkUsageLimit(WakuStoreCodec, conn): From fb30109a220ed3bd453085ab4672eb79c365246d Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Thu, 7 May 2026 18:19:34 +0200 Subject: [PATCH 142/155] update changelog for v0.38.1 Co-authored-by: Copilot --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37220a235..2ba15fcd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## v0.38.1 (2026-05-07) + +### Changes + +- Evict peer instead of abrupt disconnect and avoid sending unnecessary store requests ([#3857](https://github.com/logos-messaging/logos-delivery/pull/3857)) ([75dbeb1b](https://github.com/logos-messaging/logos-delivery/commit/75dbeb1be785df5e61c9ab0bcf8393349b9d0f5e) and [7e59b2c2](https://github.com/logos-messaging/logos-delivery/commit/7e59b2c2)) + +- RecvService now delivers store-recovered messages via MessageReceivedEvent and includes check for missed hashes before processing ([#3805](https://github.com/logos-messaging/logos-delivery/pull/3805)) ([494ea946](https://github.com/logos-messaging/logos-delivery/commit/494ea946)) + ## v0.38.0 (2026-03-16) ### Notes From a62ab1e7b13ee5dcd52708952b910b61345396fc Mon Sep 17 00:00:00 2001 From: Darshan <35736874+darshankabariya@users.noreply.github.com> Date: Mon, 11 May 2026 19:02:25 +0530 Subject: [PATCH 143/155] chore: add nim-sds (no runtime integration yet) (#3820) --- nimble.lock | 20 ++++++++++++++++++++ waku.nimble | 2 ++ 2 files changed, 22 insertions(+) diff --git a/nimble.lock b/nimble.lock index 7c76f7fa9..0a76565c4 100644 --- a/nimble.lock +++ b/nimble.lock @@ -587,6 +587,26 @@ "sha1": "09e1b2fdad55b973724d61227971afc0df0b7a81" } }, + "sds": { + "version": "#2e9a7683f0e180bf112135fae3a3803eed8490d4", + "vcsRevision": "2e9a7683f0e180bf112135fae3a3803eed8490d4", + "url": "https://github.com/logos-messaging/nim-sds.git", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronos", + "libp2p", + "chronicles", + "stew", + "stint", + "metrics", + "results", + "taskpools" + ], + "checksums": { + "sha1": "d13f1bf8d1b90b27e9edfc063b043831242cda19" + } + }, "ffi": { "version": "0.1.3", "vcsRevision": "06111de155253b34e47ed2aaed1d61d08d62cc1b", diff --git a/waku.nimble b/waku.nimble index 591307c23..f944aaae1 100644 --- a/waku.nimble +++ b/waku.nimble @@ -61,6 +61,8 @@ requires "nim >= 2.2.4", # Packages not on nimble (use git URLs) requires "https://github.com/logos-messaging/nim-ffi" +requires "https://github.com/logos-messaging/nim-sds.git#2e9a7683f0e180bf112135fae3a3803eed8490d4" + requires "https://github.com/vacp2p/nim-lsquic" requires "https://github.com/vacp2p/nim-jwt.git#057ec95eb5af0eea9c49bfe9025b3312c95dc5f2" From 71a369ffad2ed15a3d18c9662c0da0db53e7f5d4 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 11 May 2026 15:22:22 -0300 Subject: [PATCH 144/155] feat: allow a port value of zero for service ports (auto-assign port) (#3828) * any port set to 0 on conf results in a random port bound * Debug API MyBoundPorts reports actually bound ports for all services, reports 0 if disabled * write back bound values to both WakuConf and WakuNode.ports * setupDiscoveryV5 returns Result and errors out on port 0 * rename setupAndStartDiscv5WithAutoPort to setupAndStartDiscv5 * updateWaku ENR rebuild now runs after discv5 startup * Add DefaultP2pTcpPort, DefaultDiscv5UdpPort, DefaultWebSocketPort, DefaultRestPort, DefaultMetricsHttpPort * add tests --- tests/api/test_node_conf.nim | 2 + tests/factory/test_node_factory.nim | 109 ++++++++++++++++-- tests/factory/test_waku_conf.nim | 2 +- tests/test_waku_netconfig.nim | 2 +- tests/testlib/wakunode.nim | 1 - tests/waku_discv5/test_waku_discv5.nim | 6 +- tests/wakunode2/test_app.nim | 52 ++++++++- tests/wakunode_rest/test_rest_debug.nim | 1 + waku/discovery/waku_discv5.nim | 53 ++++++++- .../conf_builder/discv5_conf_builder.nim | 8 +- .../metrics_server_conf_builder.nim | 4 +- .../conf_builder/rest_server_conf_builder.nim | 6 +- .../conf_builder/waku_conf_builder.nim | 23 ++-- .../conf_builder/web_socket_conf_builder.nim | 10 +- waku/factory/internal_config.nim | 2 +- waku/factory/waku.nim | 75 +++++++----- waku/factory/waku_state_info.nim | 5 +- waku/net/auto_port.nim | 48 ++++++++ waku/net/bound_ports.nim | 20 ++++ waku/{node => net}/net_config.nim | 0 waku/node/waku_metrics.nim | 40 ++++--- waku/node/waku_node.nim | 4 +- waku/waku_node.nim | 2 +- 23 files changed, 376 insertions(+), 99 deletions(-) create mode 100644 waku/net/auto_port.nim create mode 100644 waku/net/bound_ports.nim rename waku/{node => net}/net_config.nim (100%) diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index b19739393..e171c5207 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -376,6 +376,7 @@ suite "WakuConfBuilder - store retention policies": test "Multiple retention policies": ## Given var b = WakuConfBuilder.init() + b.withP2pTcpPort(0'u16) b.storeServiceConf.withEnabled(true) b.storeServiceConf.withDbUrl("sqlite://test.db") b.storeServiceConf.withRetentionPolicies( @@ -420,6 +421,7 @@ suite "WakuConfBuilder - store retention policies": test "Store disabled - no retention policy applied": ## Given var b = WakuConfBuilder.init() + b.withP2pTcpPort(0'u16) # storeServiceConf not enabled ## When diff --git a/tests/factory/test_node_factory.nim b/tests/factory/test_node_factory.nim index 4b2085e82..1fe242532 100644 --- a/tests/factory/test_node_factory.nim +++ b/tests/factory/test_node_factory.nim @@ -1,21 +1,23 @@ {.used.} import - std/[options, sequtils], + std/[net, options, sequtils, strutils], testutils/unittests, chronos, - libp2p/multiaddress, - libp2p/protocols/connectivity/relay/relay -import eth/p2p/discoveryv5/enr + chronos/transports/[stream, datagram, common], + metrics/chronos_httpserver, + libp2p/[crypto/crypto, multiaddress, protocols/connectivity/relay/relay], + eth/p2p/discoveryv5/enr import - ../testlib/wakunode, - waku/waku_node, - waku/waku_enr, - waku/factory/node_factory, - waku/factory/internal_config, - waku/factory/conf_builder/conf_builder, - waku/factory/conf_builder/web_socket_conf_builder + tests/testlib/[wakunode, wakucore], + waku/[waku_node, waku_enr, net/auto_port, discovery/waku_discv5, node/waku_metrics], + waku/factory/[ + node_factory, + internal_config, + conf_builder/conf_builder, + conf_builder/web_socket_conf_builder, + ] suite "Node Factory": asynctest "Set up a node based on default configurations": @@ -115,5 +117,90 @@ asynctest "Start a node based on default test configuration": check: node.started == true + # Default conf has p2pTcpPort=0, so the OS must have assigned a real port. + var hasNonZeroTcp = false + for a in node.switch.peerInfo.listenAddrs: + let s = $a + if ("/tcp/" in s) and not ("/tcp/0" in s): + hasNonZeroTcp = true + check hasNonZeroTcp + ## Cleanup await node.stop() + +suite "Auto-port retry": + asynctest "metrics binds on free TCP port, fails on taken": + let takenPort = Port(55100) + let freePort = Port(55101) + let taken = createStreamServer(initTAddress("127.0.0.1", takenPort)) + defer: + taken.stop() + await taken.closeWait() + + proc buildMetricsConf(port: Port): MetricsServerConf = + var b = MetricsServerConfBuilder.init() + b.withEnabled(true) + b.withHttpPort(port) + b.build().value.get() + + let failRes = await startMetricsServerAndLogging(buildMetricsConf(takenPort), 0'u16) + check failRes.isErr() + + let okRes = await startMetricsServerAndLogging(buildMetricsConf(freePort), 0'u16) + check okRes.isOk() + if okRes.isOk(): + await okRes.get().server.close() + + asynctest "discv5 binds on free UDP port, fails on taken": + let takenPort = Port(55200) + let freePort = Port(55201) + + proc dummyCb( + transp: DatagramTransport, raddr: TransportAddress + ): Future[void] {.async: (raises: []).} = + discard + + let takenUdp = + newDatagramTransport(dummyCb, local = initTAddress("0.0.0.0", takenPort)) + defer: + await takenUdp.closeWait() + + let nodeKey = generateSecp256k1Key() + let node = newTestWakuNode(nodeKey, parseIpAddress("0.0.0.0"), Port(0)) + await node.start() + defer: + await node.stop() + + proc buildDiscv5Conf(port: Port): Discv5Conf = + var b = Discv5ConfBuilder.init() + b.withEnabled(true) + b.withUdpPort(port) + b.build().value.get() + + let failRes = await setupAndStartDiscv5( + node.enr, + node.peerManager, + node.topicSubscriptionQueue, + buildDiscv5Conf(takenPort), + @[], + node.rng, + nodeKey, + parseIpAddress("0.0.0.0"), + 0'u16, + ) + check failRes.isErr() + + let okRes = await setupAndStartDiscv5( + node.enr, + node.peerManager, + node.topicSubscriptionQueue, + buildDiscv5Conf(freePort), + @[], + node.rng, + nodeKey, + parseIpAddress("0.0.0.0"), + 0'u16, + ) + check okRes.isOk() + if okRes.isOk(): + await okRes.get().stop() diff --git a/tests/factory/test_waku_conf.nim b/tests/factory/test_waku_conf.nim index eeacf791b..885e22867 100644 --- a/tests/factory/test_waku_conf.nim +++ b/tests/factory/test_waku_conf.nim @@ -4,7 +4,7 @@ import libp2p/crypto/[crypto, secp], libp2p/multiaddress, nimcrypto/utils, - std/[options, random, sequtils], + std/[net, options, random, sequtils], results, testutils/unittests import diff --git a/tests/test_waku_netconfig.nim b/tests/test_waku_netconfig.nim index 20d95d59b..0aff64121 100644 --- a/tests/test_waku_netconfig.nim +++ b/tests/test_waku_netconfig.nim @@ -5,7 +5,7 @@ import chronos, confutils/toml/std/net, libp2p/multiaddress, testutils/unittests import ./testlib/wakunode, waku/waku_enr/capabilities include - waku/node/net_config, + waku/net/net_config, waku/factory/conf_builder/web_socket_conf_builder, waku/factory/conf_builder/conf_builder diff --git a/tests/testlib/wakunode.nim b/tests/testlib/wakunode.nim index e904604ab..77c017d96 100644 --- a/tests/testlib/wakunode.nim +++ b/tests/testlib/wakunode.nim @@ -27,7 +27,6 @@ import # TODO: migrate to usage of a test cluster conf proc defaultTestWakuConfBuilder*(): WakuConfBuilder = var builder = WakuConfBuilder.init() - builder.withP2pTcpPort(Port(0)) builder.withP2pListenAddress(parseIpAddress("0.0.0.0")) builder.restServerConf.withListenAddress(parseIpAddress("127.0.0.1")) builder.withDnsAddrsNameServers( diff --git a/tests/waku_discv5/test_waku_discv5.nim b/tests/waku_discv5/test_waku_discv5.nim index 20a0c6965..936c01826 100644 --- a/tests/waku_discv5/test_waku_discv5.nim +++ b/tests/waku_discv5/test_waku_discv5.nim @@ -506,7 +506,8 @@ suite "Waku Discovery v5": waku.conf.nodeKey, waku.conf.endpointConf.p2pListenAddress, waku.conf.portsShift, - ) + ).valueOr: + raiseAssert "failed setup discv5 in test: " & $error check: waku.node.peerManager.switch.peerStore.peers().anyIt( @@ -537,7 +538,8 @@ suite "Waku Discovery v5": waku.conf.nodeKey, waku.conf.endpointConf.p2pListenAddress, waku.conf.portsShift, - ) + ).valueOr: + raiseAssert "failed setup discv5 in test: " & $error check: not waku.node.peerManager.switch.peerStore.peers().anyIt( diff --git a/tests/wakunode2/test_app.nim b/tests/wakunode2/test_app.nim index 6ec6043fe..7621ab1e7 100644 --- a/tests/wakunode2/test_app.nim +++ b/tests/wakunode2/test_app.nim @@ -1,14 +1,13 @@ {.used.} import + std/json, testutils/unittests, chronicles, chronos, - libp2p/crypto/crypto, - libp2p/crypto/secp, - libp2p/multiaddress, - libp2p/switch -import ../testlib/wakucore, ../testlib/wakunode + libp2p/[crypto/crypto, crypto/secp, multiaddress, switch], + tests/testlib/[wakucore, wakunode], + waku/factory/conf_builder/conf_builder include waku/factory/waku, waku/common/enr/typed_record @@ -99,3 +98,46 @@ suite "Wakunode2 - Waku initialization": ## Cleanup (waitFor waku.stop()).isOkOr: raiseAssert error + + test "explicit port=0 triggers auto-bind across all services": + var builder = defaultTestWakuConfBuilder() + builder.withP2pTcpPort(Port(0)) + builder.discv5Conf.withEnabled(true) + builder.discv5Conf.withUdpPort(Port(0)) + builder.restServerConf.withEnabled(true) + builder.restServerConf.withRelayCacheCapacity(50'u32) + builder.restServerConf.withPort(Port(0)) + builder.metricsServerConf.withEnabled(true) + builder.metricsServerConf.withHttpPort(Port(0)) + builder.webSocketConf.withEnabled(true) + builder.webSocketConf.withWebSocketPort(Port(0)) + + let conf = builder.build().valueOr: + raiseAssert error + + check: + conf.endpointConf.p2pTcpPort == Port(0) + conf.discv5Conf.get().udpPort == Port(0) + conf.restServerConf.get().port == Port(0) + conf.metricsServerConf.get().httpPort == Port(0) + conf.webSocketConf.get().port == Port(0) + + var waku = (waitFor Waku.new(conf)).valueOr: + raiseAssert error + defer: + (waitFor waku.stop()).isOkOr: + raiseAssert error + + (waitFor startWaku(addr waku)).isOkOr: + raiseAssert error + + let portsJson = waku.stateInfo.getNodeInfoItem(NodeInfoId.MyBoundPorts) + let parsed = parseJson(portsJson) + + check: + parsed.kind == JObject + parsed["tcp"].getInt() != 0 + parsed["webSocket"].getInt() != 0 + parsed["rest"].getInt() != 0 + parsed["discv5Udp"].getInt() != 0 + parsed["metrics"].getInt() != 0 diff --git a/tests/wakunode_rest/test_rest_debug.nim b/tests/wakunode_rest/test_rest_debug.nim index 4bd2e8c02..1171f5878 100644 --- a/tests/wakunode_rest/test_rest_debug.nim +++ b/tests/wakunode_rest/test_rest_debug.nim @@ -1,6 +1,7 @@ {.used.} import + std/options, testutils/unittests, presto, presto/client as presto_client, diff --git a/waku/discovery/waku_discv5.nim b/waku/discovery/waku_discv5.nim index 0eb329fa4..c1b253c8c 100644 --- a/waku/discovery/waku_discv5.nim +++ b/waku/discovery/waku_discv5.nim @@ -10,7 +10,7 @@ import eth/keys as eth_keys, eth/p2p/discoveryv5/node, eth/p2p/discoveryv5/protocol -import ../node/peer_manager/peer_manager, ../waku_core, ../waku_enr +import waku/[net/auto_port, node/peer_manager/peer_manager, waku_core, waku_enr] export protocol, waku_enr @@ -409,7 +409,15 @@ proc setupDiscoveryV5*( key: crypto.PrivateKey, p2pListenAddress: IpAddress, portsShift: uint16, -): WakuDiscoveryV5 = +): Result[WakuDiscoveryV5, string] = + ## Public only for testing. Callers should use `setupAndStartDiscv5`, which + ## additionally handles `udpPort == 0` via auto-port retry. + if conf.udpPort == Port(0): + return err( + "setupDiscoveryV5: udpPort must be non-zero; " & + "use setupAndStartDiscv5 for port=0 auto-port retry" + ) + let dynamicBootstrapEnrs = dynamicBootstrapNodes.filterIt(it.hasUdpPort()).mapIt(it.enr.get()) @@ -441,10 +449,47 @@ proc setupDiscoveryV5*( autoupdateRecord: conf.enrAutoUpdate, ) - WakuDiscoveryV5.new( - rng, discv5Conf, some(myENR), some(nodePeerManager), nodeTopicSubscriptionQueue + return ok( + WakuDiscoveryV5.new( + rng, discv5Conf, some(myENR), some(nodePeerManager), nodeTopicSubscriptionQueue + ) ) +proc setupAndStartDiscv5*( + myENR: enr.Record, + nodePeerManager: PeerManager, + nodeTopicSubscriptionQueue: AsyncEventQueue[SubscriptionEvent], + conf: Discv5Conf, + dynamicBootstrapNodes: seq[RemotePeerInfo], + rng: ref HmacDrbgContext, + key: crypto.PrivateKey, + p2pListenAddress: IpAddress, + portsShift: uint16, +): Future[Result[WakuDiscoveryV5, string]] {.async: (raises: []).} = + ## Construct and start a `WakuDiscoveryV5` instance, handling auto-port + ## retry when the caller asks for `udpPort == 0`. + proc attempt( + port: Port + ): Future[Result[WakuDiscoveryV5, string]] {.async: (raises: []).} = + var c = conf + c.udpPort = port + let wd = setupDiscoveryV5( + myENR, nodePeerManager, nodeTopicSubscriptionQueue, c, dynamicBootstrapNodes, rng, + key, p2pListenAddress, portsShift, + ).valueOr: + return err(error) + let startRes = await wd.start() + if startRes.isErr(): + return err("failed to start discovery, attempt: " & startRes.error) + return ok(wd) + + let wd = (await tryWithAutoPort[WakuDiscoveryV5](conf.udpPort, attempt)).valueOr: + return err("setupAndStartDiscv5: " & error) + return ok(wd) + +proc udpPort*(wd: WakuDiscoveryV5): Port = + wd.conf.port + proc updateBootstrapRecords*( self: var WakuDiscoveryV5, newRecordsString: string ): Result[void, string] = diff --git a/waku/factory/conf_builder/discv5_conf_builder.nim b/waku/factory/conf_builder/discv5_conf_builder.nim index e2729021e..5dd269d23 100644 --- a/waku/factory/conf_builder/discv5_conf_builder.nim +++ b/waku/factory/conf_builder/discv5_conf_builder.nim @@ -4,6 +4,8 @@ import ../waku_conf logScope: topics = "waku conf builder discv5" +const DefaultDiscv5UdpPort*: Port = Port(9000) + ########################### ## Discv5 Config Builder ## ########################### @@ -38,8 +40,8 @@ proc withTableIpLimit*(b: var Discv5ConfBuilder, tableIpLimit: uint) = proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: Port) = b.udpPort = some(udpPort) -proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: uint) = - b.udpPort = some(Port(udpPort.uint16)) +proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: uint16) = + b.udpPort = some(Port(udpPort)) proc withBootstrapNodes*(b: var Discv5ConfBuilder, bootstrapNodes: seq[string]) = # TODO: validate ENRs? @@ -57,7 +59,7 @@ proc build*(b: Discv5ConfBuilder): Result[Option[Discv5Conf], string] = bucketIpLimit: b.bucketIpLimit.get(2), enrAutoUpdate: b.enrAutoUpdate.get(true), tableIpLimit: b.tableIpLimit.get(10), - udpPort: b.udpPort.get(9000.Port), + udpPort: b.udpPort.get(DefaultDiscv5UdpPort), ) ) ) diff --git a/waku/factory/conf_builder/metrics_server_conf_builder.nim b/waku/factory/conf_builder/metrics_server_conf_builder.nim index 0f0d18564..8b2ea4eb8 100644 --- a/waku/factory/conf_builder/metrics_server_conf_builder.nim +++ b/waku/factory/conf_builder/metrics_server_conf_builder.nim @@ -4,6 +4,8 @@ import ../waku_conf logScope: topics = "waku conf builder metrics server" +const DefaultMetricsHttpPort*: Port = Port(8008) + ################################### ## Metrics Server Config Builder ## ################################### @@ -40,7 +42,7 @@ proc build*(b: MetricsServerConfBuilder): Result[Option[MetricsServerConf], stri some( MetricsServerConf( httpAddress: b.httpAddress.get(static parseIpAddress("127.0.0.1")), - httpPort: b.httpPort.get(8008.Port), + httpPort: b.httpPort.get(DefaultMetricsHttpPort), logging: b.logging.get(false), ) ) diff --git a/waku/factory/conf_builder/rest_server_conf_builder.nim b/waku/factory/conf_builder/rest_server_conf_builder.nim index 2efd91f02..dcafbb56a 100644 --- a/waku/factory/conf_builder/rest_server_conf_builder.nim +++ b/waku/factory/conf_builder/rest_server_conf_builder.nim @@ -4,6 +4,8 @@ import ../waku_conf logScope: topics = "waku conf builder rest server" +const DefaultRestPort*: Port = Port(8645) + ################################ ## REST Server Config Builder ## ################################ @@ -46,8 +48,6 @@ proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] = if b.listenAddress.isNone(): return err("restServer.listenAddress is not specified") - if b.port.isNone(): - return err("restServer.port is not specified") if b.relayCacheCapacity.isNone(): return err("restServer.relayCacheCapacity is not specified") @@ -56,7 +56,7 @@ proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] = RestServerConf( allowOrigin: b.allowOrigin, listenAddress: b.listenAddress.get(), - port: b.port.get(), + port: b.port.get(DefaultRestPort), admin: b.admin.get(false), relayCacheCapacity: b.relayCacheCapacity.get(), ) diff --git a/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index 78dbd9eb9..5954bbe58 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -8,11 +8,13 @@ import results import - ../waku_conf, - ../networks_config, - ../../common/logging, - ../../common/utils/parse_size_units, - ../../waku_enr/capabilities, + waku/[ + factory/waku_conf, + factory/networks_config, + common/logging, + common/utils/parse_size_units, + waku_enr/capabilities, + ], tools/confutils/entry_nodes import @@ -32,7 +34,9 @@ import logScope: topics = "waku conf builder" -const DefaultMaxConnections* = 150 +const + DefaultMaxConnections* = 150 + DefaultP2pTcpPort*: Port = Port(60000) type MaxMessageSizeKind* = enum mmskNone @@ -574,12 +578,7 @@ proc build*( warn "Nat Strategy is not specified, defaulting to none" "none" - let p2pTcpPort = - if builder.p2pTcpPort.isSome(): - builder.p2pTcpPort.get() - else: - warn "P2P Listening TCP Port is not specified, listening on 60000" - 60000.Port + let p2pTcpPort = builder.p2pTcpPort.get(DefaultP2pTcpPort) let p2pListenAddress = if builder.p2pListenAddress.isSome(): diff --git a/waku/factory/conf_builder/web_socket_conf_builder.nim b/waku/factory/conf_builder/web_socket_conf_builder.nim index 88edc0941..61334d958 100644 --- a/waku/factory/conf_builder/web_socket_conf_builder.nim +++ b/waku/factory/conf_builder/web_socket_conf_builder.nim @@ -4,6 +4,8 @@ import waku/factory/waku_conf logScope: topics = "waku conf builder websocket" +const DefaultWebSocketPort*: Port = Port(8000) + ############################## ## WebSocket Config Builder ## ############################## @@ -41,14 +43,12 @@ proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] = if not b.enabled.get(false): return ok(none(WebSocketConf)) - if b.webSocketPort.isNone(): - return err("websocket.port is not specified") - if not b.secureEnabled.get(false): return ok( some( WebSocketConf( - port: b.websocketPort.get(), secureConf: none(WebSocketSecureConf) + port: b.webSocketPort.get(DefaultWebSocketPort), + secureConf: none(WebSocketSecureConf), ) ) ) @@ -61,7 +61,7 @@ proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] = return ok( some( WebSocketConf( - port: b.webSocketPort.get(), + port: b.webSocketPort.get(DefaultWebSocketPort), secureConf: some( WebSocketSecureConf(keyPath: b.keyPath.get(), certPath: b.certPath.get()) ), diff --git a/waku/factory/internal_config.nim b/waku/factory/internal_config.nim index fd85c26a5..fa36aff57 100644 --- a/waku/factory/internal_config.nim +++ b/waku/factory/internal_config.nim @@ -8,7 +8,7 @@ import std/[options, sequtils, net], results -import ../common/utils/nat, ../node/net_config, ../waku_enr, ../waku_core, ./waku_conf +import waku/[common/utils/nat, net/net_config, waku_enr, waku_core], ./waku_conf proc tryBuildEnrRecord( conf: WakuConf, netConfig: NetConfig, multiaddrs: seq[MultiAddress] diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 45e0edee0..395841130 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -202,6 +202,11 @@ proc new*( else: nil + if not restServer.isNil(): + let boundRestPort = restServer.httpServer.address.port + node.ports.rest = boundRestPort.uint16 + wakuConf.restServerConf.get().port = boundRestPort + # Set the extMultiAddrsOnly flag so the node knows not to replace explicit addresses node.extMultiAddrsOnly = wakuConf.endpointConf.extMultiAddrsOnly @@ -249,7 +254,7 @@ proc getPorts( return ok((tcpPort: tcpPort, websocketPort: websocketPort)) proc getRunningNetConfig(waku: ptr Waku): Future[Result[NetConfig, string]] {.async.} = - var conf = waku[].conf + let conf = waku[].conf let (tcpPort, websocketPort) = getPorts(waku[].node.switch.peerInfo.listenAddrs).valueOr: return err("Could not retrieve ports: " & error) @@ -281,6 +286,10 @@ proc updateEnr(waku: ptr Waku): Future[Result[void, string]] {.async.} = waku[].node.enr = record + # If TCP/WS was configured with port 0, node.announcedAddresses was built + # pre-bind with a port value of 0. In any case, the resync is harmless. + waku[].node.announcedAddresses = netConf.announcedAddresses + return ok() proc updateAddressInENR(waku: ptr Waku): Result[void, string] = @@ -312,11 +321,8 @@ proc updateAddressInENR(waku: ptr Waku): Result[void, string] = return ok() proc updateWaku(waku: ptr Waku): Future[Result[void, string]] {.async.} = - let conf = waku[].conf - if conf.endpointConf.p2pTcpPort == Port(0) or - (conf.websocketConf.isSome() and conf.websocketConf.get.port == Port(0)): - (await updateEnr(waku)).isOkOr: - return err("error calling updateEnr: " & $error) + (await updateEnr(waku)).isOkOr: + return err("error calling updateEnr: " & $error) ?updateAnnouncedAddrWithPrimaryIpAddr(waku[].node) @@ -390,29 +396,37 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: (await startNode(waku.node, waku.conf, waku.dynamicBootstrapNodes)).isOkOr: return err("error while calling startNode: " & $error) - ## Update waku data that is set dynamically on node start - try: - (await updateWaku(waku)).isOkOr: - return err("Error in updateApp: " & $error) - except CatchableError: - return err("Caught exception in updateApp: " & getCurrentExceptionMsg()) + let bound = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr: + return err("failed to read bound ports from switch: " & $error) + waku[].node.ports.tcp = bound.tcpPort.get(Port(0)).uint16 + waku[].node.ports.webSocket = bound.websocketPort.get(Port(0)).uint16 ## Discv5 if conf.discv5Conf.isSome(): - waku[].wakuDiscV5 = waku_discv5.setupDiscoveryV5( - waku.node.enr, - waku.node.peerManager, - waku.node.topicSubscriptionQueue, - conf.discv5Conf.get(), - waku.dynamicBootstrapNodes, - waku.rng, - conf.nodeKey, - conf.endpointConf.p2pListenAddress, - conf.portsShift, - ) + waku[].wakuDiscV5 = ( + await waku_discv5.setupAndStartDiscv5( + waku.node.enr, + waku.node.peerManager, + waku.node.topicSubscriptionQueue, + conf.discv5Conf.get(), + waku.dynamicBootstrapNodes, + waku.rng, + conf.nodeKey, + conf.endpointConf.p2pListenAddress, + conf.portsShift, + ) + ).valueOr: + return err("failed to start waku discovery v5: " & error) - (await waku.wakuDiscV5.start()).isOkOr: - return err("failed to start waku discovery v5: " & $error) + waku[].node.ports.discv5Udp = waku[].wakuDiscV5.udpPort.uint16 + waku[].conf.discv5Conf.get().udpPort = waku[].wakuDiscV5.udpPort + + ## Update waku data that is set dynamically on node start + try: + (await updateWaku(waku)).isOkOr: + return err("Error in startWaku: " & $error) + except CatchableError: + return err("Caught exception in startWaku: " & getCurrentExceptionMsg()) ## Reliability if not waku[].deliveryService.isNil(): @@ -482,14 +496,15 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: if conf.metricsServerConf.isSome(): try: - waku[].metricsServer = ( - await ( - waku_metrics.startMetricsServerAndLogging( - conf.metricsServerConf.get(), conf.portsShift - ) + let (server, port) = ( + await waku_metrics.startMetricsServerAndLogging( + conf.metricsServerConf.get(), conf.portsShift ) ).valueOr: return err("Starting monitoring and external interfaces failed: " & error) + waku[].metricsServer = server + waku[].node.ports.metrics = port.uint16 + waku[].conf.metricsServerConf.get().httpPort = port except CatchableError: return err( "Caught exception starting monitoring and external interfaces failed: " & diff --git a/waku/factory/waku_state_info.nim b/waku/factory/waku_state_info.nim index 5dc72a693..397b90d6d 100644 --- a/waku/factory/waku_state_info.nim +++ b/waku/factory/waku_state_info.nim @@ -6,7 +6,7 @@ import std/[tables, sequtils, strutils] import metrics, eth/p2p/discoveryv5/enr, libp2p/peerid -import waku/waku_node +import waku/[waku_node, net/bound_ports] type NodeInfoId* {.pure.} = enum @@ -15,6 +15,7 @@ type MyMultiaddresses MyENR MyPeerId + MyBoundPorts WakuStateInfo* {.requiresInit.} = object node: WakuNode @@ -43,6 +44,8 @@ proc getNodeInfoItem*(self: WakuStateInfo, infoItemId: NodeInfoId): string = return self.node.enr.toURI() of NodeInfoId.MyPeerId: return $PeerId(self.node.peerId()) + of NodeInfoId.MyBoundPorts: + return $self.node.ports else: return "unknown info item id" diff --git a/waku/net/auto_port.nim b/waku/net/auto_port.nim new file mode 100644 index 000000000..38176d27d --- /dev/null +++ b/waku/net/auto_port.nim @@ -0,0 +1,48 @@ +{.push raises: [].} + +import std/[net, random] +import chronos, results + +const + AutoPortRetryCount* = 20 + AutoPortMin = 50000'u16 + AutoPortMax = 59000'u16 + AutoPortAttemptTimeout = chronos.seconds(30) + +proc getAutoPort*(): uint16 = + var rng = initRand() + uint16(rng.rand(AutoPortMin.int .. AutoPortMax.int)) + +proc tryWithAutoPort*[T]( + startingPort: Port, + attempt: proc(p: Port): Future[Result[T, string]] {.async: (raises: []).}, +): Future[Result[T, string]] {.async: (raises: []).} = + ## If `startingPort == Port(0)`, call `attempt` up to `AutoPortRetryCount` + ## times with random ports. Otherwise call it once with `startingPort`. + ## Returns the first ok or the last err. + let autoMode = startingPort == Port(0) + let attempts = if autoMode: AutoPortRetryCount else: 1 + var lastErr = "" + for i in 1 .. attempts: + let port = + if autoMode: + Port(getAutoPort()) + else: + startingPort + let fut = attempt(port) + let res = + try: + if await fut.withTimeout(AutoPortAttemptTimeout): + await fut + else: + fut.cancelSoon() + Result[T, string].err("bind attempt timed out") + except CancelledError: + fut.cancelSoon() + Result[T, string].err("bind attempt cancelled") + if res.isOk(): + return ok(res.get()) + lastErr = res.error + if autoMode: + return err("auto-port exhausted; last error: " & lastErr) + return err("port bind failed: " & lastErr) diff --git a/waku/net/bound_ports.nim b/waku/net/bound_ports.nim new file mode 100644 index 000000000..f8f561940 --- /dev/null +++ b/waku/net/bound_ports.nim @@ -0,0 +1,20 @@ +{.push raises: [].} + +import std/json + +type BoundPorts* {.requiresInit.} = object + ## Set by the factory once each service has bound to a port. + ## A value of 0 means the service was not enabled or did not bind. + tcp*: uint16 + webSocket*: uint16 + rest*: uint16 + discv5Udp*: uint16 + metrics*: uint16 + +proc init*(T: type BoundPorts): BoundPorts = + return BoundPorts( + tcp: 0'u16, webSocket: 0'u16, rest: 0'u16, discv5Udp: 0'u16, metrics: 0'u16 + ) + +proc `$`*(p: BoundPorts): string = + return $(%*p) diff --git a/waku/node/net_config.nim b/waku/net/net_config.nim similarity index 100% rename from waku/node/net_config.nim rename to waku/net/net_config.nim diff --git a/waku/node/waku_metrics.nim b/waku/node/waku_metrics.nim index 8d38624c1..af74b1532 100644 --- a/waku/node/waku_metrics.nim +++ b/waku/node/waku_metrics.nim @@ -2,8 +2,7 @@ import chronicles, chronos, metrics, metrics/chronos_httpserver import - ../waku_rln_relay/protocol_metrics as rln_metrics, - ../utils/collector, + waku/[net/auto_port, waku_rln_relay/protocol_metrics as rln_metrics, utils/collector], ./peer_manager, ./waku_node @@ -57,27 +56,36 @@ proc startMetricsLog*() = discard setTimer(Moment.fromNow(LogInterval), logMetrics) +type StartedMetricsServer* = tuple[server: MetricsHttpServerRef, port: Port] + proc startMetricsServer( serverIp: IpAddress, serverPort: Port -): Future[Result[MetricsHttpServerRef, string]] {.async.} = - info "Starting metrics HTTP server", serverIp = $serverIp, serverPort = $serverPort +): Future[Result[StartedMetricsServer, string]] {.async.} = + proc attempt( + port: Port + ): Future[Result[StartedMetricsServer, string]] {.async: (raises: []).} = + info "Starting metrics HTTP server", serverIp = $serverIp, serverPort = $port - let server = MetricsHttpServerRef.new($serverIp, serverPort).valueOr: - return err("metrics HTTP server start failed: " & $error) + let server = MetricsHttpServerRef.new($serverIp, port).valueOr: + return err("fail to start service metrics server, attempt:" & $error) - try: - await server.start() - except CatchableError: - return err("metrics HTTP server start failed: " & getCurrentExceptionMsg()) + try: + await server.start() + except CatchableError: + return + err("exception while startMetricsServer, attempt: " & getCurrentExceptionMsg()) - info "Metrics HTTP server started", serverIp = $serverIp, serverPort = $serverPort - return ok(server) + info "Metrics HTTP server started", serverIp = $serverIp, serverPort = $port + return ok((server: server, port: port)) + + let started = (await tryWithAutoPort[StartedMetricsServer](serverPort, attempt)).valueOr: + return err("metrics HTTP server start failed: " & error) + return ok(started) proc startMetricsServerAndLogging*( conf: MetricsServerConf, portsShift: uint16 -): Future[Result[MetricsHttpServerRef, string]] {.async.} = - var metricsServer: MetricsHttpServerRef - metricsServer = ( +): Future[Result[StartedMetricsServer, string]] {.async.} = + let started = ( await ( startMetricsServer(conf.httpAddress, Port(conf.httpPort.uint16 + portsShift)) ) @@ -87,4 +95,4 @@ proc startMetricsServerAndLogging*( if conf.logging: startMetricsLog() - return ok(metricsServer) + return ok(started) diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 45080d9d0..7cd334b53 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -62,7 +62,7 @@ import events/message_events, ], waku/discovery/waku_kademlia, - ./net_config, + waku/net/[bound_ports, net_config], ./peer_manager, ./health_monitor/health_status, ./health_monitor/topic_health @@ -140,6 +140,7 @@ type wakuMix*: WakuMix kademliaDiscoveryLoop*: Future[void] wakuKademlia*: WakuKademlia + ports*: BoundPorts proc deduceRelayShard( node: WakuNode, @@ -224,6 +225,7 @@ proc new*( announcedAddresses: netConfig.announcedAddresses, topicSubscriptionQueue: queue, rateLimitSettings: rateLimitSettings, + ports: BoundPorts.init(), ) peerManager.setShardGetter(node.getShardsGetter(@[])) diff --git a/waku/waku_node.nim b/waku/waku_node.nim index e782e616b..c8b13d4ea 100644 --- a/waku/waku_node.nim +++ b/waku/waku_node.nim @@ -1,5 +1,5 @@ import - ./node/net_config, + ./net/net_config, ./node/waku_switch as switch, ./node/waku_node as node, ./node/health_monitor as health_monitor, From f23983f48841564be6d862dbc15636a2309c9b8f Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Wed, 13 May 2026 15:29:11 +0200 Subject: [PATCH 145/155] ensure peers are retrieved in random order from peer store (#3860) --- tests/waku_store/test_client.nim | 23 ++++++++++++++++++++++- waku/waku_store/client.nim | 5 +++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/waku_store/test_client.nim b/tests/waku_store/test_client.nim index ec462eb56..d9c94a10c 100644 --- a/tests/waku_store/test_client.nim +++ b/tests/waku_store/test_client.nim @@ -1,6 +1,6 @@ {.used.} -import std/options, testutils/unittests, chronos, libp2p/crypto/crypto +import std/[options, sets], testutils/unittests, chronos, libp2p/crypto/crypto import waku/[node/peer_manager, waku_core, waku_store, waku_store/client, common/paging], @@ -223,3 +223,24 @@ suite "Store Client": not await handlerFuture.withTimeout(FUTURE_TIMEOUT) queryResponse.isErr() queryResponse.error.kind == ErrorCode.PEER_DIAL_FAILURE + + asyncTest "queryToAny shuffles peers across calls": + # Register several fake store peers (no servers running) so every dial + # fails. PEER_DIAL_FAILURE carries the peerId of the last peer tried in + # the shuffled order, so observing different "last" peerIds across calls + # confirms shuffle is active inside queryToAny. + for _ in 0 ..< 3: + let fakeSwitch = newTestSwitch() + let peerInfo = fakeSwitch.peerInfo.toRemotePeerInfo() + peerInfo.protocols = @[WakuStoreCodec] + clientSwitch.peerStore.addPeer(peerInfo) + + var observedLastPeers: HashSet[string] + for _ in 0 ..< 20: + let res = await client.queryToAny(storeQuery) + check: + res.isErr() + res.error.kind == ErrorCode.PEER_DIAL_FAILURE + observedLastPeers.incl(res.error.address) + + check observedLastPeers.len >= 2 diff --git a/waku/waku_store/client.nim b/waku/waku_store/client.nim index 9b26d44a8..b49662811 100644 --- a/waku/waku_store/client.nim +++ b/waku/waku_store/client.nim @@ -1,7 +1,7 @@ {.push raises: [].} import - std/[options, tables, sequtils, algorithm], + std/[options, tables, sequtils, algorithm, random], results, chronicles, chronos, @@ -100,7 +100,8 @@ proc queryToAny*( if peers.len == 0: return err(StoreError(kind: BAD_RESPONSE, cause: "no service store peer connected")) - # Shuffle to distribute load and limit retries + # Shuffle to distribute load across store peers and limit retries + shuffle(peers) let peersToTry = peers[0 ..< min(peers.len, MaxQueryRetries)] var lastError: StoreError From cb35b59f951a8b6ba3815fd7f0cfb95c99192753 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Wed, 13 May 2026 12:09:56 -0300 Subject: [PATCH 146/155] stop recv_service from delivering messages on unsubscribed topics for store-recovered messages (#3874) * fix/harden recv_service so it won't deliver messages on unsubscribed content topics * fix SubscrptionManager's subscribed-content-topics iterator * fix broken store-message-receive test * misc cleanups --- tests/api/test_api_receive.nim | 26 +++++++----- .../recv_service/recv_service.nim | 41 ++++++++++--------- .../delivery_service/subscription_manager.nim | 9 ++++ 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/tests/api/test_api_receive.nim b/tests/api/test_api_receive.nim index 52f8713f9..24251f161 100644 --- a/tests/api/test_api_receive.nim +++ b/tests/api/test_api_receive.nim @@ -138,7 +138,20 @@ suite "Messaging API, Receive Service (store recovery)": break await sleepAsync(100.milliseconds) - # publish before subscriber exists, gets archived + # create the subscriber before publishing. + # RecvService captures startTimeToCheck at construction time; the + # message's timestamp must land after that point to fall inside + # checkStore's time window. + var subscriber: Waku + lockNewGlobalBrokerContext: + subscriber = (await createNode(createApiNodeConf(numShards))).expect( + "Failed to create subscriber" + ) + (await startWaku(addr subscriber)).expect("Failed to start subscriber") + + # publish after the subscriber exists but before it connects to the + # store; the message reaches the archive but the subscriber doesn't + # see it via live relay. let missedPayload = "This message was missed".toBytes() let missedMsg = WakuMessage( payload: missedPayload, contentTopic: testTopic, version: 0, timestamp: now() @@ -159,15 +172,8 @@ suite "Messaging API, Receive Service (store recovery)": await sleepAsync(100.milliseconds) raiseAssert "Message was not archived in time" - # create subscriber - var subscriber: Waku - lockNewGlobalBrokerContext: - subscriber = (await createNode(createApiNodeConf(numShards))).expect( - "Failed to create subscriber" - ) - (await startWaku(addr subscriber)).expect("Failed to start subscriber") - - # connect subscriber to store (not publisher, so msg won't come via relay to it) + # connect subscriber to store after the message is already archived so + # gossipsub doesn't replay it via the live path await subscriber.node.connectToNodes(@[storeNodePeerInfo]) # subscribe to content topic diff --git a/waku/node/delivery_service/recv_service/recv_service.nim b/waku/node/delivery_service/recv_service/recv_service.nim index 64f4d683d..0f077a289 100644 --- a/waku/node/delivery_service/recv_service/recv_service.nim +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -70,20 +70,30 @@ proc getMissingMsgsFromStore( ) ) -proc processIncomingMessageOfInterest( +proc processIncomingMessage( self: RecvService, pubsubTopic: string, message: WakuMessage ): bool = - ## Deduplicate (by hash), store (saves in recently-seen messages) and emit - ## the MAPI MessageReceivedEvent for every unique incoming message. - ## Returns true if the message was new and the MessageReceivedEvent was properly emitted. + ## Return false if the incoming message is from a non-subscribed topic, + ## or if the message is a duplicate (recently-seen). Otherwise, save it as + ## recently-seen, emit a MessageReceivedEvent, and return true. + + if not self.subscriptionManager.isSubscribed(pubsubTopic, message.contentTopic): + trace "skipping message as I am not subscribed", + shard = pubsubTopic, contentTopic = message.contentTopic + return false let msgHash = computeMessageHash(pubsubTopic, message) - if not self.recentReceivedMsgs.anyIt(it.msgHash == msgHash): - let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp) - self.recentReceivedMsgs.add(rxMsg) - MessageReceivedEvent.emit(self.brokerCtx, msgHash.to0xHex(), message) - return true - return false + if self.recentReceivedMsgs.anyIt(it.msgHash == msgHash): + trace "skipping duplicate message", + shard = pubsubTopic, + contentTopic = message.contentTopic, + msg_hash = msgHash.to0xHex() + return false + + let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp) + self.recentReceivedMsgs.add(rxMsg) + MessageReceivedEvent.emit(self.brokerCtx, msgHash.to0xHex(), message) + return true proc checkStore*(self: RecvService) {.async.} = ## Checks the store for messages that were not received directly and @@ -121,7 +131,7 @@ proc checkStore*(self: RecvService) {.async.} = let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes) if missingMsgsRet.isOk(): for msgTuple in missingMsgsRet.get(): - if self.processIncomingMessageOfInterest(msgTuple.pubsubTopic, msgTuple.msg): + if self.processIncomingMessage(msgTuple.pubsubTopic, msgTuple.msg): info "recv service store-recovered message", msg_hash = shortLog(msgTuple.hash), pubsubTopic = msgTuple.pubsubTopic else: @@ -163,14 +173,7 @@ proc startRecvService*(self: RecvService) = self.seenMsgListener = MessageSeenEvent.listen( self.brokerCtx, proc(event: MessageSeenEvent) {.async: (raises: []).} = - if not self.subscriptionManager.isSubscribed( - event.topic, event.message.contentTopic - ): - trace "skipping message as I am not subscribed", - shard = event.topic, contenttopic = event.message.contentTopic - return - - discard self.processIncomingMessageOfInterest(event.topic, event.message), + discard self.processIncomingMessage(event.topic, event.message), ).valueOr: error "Failed to set MessageSeenEvent listener", error = error quit(QuitFailure) diff --git a/waku/node/delivery_service/subscription_manager.nim b/waku/node/delivery_service/subscription_manager.nim index f00d9024c..c34335057 100644 --- a/waku/node/delivery_service/subscription_manager.nim +++ b/waku/node/delivery_service/subscription_manager.nim @@ -61,7 +61,16 @@ type SubscriptionManager* = ref object of RootObj iterator subscribedTopics*( self: SubscriptionManager ): (PubsubTopic, HashSet[ContentTopic]) = + ## Iterate over all subscribed content topics, batched per shard. + ## This is guaranteed to return a non-empty `topics` (content topics) list on iteration. + for pubsub, topics in self.contentTopicSubs.pairs: + # We are iterating over subscribed content topics; if we are subscribed to + # a shard but have no subscription (interest) for any content topic in that + # shard, then avoid triggering an iteration that doesn't advance the intent + # to iterate over content topic subscriptions. + if topics.len == 0: + continue yield (pubsub, topics) proc edgeFilterPeerCount*(sm: SubscriptionManager, shard: PubsubTopic): int = From 34c197c5cdad1d5787f8f51b66d7e83014e5480b Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Fri, 15 May 2026 17:39:38 +0200 Subject: [PATCH 147/155] avoid keeping delivery tasks in propagated state when check store is disabled (#3843) --- waku/node/delivery_service/send_service/send_service.nim | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/waku/node/delivery_service/send_service/send_service.nim b/waku/node/delivery_service/send_service/send_service.nim index a3c44bc0c..e6d3a2eda 100644 --- a/waku/node/delivery_service/send_service/send_service.nim +++ b/waku/node/delivery_service/send_service/send_service.nim @@ -225,9 +225,12 @@ proc evaluateAndCleanUp(self: SendService) = it.state != DeliveryState.FailedToDeliver ) - # remove propagated ephemeral messages as no store check is possible + # remove propagated messages when no store confirmation will follow self.taskCache.keepItIf( - not (it.isEphemeral() and it.state == DeliveryState.SuccessfullyPropagated) + not ( + it.state == DeliveryState.SuccessfullyPropagated and + (it.isEphemeral() or not self.checkStoreForMessages) + ) ) proc trySendMessages(self: SendService) {.async.} = From 42e0aa43d152690f6ae6abf18f546c9127e8a892 Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Sat, 16 May 2026 00:09:07 +0200 Subject: [PATCH 148/155] feat: persistency (#3880) * persistency: per-job SQLite-backed storage layer (singleton, brokered) Adds a backend-neutral CRUD library at waku/persistency/, plus the nim-brokers dependency swap that enables it. Architecture (ports-and-adapters): * Persistency: process-wide singleton, one root directory. * Job: one tenant, one DB file, one worker thread, one BrokerContext. * Backend: SQLite via waku/common/databases/db_sqlite. Uniform schema kv(category BLOB, key BLOB, payload BLOB) PRIMARY KEY (category, key) WITHOUT ROWID, WAL mode. * Writes are fire-and-forget via EventBroker(mt) PersistEvent. * Reads are async via five RequestBroker(mt) shapes (KvGet, KvExists, KvScan, KvCount, KvDelete). Reads return Result[T, PersistencyError]. * One storage thread per job; tenants isolated by BrokerContext. Public surface (waku/persistency/persistency.nim): Persistency.instance(rootDir) / Persistency.instance() / Persistency.reset() p.openJob(id) / p.closeJob(id) / p.dropJob(id) / p.close() p.job(id) / p[id] / p.hasJob(id) Writes (Job form & string-id form, fire-and-forget): persist / persistPut / persistDelete / persistEncoded Reads (Job form & string-id form, async Result): get / exists / scan / scanPrefix / count / deleteAcked Key & payload encoding (keys.nim, payload.nim): * encodePart family + variadic key(...) / payload(...) macros + single-value toKey / toPayload. * Primitives: string and openArray[byte] are 2-byte BE length + bytes; int{8..64} are sign-flipped 8-byte BE; uint{16..64} are 8-byte BE; bool/byte/char are 1 byte; enums are int64(ord(v)). * Generic encodePart[T: tuple | object] recurses through fields() so any composite Nim type is encodable without ceremony. * Stable across Nim/C compiler upgrades: no sizeof, no memcpy, no cast on pointers, no host-endianness dependency. * `rawKey(bytes)` + `persistPut(..., openArray[byte])` let callers bypass the built-in encoder with their own format (CBOR, protobuf...). Lifecycle: * Persistency.new is private; Persistency.instance is the only public constructor. Same rootDir is idempotent; conflicting rootDir is peInvalidArgument. Persistency.reset for test/restart paths. * openJob opens-or-creates the per-job SQLite file; an existing file is reused with its data preserved. * Teardown integration: Persistency.instance registers a Teardown MultiRequestBroker provider that closes all jobs and clears the singleton slot when Waku.stop() issues Teardown.request. Internal layering: types.nim pure value types (Key, KeyRange, KvRow, TxOp, PersistencyError) keys.nim encodePart primitives + key(...) macro payload.nim toPayload + payload(...) macro schema.nim CREATE TABLE + connection pragmas + user_version backend_sqlite.nim KvBackend, applyOps (single source of write SQL), getOne/existsOne/deleteOne, scanRange (asc/desc, half-open ranges, open-ended stop), countRange backend_comm.nim EventBroker(mt) PersistEvent + 5 RequestBroker(mt) declarations; encodeErr/decodeErr boundary helpers backend_thread.nim startStorageThread / stopStorageThread (shared allocShared0 arg, cstring dbPath, atomic ready/shutdown flags); per-thread provider registration persistency.nim Persistency + Job types, singleton state, public facade ../requests/lifecycle_requests.nim Teardown MultiRequestBroker Tests (69 cases, all passing): test_keys.nim sort-order invariants (length-prefix strings, sign-flipped ints, composite tuples, prefix range) test_backend.nim round-trip / replace / delete-return-value / batched atomicity / asc-desc-half-open-open- ended scans / category isolation / batch txDelete test_lifecycle.nim open-or-create rootDir / non-dir collision / reopen across sessions / idempotent openJob / two-tenant parallel isolation / closeJob joins worker / dropJob removes file / acked delete test_facade.nim put-then-get / atomic batch / scanPrefix asc/desc / deleteAcked hit-miss / fire-and-forget delete / two-tenant facade isolation test_encoding.nim tuple/named-tuple/object keys, embedded Key, enum encoding, field-major composite sort, payload struct encoding, end-to-end struct round-trip through SQLite test_string_lookup.nim peJobNotFound semantics / hasJob / subscript / persistPut+get via id / reads short-circuit / writes drop+warn / persistEncoded via id / scan parity Job-ref vs id test_singleton.nim idempotent same-rootDir / different-rootDir rejection / no-arg instance lifecycle / reset retargets / reset idempotence / Teardown.request end-to-end Prerequisite delivered in the same series: replace the in-tree broker implementation with the external nim-brokers package; update all broker call-sites (waku_filter_v2, waku_relay, waku_rln_relay, delivery_service, peer_manager, requests/*, factory/*, api tests, etc.) to the new package API; chat2 made to compile again. Note: SDS adapter (Phase 5 of the design) is deferred -- nim-sds is still developed side-by-side and the persistency layer is intentionally SDS-agnostic. Co-Authored-By: Claude Opus 4.7 * persistency: pin nim-brokers by URL+commit (workaround for stale registry) The bare `brokers >= 2.0.1` form cannot resolve on machines where the local nimble SAT solver enumerates only the registry-recorded 0.1.0 for brokers. The nim-lang/packages entry for `brokers` carries no per-tag metadata (only the URL), so until that registry entry is refreshed the SAT solver clamps the available-versions list to 0.1.0 and rejects the >= 2.0.1 constraint -- even though pkgs2 and pkgcache both have v2.0.1 cloned locally. Pinning by URL+commit bypasses the registry path entirely. Inline comment in waku.nimble documents the situation and the path back to the bare form once nim-lang/packages is updated. Co-Authored-By: Claude Opus 4.7 * persistency: nph format pass Run `nph` on all 57 Nim files touched by this PR. Pure formatting: 17 files re-styled, no semantic change. Suite still 69/69. Co-Authored-By: Claude Opus 4.7 * Fix build, add local-storage-path config, lazy init of Persistency from Waku start * fix: fix nix deps * fixes for nix build, regenerate deps * reverting accidental dependency changes * Fixing deps * Apply suggestions from code review Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> * persistency tests: migrate to suite / asyncTest / await Match the in-tree test convention (procSuite -> suite, sync test + waitFor -> asyncTest + await): - procSuite "X": -> suite "X": - For tests doing async work: test -> asyncTest, waitFor -> await. - Poll helpers (proc waitFor(t: Job, ...) in test_lifecycle.nim, proc waitUntilExists(...) in test_facade.nim and test_string_lookup.nim) -> Future[bool] {.async.}, internal `waitFor X` -> `await X`, internal `sleep(N)` -> `await sleepAsync(chronos.milliseconds(N))`. - Renamed test_lifecycle.nim's helper proc from `waitFor(t: Job, ...)` -> `pollExists(t: Job, ...)`; the previous name shadowed chronos.waitFor in the chronos macro expansion. - `chronos.milliseconds(N)` explicitly qualified because `std/times` also exports `milliseconds` (returning TimeInterval, not Duration). - `check await x` -> `let okN = await x; check okN` to dodge chronos's "yield in expr not lowered" with await-as-macro-argument. - `(await x).foo()` -> `let awN = await x; ... awN.foo() ...` for the same reason. waku/persistency/persistency.nim: nph also pulled the proc signatures across multiple lines; restored explicit `Future[void] {.async.}` return types after the colon (an intermediate nph pass had elided them). Suite: 71 / 71 OK against the new async write surface. Co-Authored-By: Claude Opus 4.7 * use idiomatic valueOr instead of ifs * Reworked persistency shutdown, remove not necessary teardown mechanism * Use const for DefaultStoragePath * format to follow coding guidelines - no use of result and explicit returns - no functional change --------- Co-authored-by: Claude Opus 4.7 Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> --- .gitignore | 1 + AGENTS.md | 42 + apps/chat2/chat2.nim | 3 +- apps/chat2/config_chat2.nim | 10 +- config.nims | 2 +- examples/api_example/api_example.nim | 6 +- .../logos_delivery_api/node_api.nim | 10 +- nimble.lock | 32 + nix/deps.nix | 21 + tests/all_tests_waku.nim | 3 + tests/api/test_api_health.nim | 7 +- tests/api/test_api_receive.nim | 8 +- tests/api/test_api_send.nim | 24 +- tests/api/test_api_subscription.nim | 42 +- tests/common/test_all.nim | 5 +- tests/common/test_event_broker.nim | 201 ----- tests/common/test_multi_request_broker.nim | 343 ------- tests/common/test_request_broker.nim | 675 -------------- tests/node/test_wakunode_health_monitor.nim | 8 +- tests/persistency/test_all.nim | 9 + tests/persistency/test_backend.nim | 195 ++++ tests/persistency/test_encoding.nim | 154 ++++ tests/persistency/test_facade.nim | 196 ++++ tests/persistency/test_keys.nim | 135 +++ tests/persistency/test_lifecycle.nim | 302 +++++++ tests/persistency/test_singleton.nim | 79 ++ tests/persistency/test_string_lookup.nim | 184 ++++ tests/waku_relay/utils.nim | 10 +- tests/waku_rln_relay/test_waku_rln_relay.nim | 4 +- .../test_wakunode_rln_relay.nim | 7 +- tests/wakunode_rest/test_rest_relay.nim | 2 +- tools/confutils/cli_args.nim | 8 + tools/gen-nix-deps.sh | 10 +- waku.nimble | 10 + waku/common/broker/broker_context.nim | 68 -- waku/common/broker/event_broker.nim | 411 --------- waku/common/broker/helper/broker_utils.nim | 206 ----- waku/common/broker/multi_request_broker.nim | 743 ---------------- waku/common/broker/request_broker.nim | 841 ------------------ waku/events/delivery_events.nim | 3 +- waku/events/events.nim | 4 +- waku/events/health_events.nim | 2 +- waku/events/message_events.nim | 4 +- waku/events/peer_events.nim | 2 +- waku/factory/builder.nim | 7 +- .../conf_builder/waku_conf_builder.nim | 7 + waku/factory/waku.nim | 11 +- waku/factory/waku_conf.nim | 2 + .../recv_service/recv_service.nim | 4 +- .../send_service/delivery_task.nim | 2 +- .../send_service/lightpush_processor.nim | 8 +- .../send_service/relay_processor.nim | 2 +- .../send_service/send_processor.nim | 2 +- .../send_service/send_service.nim | 2 +- .../delivery_service/subscription_manager.nim | 5 +- .../health_monitor/node_health_monitor.nim | 6 +- waku/node/kernel_api/relay.nim | 4 +- waku/node/peer_manager/peer_manager.nim | 4 +- waku/node/waku_node.nim | 6 +- waku/persistency/backend_comm.nim | 161 ++++ waku/persistency/backend_sqlite.nim | 247 +++++ waku/persistency/backend_thread.nim | 271 ++++++ waku/persistency/keys.nim | 180 ++++ waku/persistency/payload.nim | 53 ++ waku/persistency/persistency.nim | 433 +++++++++ waku/persistency/schema.nim | 58 ++ waku/persistency/types.nim | 81 ++ waku/requests/health_requests.nim | 2 +- waku/requests/node_requests.nim | 2 +- waku/requests/rln_requests.nim | 3 +- waku/waku_filter_v2/client.nim | 7 +- waku/waku_relay/protocol.nim | 6 +- waku/waku_rln_relay/rln_relay.nim | 5 +- 73 files changed, 3006 insertions(+), 3607 deletions(-) delete mode 100644 tests/common/test_event_broker.nim delete mode 100644 tests/common/test_multi_request_broker.nim delete mode 100644 tests/common/test_request_broker.nim create mode 100644 tests/persistency/test_all.nim create mode 100644 tests/persistency/test_backend.nim create mode 100644 tests/persistency/test_encoding.nim create mode 100644 tests/persistency/test_facade.nim create mode 100644 tests/persistency/test_keys.nim create mode 100644 tests/persistency/test_lifecycle.nim create mode 100644 tests/persistency/test_singleton.nim create mode 100644 tests/persistency/test_string_lookup.nim delete mode 100644 waku/common/broker/broker_context.nim delete mode 100644 waku/common/broker/event_broker.nim delete mode 100644 waku/common/broker/helper/broker_utils.nim delete mode 100644 waku/common/broker/multi_request_broker.nim delete mode 100644 waku/common/broker/request_broker.nim create mode 100644 waku/persistency/backend_comm.nim create mode 100644 waku/persistency/backend_sqlite.nim create mode 100644 waku/persistency/backend_thread.nim create mode 100644 waku/persistency/keys.nim create mode 100644 waku/persistency/payload.nim create mode 100644 waku/persistency/persistency.nim create mode 100644 waku/persistency/schema.nim create mode 100644 waku/persistency/types.nim diff --git a/.gitignore b/.gitignore index 188090b19..750d0a00b 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,4 @@ nimble.paths nimbledeps **/anvil_state/state-deployed-contracts-mint-and-approved.json +.gitnexus diff --git a/AGENTS.md b/AGENTS.md index 4f735f240..28e455f47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -506,4 +506,46 @@ Language: Nim 2.x | License: MIT or Apache 2.0 Note: For specific version requirements, check `waku.nimble`. + +# GitNexus — Code Intelligence +This project is indexed by GitNexus as **logos-delivery** (2076 symbols, 2564 relationships, 12 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. + +> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. + +## Always Do + +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. + +## Never Do + +- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. +- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. + +## Resources + +| Resource | Use for | +|----------|---------| +| `gitnexus://repo/logos-delivery/context` | Codebase overview, check index freshness | +| `gitnexus://repo/logos-delivery/clusters` | All functional areas | +| `gitnexus://repo/logos-delivery/processes` | All execution flows | +| `gitnexus://repo/logos-delivery/process/{name}` | Step-by-step execution trace | + +## CLI + +| Task | Read this skill file | +|------|---------------------| +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | + + diff --git a/apps/chat2/chat2.nim b/apps/chat2/chat2.nim index e76c7be17..282e17bfd 100644 --- a/apps/chat2/chat2.nim +++ b/apps/chat2/chat2.nim @@ -13,7 +13,8 @@ import chronos, eth/keys, bearssl, - stew/[byteutils, results], + stew/[byteutils], + results, metrics, metrics/chronos_httpserver import diff --git a/apps/chat2/config_chat2.nim b/apps/chat2/config_chat2.nim index fe7865c62..b0e38c6bc 100644 --- a/apps/chat2/config_chat2.nim +++ b/apps/chat2/config_chat2.nim @@ -140,7 +140,8 @@ type metricsServerAddress* {. desc: "Listening address of the metrics server.", - defaultValue: parseIpAddress("127.0.0.1"), + defaultValue: + IpAddress(family: IpAddressFamily.IPv4, address_v4: [127'u8, 0, 0, 1]), name: "metrics-server-address" .}: IpAddress @@ -173,7 +174,10 @@ type dnsAddrsNameServers* {. desc: "DNS name server IPs to query for DNS multiaddrs resolution. Argument may be repeated.", - defaultValue: @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")], + defaultValue: @[ + IpAddress(family: IpAddressFamily.IPv4, address_v4: [1'u8, 1, 1, 1]), + IpAddress(family: IpAddressFamily.IPv4, address_v4: [1'u8, 0, 0, 1]), + ], name: "dns-addrs-name-server" .}: seq[IpAddress] @@ -348,4 +352,4 @@ proc parseCmdArg*(T: type EthRpcUrl, s: string): T = func defaultListenAddress*(conf: Chat2Conf): IpAddress = # TODO: How should we select between IPv4 and IPv6 # Maybe there should be a config option for this. - (static parseIpAddress("0.0.0.0")) + (static IpAddress(family: IpAddressFamily.IPv4, address_v4: [0'u8, 0, 0, 0])) diff --git a/config.nims b/config.nims index 0f6052c9b..ebe501db8 100644 --- a/config.nims +++ b/config.nims @@ -117,7 +117,7 @@ if defined(android): switch("passL", "--sysroot=" & sysRoot) switch("cincludes", sysRoot & "/usr/include/") # begin Nimble config (version 2) +--noNimblePath when withDir(thisDir(), system.fileExists("nimble.paths")): - --noNimblePath include "nimble.paths" # end Nimble config diff --git a/examples/api_example/api_example.nim b/examples/api_example/api_example.nim index 4a7cde5db..2093a81c0 100644 --- a/examples/api_example/api_example.nim +++ b/examples/api_example/api_example.nim @@ -33,9 +33,9 @@ proc periodicSender(w: Waku): Future[void] {.async.} = return defer: - MessageSentEvent.dropListener(sentListener) - MessageErrorEvent.dropListener(errorListener) - MessagePropagatedEvent.dropListener(propagatedListener) + await MessageSentEvent.dropListener(sentListener) + await MessageErrorEvent.dropListener(errorListener) + await MessagePropagatedEvent.dropListener(propagatedListener) ## Periodically sends a Waku message every 30 seconds var counter = 0 diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim index 90630717b..2e30d1b43 100644 --- a/liblogosdelivery/logos_delivery_api/node_api.nim +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -184,11 +184,11 @@ proc logosdelivery_stop_node( requireInitializedNode(ctx, "STOP_NODE"): return err(errMsg) - MessageErrorEvent.dropAllListeners(ctx.myLib[].brokerCtx) - MessageSentEvent.dropAllListeners(ctx.myLib[].brokerCtx) - MessagePropagatedEvent.dropAllListeners(ctx.myLib[].brokerCtx) - MessageReceivedEvent.dropAllListeners(ctx.myLib[].brokerCtx) - EventConnectionStatusChange.dropAllListeners(ctx.myLib[].brokerCtx) + await MessageErrorEvent.dropAllListeners(ctx.myLib[].brokerCtx) + await MessageSentEvent.dropAllListeners(ctx.myLib[].brokerCtx) + await MessagePropagatedEvent.dropAllListeners(ctx.myLib[].brokerCtx) + await MessageReceivedEvent.dropAllListeners(ctx.myLib[].brokerCtx) + await EventConnectionStatusChange.dropAllListeners(ctx.myLib[].brokerCtx) (await ctx.myLib[].stop()).isOkOr: let errMsg = $error diff --git a/nimble.lock b/nimble.lock index 0a76565c4..7a36d72c4 100644 --- a/nimble.lock +++ b/nimble.lock @@ -263,6 +263,21 @@ "sha1": "8bc8c30b107fdba73b677e5f257c6c42ae1cdc8e" } }, + "cbor_serialization": { + "version": "0.3.0", + "vcsRevision": "1664160e04d153573373afddc552b9cbf6fbe4dc", + "url": "https://github.com/vacp2p/nim-cbor-serialization", + "downloadMethod": "git", + "dependencies": [ + "nim", + "serialization", + "stew", + "results" + ], + "checksums": { + "sha1": "ab126eae09a6e39c72972a6a0b83cb06a2ffe8f0" + } + }, "json_serialization": { "version": "0.4.4", "vcsRevision": "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44", @@ -312,6 +327,23 @@ "sha1": "8df97c45683abe2337bdff43b844c4fbcc124ca2" } }, + "brokers": { + "version": "#v2.0.1", + "vcsRevision": "2093ca4d50e581adda73fee7fd16231f990f4cbe", + "url": "https://github.com/NagyZoltanPeter/nim-brokers.git", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronos", + "results", + "chronicles", + "testutils", + "cbor_serialization" + ], + "checksums": { + "sha1": "cc74c987af94537e9d44d1b0143aa417299040c5" + } + }, "stint": { "version": "0.8.2", "vcsRevision": "470b7892561b5179ab20bd389a69217d6213fe58", diff --git a/nix/deps.nix b/nix/deps.nix index 0d9986528..63eeb597a 100644 --- a/nix/deps.nix +++ b/nix/deps.nix @@ -129,6 +129,13 @@ fetchSubmodules = true; }; + cbor_serialization = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-cbor-serialization"; + rev = "1664160e04d153573373afddc552b9cbf6fbe4dc"; + sha256 = "0c1rj4fk0fcqvsf0yqhxvm8h10aww75gi4yfsjhlczh88ypywii2"; + fetchSubmodules = true; + }; + json_serialization = pkgs.fetchgit { url = "https://github.com/status-im/nim-json-serialization"; rev = "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44"; @@ -150,6 +157,13 @@ fetchSubmodules = true; }; + brokers = pkgs.fetchgit { + url = "https://github.com/NagyZoltanPeter/nim-brokers.git"; + rev = "2093ca4d50e581adda73fee7fd16231f990f4cbe"; + sha256 = "0a4ix2q6riqfrd0hfnajisy159qdmk5imwzymppj23rwc8n7d2dx"; + fetchSubmodules = true; + }; + stint = pkgs.fetchgit { url = "https://github.com/status-im/nim-stint"; rev = "470b7892561b5179ab20bd389a69217d6213fe58"; @@ -262,6 +276,13 @@ fetchSubmodules = true; }; + sds = pkgs.fetchgit { + url = "https://github.com/logos-messaging/nim-sds.git"; + rev = "2e9a7683f0e180bf112135fae3a3803eed8490d4"; + sha256 = "1dbpvp3zhvdlfxdyggz5waga1vg3b6ndd3acfzhnx8k1wdr01c6f"; + fetchSubmodules = true; + }; + ffi = pkgs.fetchgit { url = "https://github.com/logos-messaging/nim-ffi"; rev = "06111de155253b34e47ed2aaed1d61d08d62cc1b"; diff --git a/tests/all_tests_waku.nim b/tests/all_tests_waku.nim index 879b1a55a..e64922f4c 100644 --- a/tests/all_tests_waku.nim +++ b/tests/all_tests_waku.nim @@ -85,3 +85,6 @@ import ./api/test_all # Waku tools tests import ./tools/test_all + +# Persistency library tests +import ./persistency/test_all diff --git a/tests/api/test_api_health.nim b/tests/api/test_api_health.nim index f3dd340af..d949db24f 100644 --- a/tests/api/test_api_health.nim +++ b/tests/api/test_api_health.nim @@ -2,11 +2,12 @@ import std/[options, sequtils, times] import chronos, testutils/unittests, stew/byteutils, libp2p/[switch, peerinfo] +import brokers/broker_context import ../testlib/[common, wakucore, wakunode, testasync] import waku, - waku/[waku_node, waku_core, waku_relay/protocol, common/broker/broker_context], + waku/[waku_node, waku_core, waku_relay/protocol], waku/node/health_monitor/[topic_health, health_status, protocol_health, health_report], waku/requests/health_requests, waku/requests/node_requests, @@ -43,7 +44,7 @@ proc waitForConnectionStatus( if not await future.withTimeout(TestTimeout): raiseAssert "Timeout waiting for status: " & $expected finally: - EventConnectionStatusChange.dropListener(brokerCtx, handle) + await EventConnectionStatusChange.dropListener(brokerCtx, handle) proc waitForShardHealthy( brokerCtx: BrokerContext @@ -67,7 +68,7 @@ proc waitForShardHealthy( else: raiseAssert "Timeout waiting for shard health event" finally: - EventShardTopicHealthChange.dropListener(brokerCtx, handle) + await EventShardTopicHealthChange.dropListener(brokerCtx, handle) suite "LM API health checking": var diff --git a/tests/api/test_api_receive.nim b/tests/api/test_api_receive.nim index 24251f161..d6aa954a4 100644 --- a/tests/api/test_api_receive.nim +++ b/tests/api/test_api_receive.nim @@ -3,6 +3,7 @@ import std/[options, sequtils, net, sets] import chronos, testutils/unittests, stew/byteutils import libp2p/[peerid, peerinfo, crypto/crypto] +import brokers/broker_context import ../testlib/[common, wakucore, wakunode, testasync] import ../waku_archive/archive_utils @@ -11,7 +12,6 @@ import waku/[ waku_node, waku_core, - common/broker/broker_context, events/message_events, waku_relay/protocol, waku_archive, @@ -52,8 +52,8 @@ proc newReceiveEventListenerManager( return manager -proc teardown(manager: ReceiveEventListenerManager) = - MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener) +proc teardown(manager: ReceiveEventListenerManager) {.async.} = + await MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener) proc waitForEvents( manager: ReceiveEventListenerManager, timeout: Duration @@ -182,7 +182,7 @@ suite "Messaging API, Receive Service (store recovery)": # listen before triggering store check let eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) defer: - eventManager.teardown() + await eventManager.teardown() # trigger store check, should recover and deliver via MessageReceivedEvent await subscriber.deliveryService.recvService.checkStore() diff --git a/tests/api/test_api_send.nim b/tests/api/test_api_send.nim index 28f0ca2ff..084119041 100644 --- a/tests/api/test_api_send.nim +++ b/tests/api/test_api_send.nim @@ -2,10 +2,10 @@ import std/strutils import chronos, testutils/unittests, stew/byteutils, libp2p/[switch, peerinfo] +import brokers/broker_context import ../testlib/[common, wakucore, wakunode, testasync] import ../waku_archive/archive_utils -import - waku, waku/[waku_node, waku_core, waku_relay/protocol, common/broker/broker_context] +import waku, waku/[waku_node, waku_core, waku_relay/protocol] import waku/factory/waku_conf import tools/confutils/cli_args @@ -77,10 +77,12 @@ proc newSendEventListenerManager(brokerCtx: BrokerContext): SendEventListenerMan return manager -proc teardown(manager: SendEventListenerManager) = - MessageSentEvent.dropListener(manager.brokerCtx, manager.sentListener) - MessageErrorEvent.dropListener(manager.brokerCtx, manager.errorListener) - MessagePropagatedEvent.dropListener(manager.brokerCtx, manager.propagatedListener) +proc teardown(manager: SendEventListenerManager) {.async.} = + await MessageSentEvent.dropListener(manager.brokerCtx, manager.sentListener) + await MessageErrorEvent.dropListener(manager.brokerCtx, manager.errorListener) + await MessagePropagatedEvent.dropListener( + manager.brokerCtx, manager.propagatedListener + ) proc waitForEvents( manager: SendEventListenerManager, timeout: Duration @@ -270,7 +272,7 @@ suite "Waku API - Send": let eventManager = newSendEventListenerManager(node.brokerCtx) defer: - eventManager.teardown() + await eventManager.teardown() let envelope = MessageEnvelope.init( ContentTopic("/waku/2/default-content/proto"), "test payload" @@ -302,7 +304,7 @@ suite "Waku API - Send": let eventManager = newSendEventListenerManager(node.brokerCtx) defer: - eventManager.teardown() + await eventManager.teardown() let envelope = MessageEnvelope.init( ContentTopic("/waku/2/default-content/proto"), "test payload" @@ -332,7 +334,7 @@ suite "Waku API - Send": let eventManager = newSendEventListenerManager(node.brokerCtx) defer: - eventManager.teardown() + await eventManager.teardown() let envelope = MessageEnvelope.init( ContentTopic("/waku/2/default-content/proto"), "test payload" @@ -362,7 +364,7 @@ suite "Waku API - Send": let eventManager = newSendEventListenerManager(node.brokerCtx) defer: - eventManager.teardown() + await eventManager.teardown() let envelope = MessageEnvelope.init( ContentTopic("/waku/2/default-content/proto"), "test payload" @@ -416,7 +418,7 @@ suite "Waku API - Send": let eventManager = newSendEventListenerManager(node.brokerCtx) defer: - eventManager.teardown() + await eventManager.teardown() let envelope = MessageEnvelope.init( ContentTopic("/waku/2/default-content/proto"), "test payload" diff --git a/tests/api/test_api_subscription.nim b/tests/api/test_api_subscription.nim index e0ceb9226..32d4e742f 100644 --- a/tests/api/test_api_subscription.nim +++ b/tests/api/test_api_subscription.nim @@ -3,6 +3,7 @@ import std/[strutils, sequtils, net, options, sets, tables] import chronos, testutils/unittests, stew/byteutils import libp2p/[peerid, peerinfo, multiaddress, crypto/crypto] +import brokers/broker_context import ../testlib/[common, wakucore, wakunode, testasync] import @@ -10,7 +11,6 @@ import waku/[ waku_node, waku_core, - common/broker/broker_context, events/message_events, waku_relay/protocol, node/kernel_api/filter, @@ -51,8 +51,8 @@ proc newReceiveEventListenerManager( return manager -proc teardown(manager: ReceiveEventListenerManager) = - MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener) +proc teardown(manager: ReceiveEventListenerManager) {.async.} = + await MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener) proc waitForEvents( manager: ReceiveEventListenerManager, timeout: Duration @@ -208,7 +208,7 @@ suite "Messaging API, SubscriptionManager": let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) defer: - eventManager.teardown() + await eventManager.teardown() discard (await net.publishToMesh(testTopic, "Hello, world!".toBytes())).expect( "Publish failed" @@ -229,7 +229,7 @@ suite "Messaging API, SubscriptionManager": let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) defer: - eventManager.teardown() + await eventManager.teardown() discard (await net.publishToMesh(ignoredTopic, "Ghost Msg".toBytes())).expect( "Publish failed" @@ -250,7 +250,7 @@ suite "Messaging API, SubscriptionManager": let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) defer: - eventManager.teardown() + await eventManager.teardown() discard (await net.publishToMesh(testTopic, "Should be dropped".toBytes())).expect( "Publish failed" @@ -271,7 +271,7 @@ suite "Messaging API, SubscriptionManager": let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) defer: - eventManager.teardown() + await eventManager.teardown() net.subscriber.unsubscribe(topicA).expect("failed to unsub A") @@ -298,7 +298,7 @@ suite "Messaging API, SubscriptionManager": let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) defer: - eventManager.teardown() + await eventManager.teardown() discard (await net.publishToMesh(glitchTopic, "Ghost Msg".toBytes())).expect( "Publish failed" @@ -322,7 +322,7 @@ suite "Messaging API, SubscriptionManager": (await net.publishToMesh(testTopic, "Msg 1".toBytes())).expect("Pub 1 failed") require await eventManager.waitForEvents(TestTimeout) - eventManager.teardown() + await eventManager.teardown() # Unsubscribe and verify teardown net.subscriber.unsubscribe(testTopic).expect("Unsub failed") @@ -332,7 +332,7 @@ suite "Messaging API, SubscriptionManager": (await net.publishToMesh(testTopic, "Ghost".toBytes())).expect("Ghost pub failed") check not await eventManager.waitForEvents(NegativeTestTimeout) - eventManager.teardown() + await eventManager.teardown() # Resubscribe (await net.subscriber.subscribe(testTopic)).expect("Resub failed") @@ -364,7 +364,7 @@ suite "Messaging API, SubscriptionManager": let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 2) defer: - eventManager.teardown() + await eventManager.teardown() discard (await net.publishToMesh(topicA, "Msg on Shard A".toBytes())).expect( "Publish A failed" @@ -400,7 +400,7 @@ suite "Messaging API, SubscriptionManager": # here we just give a chance for any messages that we don't expect to arrive await sleepAsync(1.seconds) - eventManager.teardown() + await eventManager.teardown() # weak check (but catches most bugs) require eventManager.receivedMessages.len == expected.len @@ -451,7 +451,7 @@ suite "Messaging API, SubscriptionManager": let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) defer: - eventManager.teardown() + await eventManager.teardown() discard (await net.publishToMeshAfterEdgeReady(testTopic, "Hello, edge!".toBytes())).expect( "Publish failed" @@ -472,7 +472,7 @@ suite "Messaging API, SubscriptionManager": let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) defer: - eventManager.teardown() + await eventManager.teardown() discard (await net.publishToMesh(ignoredTopic, "Ghost Msg".toBytes())).expect( "Publish failed" @@ -493,7 +493,7 @@ suite "Messaging API, SubscriptionManager": let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) defer: - eventManager.teardown() + await eventManager.teardown() discard (await net.publishToMesh(testTopic, "Should be dropped".toBytes())).expect( "Publish failed" @@ -517,7 +517,7 @@ suite "Messaging API, SubscriptionManager": let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) defer: - eventManager.teardown() + await eventManager.teardown() net.subscriber.unsubscribe(topicA).expect("failed to unsub A") @@ -546,7 +546,7 @@ suite "Messaging API, SubscriptionManager": ) require await eventManager.waitForEvents(TestTimeout) - eventManager.teardown() + await eventManager.teardown() net.subscriber.unsubscribe(testTopic).expect("Unsub failed") eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) @@ -555,7 +555,7 @@ suite "Messaging API, SubscriptionManager": (await net.publishToMesh(testTopic, "Ghost".toBytes())).expect("Ghost pub failed") check not await eventManager.waitForEvents(NegativeTestTimeout) - eventManager.teardown() + await eventManager.teardown() (await net.subscriber.subscribe(testTopic)).expect("Resub failed") eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) @@ -653,7 +653,7 @@ suite "Messaging API, SubscriptionManager": require await eventManager.waitForEvents(TestTimeout) check eventManager.receivedMessages[0].payload == "Before failover".toBytes() - eventManager.teardown() + await eventManager.teardown() # Disconnect meshBuddy from edge (keeps relay mesh alive for publishing) await subscriber.node.disconnectNode(meshBuddyPeerInfo) @@ -678,7 +678,7 @@ suite "Messaging API, SubscriptionManager": require await eventManager.waitForEvents(TestTimeout) check eventManager.receivedMessages[0].payload == "After failover".toBytes() - eventManager.teardown() + await eventManager.teardown() (await subscriber.stop()).expect("Failed to stop subscriber") await meshBuddy.stop() @@ -801,7 +801,7 @@ suite "Messaging API, SubscriptionManager": require await eventManager.waitForEvents(TestTimeout) check eventManager.receivedMessages[0].payload == "After replacement".toBytes() - eventManager.teardown() + await eventManager.teardown() (await subscriber.stop()).expect("Failed to stop subscriber") await sparePeer.stop() diff --git a/tests/common/test_all.nim b/tests/common/test_all.nim index d597a7424..1070c34e4 100644 --- a/tests/common/test_all.nim +++ b/tests/common/test_all.nim @@ -8,7 +8,4 @@ import ./test_parse_size, ./test_requestratelimiter, ./test_ratelimit_setting, - ./test_timed_map, - ./test_event_broker, - ./test_request_broker, - ./test_multi_request_broker + ./test_timed_map diff --git a/tests/common/test_event_broker.nim b/tests/common/test_event_broker.nim deleted file mode 100644 index bcd081f4f..000000000 --- a/tests/common/test_event_broker.nim +++ /dev/null @@ -1,201 +0,0 @@ -import chronos -import std/sequtils -import testutils/unittests - -import waku/common/broker/event_broker - -type ExternalDefinedEventType = object - label*: string - -EventBroker: - type IntEvent = int - -EventBroker: - type ExternalAliasEvent = distinct ExternalDefinedEventType - -EventBroker: - type SampleEvent = object - value*: int - label*: string - -EventBroker: - type BinaryEvent = object - flag*: bool - -EventBroker: - type RefEvent = ref object - payload*: seq[int] - -template waitForListeners() = - waitFor sleepAsync(1.milliseconds) - -suite "EventBroker": - test "delivers events to all listeners": - var seen: seq[(int, string)] = @[] - - discard SampleEvent.listen( - proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = - seen.add((evt.value, evt.label)) - ) - - discard SampleEvent.listen( - proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = - seen.add((evt.value * 2, evt.label & "!")) - ) - - let evt = SampleEvent(value: 5, label: "hi") - SampleEvent.emit(evt) - waitForListeners() - - check seen.len == 2 - check seen.anyIt(it == (5, "hi")) - check seen.anyIt(it == (10, "hi!")) - - SampleEvent.dropAllListeners() - - test "forget removes a single listener": - var counter = 0 - - let handleA = SampleEvent.listen( - proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = - inc counter - ) - - let handleB = SampleEvent.listen( - proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = - inc(counter, 2) - ) - - SampleEvent.dropListener(handleA.get()) - let eventVal = SampleEvent(value: 1, label: "one") - SampleEvent.emit(eventVal) - waitForListeners() - check counter == 2 - - SampleEvent.dropAllListeners() - - test "forgetAll clears every listener": - var triggered = false - - let handle1 = SampleEvent.listen( - proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = - triggered = true - ) - let handle2 = SampleEvent.listen( - proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = - discard - ) - - SampleEvent.dropAllListeners() - SampleEvent.emit(42, "noop") - SampleEvent.emit(label = "noop", value = 42) - waitForListeners() - check not triggered - - let freshHandle = SampleEvent.listen( - proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = - discard - ) - check freshHandle.get().id > 0'u64 - SampleEvent.dropListener(freshHandle.get()) - - test "broker helpers operate via typedesc": - var toggles: seq[bool] = @[] - - let handle = BinaryEvent.listen( - proc(evt: BinaryEvent): Future[void] {.async: (raises: []).} = - toggles.add(evt.flag) - ) - - BinaryEvent(flag: true).emit() - waitForListeners() - let binaryEvent = BinaryEvent(flag: false) - BinaryEvent.emit(binaryEvent) - waitForListeners() - - check toggles == @[true, false] - BinaryEvent.dropAllListeners() - - test "ref typed event": - var counter: int = 0 - - let handle = RefEvent.listen( - proc(evt: RefEvent): Future[void] {.async: (raises: []).} = - for n in evt.payload: - counter += n - ) - - RefEvent(payload: @[1, 2, 3]).emit() - waitForListeners() - RefEvent.emit(payload = @[4, 5, 6]) - waitForListeners() - - check counter == 21 # 1+2+3 + 4+5+6 - RefEvent.dropAllListeners() - - test "supports BrokerContext-scoped listeners": - SampleEvent.dropAllListeners() - - let ctxA = NewBrokerContext() - let ctxB = NewBrokerContext() - - var seenA: seq[int] = @[] - var seenB: seq[int] = @[] - - discard SampleEvent.listen( - ctxA, - proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = - seenA.add(evt.value), - ) - - discard SampleEvent.listen( - ctxB, - proc(evt: SampleEvent): Future[void] {.async: (raises: []).} = - seenB.add(evt.value), - ) - - SampleEvent.emit(ctxA, SampleEvent(value: 1, label: "a")) - SampleEvent.emit(ctxB, SampleEvent(value: 2, label: "b")) - waitForListeners() - - check seenA == @[1] - check seenB == @[2] - - SampleEvent.dropAllListeners(ctxA) - SampleEvent.emit(ctxA, SampleEvent(value: 3, label: "a2")) - SampleEvent.emit(ctxB, SampleEvent(value: 4, label: "b2")) - waitForListeners() - - check seenA == @[1] - check seenB == @[2, 4] - - SampleEvent.dropAllListeners(ctxB) - - test "supports non-object event types (auto-distinct)": - var seen: seq[int] = @[] - - discard IntEvent.listen( - proc(evt: IntEvent): Future[void] {.async: (raises: []).} = - seen.add(int(evt)) - ) - - IntEvent.emit(IntEvent(42)) - waitForListeners() - - check seen == @[42] - IntEvent.dropAllListeners() - - test "supports externally-defined type aliases (auto-distinct)": - var seen: seq[string] = @[] - - discard ExternalAliasEvent.listen( - proc(evt: ExternalAliasEvent): Future[void] {.async: (raises: []).} = - let base = ExternalDefinedEventType(evt) - seen.add(base.label) - ) - - ExternalAliasEvent.emit(ExternalAliasEvent(ExternalDefinedEventType(label: "x"))) - waitForListeners() - - check seen == @["x"] - ExternalAliasEvent.dropAllListeners() diff --git a/tests/common/test_multi_request_broker.nim b/tests/common/test_multi_request_broker.nim deleted file mode 100644 index 39ed90eea..000000000 --- a/tests/common/test_multi_request_broker.nim +++ /dev/null @@ -1,343 +0,0 @@ -{.used.} - -import testutils/unittests -import chronos -import std/sequtils -import std/strutils - -import waku/common/broker/multi_request_broker - -MultiRequestBroker: - type NoArgResponse = object - label*: string - - proc signatureFetch*(): Future[Result[NoArgResponse, string]] {.async.} - -MultiRequestBroker: - type ArgResponse = object - id*: string - - proc signatureFetch*( - suffix: string, numsuffix: int - ): Future[Result[ArgResponse, string]] {.async.} - -MultiRequestBroker: - type DualResponse = ref object - note*: string - suffix*: string - - proc signatureBase*(): Future[Result[DualResponse, string]] {.async.} - proc signatureWithInput*( - suffix: string - ): Future[Result[DualResponse, string]] {.async.} - -type ExternalBaseType = string - -MultiRequestBroker: - type NativeIntResponse = int - - proc signatureFetch*(): Future[Result[NativeIntResponse, string]] {.async.} - -MultiRequestBroker: - type ExternalAliasResponse = ExternalBaseType - - proc signatureFetch*(): Future[Result[ExternalAliasResponse, string]] {.async.} - -MultiRequestBroker: - type AlreadyDistinctResponse = distinct int - - proc signatureFetch*(): Future[Result[AlreadyDistinctResponse, string]] {.async.} - -suite "MultiRequestBroker": - test "aggregates zero-argument providers": - discard NoArgResponse.setProvider( - proc(): Future[Result[NoArgResponse, string]] {.async.} = - ok(NoArgResponse(label: "one")) - ) - - discard NoArgResponse.setProvider( - proc(): Future[Result[NoArgResponse, string]] {.async.} = - discard catch: - await sleepAsync(1.milliseconds) - ok(NoArgResponse(label: "two")) - ) - - let responses = waitFor NoArgResponse.request() - check responses.get().len == 2 - check responses.get().anyIt(it.label == "one") - check responses.get().anyIt(it.label == "two") - - NoArgResponse.clearProviders() - - test "aggregates argument providers": - discard ArgResponse.setProvider( - proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} = - ok(ArgResponse(id: suffix & "-a-" & $num)) - ) - - discard ArgResponse.setProvider( - proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} = - ok(ArgResponse(id: suffix & "-b-" & $num)) - ) - - let keyed = waitFor ArgResponse.request("topic", 1) - check keyed.get().len == 2 - check keyed.get().anyIt(it.id == "topic-a-1") - check keyed.get().anyIt(it.id == "topic-b-1") - - ArgResponse.clearProviders() - - test "clearProviders resets both provider lists": - discard DualResponse.setProvider( - proc(): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "base", suffix: "")) - ) - - discard DualResponse.setProvider( - proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "base" & suffix, suffix: suffix)) - ) - - let noArgs = waitFor DualResponse.request() - check noArgs.get().len == 1 - - let param = waitFor DualResponse.request("-extra") - check param.get().len == 1 - check param.get()[0].suffix == "-extra" - - DualResponse.clearProviders() - - let emptyNoArgs = waitFor DualResponse.request() - check emptyNoArgs.get().len == 0 - - let emptyWithArgs = waitFor DualResponse.request("-extra") - check emptyWithArgs.get().len == 0 - - test "request returns empty seq when no providers registered": - let empty = waitFor NoArgResponse.request() - check empty.get().len == 0 - - test "failed providers will fail the request": - NoArgResponse.clearProviders() - discard NoArgResponse.setProvider( - proc(): Future[Result[NoArgResponse, string]] {.async.} = - err("boom") - ) - - discard NoArgResponse.setProvider( - proc(): Future[Result[NoArgResponse, string]] {.async.} = - ok(NoArgResponse(label: "survivor")) - ) - - let filtered = waitFor NoArgResponse.request() - check filtered.isErr() - - NoArgResponse.clearProviders() - - test "deduplicates identical zero-argument providers": - NoArgResponse.clearProviders() - var invocations = 0 - let sharedHandler = proc(): Future[Result[NoArgResponse, string]] {.async.} = - inc invocations - ok(NoArgResponse(label: "dup")) - - let first = NoArgResponse.setProvider(sharedHandler) - let second = NoArgResponse.setProvider(sharedHandler) - - check first.get().id == second.get().id - check first.get().kind == second.get().kind - - let dupResponses = waitFor NoArgResponse.request() - check dupResponses.get().len == 1 - check invocations == 1 - - NoArgResponse.clearProviders() - - test "removeProvider deletes registered handlers": - var removedCalled = false - var keptCalled = false - - let removable = NoArgResponse.setProvider( - proc(): Future[Result[NoArgResponse, string]] {.async.} = - removedCalled = true - ok(NoArgResponse(label: "removed")) - ) - - discard NoArgResponse.setProvider( - proc(): Future[Result[NoArgResponse, string]] {.async.} = - keptCalled = true - ok(NoArgResponse(label: "kept")) - ) - - NoArgResponse.removeProvider(removable.get()) - - let afterRemoval = (waitFor NoArgResponse.request()).valueOr: - assert false, "request failed" - @[] - check afterRemoval.len == 1 - check afterRemoval[0].label == "kept" - check not removedCalled - check keptCalled - - NoArgResponse.clearProviders() - - test "removeProvider works for argument signatures": - var invoked: seq[string] = @[] - - discard ArgResponse.setProvider( - proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} = - invoked.add("first" & suffix) - ok(ArgResponse(id: suffix & "-one-" & $num)) - ) - - let handle = ArgResponse.setProvider( - proc(suffix: string, num: int): Future[Result[ArgResponse, string]] {.async.} = - invoked.add("second" & suffix) - ok(ArgResponse(id: suffix & "-two-" & $num)) - ) - - ArgResponse.removeProvider(handle.get()) - - let single = (waitFor ArgResponse.request("topic", 1)).valueOr: - assert false, "request failed" - @[] - check single.len == 1 - check single[0].id == "topic-one-1" - check invoked == @["firsttopic"] - - ArgResponse.clearProviders() - - test "catches exception from providers and report error": - let firstHandler = NoArgResponse.setProvider( - proc(): Future[Result[NoArgResponse, string]] {.async.} = - raise newException(ValueError, "first handler raised") - ) - - discard NoArgResponse.setProvider( - proc(): Future[Result[NoArgResponse, string]] {.async.} = - ok(NoArgResponse(label: "just ok")) - ) - - let afterException = waitFor NoArgResponse.request() - check afterException.isErr() - check afterException.error().contains("first handler raised") - - NoArgResponse.clearProviders() - - test "ref providers returning nil fail request": - DualResponse.clearProviders() - - test "supports native request types": - NativeIntResponse.clearProviders() - - discard NativeIntResponse.setProvider( - proc(): Future[Result[NativeIntResponse, string]] {.async.} = - ok(NativeIntResponse(1)) - ) - - discard NativeIntResponse.setProvider( - proc(): Future[Result[NativeIntResponse, string]] {.async.} = - ok(NativeIntResponse(2)) - ) - - let res = waitFor NativeIntResponse.request() - check res.isOk() - check res.get().len == 2 - check res.get().anyIt(int(it) == 1) - check res.get().anyIt(int(it) == 2) - - NativeIntResponse.clearProviders() - - test "supports external request types": - ExternalAliasResponse.clearProviders() - - discard ExternalAliasResponse.setProvider( - proc(): Future[Result[ExternalAliasResponse, string]] {.async.} = - ok(ExternalAliasResponse("hello")) - ) - - let res = waitFor ExternalAliasResponse.request() - check res.isOk() - check res.get().len == 1 - check ExternalBaseType(res.get()[0]) == "hello" - - ExternalAliasResponse.clearProviders() - - test "supports already-distinct request types": - AlreadyDistinctResponse.clearProviders() - - discard AlreadyDistinctResponse.setProvider( - proc(): Future[Result[AlreadyDistinctResponse, string]] {.async.} = - ok(AlreadyDistinctResponse(7)) - ) - - let res = waitFor AlreadyDistinctResponse.request() - check res.isOk() - check res.get().len == 1 - check int(res.get()[0]) == 7 - - AlreadyDistinctResponse.clearProviders() - - test "context-aware providers are isolated": - NoArgResponse.clearProviders() - let ctxA = NewBrokerContext() - let ctxB = NewBrokerContext() - - discard NoArgResponse.setProvider( - ctxA, - proc(): Future[Result[NoArgResponse, string]] {.async.} = - ok(NoArgResponse(label: "a")), - ) - discard NoArgResponse.setProvider( - ctxB, - proc(): Future[Result[NoArgResponse, string]] {.async.} = - ok(NoArgResponse(label: "b")), - ) - - let resA = waitFor NoArgResponse.request(ctxA) - check resA.isOk() - check resA.get().len == 1 - check resA.get()[0].label == "a" - - let resB = waitFor NoArgResponse.request(ctxB) - check resB.isOk() - check resB.get().len == 1 - check resB.get()[0].label == "b" - - let resDefault = waitFor NoArgResponse.request() - check resDefault.isOk() - check resDefault.get().len == 0 - - NoArgResponse.clearProviders(ctxA) - let clearedA = waitFor NoArgResponse.request(ctxA) - check clearedA.isOk() - check clearedA.get().len == 0 - - let stillB = waitFor NoArgResponse.request(ctxB) - check stillB.isOk() - check stillB.get().len == 1 - check stillB.get()[0].label == "b" - - NoArgResponse.clearProviders(ctxB) - - discard DualResponse.setProvider( - proc(): Future[Result[DualResponse, string]] {.async.} = - let nilResponse: DualResponse = nil - ok(nilResponse) - ) - - let zeroArg = waitFor DualResponse.request() - check zeroArg.isErr() - - DualResponse.clearProviders() - - discard DualResponse.setProvider( - proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = - let nilResponse: DualResponse = nil - ok(nilResponse) - ) - - let withInput = waitFor DualResponse.request("-extra") - check withInput.isErr() - - DualResponse.clearProviders() diff --git a/tests/common/test_request_broker.nim b/tests/common/test_request_broker.nim deleted file mode 100644 index b1e16979b..000000000 --- a/tests/common/test_request_broker.nim +++ /dev/null @@ -1,675 +0,0 @@ -{.used.} - -import testutils/unittests -import chronos -import std/strutils - -import waku/common/broker/request_broker - -## --------------------------------------------------------------------------- -## Async-mode brokers + tests -## --------------------------------------------------------------------------- - -RequestBroker: - type SimpleResponse = object - value*: string - - proc signatureFetch*(): Future[Result[SimpleResponse, string]] {.async.} - -RequestBroker: - type KeyedResponse = object - key*: string - payload*: string - - proc signatureFetchWithKey*( - key: string, subKey: int - ): Future[Result[KeyedResponse, string]] {.async.} - -RequestBroker: - type DualResponse = object - note*: string - count*: int - - proc signatureNoInput*(): Future[Result[DualResponse, string]] {.async.} - proc signatureWithInput*( - suffix: string - ): Future[Result[DualResponse, string]] {.async.} - -RequestBroker(async): - type ImplicitResponse = ref object - note*: string - -static: - doAssert typeof(SimpleResponse.request()) is Future[Result[SimpleResponse, string]] - -suite "RequestBroker macro (async mode)": - test "serves zero-argument providers": - check SimpleResponse - .setProvider( - proc(): Future[Result[SimpleResponse, string]] {.async.} = - ok(SimpleResponse(value: "hi")) - ) - .isOk() - - let res = waitFor SimpleResponse.request() - check res.isOk() - check res.value.value == "hi" - - SimpleResponse.clearProvider() - - test "zero-argument request errors when unset": - let res = waitFor SimpleResponse.request() - check res.isErr() - check res.error.contains("no zero-arg provider") - - test "serves input-based providers": - var seen: seq[string] = @[] - check KeyedResponse - .setProvider( - proc( - key: string, subKey: int - ): Future[Result[KeyedResponse, string]] {.async.} = - seen.add(key) - ok(KeyedResponse(key: key, payload: key & "-payload+" & $subKey)) - ) - .isOk() - - let res = waitFor KeyedResponse.request("topic", 1) - check res.isOk() - check res.value.key == "topic" - check res.value.payload == "topic-payload+1" - check seen == @["topic"] - - KeyedResponse.clearProvider() - - test "catches provider exception": - check KeyedResponse - .setProvider( - proc( - key: string, subKey: int - ): Future[Result[KeyedResponse, string]] {.async.} = - raise newException(ValueError, "simulated failure") - ) - .isOk() - - let res = waitFor KeyedResponse.request("neglected", 11) - check res.isErr() - check res.error.contains("simulated failure") - - KeyedResponse.clearProvider() - - test "input request errors when unset": - let res = waitFor KeyedResponse.request("foo", 2) - check res.isErr() - check res.error.contains("input signature") - - test "supports both provider types simultaneously": - check DualResponse - .setProvider( - proc(): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "base", count: 1)) - ) - .isOk() - - check DualResponse - .setProvider( - proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "base" & suffix, count: suffix.len)) - ) - .isOk() - - let noInput = waitFor DualResponse.request() - check noInput.isOk() - check noInput.value.note == "base" - - let withInput = waitFor DualResponse.request("-extra") - check withInput.isOk() - check withInput.value.note == "base-extra" - check withInput.value.count == 6 - - DualResponse.clearProvider() - - test "clearProvider resets both entries": - check DualResponse - .setProvider( - proc(): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "temp", count: 0)) - ) - .isOk() - DualResponse.clearProvider() - - let res = waitFor DualResponse.request() - check res.isErr() - - test "implicit zero-argument provider works by default": - check ImplicitResponse - .setProvider( - proc(): Future[Result[ImplicitResponse, string]] {.async.} = - ok(ImplicitResponse(note: "auto")) - ) - .isOk() - - let res = waitFor ImplicitResponse.request() - check res.isOk() - - ImplicitResponse.clearProvider() - check res.value.note == "auto" - - test "implicit zero-argument request errors when unset": - let res = waitFor ImplicitResponse.request() - check res.isErr() - check res.error.contains("no zero-arg provider") - - test "no provider override": - check DualResponse - .setProvider( - proc(): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "base", count: 1)) - ) - .isOk() - - check DualResponse - .setProvider( - proc(suffix: string): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "base" & suffix, count: suffix.len)) - ) - .isOk() - - let overrideProc = proc(): Future[Result[DualResponse, string]] {.async.} = - ok(DualResponse(note: "something else", count: 1)) - - check DualResponse.setProvider(overrideProc).isErr() - - let noInput = waitFor DualResponse.request() - check noInput.isOk() - check noInput.value.note == "base" - - let stillResponse = waitFor DualResponse.request(" still works") - check stillResponse.isOk() - check stillResponse.value.note.contains("base still works") - - DualResponse.clearProvider() - - let noResponse = waitFor DualResponse.request() - check noResponse.isErr() - check noResponse.error.contains("no zero-arg provider") - - let noResponseArg = waitFor DualResponse.request("Should not work") - check noResponseArg.isErr() - check noResponseArg.error.contains("no provider") - - check DualResponse.setProvider(overrideProc).isOk() - - let nowSuccWithOverride = waitFor DualResponse.request() - check nowSuccWithOverride.isOk() - check nowSuccWithOverride.value.note == "something else" - check nowSuccWithOverride.value.count == 1 - - DualResponse.clearProvider() - - test "supports keyed providers (async, zero-arg)": - SimpleResponse.clearProvider() - - check SimpleResponse - .setProvider( - proc(): Future[Result[SimpleResponse, string]] {.async.} = - ok(SimpleResponse(value: "default")) - ) - .isOk() - - check SimpleResponse - .setProvider( - BrokerContext(0x11111111'u32), - proc(): Future[Result[SimpleResponse, string]] {.async.} = - ok(SimpleResponse(value: "one")), - ) - .isOk() - - check SimpleResponse - .setProvider( - BrokerContext(0x22222222'u32), - proc(): Future[Result[SimpleResponse, string]] {.async.} = - ok(SimpleResponse(value: "two")), - ) - .isOk() - - let defaultRes = waitFor SimpleResponse.request() - check defaultRes.isOk() - check defaultRes.value.value == "default" - - let res1 = waitFor SimpleResponse.request(BrokerContext(0x11111111'u32)) - check res1.isOk() - check res1.value.value == "one" - - let res2 = waitFor SimpleResponse.request(BrokerContext(0x22222222'u32)) - check res2.isOk() - check res2.value.value == "two" - - let missing = waitFor SimpleResponse.request(BrokerContext(0x33333333'u32)) - check missing.isErr() - check missing.error.contains("no provider registered for broker context") - - check SimpleResponse - .setProvider( - BrokerContext(0x11111111'u32), - proc(): Future[Result[SimpleResponse, string]] {.async.} = - ok(SimpleResponse(value: "dup")), - ) - .isErr() - - SimpleResponse.clearProvider() - - test "supports keyed providers (async, with args)": - KeyedResponse.clearProvider() - - check KeyedResponse - .setProvider( - proc( - key: string, subKey: int - ): Future[Result[KeyedResponse, string]] {.async.} = - ok(KeyedResponse(key: "default-" & key, payload: $subKey)) - ) - .isOk() - - check KeyedResponse - .setProvider( - BrokerContext(0xABCDEF01'u32), - proc( - key: string, subKey: int - ): Future[Result[KeyedResponse, string]] {.async.} = - ok(KeyedResponse(key: "k1-" & key, payload: "p" & $subKey)), - ) - .isOk() - - check KeyedResponse - .setProvider( - BrokerContext(0xABCDEF02'u32), - proc( - key: string, subKey: int - ): Future[Result[KeyedResponse, string]] {.async.} = - ok(KeyedResponse(key: "k2-" & key, payload: "q" & $subKey)), - ) - .isOk() - - let d = waitFor KeyedResponse.request("topic", 7) - check d.isOk() - check d.value.key == "default-topic" - - let k1 = waitFor KeyedResponse.request(BrokerContext(0xABCDEF01'u32), "topic", 7) - check k1.isOk() - check k1.value.key == "k1-topic" - check k1.value.payload == "p7" - - let k2 = waitFor KeyedResponse.request(BrokerContext(0xABCDEF02'u32), "topic", 7) - check k2.isOk() - check k2.value.key == "k2-topic" - check k2.value.payload == "q7" - - let miss = waitFor KeyedResponse.request(BrokerContext(0xDEADBEEF'u32), "topic", 7) - check miss.isErr() - check miss.error.contains("no provider registered for broker context") - - KeyedResponse.clearProvider() - -## --------------------------------------------------------------------------- -## Sync-mode brokers + tests -## --------------------------------------------------------------------------- - -RequestBroker(sync): - type SimpleResponseSync = object - value*: string - - proc signatureFetch*(): Result[SimpleResponseSync, string] - -RequestBroker(sync): - type KeyedResponseSync = object - key*: string - payload*: string - - proc signatureFetchWithKey*( - key: string, subKey: int - ): Result[KeyedResponseSync, string] - -RequestBroker(sync): - type DualResponseSync = object - note*: string - count*: int - - proc signatureNoInput*(): Result[DualResponseSync, string] - proc signatureWithInput*(suffix: string): Result[DualResponseSync, string] - -RequestBroker(sync): - type ImplicitResponseSync = ref object - note*: string - -static: - doAssert typeof(SimpleResponseSync.request()) is Result[SimpleResponseSync, string] - doAssert not ( - typeof(SimpleResponseSync.request()) is Future[Result[SimpleResponseSync, string]] - ) - doAssert typeof(KeyedResponseSync.request("topic", 1)) is - Result[KeyedResponseSync, string] - -suite "RequestBroker macro (sync mode)": - test "serves zero-argument providers (sync)": - check SimpleResponseSync - .setProvider( - proc(): Result[SimpleResponseSync, string] = - ok(SimpleResponseSync(value: "hi")) - ) - .isOk() - - let res = SimpleResponseSync.request() - check res.isOk() - check res.value.value == "hi" - - SimpleResponseSync.clearProvider() - - test "zero-argument request errors when unset (sync)": - let res = SimpleResponseSync.request() - check res.isErr() - check res.error.contains("no zero-arg provider") - - test "serves input-based providers (sync)": - var seen: seq[string] = @[] - check KeyedResponseSync - .setProvider( - proc(key: string, subKey: int): Result[KeyedResponseSync, string] = - seen.add(key) - ok(KeyedResponseSync(key: key, payload: key & "-payload+" & $subKey)) - ) - .isOk() - - let res = KeyedResponseSync.request("topic", 1) - check res.isOk() - check res.value.key == "topic" - check res.value.payload == "topic-payload+1" - check seen == @["topic"] - - KeyedResponseSync.clearProvider() - - test "catches provider exception (sync)": - check KeyedResponseSync - .setProvider( - proc(key: string, subKey: int): Result[KeyedResponseSync, string] = - raise newException(ValueError, "simulated failure") - ) - .isOk() - - let res = KeyedResponseSync.request("neglected", 11) - check res.isErr() - check res.error.contains("simulated failure") - - KeyedResponseSync.clearProvider() - - test "input request errors when unset (sync)": - let res = KeyedResponseSync.request("foo", 2) - check res.isErr() - check res.error.contains("input signature") - - test "supports both provider types simultaneously (sync)": - check DualResponseSync - .setProvider( - proc(): Result[DualResponseSync, string] = - ok(DualResponseSync(note: "base", count: 1)) - ) - .isOk() - - check DualResponseSync - .setProvider( - proc(suffix: string): Result[DualResponseSync, string] = - ok(DualResponseSync(note: "base" & suffix, count: suffix.len)) - ) - .isOk() - - let noInput = DualResponseSync.request() - check noInput.isOk() - check noInput.value.note == "base" - - let withInput = DualResponseSync.request("-extra") - check withInput.isOk() - check withInput.value.note == "base-extra" - check withInput.value.count == 6 - - DualResponseSync.clearProvider() - - test "clearProvider resets both entries (sync)": - check DualResponseSync - .setProvider( - proc(): Result[DualResponseSync, string] = - ok(DualResponseSync(note: "temp", count: 0)) - ) - .isOk() - DualResponseSync.clearProvider() - - let res = DualResponseSync.request() - check res.isErr() - - test "implicit zero-argument provider works by default (sync)": - check ImplicitResponseSync - .setProvider( - proc(): Result[ImplicitResponseSync, string] = - ok(ImplicitResponseSync(note: "auto")) - ) - .isOk() - - let res = ImplicitResponseSync.request() - check res.isOk() - - ImplicitResponseSync.clearProvider() - check res.value.note == "auto" - - test "implicit zero-argument request errors when unset (sync)": - let res = ImplicitResponseSync.request() - check res.isErr() - check res.error.contains("no zero-arg provider") - - test "implicit zero-argument provider raises error (sync)": - check ImplicitResponseSync - .setProvider( - proc(): Result[ImplicitResponseSync, string] = - raise newException(ValueError, "simulated failure") - ) - .isOk() - - let res = ImplicitResponseSync.request() - check res.isErr() - check res.error.contains("simulated failure") - - ImplicitResponseSync.clearProvider() - - test "supports keyed providers (sync, zero-arg)": - SimpleResponseSync.clearProvider() - - check SimpleResponseSync - .setProvider( - proc(): Result[SimpleResponseSync, string] = - ok(SimpleResponseSync(value: "default")) - ) - .isOk() - - check SimpleResponseSync - .setProvider( - BrokerContext(0x10101010'u32), - proc(): Result[SimpleResponseSync, string] = - ok(SimpleResponseSync(value: "ten")), - ) - .isOk() - - let defaultRes = SimpleResponseSync.request() - check defaultRes.isOk() - check defaultRes.value.value == "default" - - let keyedRes = SimpleResponseSync.request(BrokerContext(0x10101010'u32)) - check keyedRes.isOk() - check keyedRes.value.value == "ten" - - let miss = SimpleResponseSync.request(BrokerContext(0x20202020'u32)) - check miss.isErr() - check miss.error.contains("no provider registered for broker context") - - SimpleResponseSync.clearProvider() - - test "supports keyed providers (sync, with args)": - KeyedResponseSync.clearProvider() - - check KeyedResponseSync - .setProvider( - proc(key: string, subKey: int): Result[KeyedResponseSync, string] = - ok(KeyedResponseSync(key: "default-" & key, payload: $subKey)) - ) - .isOk() - - check KeyedResponseSync - .setProvider( - BrokerContext(0xA0A0A0A0'u32), - proc(key: string, subKey: int): Result[KeyedResponseSync, string] = - ok(KeyedResponseSync(key: "k-" & key, payload: "p" & $subKey)), - ) - .isOk() - - let d = KeyedResponseSync.request("topic", 2) - check d.isOk() - check d.value.key == "default-topic" - - let keyed = KeyedResponseSync.request(BrokerContext(0xA0A0A0A0'u32), "topic", 2) - check keyed.isOk() - check keyed.value.key == "k-topic" - check keyed.value.payload == "p2" - - let miss = KeyedResponseSync.request(BrokerContext(0xB0B0B0B0'u32), "topic", 2) - check miss.isErr() - check miss.error.contains("no provider registered for broker context") - - KeyedResponseSync.clearProvider() - -## --------------------------------------------------------------------------- -## POD / external type brokers + tests (distinct/alias behavior) -## --------------------------------------------------------------------------- - -type ExternalDefinedTypeAsync = object - label*: string - -type ExternalDefinedTypeSync = object - label*: string - -type ExternalDefinedTypeShared = object - label*: string - -RequestBroker: - type PodResponse = int - - proc signatureFetch*(): Future[Result[PodResponse, string]] {.async.} - -RequestBroker: - type ExternalAliasedResponse = ExternalDefinedTypeAsync - - proc signatureFetch*(): Future[Result[ExternalAliasedResponse, string]] {.async.} - -RequestBroker(sync): - type ExternalAliasedResponseSync = ExternalDefinedTypeSync - - proc signatureFetch*(): Result[ExternalAliasedResponseSync, string] - -RequestBroker(sync): - type DistinctStringResponseA = distinct string - -RequestBroker(sync): - type DistinctStringResponseB = distinct string - -RequestBroker(sync): - type ExternalDistinctResponseA = distinct ExternalDefinedTypeShared - -RequestBroker(sync): - type ExternalDistinctResponseB = distinct ExternalDefinedTypeShared - -suite "RequestBroker macro (POD/external types)": - test "supports non-object response types (async)": - check PodResponse - .setProvider( - proc(): Future[Result[PodResponse, string]] {.async.} = - ok(PodResponse(123)) - ) - .isOk() - - let res = waitFor PodResponse.request() - check res.isOk() - check int(res.value) == 123 - - PodResponse.clearProvider() - - test "supports aliased external types (async)": - check ExternalAliasedResponse - .setProvider( - proc(): Future[Result[ExternalAliasedResponse, string]] {.async.} = - ok(ExternalAliasedResponse(ExternalDefinedTypeAsync(label: "ext"))) - ) - .isOk() - - let res = waitFor ExternalAliasedResponse.request() - check res.isOk() - check ExternalDefinedTypeAsync(res.value).label == "ext" - - ExternalAliasedResponse.clearProvider() - - test "supports aliased external types (sync)": - check ExternalAliasedResponseSync - .setProvider( - proc(): Result[ExternalAliasedResponseSync, string] = - ok(ExternalAliasedResponseSync(ExternalDefinedTypeSync(label: "ext"))) - ) - .isOk() - - let res = ExternalAliasedResponseSync.request() - check res.isOk() - check ExternalDefinedTypeSync(res.value).label == "ext" - - ExternalAliasedResponseSync.clearProvider() - - test "distinct response types avoid overload ambiguity (sync)": - check DistinctStringResponseA - .setProvider( - proc(): Result[DistinctStringResponseA, string] = - ok(DistinctStringResponseA("a")) - ) - .isOk() - - check DistinctStringResponseB - .setProvider( - proc(): Result[DistinctStringResponseB, string] = - ok(DistinctStringResponseB("b")) - ) - .isOk() - - check ExternalDistinctResponseA - .setProvider( - proc(): Result[ExternalDistinctResponseA, string] = - ok(ExternalDistinctResponseA(ExternalDefinedTypeShared(label: "ea"))) - ) - .isOk() - - check ExternalDistinctResponseB - .setProvider( - proc(): Result[ExternalDistinctResponseB, string] = - ok(ExternalDistinctResponseB(ExternalDefinedTypeShared(label: "eb"))) - ) - .isOk() - - let resA = DistinctStringResponseA.request() - let resB = DistinctStringResponseB.request() - check resA.isOk() - check resB.isOk() - check string(resA.value) == "a" - check string(resB.value) == "b" - - let resEA = ExternalDistinctResponseA.request() - let resEB = ExternalDistinctResponseB.request() - check resEA.isOk() - check resEB.isOk() - check ExternalDefinedTypeShared(resEA.value).label == "ea" - check ExternalDefinedTypeShared(resEB.value).label == "eb" - - DistinctStringResponseA.clearProvider() - DistinctStringResponseB.clearProvider() - ExternalDistinctResponseA.clearProvider() - ExternalDistinctResponseB.clearProvider() diff --git a/tests/node/test_wakunode_health_monitor.nim b/tests/node/test_wakunode_health_monitor.nim index 8a3ddd104..08f641a75 100644 --- a/tests/node/test_wakunode_health_monitor.nim +++ b/tests/node/test_wakunode_health_monitor.nim @@ -2,6 +2,7 @@ import std/[json, options, sequtils, strutils, tables], testutils/unittests, chronos, results +import brokers/broker_context import waku/[ @@ -23,7 +24,6 @@ import events/health_events, events/peer_events, waku_archive, - common/broker/broker_context, ] import ../testlib/[wakunode, wakucore], ../waku_archive/archive_utils @@ -277,7 +277,7 @@ suite "Health Monitor - events": await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()]) let metadataOk = await metadataFut.withTimeout(TestConnectivityTimeLimit) - WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis) + await WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis) require metadataOk let connectTimeLimit = Moment.now() + TestConnectivityTimeLimit @@ -380,7 +380,7 @@ suite "Health Monitor - events": await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()]) let metadataOk = await metadataFut.withTimeout(TestConnectivityTimeLimit) - WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis) + await WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis) require metadataOk var deadline = Moment.now() + TestConnectivityTimeLimit @@ -413,7 +413,7 @@ suite "Health Monitor - events": subMgr.subscribe(contentTopic).expect("Failed to subscribe") let shardHealthOk = await shardHealthFut.withTimeout(TestConnectivityTimeLimit) - EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis) + await EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis) check shardHealthOk == true check subMgr.edgeFilterSubStates.len > 0 diff --git a/tests/persistency/test_all.nim b/tests/persistency/test_all.nim new file mode 100644 index 000000000..194977692 --- /dev/null +++ b/tests/persistency/test_all.nim @@ -0,0 +1,9 @@ +{.used.} + +import ./test_keys +import ./test_backend +import ./test_lifecycle +import ./test_facade +import ./test_encoding +import ./test_string_lookup +import ./test_singleton diff --git a/tests/persistency/test_backend.nim b/tests/persistency/test_backend.nim new file mode 100644 index 000000000..e5689d95f --- /dev/null +++ b/tests/persistency/test_backend.nim @@ -0,0 +1,195 @@ +{.used.} + +import std/options +import results +import testutils/unittests +import waku/persistency/[types, keys, backend_sqlite] + +template str(b: seq[byte]): string = + var s = newString(b.len) + for i, x in b: + s[i] = char(x) + s + +proc payload(s: string): seq[byte] = + result = newSeq[byte](s.len) + for i, c in s: + result[i] = byte(c) + +suite "Persistency SQLite backend": + test "open in-memory backend and round-trip a single value": + let b = openBackendInMemory().get() + defer: + b.close() + + b + .applyOps( + [ + TxOp( + category: "msg", + key: key("c1", 1'i64), + kind: txPut, + payload: payload("hello"), + ) + ] + ) + .get() + + let got = b.getOne("msg", key("c1", 1'i64)).get() + check got.isSome + check str(got.get) == "hello" + + check b.existsOne("msg", key("c1", 1'i64)).get() + check not b.existsOne("msg", key("c1", 2'i64)).get() + + test "INSERT OR REPLACE overwrites payload for the same key": + let b = openBackendInMemory().get() + defer: + b.close() + let k = key("c1", 1'i64) + b.applyOps([TxOp(category: "msg", key: k, kind: txPut, payload: payload("v1"))]).get() + b.applyOps([TxOp(category: "msg", key: k, kind: txPut, payload: payload("v2"))]).get() + check str(b.getOne("msg", k).get().get) == "v2" + + test "deleteOne reports whether the row existed": + let b = openBackendInMemory().get() + defer: + b.close() + let k = key("c1", 1'i64) + check not b.deleteOne("msg", k).get() + b.applyOps([TxOp(category: "msg", key: k, kind: txPut, payload: payload("x"))]).get() + check b.deleteOne("msg", k).get() + check not b.existsOne("msg", k).get() + + test "applyOps batches multiple ops atomically": + let b = openBackendInMemory().get() + defer: + b.close() + b + .applyOps( + [ + TxOp( + category: "msg", key: key("c1", 1'i64), kind: txPut, payload: payload("a") + ), + TxOp( + category: "msg", key: key("c1", 2'i64), kind: txPut, payload: payload("b") + ), + TxOp( + category: "msg", key: key("c1", 3'i64), kind: txPut, payload: payload("c") + ), + ] + ) + .get() + check b.countRange("msg", prefixRange(key("c1"))).get() == 3 + + test "scanRange ascending yields rows in key order": + let b = openBackendInMemory().get() + defer: + b.close() + let inserts = @[5'i64, 1, 4, 2, 3] + var ops: seq[TxOp] = @[] + for i in inserts: + ops.add( + TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i)) + ) + b.applyOps(ops).get() + + let rows = b.scanRange("msg", prefixRange(key("c1"))).get() + check rows.len == 5 + var seenOrder: seq[string] + for r in rows: + seenOrder.add(str(r.payload)) + check seenOrder == @["1", "2", "3", "4", "5"] + + test "scanRange descending yields rows in reverse key order": + let b = openBackendInMemory().get() + defer: + b.close() + for i in [1'i64, 2, 3]: + b + .applyOps( + [TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i))] + ) + .get() + let rows = b.scanRange("msg", prefixRange(key("c1")), reverse = true).get() + check rows.len == 3 + check str(rows[0].payload) == "3" + check str(rows[2].payload) == "1" + + test "scanRange respects half-open [start, stop) bounds": + let b = openBackendInMemory().get() + defer: + b.close() + for i in [1'i64, 2, 3, 4, 5]: + b + .applyOps( + [TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i))] + ) + .get() + let rng = KeyRange(start: key("c1", 2'i64), stop: key("c1", 4'i64)) + let rows = b.scanRange("msg", rng).get() + check rows.len == 2 # 2 and 3, not 4 + check str(rows[0].payload) == "2" + check str(rows[1].payload) == "3" + + test "scanRange with empty stop is open-ended": + let b = openBackendInMemory().get() + defer: + b.close() + for i in [1'i64, 2, 3]: + b + .applyOps( + [TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i))] + ) + .get() + let rng = KeyRange(start: key("c1", 2'i64), stop: rawKey(@[])) + let rows = b.scanRange("msg", rng).get() + check rows.len == 2 + check str(rows[1].payload) == "3" + + test "categories isolate keyspaces": + let b = openBackendInMemory().get() + defer: + b.close() + let k = key("c1", 1'i64) + b + .applyOps( + [ + TxOp(category: "log", key: k, kind: txPut, payload: payload("log-1")), + TxOp( + category: "outgoing", key: k, kind: txPut, payload: payload("outgoing-1") + ), + ] + ) + .get() + check str(b.getOne("log", k).get().get) == "log-1" + check str(b.getOne("outgoing", k).get().get) == "outgoing-1" + check b.countRange("log", prefixRange(key("c1"))).get() == 1 + check b.countRange("outgoing", prefixRange(key("c1"))).get() == 1 + + test "txDelete inside a batch removes the row": + let b = openBackendInMemory().get() + defer: + b.close() + let k = key("c1", 1'i64) + b + .applyOps( + [ + TxOp(category: "msg", key: k, kind: txPut, payload: payload("v")), + TxOp(category: "msg", key: k, kind: txDelete), + ] + ) + .get() + check not b.existsOne("msg", k).get() + + test "missing key returns none": + let b = openBackendInMemory().get() + defer: + b.close() + check b.getOne("msg", key("nope")).get().isNone + + test "countRange of empty category is zero": + let b = openBackendInMemory().get() + defer: + b.close() + check b.countRange("msg", prefixRange(key("c1"))).get() == 0 diff --git a/tests/persistency/test_encoding.nim b/tests/persistency/test_encoding.nim new file mode 100644 index 000000000..22bd58209 --- /dev/null +++ b/tests/persistency/test_encoding.nim @@ -0,0 +1,154 @@ +{.used.} + +import std/[algorithm, options, os, times] +import chronos, results +import testutils/unittests +import waku/persistency/persistency + +# Reusable byte-wise comparator (Key has its own `<`, but we sometimes +# want to sort `seq[Key]` here without relying on it for double-checking). +proc cmpBytes(a, b: Key): int = + let ab = bytes(a) + let bb = bytes(b) + let n = min(ab.len, bb.len) + for i in 0 ..< n: + if ab[i] != bb[i]: + return cmp(ab[i], bb[i]) + cmp(ab.len, bb.len) + +template str(b: seq[byte]): string = + var s = newString(b.len) + for i, x in b: + s[i] = char(x) + s + +# Shared payload types used by multiple tests. +type + Mood = enum + moodCalm + moodHappy + moodAngry + + Header = object + sender: string + epoch: int64 + + Msg = object + header: Header + mood: Mood + body: seq[byte] + +suite "Persistency generic encoding": + # ── Key macro: composite types ──────────────────────────────────────── + + test "key macro accepts plain tuples": + let k1 = key(("ch", 1'i64)) + let k2 = key("ch", 1'i64) + # A plain tuple is encoded field-by-field, so the result is identical + # to passing the fields directly. + check k1 == k2 + + test "key macro accepts named tuples": + type Coord = tuple[lane: string, seqNum: int64] + let k = key((lane: "a", seqNum: 7'i64)) + let kFlat = key("a", 7'i64) + check k == kFlat + + test "key macro accepts a user object": + let k1 = key(Header(sender: "alice", epoch: 5'i64)) + let k2 = key("alice", 5'i64) + check k1 == k2 + + test "key macro accepts nested object inside another arg": + let k1 = key("v1", Header(sender: "alice", epoch: 5'i64)) + let k2 = key("v1", "alice", 5'i64) + check k1 == k2 + + test "key macro encodes enums": + let k1 = key(moodAngry) + let k2 = key(int64(ord(moodAngry))) + check k1 == k2 + + test "toKey is equivalent to single-arg key()": + check toKey("x") == key("x") + check toKey(42'i64) == key(42'i64) + check toKey(Header(sender: "a", epoch: 1)) == key("a", 1'i64) + + test "tuple-encoded keys preserve field-major sort order": + let inputs = @[ + key(("a", 0'i64)), + key(("a", 1'i64)), + key(("a", int64.high)), + key(("b", int64.low)), + key(("b", 0'i64)), + ] + var shuffled = @[inputs[3], inputs[0], inputs[4], inputs[2], inputs[1]] + shuffled.sort(cmpBytes) + check shuffled == inputs + + test "embedded Key encodes verbatim": + let inner = key("a", 7'i64) + let outer = key("prefix", inner) + # Expanded: bytes of "prefix" + raw bytes of inner. + let expanded = key("prefix", "a", 7'i64) + check outer == expanded + + # ── Payload macro / toPayload ───────────────────────────────────────── + + test "toPayload encodes primitives": + check str(toPayload("hi")).len == 4 # 2-byte len prefix + 2 chars + check toPayload(42'i64).len == 8 + check toPayload(true) == @[1'u8] + check toPayload(false) == @[0'u8] + + test "toPayload encodes objects field-by-field": + let m = Msg( + header: Header(sender: "alice", epoch: 9'i64), + mood: moodHappy, + body: @[0xAA'u8, 0xBB, 0xCC], + ) + let p = toPayload(m) + let pManual = payload("alice", 9'i64, int64(ord(moodHappy)), @[0xAA'u8, 0xBB, 0xCC]) + check p == pManual + + test "payload macro concatenates parts": + let p = payload("v1", 1'i64, @[0xDE'u8, 0xAD]) + # Same as building each piece separately. + var expected: seq[byte] = @[] + encodePart(expected, "v1") + encodePart(expected, 1'i64) + encodePart(expected, @[0xDE'u8, 0xAD]) + check p == expected + + # ── End-to-end through the facade ───────────────────────────────────── + + asyncTest "persistEncoded round-trips a struct through SQLite": + let root = getTempDir() / ("persistency_enc_" & $epochTime().int) + removeDir(root) + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let job = p.openJob("t").get() + + let m = Msg( + header: Header(sender: "alice", epoch: 1'i64), + mood: moodHappy, + body: @[1'u8, 2, 3], + ) + let k = key("channel-42", m.header.epoch) + await job.persistEncoded("msg", k, m) + + # Poll for the row, then read it back as raw bytes. + let deadline = epochTime() + 1.0 + var got: Option[seq[byte]] + while epochTime() < deadline: + let r = await job.get("msg", k) + check r.isOk + got = r.get() + if got.isSome: + break + await sleepAsync(chronos.milliseconds(2)) + check got.isSome + check got.get == toPayload(m) diff --git a/tests/persistency/test_facade.nim b/tests/persistency/test_facade.nim new file mode 100644 index 000000000..5b5f9eac1 --- /dev/null +++ b/tests/persistency/test_facade.nim @@ -0,0 +1,196 @@ +{.used.} + +import std/[options, os, strutils, times] +import chronos, results +import testutils/unittests +import waku/persistency/persistency + +proc payload(s: string): seq[byte] = + result = newSeq[byte](s.len) + for i, c in s: + result[i] = byte(c) + +template str(b: seq[byte]): string = + var s = newString(b.len) + for i, x in b: + s[i] = char(x) + s + +proc tmpRoot(label: string): string = + let p = getTempDir() / ("persistency_facade_" & label & "_" & $epochTime().int) + removeDir(p) + p + +# Bounded poll on exists() to bridge the documented persist->read race. +proc waitUntilExists( + t: Job, category: string, k: Key, timeoutMs = 1000 +): Future[bool] {.async.} = + let deadline = epochTime() + (timeoutMs.float / 1000.0) + while epochTime() < deadline: + let r = await t.exists(category, k) + if r.isOk and r.get(): + return true + await sleepAsync(chronos.milliseconds(2)) + return false + +suite "Persistency facade": + asyncTest "persistPut then get round-trips": + let root = tmpRoot("put_get") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + let k = key("c", 1'i64) + await t.persistPut("msg", k, payload("hi")) + let ckOk1 = await t.waitUntilExists("msg", k) + check ckOk1 + + let aw1 = await t.get("msg", k) + let got = aw1.get() + check got.isSome + check str(got.get) == "hi" + + asyncTest "persist (batch) is atomic and visible together": + let root = tmpRoot("batch") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + var ops: seq[TxOp] + for i in 1'i64 .. 4: + ops.add( + TxOp(category: "msg", key: key("c", i), kind: txPut, payload: payload($i)) + ) + await t.persist(ops) + let ckOk2 = await t.waitUntilExists("msg", key("c", 4'i64)) + check ckOk2 + + let aw2 = await t.count("msg", prefixRange(key("c"))) + let cnt = aw2.get() + check cnt == 4 + + asyncTest "scanPrefix returns rows in key order": + let root = tmpRoot("scan") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + for i in [3'i64, 1, 4, 1, 5, 9, 2]: + await t.persistPut("msg", key("c", i), payload($i)) + let ckOk3 = await t.waitUntilExists("msg", key("c", 9'i64)) + check ckOk3 + + let aw3 = await t.scanPrefix("msg", key("c")) + let rows = aw3.get() + # 7 ops with duplicate key i=1 -> 6 distinct rows + check rows.len == 6 + + var seenOrder: seq[int] + for r in rows: + seenOrder.add(parseInt(str(r.payload))) + check seenOrder == @[1, 2, 3, 4, 5, 9] + + asyncTest "scanPrefix reverse=true returns rows in reverse order": + let root = tmpRoot("scan_rev") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + for i in 1'i64 .. 3: + await t.persistPut("msg", key("c", i), payload($i)) + let ckOk4 = await t.waitUntilExists("msg", key("c", 3'i64)) + check ckOk4 + + let aw4 = await t.scanPrefix("msg", key("c"), reverse = true) + let rows = aw4.get() + check rows.len == 3 + check str(rows[0].payload) == "3" + check str(rows[2].payload) == "1" + + asyncTest "deleteAcked round-trips and reports row presence": + let root = tmpRoot("delete") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + let k = key("c", 1'i64) + let aw5 = await t.deleteAcked("msg", k) + let miss = aw5.get() + check miss == false + + await t.persistPut("msg", k, payload("v")) + let ckOk5 = await t.waitUntilExists("msg", k) + check ckOk5 + + let aw6 = await t.deleteAcked("msg", k) + let hit = aw6.get() + check hit == true + let aw7 = await t.exists("msg", k) + check aw7.get() == false + + asyncTest "persistDelete fire-and-forget removes the row": + let root = tmpRoot("fadel") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + let k = key("c", 1'i64) + await t.persistPut("msg", k, payload("v")) + let ckOk6 = await t.waitUntilExists("msg", k) + check ckOk6 + await t.persistDelete("msg", k) + # Poll for absence. + let deadline = epochTime() + 1.0 + var gone = false + while epochTime() < deadline: + let aw8 = await t.exists("msg", k) + if not aw8.get(): + gone = true + break + await sleepAsync(chronos.milliseconds(2)) + check gone + + asyncTest "two jobs do not see each other's data via the facade": + let root = tmpRoot("iso") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let a = p.openJob("a").get() + let b = p.openJob("b").get() + + let k = key("c", 1'i64) + await a.persistPut("msg", k, payload("A")) + await b.persistPut("msg", k, payload("B")) + let ckOk7 = await a.waitUntilExists("msg", k) + check ckOk7 + let ckOk8 = await b.waitUntilExists("msg", k) + check ckOk8 + + let aw9 = await a.get("msg", k) + check str(aw9.get().get) == "A" + let aw10 = await b.get("msg", k) + check str(aw10.get().get) == "B" + let aw11 = await a.count("msg", prefixRange(key("c"))) + check aw11.get() == 1 + let aw12 = await b.count("msg", prefixRange(key("c"))) + check aw12.get() == 1 diff --git a/tests/persistency/test_keys.nim b/tests/persistency/test_keys.nim new file mode 100644 index 000000000..e33020849 --- /dev/null +++ b/tests/persistency/test_keys.nim @@ -0,0 +1,135 @@ +{.used.} + +import std/[algorithm, sequtils] +import testutils/unittests +import waku/persistency/[types, keys] + +proc cmpBytes(a, b: Key): int = + let ab = bytes(a) + let bb = bytes(b) + let n = min(ab.len, bb.len) + for i in 0 ..< n: + if ab[i] != bb[i]: + return cmp(ab[i], bb[i]) + cmp(ab.len, bb.len) + +suite "Persistency keys": + test "string components sort by length, then byte order": + var ks = @[key("ab"), key(""), key("a"), key("aa"), key("b")] + ks.sort(cmpBytes) + # length-prefix encoding => shorter strings always sort before longer + # ones; same-length strings sort in byte order. + check ks == @[key(""), key("a"), key("b"), key("aa"), key("ab")] + + test "same-length strings sort in byte order": + var ks = @[key("delta"), key("alpha"), key("gamma"), key("bravo")] + ks.sort(cmpBytes) + check ks == @[key("alpha"), key("bravo"), key("delta"), key("gamma")] + + test "int64 sign-flip preserves order across negative/zero/positive": + let inputs = @[ + key("c", int64.low), + key("c", -2'i64), + key("c", -1'i64), + key("c", 0'i64), + key("c", 1'i64), + key("c", 2'i64), + key("c", int64.high), + ] + var shuffled = inputs + # rotate so the natural order is not the input order + shuffled = @[ + shuffled[3], + shuffled[6], + shuffled[0], + shuffled[5], + shuffled[1], + shuffled[4], + shuffled[2], + ] + shuffled.sort(cmpBytes) + check shuffled == inputs + + test "uint64 big-endian preserves order": + let inputs = @[ + key("u", 0'u64), + key("u", 1'u64), + key("u", 256'u64), + key("u", 1_000_000'u64), + key("u", uint64.high - 1), + key("u", uint64.high), + ] + var shuffled = @[inputs[3], inputs[0], inputs[5], inputs[2], inputs[1], inputs[4]] + shuffled.sort(cmpBytes) + check shuffled == inputs + + test "composite (string, string) tuple ordering": + # First component "a" / "b" — both length 1, so byte order applies. + # Second components grouped by first; within each group, again + # length-then-byte: "" (len 0) < "a","z" (len 1) < "ab" (len 2). + let inputs = @[ + key("a", ""), + key("a", "a"), + key("a", "z"), + key("a", "ab"), + key("b", ""), + key("b", "a"), + ] + var shuffled = inputs.reversed() + shuffled.sort(cmpBytes) + check shuffled == inputs + + test "composite (string, int64) tuple ordering": + let inputs = @[ + key("a", int64.low), + key("a", -1'i64), + key("a", 0'i64), + key("a", 1'i64), + key("b", int64.low), + key("b", 0'i64), + ] + var shuffled = inputs.reversed() + shuffled.sort(cmpBytes) + check shuffled == inputs + + test "shorter composite key precedes longer one sharing its prefix": + check key("a") < key("a", 0'i64) + check key("a") < key("a", "") + check key("a", "x") < key("a", "x", "y") + + test "Key equality is byte-wise": + check key("a", 1'i64) == key("a", 1'i64) + check not (key("a", 1'i64) == key("a", 2'i64)) + + test "prefixRange.start equals prefix": + let r = prefixRange(key("a")) + check r.start == key("a") + + test "prefixRange.stop excludes the prefix and admits all extensions": + let r = prefixRange(key("a")) + let extensions = @[ + key("a"), + key("a", 0'i64), + key("a", int64.high), + key("a", "x"), + key("a", uint64.high), + ] + for k in extensions: + check r.start <= k + check k < r.stop + + test "prefixRange.stop excludes siblings outside the prefix": + let r = prefixRange(key("a")) + # "b" has the same encoded length as "a" but a higher last byte, so it + # should be at-or-above the exclusive stop. + check not (key("b") < r.stop) + # "ab" has more bytes — its 2-byte length prefix bumps it past stop. + check not (key("ab") < r.stop) + # The empty key sits before the start. + check key("") < r.start + + test "prefixRange handles all-0xFF prefix as open-ended": + let prefix = rawKey(@[0xFF'u8, 0xFF, 0xFF]) + let r = prefixRange(prefix) + check r.start == prefix + check bytes(r.stop).len == 0 diff --git a/tests/persistency/test_lifecycle.nim b/tests/persistency/test_lifecycle.nim new file mode 100644 index 000000000..6b1a6ee60 --- /dev/null +++ b/tests/persistency/test_lifecycle.nim @@ -0,0 +1,302 @@ +{.used.} + +import std/[options, os, times] +import chronos, results +import testutils/unittests +import brokers/[event_broker, request_broker] +import waku/persistency/persistency +import waku/persistency/backend_comm + +proc payloadBytes(s: string): seq[byte] = + result = newSeq[byte](s.len) + for i, c in s: + result[i] = byte(c) + +template str(b: seq[byte]): string = + var s = newString(b.len) + for i, x in b: + s[i] = char(x) + s + +proc tmpRoot(label: string): string = + let p = getTempDir() / ("persistency_test_" & label & "_" & $epochTime().int) + removeDir(p) + p + +# Cross-thread persist: emit a PersistEvent then poll until the row shows up +# via KvExists. The PersistEvent listener is fire-and-forget, so reads +# immediately after emit are racy by design (documented in v1). +proc pollExists( + t: Job, category: string, k: Key, timeoutMs = 1000 +): Future[bool] {.async.} = + let deadline = epochTime() + (timeoutMs.float / 1000.0) + while epochTime() < deadline: + let r = await KvExists.request(t.context, category, k) + if r.isOk and r.get().value: + return true + await sleepAsync(chronos.milliseconds(2)) + return false + +suite "Persistency lifecycle": + test "Persistency.instance accepts a pre-existing rootDir": + let root = tmpRoot("preexisting") + defer: + removeDir(root) + createDir(root) # pretend a previous run left it + let marker = root / "do-not-touch.txt" + writeFile(marker, "hi") + defer: + removeFile(marker) + + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + # The pre-existing file is untouched. + check fileExists(marker) + check readFile(marker) == "hi" + + test "Persistency.instance refuses a non-directory path": + let root = tmpRoot("collision") + defer: + removeFile(root) + writeFile(root, "im a file not a dir") # collide with rootDir name + let r = Persistency.instance(root) + check r.isErr + check r.error.kind == peInvalidArgument + + test "Persistency.instance defers rootDir creation until first openJob": + let root = tmpRoot("lazy") + defer: + removeDir(root) + check not dirExists(root) + + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + # instance() must not have touched the filesystem + check not dirExists(root) + + discard p.openJob("first").get() + # first openJob materialises the directory + check dirExists(root) + + test "Persistency.instance refuses a path whose ancestor is not a directory": + let parent = tmpRoot("bad-parent") + defer: + removeFile(parent) + writeFile(parent, "not a directory") + let root = parent / "child" + let r = Persistency.instance(root) + check r.isErr + check r.error.kind == peInvalidArgument + + asyncTest "openJob reuses an existing DB file across processes-of-one": + let root = tmpRoot("reopen") + defer: + removeDir(root) + + # First "session": write something then close. + block firstSession: + let p = Persistency.instance(root).get() + let j = p.openJob("persist").get() + await j.persistPut("msg", key("c", 1'i64), payloadBytes("v1")) + let ckOk1 = await j.pollExists("msg", key("c", 1'i64)) + check ckOk1 + Persistency.reset() + + check fileExists(root / "persist.db") + + # Second "session": reopen and read the data back. + block secondSession: + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let j = p.openJob("persist").get() + let aw1 = await KvGet.request(j.context, "msg", key("c", 1'i64)) + let got = aw1.get() + check got.value.isSome + check str(got.value.get) == "v1" + + test "openJob is idempotent within a session": + let root = tmpRoot("idem") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let a = p.openJob("same").get() + let b = p.openJob("same").get() + check a.id == b.id + check a.context == b.context + + test "openJob materialises rootDir and launches a worker": + let root = tmpRoot("basic") + defer: + removeDir(root) + + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let t = p.openJob("alpha").get() + check t.id == "alpha" + check t.running + check fileExists(root / "alpha.db") + + asyncTest "persist then read round-trips via brokers": + let root = tmpRoot("rw") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t1").get() + + let k = key("c", 1'i64) + let ev = PersistEvent( + ops: @[TxOp(category: "msg", key: k, kind: txPut, payload: payloadBytes("hello"))] + ) + await PersistEvent.emit(t.context, ev) + let ckOk2 = await t.pollExists("msg", k) + check ckOk2 + + let aw2 = await KvGet.request(t.context, "msg", k) + let got = aw2.get() + check got.value.isSome + check str(got.value.get) == "hello" + + asyncTest "two jobs run in parallel with isolated DBs": + let root = tmpRoot("isolation") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let a = p.openJob("alpha").get() + let b = p.openJob("beta").get() + check a.context != b.context + + let k = key("shared", 1'i64) + await PersistEvent.emit( + a.context, + PersistEvent( + ops: @[ + TxOp( + category: "msg", key: k, kind: txPut, payload: payloadBytes("from-alpha") + ) + ] + ), + ) + await PersistEvent.emit( + b.context, + PersistEvent( + ops: @[ + TxOp(category: "msg", key: k, kind: txPut, payload: payloadBytes("from-beta")) + ] + ), + ) + let ckOk3 = await a.pollExists("msg", k) + check ckOk3 + let ckOk4 = await b.pollExists("msg", k) + check ckOk4 + + let aw3 = await KvGet.request(a.context, "msg", k) + let aGot = aw3.get() + let aw4 = await KvGet.request(b.context, "msg", k) + let bGot = aw4.get() + check str(aGot.value.get) == "from-alpha" + check str(bGot.value.get) == "from-beta" + + # Each job has its own DB file. + check fileExists(root / "alpha.db") + check fileExists(root / "beta.db") + + asyncTest "closeJob joins the worker and frees the slot": + let root = tmpRoot("close") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let t = p.openJob("x").get() + let ctx = t.context + p.closeJob("x") + check not t.running + + # After close, requests on the old context have no provider. + let r = await KvExists.request(ctx, "msg", key("k")) + check r.isErr + + test "dropJob removes the DB file": + let root = tmpRoot("drop") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + discard p.openJob("ephemeral").get() + check fileExists(root / "ephemeral.db") + p.dropJob("ephemeral") + check not fileExists(root / "ephemeral.db") + + asyncTest "scan and count over a range": + let root = tmpRoot("scan") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + var ops: seq[TxOp] + for i in 1'i64 .. 5: + ops.add( + TxOp(category: "msg", key: key("c", i), kind: txPut, payload: payloadBytes($i)) + ) + await PersistEvent.emit(t.context, PersistEvent(ops: ops)) + # Wait for the last insert to land. + let ckOk5 = await t.pollExists("msg", key("c", 5'i64)) + check ckOk5 + + let rng = prefixRange(key("c")) + let aw5 = await KvCount.request(t.context, "msg", rng) + let cnt = aw5.get() + check cnt.n == 5 + + let aw6 = await KvScan.request(t.context, "msg", rng, false) + let scn = aw6.get() + check scn.rows.len == 5 + check str(scn.rows[0].payload) == "1" + check str(scn.rows[4].payload) == "5" + + asyncTest "acked delete reports whether the row existed": + let root = tmpRoot("delete") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + let k = key("d", 1'i64) + let aw7 = await KvDelete.request(t.context, "msg", k) + let r1 = aw7.get() + check r1.existed == false + + await PersistEvent.emit( + t.context, + PersistEvent( + ops: @[TxOp(category: "msg", key: k, kind: txPut, payload: payloadBytes("v"))] + ), + ) + let ckOk6 = await t.pollExists("msg", k) + check ckOk6 + + let aw8 = await KvDelete.request(t.context, "msg", k) + let r2 = aw8.get() + check r2.existed == true + let aw9 = await KvExists.request(t.context, "msg", k) + let r3 = aw9.get() + check r3.value == false diff --git a/tests/persistency/test_singleton.nim b/tests/persistency/test_singleton.nim new file mode 100644 index 000000000..f17841611 --- /dev/null +++ b/tests/persistency/test_singleton.nim @@ -0,0 +1,79 @@ +{.used.} + +import std/[os, strutils, times] +import chronos, results +import testutils/unittests +import brokers/multi_request_broker +import waku/persistency/persistency + +proc tmpRoot(label: string): string = + let p = getTempDir() / ("persistency_singleton_" & label & "_" & $epochTime().int) + removeDir(p) + p + +suite "Persistency singleton": + test "instance(rootDir) is idempotent with the same rootDir": + let root = tmpRoot("idem") + defer: + removeDir(root) + defer: + Persistency.reset() + + let p1 = Persistency.instance(root).get() + let p2 = Persistency.instance(root).get() + check p1 == p2 + + test "instance(rootDir) refuses re-init with a different rootDir": + let rootA = tmpRoot("a") + let rootB = tmpRoot("b") + defer: + removeDir(rootA) + defer: + removeDir(rootB) + defer: + Persistency.reset() + + discard Persistency.instance(rootA).get() + let r = Persistency.instance(rootB) + check r.isErr + check r.error.kind == peInvalidArgument + + test "no-arg instance() fails before init, succeeds after": + let root = tmpRoot("noarg") + defer: + removeDir(root) + defer: + Persistency.reset() + + let before = Persistency.instance() + check before.isErr + check before.error.kind == peClosed + + discard Persistency.instance(root).get() + let after = Persistency.instance() + check after.isOk + + test "reset() makes the next instance() target a different rootDir": + let rootA = tmpRoot("rs-a") + let rootB = tmpRoot("rs-b") + defer: + removeDir(rootA) + defer: + removeDir(rootB) + defer: + Persistency.reset() + + let pA = Persistency.instance(rootA).get() + check pA.rootDir == rootA + Persistency.reset() + + let pB = Persistency.instance(rootB).get() + check pB.rootDir == rootB + check pA != pB + + test "reset() is idempotent": + defer: + Persistency.reset() + Persistency.reset() + Persistency.reset() + check Persistency.instance().isErr diff --git a/tests/persistency/test_string_lookup.nim b/tests/persistency/test_string_lookup.nim new file mode 100644 index 000000000..11ac5fed3 --- /dev/null +++ b/tests/persistency/test_string_lookup.nim @@ -0,0 +1,184 @@ +{.used.} + +import std/[options, os, times] +import chronos, results +import testutils/unittests +import waku/persistency/persistency + +proc payloadBytes(s: string): seq[byte] = + result = newSeq[byte](s.len) + for i, c in s: + result[i] = byte(c) + +template str(b: seq[byte]): string = + var s = newString(b.len) + for i, x in b: + s[i] = char(x) + s + +proc tmpRoot(label: string): string = + let p = getTempDir() / ("persistency_lookup_" & label & "_" & $epochTime().int) + removeDir(p) + p + +# Bridge the persist->read race (writes are fire-and-forget in v1). +proc waitUntilExists( + p: Persistency, jobId, category: string, k: Key, timeoutMs = 1000 +): Future[bool] {.async.} = + let deadline = epochTime() + (timeoutMs.float / 1000.0) + while epochTime() < deadline: + let r = await p.exists(jobId, category, k) + if r.isOk and r.get(): + return true + await sleepAsync(chronos.milliseconds(2)) + return false + +suite "Persistency string-id lookup": + test "job(p, id) returns peJobNotFound when not open": + let root = tmpRoot("notfound") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let r = p.job("nope") + check r.isErr + check r.error.kind == peJobNotFound + + test "job(p, id) returns the Job after openJob": + let root = tmpRoot("found") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let opened = p.openJob("alpha").get() + let looked = p.job("alpha").get() + check looked.id == "alpha" + check looked == opened # same ref, no need to peek at .context + + test "hasJob mirrors p.job()": + let root = tmpRoot("has") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + check not p.hasJob("x") + discard p.openJob("x") + check p.hasJob("x") + p.closeJob("x") + check not p.hasJob("x") + + test "subscript [] returns the open Job": + let root = tmpRoot("subscript") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + discard p.openJob("a").get() + let j = p["a"] + check j.id == "a" + + asyncTest "string-lookup persistPut + get round-trips without a Job ref": + let root = tmpRoot("rw") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + discard p.openJob("svc").get() + + let k = key("c", 1'i64) + await p.persistPut("svc", "msg", k, payloadBytes("hello")) + let ckOk1 = await p.waitUntilExists("svc", "msg", k) + check ckOk1 + + let aw1 = await p.get("svc", "msg", k) + let got = aw1.get() + check got.isSome + check str(got.get) == "hello" + + asyncTest "string-lookup reads short-circuit with peJobNotFound": + let root = tmpRoot("missingread") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let g = await p.get("nope", "msg", key("k")) + check g.isErr + check g.error.kind == peJobNotFound + + let c = await p.count("nope", "msg", prefixRange(key("k"))) + check c.isErr + check c.error.kind == peJobNotFound + + let d = await p.deleteAcked("nope", "msg", key("k")) + check d.isErr + check d.error.kind == peJobNotFound + + asyncTest "string-lookup writes to an unknown job are dropped, not raised": + let root = tmpRoot("missingwrite") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + # Should not raise and should not leak any state. + await p.persistPut("ghost", "msg", key("k"), payloadBytes("v")) + await p.persistDelete("ghost", "msg", key("k")) + await p.persistEncoded("ghost", "msg", key("k"), 42'i64) + check not p.hasJob("ghost") + + asyncTest "string-lookup persistEncoded round-trips a struct": + let root = tmpRoot("encoded") + defer: + removeDir(root) + type Item = object + tag: string + n: int64 + + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + discard p.openJob("e").get() + + let k = key("items", 1'i64) + await p.persistEncoded("e", "msg", k, Item(tag: "alpha", n: 7)) + let ckOk2 = await p.waitUntilExists("e", "msg", k) + check ckOk2 + + let aw2 = await p.get("e", "msg", k) + let got = aw2.get() + check got.isSome + check got.get == toPayload(Item(tag: "alpha", n: 7)) + + asyncTest "string-lookup scan returns the same rows as Job-form": + let root = tmpRoot("scan") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let j = p.openJob("s").get() + + for i in 1'i64 .. 3: + await p.persistPut("s", "msg", key("c", i), payloadBytes($i)) + let ckOk3 = await p.waitUntilExists("s", "msg", key("c", 3'i64)) + check ckOk3 + + let aw3 = await p.scanPrefix("s", "msg", key("c")) + let viaId = aw3.get() + let aw4 = await j.scanPrefix("msg", key("c")) + let viaRef = aw4.get() + check viaId.len == viaRef.len + for i in 0 ..< viaId.len: + check viaId[i].key == viaRef[i].key + check viaId[i].payload == viaRef[i].payload diff --git a/tests/waku_relay/utils.nim b/tests/waku_relay/utils.nim index 4e958a4ea..069600106 100644 --- a/tests/waku_relay/utils.nim +++ b/tests/waku_relay/utils.nim @@ -8,17 +8,13 @@ import libp2p/switch, libp2p/protocols/pubsub/pubsub +import brokers/broker_context + from std/times import epochTime import waku/[ - waku_relay, - node/waku_node, - node/peer_manager, - waku_core, - waku_node, - waku_rln_relay, - common/broker/broker_context, + waku_relay, node/waku_node, node/peer_manager, waku_core, waku_node, waku_rln_relay ], ../waku_store/store_utils, ../waku_archive/archive_utils, diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index 08d3daedb..099226b76 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -8,6 +8,9 @@ import chronicles, stint, libp2p/crypto/crypto + +import brokers/broker_context + import waku/[ waku_core, @@ -15,7 +18,6 @@ import waku_rln_relay/rln, waku_rln_relay/protocol_metrics, waku_keystore, - common/broker/broker_context, ], ./rln/waku_rln_relay_utils, ./utils_onchain, diff --git a/tests/waku_rln_relay/test_wakunode_rln_relay.nim b/tests/waku_rln_relay/test_wakunode_rln_relay.nim index 19a47e1aa..414a445fa 100644 --- a/tests/waku_rln_relay/test_wakunode_rln_relay.nim +++ b/tests/waku_rln_relay/test_wakunode_rln_relay.nim @@ -7,13 +7,14 @@ import chronicles, chronos, libp2p/switch, - libp2p/protocols/pubsub/pubsub + libp2p/protocols/pubsub/pubsub, + brokers/broker_context + import waku/[waku_core, waku_node, waku_rln_relay], ../testlib/[wakucore, futures, wakunode, testutils], ./utils_onchain, - ./rln/waku_rln_relay_utils, - waku/common/broker/broker_context + ./rln/waku_rln_relay_utils from std/times import epochTime diff --git a/tests/wakunode_rest/test_rest_relay.nim b/tests/wakunode_rest/test_rest_relay.nim index b791da29f..a98b75520 100644 --- a/tests/wakunode_rest/test_rest_relay.nim +++ b/tests/wakunode_rest/test_rest_relay.nim @@ -7,6 +7,7 @@ import presto, presto/client as presto_client, libp2p/crypto/crypto +import brokers/broker_context import waku/[ common/base64, @@ -21,7 +22,6 @@ import rest_api/endpoint/relay/client as relay_rest_client, waku_relay, waku_rln_relay, - common/broker/broker_context, ], ../testlib/wakucore, ../testlib/wakunode, diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index d63b5880c..183de3b80 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -698,6 +698,12 @@ with the drawback of consuming some more bandwidth.""", name: "rate-limit" .}: seq[string] + localStoragePath* {. + desc: "Path to store local data.", + defaultValue: "./data", + name: "local-storage-path" + .}: string + ## Parsing # NOTE: Keys are different in nim-libp2p @@ -1119,6 +1125,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = if n.rateLimits.len > 0: b.rateLimitConf.withRateLimits(n.rateLimits) + b.withLocalStoragePath(n.localStoragePath) + b.kademliaDiscoveryConf.withEnabled(n.enableKadDiscovery) b.kademliaDiscoveryConf.withBootstrapNodes(n.kadBootstrapNodes) diff --git a/tools/gen-nix-deps.sh b/tools/gen-nix-deps.sh index 9bb43e638..d24641ecd 100755 --- a/tools/gen-nix-deps.sh +++ b/tools/gen-nix-deps.sh @@ -36,7 +36,10 @@ fi echo "[*] Generating $OUTFILE from $LOCKFILE" mkdir -p "$(dirname "$OUTFILE")" -cat > "$OUTFILE" <<'EOF' +TMPFILE=$(mktemp) +trap 'rm -f "$TMPFILE"' EXIT + +cat > "$TMPFILE" <<'EOF' # AUTOGENERATED from nimble.lock — do not edit manually. # Regenerate with: ./tools/gen-nix-deps.sh nimble.lock nix/deps.nix { pkgs }: @@ -62,7 +65,7 @@ jq -c ' --fetch-submodules \ | jq -r '.sha256') - cat >> "$OUTFILE" <> "$TMPFILE" <> "$OUTFILE" <<'EOF' +cat >> "$TMPFILE" <<'EOF' } EOF +mv "$TMPFILE" "$OUTFILE" echo "[✓] Wrote $OUTFILE" diff --git a/waku.nimble b/waku.nimble index f944aaae1..57ce858a4 100644 --- a/waku.nimble +++ b/waku.nimble @@ -63,6 +63,16 @@ requires "https://github.com/logos-messaging/nim-ffi" requires "https://github.com/logos-messaging/nim-sds.git#2e9a7683f0e180bf112135fae3a3803eed8490d4" +# brokers: pinned by URL+commit rather than the bare `brokers >= 2.0.1` +# form because the nim-lang/packages registry entry for `brokers` only +# carries metadata for the original v0.1.0 publication. Until that +# registry entry is refreshed, the local SAT solver enumerates "0.1.0" +# as the only available version and cannot satisfy `>= 2.0.1`. The URL +# pin below bypasses the registry and locks the exact commit of the +# v2.0.1 tag. Revert to the bare form once nim-lang/packages is +# updated. +requires "https://github.com/NagyZoltanPeter/nim-brokers.git#v2.0.1" + requires "https://github.com/vacp2p/nim-lsquic" requires "https://github.com/vacp2p/nim-jwt.git#057ec95eb5af0eea9c49bfe9025b3312c95dc5f2" diff --git a/waku/common/broker/broker_context.nim b/waku/common/broker/broker_context.nim deleted file mode 100644 index 483a2e3a7..000000000 --- a/waku/common/broker/broker_context.nim +++ /dev/null @@ -1,68 +0,0 @@ -{.push raises: [].} - -import std/[strutils, concurrency/atomics], chronos - -type BrokerContext* = distinct uint32 - -func `==`*(a, b: BrokerContext): bool = - uint32(a) == uint32(b) - -func `!=`*(a, b: BrokerContext): bool = - uint32(a) != uint32(b) - -func `$`*(bc: BrokerContext): string = - toHex(uint32(bc), 8) - -const DefaultBrokerContext* = BrokerContext(0xCAFFE14E'u32) - -# Global broker context accessor. -# -# NOTE: This intentionally creates a *single* active BrokerContext per process -# (per event loop thread). Use only if you accept serialization of all broker -# context usage through the lock. -var globalBrokerContextLock {.threadvar.}: AsyncLock -globalBrokerContextLock = newAsyncLock() -var globalBrokerContextValue {.threadvar.}: BrokerContext -globalBrokerContextValue = DefaultBrokerContext -proc globalBrokerContext*(): BrokerContext = - ## Returns the currently active global broker context. - ## - ## This is intentionally lock-free; callers should use it inside - ## `withNewGlobalBrokerContext` / `withGlobalBrokerContext`. - globalBrokerContextValue - -var gContextCounter: Atomic[uint32] - -proc NewBrokerContext*(): BrokerContext = - var nextId = gContextCounter.fetchAdd(1, moRelaxed) - if nextId == uint32(DefaultBrokerContext): - nextId = gContextCounter.fetchAdd(1, moRelaxed) - return BrokerContext(nextId) - -template lockGlobalBrokerContext*(brokerCtx: BrokerContext, body: untyped): untyped = - ## Runs `body` while holding the global broker context lock with the provided - ## `brokerCtx` installed as the globally accessible context. - ## - ## This template is intended for use from within `chronos` async procs. - block: - await noCancel(globalBrokerContextLock.acquire()) - let previousBrokerCtx = globalBrokerContextValue - globalBrokerContextValue = brokerCtx - try: - body - finally: - globalBrokerContextValue = previousBrokerCtx - try: - globalBrokerContextLock.release() - except AsyncLockError: - doAssert false, "globalBrokerContextLock.release(): lock not held" - -template lockNewGlobalBrokerContext*(body: untyped): untyped = - ## Runs `body` while holding the global broker context lock with a freshly - ## generated broker context installed as the global accessor. - ## - ## The previous global broker context (if any) is restored on exit. - lockGlobalBrokerContext(NewBrokerContext()): - body - -{.pop.} diff --git a/waku/common/broker/event_broker.nim b/waku/common/broker/event_broker.nim deleted file mode 100644 index 3fd10cea2..000000000 --- a/waku/common/broker/event_broker.nim +++ /dev/null @@ -1,411 +0,0 @@ -## EventBroker -## ------------------- -## EventBroker represents a reactive decoupling pattern, that -## allows event-driven development without -## need for direct dependencies in between emitters and listeners. -## Worth considering using it in a single or many emitters to many listeners scenario. -## -## Generates a standalone, type-safe event broker for the declared type. -## The macro exports the value type itself plus a broker companion that manages -## listeners via thread-local storage. -## -## Type definitions: -## - Inline `object` / `ref object` definitions are supported. -## - Native types, aliases, and externally-defined types are also supported. -## In that case, EventBroker will automatically wrap the declared RHS type in -## `distinct` unless you already used `distinct`. -## This keeps event types unique even when multiple brokers share the same -## underlying base type. -## -## Default vs. context aware use: -## Every generated broker is a thread-local global instance. This means EventBroker -## enables decoupled event exchange threadwise. -## -## Sometimes we use brokers inside a context (e.g. within a component that has many -## modules or subsystems). If you instantiate multiple such components in a single -## thread, and each component must have its own listener set for the same EventBroker -## type, you can use context-aware EventBroker. -## -## Context awareness is supported through the `BrokerContext` argument for -## `listen`, `emit`, `dropListener`, and `dropAllListeners`. -## Listener stores are kept separate per broker context. -## -## Default broker context is defined as `DefaultBrokerContext`. If you don't need -## context awareness, you can keep using the interfaces without the context -## argument, which operate on `DefaultBrokerContext`. -## -## Usage: -## Declare your desired event type inside an `EventBroker` macro, add any number of fields.: -## ```nim -## EventBroker: -## type TypeName = object -## field1*: FieldType -## field2*: AnotherFieldType -## ``` -## -## After this, you can register async listeners anywhere in your code with -## `TypeName.listen(...)`, which returns a handle to the registered listener. -## Listeners are async procs or lambdas that take a single argument of the event type. -## Any number of listeners can be registered in different modules. -## -## Events can be emitted from anywhere with no direct dependency on the listeners by -## calling `TypeName.emit(...)` with an instance of the event type. -## This will asynchronously notify all registered listeners with the emitted event. -## -## Whenever you no longer need a listener (or your object instance that listen to the event goes out of scope), -## you can remove it from the broker with the handle returned by `listen`. -## This is done by calling `TypeName.dropListener(handle)`. -## Alternatively, you can remove all registered listeners through `TypeName.dropAllListeners()`. -## -## -## Example: -## ```nim -## EventBroker: -## type GreetingEvent = object -## text*: string -## -## let handle = GreetingEvent.listen( -## proc(evt: GreetingEvent): Future[void] {.async.} = -## echo evt.text -## ) -## GreetingEvent.emit(text= "hi") -## GreetingEvent.dropListener(handle) -## ``` - -## Example (non-object event type): -## ```nim -## EventBroker: -## type CounterEvent = int # exported as: `distinct int` -## -## discard CounterEvent.listen( -## proc(evt: CounterEvent): Future[void] {.async.} = -## echo int(evt) -## ) -## CounterEvent.emit(CounterEvent(42)) -## ``` - -import std/[macros, tables] -import chronos, chronicles, results -import ./helper/broker_utils, broker_context - -export chronicles, results, chronos, broker_context - -macro EventBroker*(body: untyped): untyped = - when defined(eventBrokerDebug): - echo body.treeRepr - let parsed = parseSingleTypeDef(body, "EventBroker", collectFieldInfo = true) - let typeIdent = parsed.typeIdent - let objectDef = parsed.objectDef - let fieldNames = parsed.fieldNames - let fieldTypes = parsed.fieldTypes - let hasInlineFields = parsed.hasInlineFields - - let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*") - let sanitized = sanitizeIdentName(typeIdent) - let typeNameLit = newLit($typeIdent) - let handlerProcIdent = ident(sanitized & "ListenerProc") - let listenerHandleIdent = ident(sanitized & "Listener") - let brokerTypeIdent = ident(sanitized & "Broker") - let exportedHandlerProcIdent = postfix(copyNimTree(handlerProcIdent), "*") - let exportedListenerHandleIdent = postfix(copyNimTree(listenerHandleIdent), "*") - let exportedBrokerTypeIdent = postfix(copyNimTree(brokerTypeIdent), "*") - let bucketTypeIdent = ident(sanitized & "CtxBucket") - let findBucketIdxIdent = ident(sanitized & "FindBucketIdx") - let getOrCreateBucketIdxIdent = ident(sanitized & "GetOrCreateBucketIdx") - let accessProcIdent = ident("access" & sanitized & "Broker") - let globalVarIdent = ident("g" & sanitized & "Broker") - let listenImplIdent = ident("register" & sanitized & "Listener") - let dropListenerImplIdent = ident("drop" & sanitized & "Listener") - let dropAllListenersImplIdent = ident("dropAll" & sanitized & "Listeners") - let emitImplIdent = ident("emit" & sanitized & "Value") - let listenerTaskIdent = ident("notify" & sanitized & "Listener") - - result = newStmtList() - - result.add( - quote do: - type - `exportedTypeIdent` = `objectDef` - `exportedListenerHandleIdent` = object - id*: uint64 - - `exportedHandlerProcIdent` = - proc(event: `typeIdent`): Future[void] {.async: (raises: []), gcsafe.} - `bucketTypeIdent` = object - brokerCtx: BrokerContext - listeners: Table[uint64, `handlerProcIdent`] - nextId: uint64 - - `exportedBrokerTypeIdent` = ref object - buckets: seq[`bucketTypeIdent`] - - ) - - result.add( - quote do: - var `globalVarIdent` {.threadvar.}: `brokerTypeIdent` - ) - - result.add( - quote do: - proc `accessProcIdent`(): `brokerTypeIdent` = - if `globalVarIdent`.isNil(): - new(`globalVarIdent`) - `globalVarIdent`.buckets = @[ - `bucketTypeIdent`( - brokerCtx: DefaultBrokerContext, - listeners: initTable[uint64, `handlerProcIdent`](), - nextId: 1'u64, - ) - ] - `globalVarIdent` - - ) - - result.add( - quote do: - proc `findBucketIdxIdent`( - broker: `brokerTypeIdent`, brokerCtx: BrokerContext - ): int = - if brokerCtx == DefaultBrokerContext: - return 0 - for i in 1 ..< broker.buckets.len: - if broker.buckets[i].brokerCtx == brokerCtx: - return i - return -1 - - proc `getOrCreateBucketIdxIdent`( - broker: `brokerTypeIdent`, brokerCtx: BrokerContext - ): int = - let idx = `findBucketIdxIdent`(broker, brokerCtx) - if idx >= 0: - return idx - broker.buckets.add( - `bucketTypeIdent`( - brokerCtx: brokerCtx, - listeners: initTable[uint64, `handlerProcIdent`](), - nextId: 1'u64, - ) - ) - return broker.buckets.high - - proc `listenImplIdent`( - brokerCtx: BrokerContext, handler: `handlerProcIdent` - ): Result[`listenerHandleIdent`, string] = - if handler.isNil(): - return err("Must provide a non-nil event handler") - var broker = `accessProcIdent`() - - let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx) - if broker.buckets[bucketIdx].nextId == 0'u64: - broker.buckets[bucketIdx].nextId = 1'u64 - - if broker.buckets[bucketIdx].nextId == high(uint64): - error "Cannot add more listeners: ID space exhausted", - nextId = $broker.buckets[bucketIdx].nextId - return err("Cannot add more listeners, listener ID space exhausted") - - let newId = broker.buckets[bucketIdx].nextId - inc broker.buckets[bucketIdx].nextId - broker.buckets[bucketIdx].listeners[newId] = handler - return ok(`listenerHandleIdent`(id: newId)) - - ) - - result.add( - quote do: - proc `dropListenerImplIdent`( - brokerCtx: BrokerContext, handle: `listenerHandleIdent` - ) = - if handle.id == 0'u64: - return - var broker = `accessProcIdent`() - - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - return - - if broker.buckets[bucketIdx].listeners.len == 0: - return - broker.buckets[bucketIdx].listeners.del(handle.id) - if brokerCtx != DefaultBrokerContext and - broker.buckets[bucketIdx].listeners.len == 0: - broker.buckets.delete(bucketIdx) - - ) - - result.add( - quote do: - proc `dropAllListenersImplIdent`(brokerCtx: BrokerContext) = - var broker = `accessProcIdent`() - - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - return - if broker.buckets[bucketIdx].listeners.len > 0: - broker.buckets[bucketIdx].listeners.clear() - if brokerCtx != DefaultBrokerContext: - broker.buckets.delete(bucketIdx) - - ) - - result.add( - quote do: - proc listen*( - _: typedesc[`typeIdent`], handler: `handlerProcIdent` - ): Result[`listenerHandleIdent`, string] = - return `listenImplIdent`(DefaultBrokerContext, handler) - - proc listen*( - _: typedesc[`typeIdent`], - brokerCtx: BrokerContext, - handler: `handlerProcIdent`, - ): Result[`listenerHandleIdent`, string] = - return `listenImplIdent`(brokerCtx, handler) - - ) - - result.add( - quote do: - proc dropListener*(_: typedesc[`typeIdent`], handle: `listenerHandleIdent`) = - `dropListenerImplIdent`(DefaultBrokerContext, handle) - - proc dropListener*( - _: typedesc[`typeIdent`], - brokerCtx: BrokerContext, - handle: `listenerHandleIdent`, - ) = - `dropListenerImplIdent`(brokerCtx, handle) - - proc dropAllListeners*(_: typedesc[`typeIdent`]) = - `dropAllListenersImplIdent`(DefaultBrokerContext) - - proc dropAllListeners*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) = - `dropAllListenersImplIdent`(brokerCtx) - - ) - - result.add( - quote do: - proc `listenerTaskIdent`( - callback: `handlerProcIdent`, event: `typeIdent` - ) {.async: (raises: []), gcsafe.} = - if callback.isNil(): - return - try: - await callback(event) - except Exception: - error "Failed to execute event listener", error = getCurrentExceptionMsg() - - proc `emitImplIdent`( - brokerCtx: BrokerContext, event: `typeIdent` - ): Future[void] {.async: (raises: []), gcsafe.} = - when compiles(event.isNil()): - if event.isNil(): - error "Cannot emit uninitialized event object", eventType = `typeNameLit` - return - let broker = `accessProcIdent`() - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - # nothing to do as nobody is listening - return - if broker.buckets[bucketIdx].listeners.len == 0: - return - var callbacks: seq[`handlerProcIdent`] = @[] - for cb in broker.buckets[bucketIdx].listeners.values: - callbacks.add(cb) - for cb in callbacks: - asyncSpawn `listenerTaskIdent`(cb, event) - - proc emit*(event: `typeIdent`) = - asyncSpawn `emitImplIdent`(DefaultBrokerContext, event) - - proc emit*(_: typedesc[`typeIdent`], event: `typeIdent`) = - asyncSpawn `emitImplIdent`(DefaultBrokerContext, event) - - proc emit*( - _: typedesc[`typeIdent`], brokerCtx: BrokerContext, event: `typeIdent` - ) = - asyncSpawn `emitImplIdent`(brokerCtx, event) - - ) - - if hasInlineFields: - # Typedesc emit constructor overloads for inline object/ref object types. - var emitCtorParams = newTree(nnkFormalParams, newEmptyNode()) - let typedescParamType = - newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)) - emitCtorParams.add( - newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode()) - ) - for i in 0 ..< fieldNames.len: - emitCtorParams.add( - newTree( - nnkIdentDefs, - copyNimTree(fieldNames[i]), - copyNimTree(fieldTypes[i]), - newEmptyNode(), - ) - ) - - var emitCtorExpr = newTree(nnkObjConstr, copyNimTree(typeIdent)) - for i in 0 ..< fieldNames.len: - emitCtorExpr.add( - newTree( - nnkExprColonExpr, copyNimTree(fieldNames[i]), copyNimTree(fieldNames[i]) - ) - ) - - let emitCtorCallDefault = - newCall(copyNimTree(emitImplIdent), ident("DefaultBrokerContext"), emitCtorExpr) - let emitCtorBodyDefault = quote: - asyncSpawn `emitCtorCallDefault` - - let typedescEmitProcDefault = newTree( - nnkProcDef, - postfix(ident("emit"), "*"), - newEmptyNode(), - newEmptyNode(), - emitCtorParams, - newEmptyNode(), - newEmptyNode(), - emitCtorBodyDefault, - ) - result.add(typedescEmitProcDefault) - - var emitCtorParamsCtx = newTree(nnkFormalParams, newEmptyNode()) - emitCtorParamsCtx.add( - newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode()) - ) - emitCtorParamsCtx.add( - newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()) - ) - for i in 0 ..< fieldNames.len: - emitCtorParamsCtx.add( - newTree( - nnkIdentDefs, - copyNimTree(fieldNames[i]), - copyNimTree(fieldTypes[i]), - newEmptyNode(), - ) - ) - - let emitCtorCallCtx = - newCall(copyNimTree(emitImplIdent), ident("brokerCtx"), copyNimTree(emitCtorExpr)) - let emitCtorBodyCtx = quote: - asyncSpawn `emitCtorCallCtx` - - let typedescEmitProcCtx = newTree( - nnkProcDef, - postfix(ident("emit"), "*"), - newEmptyNode(), - newEmptyNode(), - emitCtorParamsCtx, - newEmptyNode(), - newEmptyNode(), - emitCtorBodyCtx, - ) - result.add(typedescEmitProcCtx) - - when defined(eventBrokerDebug): - echo result.repr diff --git a/waku/common/broker/helper/broker_utils.nim b/waku/common/broker/helper/broker_utils.nim deleted file mode 100644 index 90f2055d3..000000000 --- a/waku/common/broker/helper/broker_utils.nim +++ /dev/null @@ -1,206 +0,0 @@ -import std/macros - -type ParsedBrokerType* = object - ## Result of parsing the single `type` definition inside a broker macro body. - ## - ## - `typeIdent`: base identifier for the declared type name - ## - `objectDef`: exported type definition RHS (inline object fields exported; - ## non-object types wrapped in `distinct` unless already distinct) - ## - `isRefObject`: true only for inline `ref object` definitions - ## - `hasInlineFields`: true for inline `object` / `ref object` - ## - `fieldNames`/`fieldTypes`: populated only when `collectFieldInfo = true` - typeIdent*: NimNode - objectDef*: NimNode - isRefObject*: bool - hasInlineFields*: bool - fieldNames*: seq[NimNode] - fieldTypes*: seq[NimNode] - -proc sanitizeIdentName*(node: NimNode): string = - var raw = $node - var sanitizedName = newStringOfCap(raw.len) - for ch in raw: - case ch - of 'A' .. 'Z', 'a' .. 'z', '0' .. '9', '_': - sanitizedName.add(ch) - else: - sanitizedName.add('_') - sanitizedName - -proc ensureFieldDef*(node: NimNode) = - if node.kind != nnkIdentDefs or node.len < 3: - error("Expected field definition of the form `name: Type`", node) - let typeSlot = node.len - 2 - if node[typeSlot].kind == nnkEmpty: - error("Field `" & $node[0] & "` must declare a type", node) - -proc exportIdentNode*(node: NimNode): NimNode = - case node.kind - of nnkIdent: - postfix(copyNimTree(node), "*") - of nnkPostfix: - node - else: - error("Unsupported identifier form in field definition", node) - -proc baseTypeIdent*(defName: NimNode): NimNode = - case defName.kind - of nnkIdent: - defName - of nnkAccQuoted: - if defName.len != 1: - error("Unsupported quoted identifier", defName) - defName[0] - of nnkPostfix: - baseTypeIdent(defName[1]) - of nnkPragmaExpr: - baseTypeIdent(defName[0]) - else: - error("Unsupported type name in broker definition", defName) - -proc ensureDistinctType*(rhs: NimNode): NimNode = - ## For PODs / aliases / externally-defined types, wrap in `distinct` unless - ## it's already distinct. - if rhs.kind == nnkDistinctTy: - return copyNimTree(rhs) - newTree(nnkDistinctTy, copyNimTree(rhs)) - -proc cloneParams*(params: seq[NimNode]): seq[NimNode] = - ## Deep copy parameter definitions so they can be inserted in multiple places. - result = @[] - for param in params: - result.add(copyNimTree(param)) - -proc collectParamNames*(params: seq[NimNode]): seq[NimNode] = - ## Extract all identifier symbols declared across IdentDefs nodes. - result = @[] - for param in params: - assert param.kind == nnkIdentDefs - for i in 0 ..< param.len - 2: - let nameNode = param[i] - if nameNode.kind == nnkEmpty: - continue - result.add(ident($nameNode)) - -proc parseSingleTypeDef*( - body: NimNode, - macroName: string, - allowRefToNonObject = false, - collectFieldInfo = false, -): ParsedBrokerType = - ## Parses exactly one `type` definition from a broker macro body. - ## - ## Supported RHS: - ## - inline `object` / `ref object` (fields are auto-exported) - ## - non-object types / aliases / externally-defined types (wrapped in `distinct`) - ## - optionally: `ref SomeType` when `allowRefToNonObject = true` - var typeIdent: NimNode = nil - var objectDef: NimNode = nil - var isRefObject = false - var hasInlineFields = false - var fieldNames: seq[NimNode] = @[] - var fieldTypes: seq[NimNode] = @[] - - for stmt in body: - if stmt.kind != nnkTypeSection: - continue - for def in stmt: - if def.kind != nnkTypeDef: - continue - if not typeIdent.isNil(): - error("Only one type may be declared inside " & macroName, def) - typeIdent = baseTypeIdent(def[0]) - let rhs = def[2] - - case rhs.kind - of nnkObjectTy: - let recList = rhs[2] - if recList.kind != nnkRecList: - error(macroName & " object must declare a standard field list", rhs) - var exportedRecList = newTree(nnkRecList) - for field in recList: - case field.kind - of nnkIdentDefs: - ensureFieldDef(field) - if collectFieldInfo: - let fieldTypeNode = field[field.len - 2] - for i in 0 ..< field.len - 2: - let baseFieldIdent = baseTypeIdent(field[i]) - fieldNames.add(copyNimTree(baseFieldIdent)) - fieldTypes.add(copyNimTree(fieldTypeNode)) - var cloned = copyNimTree(field) - for i in 0 ..< cloned.len - 2: - cloned[i] = exportIdentNode(cloned[i]) - exportedRecList.add(cloned) - of nnkEmpty: - discard - else: - error( - macroName & " object definition only supports simple field declarations", - field, - ) - objectDef = newTree( - nnkObjectTy, copyNimTree(rhs[0]), copyNimTree(rhs[1]), exportedRecList - ) - isRefObject = false - hasInlineFields = true - of nnkRefTy: - if rhs.len != 1: - error(macroName & " ref type must have a single base", rhs) - if rhs[0].kind == nnkObjectTy: - let obj = rhs[0] - let recList = obj[2] - if recList.kind != nnkRecList: - error(macroName & " object must declare a standard field list", obj) - var exportedRecList = newTree(nnkRecList) - for field in recList: - case field.kind - of nnkIdentDefs: - ensureFieldDef(field) - if collectFieldInfo: - let fieldTypeNode = field[field.len - 2] - for i in 0 ..< field.len - 2: - let baseFieldIdent = baseTypeIdent(field[i]) - fieldNames.add(copyNimTree(baseFieldIdent)) - fieldTypes.add(copyNimTree(fieldTypeNode)) - var cloned = copyNimTree(field) - for i in 0 ..< cloned.len - 2: - cloned[i] = exportIdentNode(cloned[i]) - exportedRecList.add(cloned) - of nnkEmpty: - discard - else: - error( - macroName & " object definition only supports simple field declarations", - field, - ) - let exportedObjectType = newTree( - nnkObjectTy, copyNimTree(obj[0]), copyNimTree(obj[1]), exportedRecList - ) - objectDef = newTree(nnkRefTy, exportedObjectType) - isRefObject = true - hasInlineFields = true - elif allowRefToNonObject: - ## `ref SomeType` (SomeType can be defined elsewhere) - objectDef = ensureDistinctType(rhs) - isRefObject = false - hasInlineFields = false - else: - error(macroName & " ref object must wrap a concrete object definition", rhs) - else: - ## Non-object type / alias. - objectDef = ensureDistinctType(rhs) - isRefObject = false - hasInlineFields = false - - if typeIdent.isNil(): - error(macroName & " body must declare exactly one type", body) - - result = ParsedBrokerType( - typeIdent: typeIdent, - objectDef: objectDef, - isRefObject: isRefObject, - hasInlineFields: hasInlineFields, - fieldNames: fieldNames, - fieldTypes: fieldTypes, - ) diff --git a/waku/common/broker/multi_request_broker.nim b/waku/common/broker/multi_request_broker.nim deleted file mode 100644 index 2baa19940..000000000 --- a/waku/common/broker/multi_request_broker.nim +++ /dev/null @@ -1,743 +0,0 @@ -## MultiRequestBroker -## -------------------- -## MultiRequestBroker represents a proactive decoupling pattern, that -## allows defining request-response style interactions between modules without -## need for direct dependencies in between. -## Worth considering using it for use cases where you need to collect data from multiple providers. -## -## Generates a standalone, type-safe request broker for the declared type. -## The macro exports the value type itself plus a broker companion that manages -## providers via thread-local storage. -## -## Unlike `RequestBroker`, every call to `request` fan-outs to every registered -## provider and returns all collected responses. -## The request succeeds only if all providers succeed, otherwise it fails. -## -## Type definitions: -## - Inline `object` / `ref object` definitions are supported. -## - Native types, aliases, and externally-defined types are also supported. -## In that case, MultiRequestBroker will automatically wrap the declared RHS -## type in `distinct` unless you already used `distinct`. -## This keeps request types unique even when multiple brokers share the same -## underlying base type. -## -## Default vs. context aware use: -## Every generated broker is a thread-local global instance. -## Sometimes you want multiple independent provider sets for the same request -## type within the same thread (e.g. multiple components). For that, you can use -## context-aware MultiRequestBroker. -## -## Context awareness is supported through the `BrokerContext` argument for -## `setProvider`, `request`, `removeProvider`, and `clearProviders`. -## Provider stores are kept separate per broker context. -## -## Default broker context is defined as `DefaultBrokerContext`. If you don't -## need context awareness, you can keep using the interfaces without the context -## argument, which operate on `DefaultBrokerContext`. -## -## Usage: -## -## Declare collectable request data type inside a `MultiRequestBroker` macro, add any number of fields: -## ```nim -## MultiRequestBroker: -## type TypeName = object -## field1*: Type1 -## field2*: Type2 -## -## ## Define the request and provider signature, that is enforced at compile time. -## proc signature*(): Future[Result[TypeName, string]] {.async: (raises: []).} -## -## ## Also possible to define signature with arbitrary input arguments. -## proc signature*(arg1: ArgType, arg2: AnotherArgType): Future[Result[TypeName, string]] {.async: (raises: []).} -## -## ``` -## -## You can register a request processor (provider) anywhere without the need to -## know who will request. -## Register provider functions with `TypeName.setProvider(...)`. -## Providers are async procs or lambdas that return `Future[Result[TypeName, string]]`. -## `setProvider` returns a handle (or an error) that can later be used to remove -## the provider. - -## Requests can be made from anywhere with no direct dependency on the provider(s) -## by calling `TypeName.request()` (with arguments respecting the declared signature). -## This will asynchronously call all registered providers and return the collected -## responses as `Future[Result[seq[TypeName], string]]`. -## -## Whenever you don't want to process requests anymore (or your object instance that provides the request goes out of scope), -## you can remove it from the broker with `TypeName.removeProvider(handle)`. -## Alternatively, you can remove all registered providers through `TypeName.clearProviders()`. -## -## Example: -## ```nim -## MultiRequestBroker: -## type Greeting = object -## text*: string -## -## ## Define the request and provider signature, that is enforced at compile time. -## proc signature*(): Future[Result[Greeting, string]] {.async: (raises: []).} -## -## ## Also possible to define signature with arbitrary input arguments. -## proc signature*(lang: string): Future[Result[Greeting, string]] {.async: (raises: []).} -## -## ... -## let handle = Greeting.setProvider( -## proc(): Future[Result[Greeting, string]] {.async: (raises: []).} = -## ok(Greeting(text: "hello")) -## ) -## -## let anotherHandle = Greeting.setProvider( -## proc(): Future[Result[Greeting, string]] {.async: (raises: []).} = -## ok(Greeting(text: "szia")) -## ) -## -## let responses = (await Greeting.request()).valueOr(@[Greeting(text: "default")]) -## -## echo responses.len -## Greeting.clearProviders() -## ``` -## If no `signature` proc is declared, a zero-argument form is generated -## automatically, so the caller only needs to provide the type definition. - -import std/[macros, strutils, tables, sugar] -import chronos -import results -import ./helper/broker_utils -import ./broker_context - -export results, chronos, broker_context - -proc isReturnTypeValid(returnType, typeIdent: NimNode): bool = - ## Accept Future[Result[TypeIdent, string]] as the contract. - if returnType.kind != nnkBracketExpr or returnType.len != 2: - return false - if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Future"): - return false - let inner = returnType[1] - if inner.kind != nnkBracketExpr or inner.len != 3: - return false - if inner[0].kind != nnkIdent or not inner[0].eqIdent("Result"): - return false - if inner[1].kind != nnkIdent or not inner[1].eqIdent($typeIdent): - return false - inner[2].kind == nnkIdent and inner[2].eqIdent("string") - -proc makeProcType(returnType: NimNode, params: seq[NimNode]): NimNode = - var formal = newTree(nnkFormalParams) - formal.add(returnType) - for param in params: - formal.add(param) - - let pragmas = quote: - {.async.} - - newTree(nnkProcTy, formal, pragmas) - -macro MultiRequestBroker*(body: untyped): untyped = - when defined(requestBrokerDebug): - echo body.treeRepr - let parsed = parseSingleTypeDef(body, "MultiRequestBroker") - let typeIdent = parsed.typeIdent - let objectDef = parsed.objectDef - let isRefObject = parsed.isRefObject - - when defined(requestBrokerDebug): - echo "MultiRequestBroker generating type: ", $typeIdent - - let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*") - let sanitized = sanitizeIdentName(typeIdent) - let typeNameLit = newLit($typeIdent) - let isRefObjectLit = newLit(isRefObject) - let uint64Ident = ident("uint64") - let providerKindIdent = ident(sanitized & "ProviderKind") - let providerHandleIdent = ident(sanitized & "ProviderHandle") - let exportedProviderHandleIdent = postfix(copyNimTree(providerHandleIdent), "*") - let bucketTypeIdent = ident(sanitized & "CtxBucket") - let findBucketIdxIdent = ident(sanitized & "FindBucketIdx") - let getOrCreateBucketIdxIdent = ident(sanitized & "GetOrCreateBucketIdx") - let zeroKindIdent = ident("pk" & sanitized & "NoArgs") - let argKindIdent = ident("pk" & sanitized & "WithArgs") - var zeroArgSig: NimNode = nil - var zeroArgProviderName: NimNode = nil - var zeroArgFieldName: NimNode = nil - var argSig: NimNode = nil - var argParams: seq[NimNode] = @[] - var argProviderName: NimNode = nil - var argFieldName: NimNode = nil - - for stmt in body: - case stmt.kind - of nnkProcDef: - let procName = stmt[0] - let procNameIdent = - case procName.kind - of nnkIdent: - procName - of nnkPostfix: - procName[1] - else: - procName - let procNameStr = $procNameIdent - if not procNameStr.startsWith("signature"): - error("Signature proc names must start with `signature`", procName) - let params = stmt.params - if params.len == 0: - error("Signature must declare a return type", stmt) - let returnType = params[0] - if not isReturnTypeValid(returnType, typeIdent): - error( - "Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt - ) - let paramCount = params.len - 1 - if paramCount == 0: - if zeroArgSig != nil: - error("Only one zero-argument signature is allowed", stmt) - zeroArgSig = stmt - zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs") - zeroArgFieldName = ident("providerNoArgs") - elif paramCount >= 1: - if argSig != nil: - error("Only one argument-based signature is allowed", stmt) - argSig = stmt - argParams = @[] - for idx in 1 ..< params.len: - let paramDef = params[idx] - if paramDef.kind != nnkIdentDefs: - error( - "Signature parameter must be a standard identifier declaration", paramDef - ) - let paramTypeNode = paramDef[paramDef.len - 2] - if paramTypeNode.kind == nnkEmpty: - error("Signature parameter must declare a type", paramDef) - var hasName = false - for i in 0 ..< paramDef.len - 2: - if paramDef[i].kind != nnkEmpty: - hasName = true - if not hasName: - error("Signature parameter must declare a name", paramDef) - argParams.add(copyNimTree(paramDef)) - argProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderWithArgs") - argFieldName = ident("providerWithArgs") - of nnkTypeSection, nnkEmpty: - discard - else: - error("Unsupported statement inside MultiRequestBroker definition", stmt) - - if zeroArgSig.isNil() and argSig.isNil(): - zeroArgSig = newEmptyNode() - zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs") - zeroArgFieldName = ident("providerNoArgs") - - var typeSection = newTree(nnkTypeSection) - typeSection.add(newTree(nnkTypeDef, exportedTypeIdent, newEmptyNode(), objectDef)) - - var kindEnum = newTree(nnkEnumTy, newEmptyNode()) - if not zeroArgSig.isNil(): - kindEnum.add(zeroKindIdent) - if not argSig.isNil(): - kindEnum.add(argKindIdent) - typeSection.add(newTree(nnkTypeDef, providerKindIdent, newEmptyNode(), kindEnum)) - - var handleRecList = newTree(nnkRecList) - handleRecList.add(newTree(nnkIdentDefs, ident("id"), uint64Ident, newEmptyNode())) - handleRecList.add( - newTree(nnkIdentDefs, ident("kind"), providerKindIdent, newEmptyNode()) - ) - typeSection.add( - newTree( - nnkTypeDef, - exportedProviderHandleIdent, - newEmptyNode(), - newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), handleRecList), - ) - ) - - let returnType = quote: - Future[Result[`typeIdent`, string]] - - if not zeroArgSig.isNil(): - let procType = makeProcType(returnType, @[]) - typeSection.add(newTree(nnkTypeDef, zeroArgProviderName, newEmptyNode(), procType)) - if not argSig.isNil(): - let procType = makeProcType(returnType, cloneParams(argParams)) - typeSection.add(newTree(nnkTypeDef, argProviderName, newEmptyNode(), procType)) - - var bucketRecList = newTree(nnkRecList) - bucketRecList.add( - newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()) - ) - if not zeroArgSig.isNil(): - bucketRecList.add( - newTree( - nnkIdentDefs, - zeroArgFieldName, - newTree(nnkBracketExpr, ident("seq"), zeroArgProviderName), - newEmptyNode(), - ) - ) - if not argSig.isNil(): - bucketRecList.add( - newTree( - nnkIdentDefs, - argFieldName, - newTree(nnkBracketExpr, ident("seq"), argProviderName), - newEmptyNode(), - ) - ) - typeSection.add( - newTree( - nnkTypeDef, - bucketTypeIdent, - newEmptyNode(), - newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), bucketRecList), - ) - ) - - var brokerRecList = newTree(nnkRecList) - brokerRecList.add( - newTree( - nnkIdentDefs, - ident("buckets"), - newTree(nnkBracketExpr, ident("seq"), bucketTypeIdent), - newEmptyNode(), - ) - ) - let brokerTypeIdent = ident(sanitizeIdentName(typeIdent) & "Broker") - typeSection.add( - newTree( - nnkTypeDef, - brokerTypeIdent, - newEmptyNode(), - newTree( - nnkRefTy, newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), brokerRecList) - ), - ) - ) - result = newStmtList() - result.add(typeSection) - - let globalVarIdent = ident("g" & sanitizeIdentName(typeIdent) & "Broker") - let accessProcIdent = ident("access" & sanitizeIdentName(typeIdent) & "Broker") - result.add( - quote do: - var `globalVarIdent` {.threadvar.}: `brokerTypeIdent` - - proc `findBucketIdxIdent`( - broker: `brokerTypeIdent`, brokerCtx: BrokerContext - ): int = - if brokerCtx == DefaultBrokerContext: - return 0 - for i in 1 ..< broker.buckets.len: - if broker.buckets[i].brokerCtx == brokerCtx: - return i - return -1 - - proc `getOrCreateBucketIdxIdent`( - broker: `brokerTypeIdent`, brokerCtx: BrokerContext - ): int = - let idx = `findBucketIdxIdent`(broker, brokerCtx) - if idx >= 0: - return idx - broker.buckets.add(`bucketTypeIdent`(brokerCtx: brokerCtx)) - return broker.buckets.high - - proc `accessProcIdent`(): `brokerTypeIdent` = - if `globalVarIdent`.isNil(): - new(`globalVarIdent`) - `globalVarIdent`.buckets = - @[`bucketTypeIdent`(brokerCtx: DefaultBrokerContext)] - return `globalVarIdent` - - ) - - var clearBody = newStmtList() - if not zeroArgSig.isNil(): - result.add( - quote do: - proc setProvider*( - _: typedesc[`typeIdent`], - brokerCtx: BrokerContext, - handler: `zeroArgProviderName`, - ): Result[`providerHandleIdent`, string] = - if handler.isNil(): - return err("Provider handler must be provided") - let broker = `accessProcIdent`() - let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx) - for i, existing in broker.buckets[bucketIdx].`zeroArgFieldName`: - if not existing.isNil() and existing == handler: - return ok(`providerHandleIdent`(id: uint64(i + 1), kind: `zeroKindIdent`)) - broker.buckets[bucketIdx].`zeroArgFieldName`.add(handler) - return ok( - `providerHandleIdent`( - id: uint64(broker.buckets[bucketIdx].`zeroArgFieldName`.len), - kind: `zeroKindIdent`, - ) - ) - - proc setProvider*( - _: typedesc[`typeIdent`], handler: `zeroArgProviderName` - ): Result[`providerHandleIdent`, string] = - return setProvider(`typeIdent`, DefaultBrokerContext, handler) - - ) - result.add( - quote do: - proc request*( - _: typedesc[`typeIdent`], brokerCtx: BrokerContext - ): Future[Result[seq[`typeIdent`], string]] {.async: (raises: []), gcsafe.} = - var aggregated: seq[`typeIdent`] = @[] - let broker = `accessProcIdent`() - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - return ok(aggregated) - let providers = broker.buckets[bucketIdx].`zeroArgFieldName` - if providers.len == 0: - return ok(aggregated) - # var providersFut: seq[Future[Result[`typeIdent`, string]]] = collect: - var providersFut = collect(newSeq): - for provider in providers: - if provider.isNil(): - continue - provider() - - let catchable = catch: - await allFinished(providersFut) - - catchable.isOkOr: - return err("Some provider(s) failed:" & error.msg) - - for fut in catchable.get(): - if fut.failed(): - return err("Some provider(s) failed:" & fut.error.msg) - elif fut.finished(): - let providerResult = fut.value() - if providerResult.isOk: - let providerValue = providerResult.get() - when `isRefObjectLit`: - if providerValue.isNil(): - return err( - "MultiRequestBroker(" & `typeNameLit` & - "): provider returned nil result" - ) - aggregated.add(providerValue) - else: - return err("Some provider(s) failed:" & providerResult.error) - - return ok(aggregated) - - proc request*( - _: typedesc[`typeIdent`] - ): Future[Result[seq[`typeIdent`], string]] = - return request(`typeIdent`, DefaultBrokerContext) - - ) - if not argSig.isNil(): - result.add( - quote do: - proc setProvider*( - _: typedesc[`typeIdent`], - brokerCtx: BrokerContext, - handler: `argProviderName`, - ): Result[`providerHandleIdent`, string] = - if handler.isNil(): - return err("Provider handler must be provided") - let broker = `accessProcIdent`() - let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx) - for i, existing in broker.buckets[bucketIdx].`argFieldName`: - if not existing.isNil() and existing == handler: - return ok(`providerHandleIdent`(id: uint64(i + 1), kind: `argKindIdent`)) - broker.buckets[bucketIdx].`argFieldName`.add(handler) - return ok( - `providerHandleIdent`( - id: uint64(broker.buckets[bucketIdx].`argFieldName`.len), - kind: `argKindIdent`, - ) - ) - - proc setProvider*( - _: typedesc[`typeIdent`], handler: `argProviderName` - ): Result[`providerHandleIdent`, string] = - return setProvider(`typeIdent`, DefaultBrokerContext, handler) - - ) - let requestParamDefs = cloneParams(argParams) - let argNameIdents = collectParamNames(requestParamDefs) - let providerSym = genSym(nskLet, "providerVal") - var providerCall = newCall(providerSym) - for argName in argNameIdents: - providerCall.add(argName) - var formalParams = newTree(nnkFormalParams) - formalParams.add( - quote do: - Future[Result[seq[`typeIdent`], string]] - ) - formalParams.add( - newTree( - nnkIdentDefs, - ident("_"), - newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)), - newEmptyNode(), - ) - ) - formalParams.add( - newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()) - ) - for paramDef in requestParamDefs: - formalParams.add(paramDef) - let requestPragmas = quote: - {.async: (raises: []), gcsafe.} - let requestBody = quote: - var aggregated: seq[`typeIdent`] = @[] - let broker = `accessProcIdent`() - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - return ok(aggregated) - let providers = broker.buckets[bucketIdx].`argFieldName` - if providers.len == 0: - return ok(aggregated) - var providersFut = collect(newSeq): - for provider in providers: - if provider.isNil(): - continue - let `providerSym` = provider - `providerCall` - let catchable = catch: - await allFinished(providersFut) - catchable.isOkOr: - return err("Some provider(s) failed:" & error.msg) - for fut in catchable.get(): - if fut.failed(): - return err("Some provider(s) failed:" & fut.error.msg) - elif fut.finished(): - let providerResult = fut.value() - if providerResult.isOk: - let providerValue = providerResult.get() - when `isRefObjectLit`: - if providerValue.isNil(): - return err( - "MultiRequestBroker(" & `typeNameLit` & - "): provider returned nil result" - ) - aggregated.add(providerValue) - else: - return err("Some provider(s) failed:" & providerResult.error) - return ok(aggregated) - - result.add( - newTree( - nnkProcDef, - postfix(ident("request"), "*"), - newEmptyNode(), - newEmptyNode(), - formalParams, - requestPragmas, - newEmptyNode(), - requestBody, - ) - ) - - # Backward-compatible default-context overload (no brokerCtx parameter). - var formalParamsDefault = newTree(nnkFormalParams) - formalParamsDefault.add( - quote do: - Future[Result[seq[`typeIdent`], string]] - ) - formalParamsDefault.add( - newTree( - nnkIdentDefs, - ident("_"), - newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)), - newEmptyNode(), - ) - ) - for paramDef in requestParamDefs: - formalParamsDefault.add(copyNimTree(paramDef)) - - var wrapperCall = newCall(ident("request")) - wrapperCall.add(copyNimTree(typeIdent)) - wrapperCall.add(ident("DefaultBrokerContext")) - for argName in argNameIdents: - wrapperCall.add(copyNimTree(argName)) - - result.add( - newTree( - nnkProcDef, - postfix(ident("request"), "*"), - newEmptyNode(), - newEmptyNode(), - formalParamsDefault, - newEmptyNode(), - newEmptyNode(), - newStmtList(newTree(nnkReturnStmt, wrapperCall)), - ) - ) - let removeHandleCtxSym = genSym(nskParam, "handle") - let removeHandleDefaultSym = genSym(nskParam, "handle") - - when true: - # Generate clearProviders / removeProvider with macro-time knowledge about which - # provider lists exist (zero-arg and/or arg providers). - if not zeroArgSig.isNil() and not argSig.isNil(): - result.add( - quote do: - proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) = - let broker = `accessProcIdent`() - if broker.isNil(): - return - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - return - broker.buckets[bucketIdx].`zeroArgFieldName`.setLen(0) - broker.buckets[bucketIdx].`argFieldName`.setLen(0) - if brokerCtx != DefaultBrokerContext: - broker.buckets.delete(bucketIdx) - - proc clearProviders*(_: typedesc[`typeIdent`]) = - clearProviders(`typeIdent`, DefaultBrokerContext) - - proc removeProvider*( - _: typedesc[`typeIdent`], - brokerCtx: BrokerContext, - `removeHandleCtxSym`: `providerHandleIdent`, - ) = - if `removeHandleCtxSym`.id == 0'u64: - return - let broker = `accessProcIdent`() - if broker.isNil(): - return - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - return - - if `removeHandleCtxSym`.kind == `zeroKindIdent`: - let idx = int(`removeHandleCtxSym`.id) - 1 - if idx >= 0 and idx < broker.buckets[bucketIdx].`zeroArgFieldName`.len: - broker.buckets[bucketIdx].`zeroArgFieldName`[idx] = nil - elif `removeHandleCtxSym`.kind == `argKindIdent`: - let idx = int(`removeHandleCtxSym`.id) - 1 - if idx >= 0 and idx < broker.buckets[bucketIdx].`argFieldName`.len: - broker.buckets[bucketIdx].`argFieldName`[idx] = nil - - if brokerCtx != DefaultBrokerContext: - var hasAny = false - for p in broker.buckets[bucketIdx].`zeroArgFieldName`: - if not p.isNil(): - hasAny = true - break - if not hasAny: - for p in broker.buckets[bucketIdx].`argFieldName`: - if not p.isNil(): - hasAny = true - break - if not hasAny: - broker.buckets.delete(bucketIdx) - - proc removeProvider*( - _: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent` - ) = - removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`) - - ) - elif not zeroArgSig.isNil(): - result.add( - quote do: - proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) = - let broker = `accessProcIdent`() - if broker.isNil(): - return - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - return - broker.buckets[bucketIdx].`zeroArgFieldName`.setLen(0) - if brokerCtx != DefaultBrokerContext: - broker.buckets.delete(bucketIdx) - - proc clearProviders*(_: typedesc[`typeIdent`]) = - clearProviders(`typeIdent`, DefaultBrokerContext) - - proc removeProvider*( - _: typedesc[`typeIdent`], - brokerCtx: BrokerContext, - `removeHandleCtxSym`: `providerHandleIdent`, - ) = - if `removeHandleCtxSym`.id == 0'u64: - return - let broker = `accessProcIdent`() - if broker.isNil(): - return - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - return - if `removeHandleCtxSym`.kind != `zeroKindIdent`: - return - let idx = int(`removeHandleCtxSym`.id) - 1 - if idx >= 0 and idx < broker.buckets[bucketIdx].`zeroArgFieldName`.len: - broker.buckets[bucketIdx].`zeroArgFieldName`[idx] = nil - if brokerCtx != DefaultBrokerContext: - var hasAny = false - for p in broker.buckets[bucketIdx].`zeroArgFieldName`: - if not p.isNil(): - hasAny = true - break - if not hasAny: - broker.buckets.delete(bucketIdx) - - proc removeProvider*( - _: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent` - ) = - removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`) - - ) - else: - result.add( - quote do: - proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) = - let broker = `accessProcIdent`() - if broker.isNil(): - return - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - return - broker.buckets[bucketIdx].`argFieldName`.setLen(0) - if brokerCtx != DefaultBrokerContext: - broker.buckets.delete(bucketIdx) - - proc clearProviders*(_: typedesc[`typeIdent`]) = - clearProviders(`typeIdent`, DefaultBrokerContext) - - proc removeProvider*( - _: typedesc[`typeIdent`], - brokerCtx: BrokerContext, - `removeHandleCtxSym`: `providerHandleIdent`, - ) = - if `removeHandleCtxSym`.id == 0'u64: - return - let broker = `accessProcIdent`() - if broker.isNil(): - return - let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx) - if bucketIdx < 0: - return - if `removeHandleCtxSym`.kind != `argKindIdent`: - return - let idx = int(`removeHandleCtxSym`.id) - 1 - if idx >= 0 and idx < broker.buckets[bucketIdx].`argFieldName`.len: - broker.buckets[bucketIdx].`argFieldName`[idx] = nil - if brokerCtx != DefaultBrokerContext: - var hasAny = false - for p in broker.buckets[bucketIdx].`argFieldName`: - if not p.isNil(): - hasAny = true - break - if not hasAny: - broker.buckets.delete(bucketIdx) - - proc removeProvider*( - _: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent` - ) = - removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`) - - ) - - when defined(requestBrokerDebug): - echo result.repr diff --git a/waku/common/broker/request_broker.nim b/waku/common/broker/request_broker.nim deleted file mode 100644 index 46f7d7d16..000000000 --- a/waku/common/broker/request_broker.nim +++ /dev/null @@ -1,841 +0,0 @@ -## RequestBroker -## -------------------- -## RequestBroker represents a proactive decoupling pattern, that -## allows defining request-response style interactions between modules without -## need for direct dependencies in between. -## Worth considering using it in a single provider, many requester scenario. -## -## Provides a declarative way to define an immutable value type together with a -## thread-local broker that can register an asynchronous or synchronous provider, -## dispatch typed requests and clear provider. -## -## For consideration use `sync` mode RequestBroker when you need to provide simple value(s) -## where there is no long-running async operation involved. -## Typically it act as a accessor for the local state of generic setting. -## -## `async` mode is better to be used when you request date that may involve some long IO operation -## or action. -## -## Default vs. context aware use: -## Every generated broker is a thread-local global instance. This means each RequestBroker enables decoupled -## data exchange threadwise. Sometimes we use brokers inside a context - like inside a component that has many modules or subsystems. -## In case you would instantiate multiple such components in a single thread, and each component must has its own provider for the same RequestBroker type, -## in order to avoid provider collision, you can use context aware RequestBroker. -## Context awareness is supported through the `BrokerContext` argument for `setProvider`, `request`, `clearProvider` interfaces. -## Suce use requires generating a new unique `BrokerContext` value per component instance, and spread it to all modules using the brokers. -## Example, store the `BrokerContext` as a field inside the top level component instance, and spread around at initialization of the subcomponents.. -## -## Default broker context is defined as `DefaultBrokerContext` constant. But if you don't need context awareness, you can use the -## interfaces without context argument. -## -## Usage: -## Declare your desired request type inside a `RequestBroker` macro, add any number of fields. -## Define the provider signature, that is enforced at compile time. -## -## ```nim -## RequestBroker: -## type TypeName = object -## field1*: FieldType -## field2*: AnotherFieldType -## -## proc signature*(): Future[Result[TypeName, string]] -## ## Also possible to define signature with arbitrary input arguments. -## proc signature*(arg1: ArgType, arg2: AnotherArgType): Future[Result[TypeName, string]] -## -## ``` -## -## Sync mode (no `async` / `Future`) can be generated with: -## -## ```nim -## RequestBroker(sync): -## type TypeName = object -## field1*: FieldType -## -## proc signature*(): Result[TypeName, string] -## proc signature*(arg1: ArgType): Result[TypeName, string] -## ``` -## -## Note: When the request type is declared as a native type / alias / externally-defined -## type (i.e. not an inline `object` / `ref object` definition), RequestBroker -## will wrap it in `distinct` automatically unless you already used `distinct`. -## This avoids overload ambiguity when multiple brokers share the same -## underlying base type (Nim overload resolution does not consider return type). -## -## This means that for non-object request types you typically: -## - construct values with an explicit cast/constructor, e.g. `MyType("x")` -## - unwrap with a cast when needed, e.g. `string(myVal)` or `BaseType(myVal)` -## -## Example (native response type): -## ```nim -## RequestBroker(sync): -## type MyCount = int # exported as: `distinct int` -## -## MyCount.setProvider(proc(): Result[MyCount, string] = ok(MyCount(42))) -## let res = MyCount.request() -## if res.isOk(): -## let raw = int(res.get()) -## ``` -## -## Example (externally-defined type): -## ```nim -## type External = object -## label*: string -## -## RequestBroker: -## type MyExternal = External # exported as: `distinct External` -## -## MyExternal.setProvider( -## proc(): Future[Result[MyExternal, string]] {.async.} = -## ok(MyExternal(External(label: "hi"))) -## ) -## let res = await MyExternal.request() -## if res.isOk(): -## let base = External(res.get()) -## echo base.label -## ``` -## The 'TypeName' object defines the requestable data (but also can be seen as request for action with return value). -## The 'signature' proc defines the provider(s) signature, that is enforced at compile time. -## One signature can be with no arguments, another with any number of arguments - where the input arguments are -## not related to the request type - but alternative inputs for the request to be processed. -## -## After this, you can register a provider anywhere in your code with -## `TypeName.setProvider(...)`, which returns error if already having a provider. -## Providers are async procs/lambdas in default mode and sync procs in sync mode. -## -## Providers are stored as a broker-context keyed list: -## - the default provider is always stored at index 0 (reserved broker context: 0) -## - additional providers can be registered under arbitrary non-zero broker contexts -## -## The original `setProvider(handler)` / `request(...)` APIs continue to operate -## on the default provider (broker context 0) for backward compatibility. -## -## Requests can be made from anywhere with no direct dependency on the provider by -## calling `TypeName.request()` - with arguments respecting the signature(s). -## In async mode, this returns a Future[Result[TypeName, string]]. In sync mode, it returns Result[TypeName, string]. -## -## Whenever you no want to process requests (or your object instance that provides the request goes out of scope), -## you can remove it from the broker with `TypeName.clearProvider()`. -## -## -## Example: -## ```nim -## RequestBroker: -## type Greeting = object -## text*: string -## -## ## Define the request and provider signature, that is enforced at compile time. -## proc signature*(): Future[Result[Greeting, string]] {.async.} -## -## ## Also possible to define signature with arbitrary input arguments. -## proc signature*(lang: string): Future[Result[Greeting, string]] {.async.} -## -## ... -## Greeting.setProvider( -## proc(): Future[Result[Greeting, string]] {.async.} = -## ok(Greeting(text: "hello")) -## ) -## let res = await Greeting.request() -## -## -## ... -## # using native type as response for a synchronous request. -## RequestBroker(sync): -## type NeedThatInfo = string -## -##... -## NeedThatInfo.setProvider( -## proc(): Result[NeedThatInfo, string] = -## ok("this is the info you wanted") -## ) -## let res = NeedThatInfo.request().valueOr: -## echo "not ok due to: " & error -## NeedThatInfo(":-(") -## -## echo string(res) -## ``` -## If no `signature` proc is declared, a zero-argument form is generated -## automatically, so the caller only needs to provide the type definition. - -import std/[macros, strutils] -from std/sequtils import keepItIf -import chronos -import results -import ./helper/broker_utils, broker_context - -export results, chronos, keepItIf, broker_context - -proc errorFuture[T](message: string): Future[Result[T, string]] {.inline.} = - ## Build a future that is already completed with an error result. - let fut = newFuture[Result[T, string]]("request_broker.errorFuture") - fut.complete(err(Result[T, string], message)) - fut - -type RequestBrokerMode = enum - rbAsync - rbSync - -proc isAsyncReturnTypeValid(returnType, typeIdent: NimNode): bool = - ## Accept Future[Result[TypeIdent, string]] as the contract. - if returnType.kind != nnkBracketExpr or returnType.len != 2: - return false - if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Future"): - return false - let inner = returnType[1] - if inner.kind != nnkBracketExpr or inner.len != 3: - return false - if inner[0].kind != nnkIdent or not inner[0].eqIdent("Result"): - return false - if inner[1].kind != nnkIdent or not inner[1].eqIdent($typeIdent): - return false - inner[2].kind == nnkIdent and inner[2].eqIdent("string") - -proc isSyncReturnTypeValid(returnType, typeIdent: NimNode): bool = - ## Accept Result[TypeIdent, string] as the contract. - if returnType.kind != nnkBracketExpr or returnType.len != 3: - return false - if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Result"): - return false - if returnType[1].kind != nnkIdent or not returnType[1].eqIdent($typeIdent): - return false - returnType[2].kind == nnkIdent and returnType[2].eqIdent("string") - -proc isReturnTypeValid(returnType, typeIdent: NimNode, mode: RequestBrokerMode): bool = - case mode - of rbAsync: - isAsyncReturnTypeValid(returnType, typeIdent) - of rbSync: - isSyncReturnTypeValid(returnType, typeIdent) - -proc makeProcType( - returnType: NimNode, params: seq[NimNode], mode: RequestBrokerMode -): NimNode = - var formal = newTree(nnkFormalParams) - formal.add(returnType) - for param in params: - formal.add(param) - case mode - of rbAsync: - let pragmas = newTree(nnkPragma, ident("async")) - newTree(nnkProcTy, formal, pragmas) - of rbSync: - let raisesPragma = newTree( - nnkExprColonExpr, ident("raises"), newTree(nnkBracket, ident("CatchableError")) - ) - let pragmas = newTree(nnkPragma, raisesPragma, ident("gcsafe")) - newTree(nnkProcTy, formal, pragmas) - -proc parseMode(modeNode: NimNode): RequestBrokerMode = - ## Parses the mode selector for the 2-argument macro overload. - ## Supported spellings: `sync` / `async` (case-insensitive). - let raw = ($modeNode).strip().toLowerAscii() - case raw - of "sync": - rbSync - of "async": - rbAsync - else: - error("RequestBroker mode must be `sync` or `async` (default is async)", modeNode) - -proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode = - when defined(requestBrokerDebug): - echo body.treeRepr - echo "RequestBroker mode: ", $mode - let parsed = parseSingleTypeDef(body, "RequestBroker", allowRefToNonObject = true) - let typeIdent = parsed.typeIdent - let objectDef = parsed.objectDef - - when defined(requestBrokerDebug): - echo "RequestBroker generating type: ", $typeIdent - - let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*") - let typeDisplayName = sanitizeIdentName(typeIdent) - let typeNameLit = newLit(typeDisplayName) - var zeroArgSig: NimNode = nil - var zeroArgProviderName: NimNode = nil - var argSig: NimNode = nil - var argParams: seq[NimNode] = @[] - var argProviderName: NimNode = nil - - for stmt in body: - case stmt.kind - of nnkProcDef: - let procName = stmt[0] - let procNameIdent = - case procName.kind - of nnkIdent: - procName - of nnkPostfix: - procName[1] - else: - procName - let procNameStr = $procNameIdent - if not procNameStr.startsWith("signature"): - error("Signature proc names must start with `signature`", procName) - let params = stmt.params - if params.len == 0: - error("Signature must declare a return type", stmt) - let returnType = params[0] - if not isReturnTypeValid(returnType, typeIdent, mode): - case mode - of rbAsync: - error( - "Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt - ) - of rbSync: - error("Signature must return Result[`" & $typeIdent & "`, string]", stmt) - let paramCount = params.len - 1 - if paramCount == 0: - if zeroArgSig != nil: - error("Only one zero-argument signature is allowed", stmt) - zeroArgSig = stmt - zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs") - elif paramCount >= 1: - if argSig != nil: - error("Only one argument-based signature is allowed", stmt) - argSig = stmt - argParams = @[] - for idx in 1 ..< params.len: - let paramDef = params[idx] - if paramDef.kind != nnkIdentDefs: - error( - "Signature parameter must be a standard identifier declaration", paramDef - ) - let paramTypeNode = paramDef[paramDef.len - 2] - if paramTypeNode.kind == nnkEmpty: - error("Signature parameter must declare a type", paramDef) - var hasName = false - for i in 0 ..< paramDef.len - 2: - if paramDef[i].kind != nnkEmpty: - hasName = true - if not hasName: - error("Signature parameter must declare a name", paramDef) - argParams.add(copyNimTree(paramDef)) - argProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderWithArgs") - of nnkTypeSection, nnkEmpty: - discard - else: - error("Unsupported statement inside RequestBroker definition", stmt) - - if zeroArgSig.isNil() and argSig.isNil(): - zeroArgSig = newEmptyNode() - zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs") - - var typeSection = newTree(nnkTypeSection) - typeSection.add(newTree(nnkTypeDef, exportedTypeIdent, newEmptyNode(), objectDef)) - - let returnType = - case mode - of rbAsync: - quote: - Future[Result[`typeIdent`, string]] - of rbSync: - quote: - Result[`typeIdent`, string] - - if not zeroArgSig.isNil(): - let procType = makeProcType(returnType, @[], mode) - typeSection.add(newTree(nnkTypeDef, zeroArgProviderName, newEmptyNode(), procType)) - if not argSig.isNil(): - let procType = makeProcType(returnType, cloneParams(argParams), mode) - typeSection.add(newTree(nnkTypeDef, argProviderName, newEmptyNode(), procType)) - - var brokerRecList = newTree(nnkRecList) - if not zeroArgSig.isNil(): - let zeroArgProvidersFieldName = ident("providersNoArgs") - let zeroArgProvidersTupleTy = newTree( - nnkTupleTy, - newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()), - newTree(nnkIdentDefs, ident("handler"), zeroArgProviderName, newEmptyNode()), - ) - let zeroArgProvidersSeqTy = - newTree(nnkBracketExpr, ident("seq"), zeroArgProvidersTupleTy) - brokerRecList.add( - newTree( - nnkIdentDefs, zeroArgProvidersFieldName, zeroArgProvidersSeqTy, newEmptyNode() - ) - ) - if not argSig.isNil(): - let argProvidersFieldName = ident("providersWithArgs") - let argProvidersTupleTy = newTree( - nnkTupleTy, - newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()), - newTree(nnkIdentDefs, ident("handler"), argProviderName, newEmptyNode()), - ) - let argProvidersSeqTy = newTree(nnkBracketExpr, ident("seq"), argProvidersTupleTy) - brokerRecList.add( - newTree(nnkIdentDefs, argProvidersFieldName, argProvidersSeqTy, newEmptyNode()) - ) - let brokerTypeIdent = ident(sanitizeIdentName(typeIdent) & "Broker") - let brokerTypeDef = newTree( - nnkTypeDef, - brokerTypeIdent, - newEmptyNode(), - newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), brokerRecList), - ) - typeSection.add(brokerTypeDef) - result = newStmtList() - result.add(typeSection) - - let globalVarIdent = ident("g" & sanitizeIdentName(typeIdent) & "Broker") - let accessProcIdent = ident("access" & sanitizeIdentName(typeIdent) & "Broker") - - var brokerNewBody = newStmtList() - if not zeroArgSig.isNil(): - brokerNewBody.add( - quote do: - result.providersNoArgs = - @[(brokerCtx: DefaultBrokerContext, handler: default(`zeroArgProviderName`))] - ) - if not argSig.isNil(): - brokerNewBody.add( - quote do: - result.providersWithArgs = - @[(brokerCtx: DefaultBrokerContext, handler: default(`argProviderName`))] - ) - - var brokerInitChecks = newStmtList() - if not zeroArgSig.isNil(): - brokerInitChecks.add( - quote do: - if `globalVarIdent`.providersNoArgs.len == 0: - `globalVarIdent` = `brokerTypeIdent`.new() - ) - if not argSig.isNil(): - brokerInitChecks.add( - quote do: - if `globalVarIdent`.providersWithArgs.len == 0: - `globalVarIdent` = `brokerTypeIdent`.new() - ) - - result.add( - quote do: - var `globalVarIdent` {.threadvar.}: `brokerTypeIdent` - - proc new(_: type `brokerTypeIdent`): `brokerTypeIdent` = - result = `brokerTypeIdent`() - `brokerNewBody` - - proc `accessProcIdent`(): var `brokerTypeIdent` = - `brokerInitChecks` - `globalVarIdent` - - ) - - var clearBodyKeyed = newStmtList() - let brokerCtxParamIdent = ident("brokerCtx") - if not zeroArgSig.isNil(): - let zeroArgProvidersFieldName = ident("providersNoArgs") - result.add( - quote do: - proc setProvider*( - _: typedesc[`typeIdent`], handler: `zeroArgProviderName` - ): Result[void, string] = - if not `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler.isNil(): - return err("Zero-arg provider already set") - `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler = handler - return ok() - - ) - - result.add( - quote do: - proc setProvider*( - _: typedesc[`typeIdent`], - brokerCtx: BrokerContext, - handler: `zeroArgProviderName`, - ): Result[void, string] = - if brokerCtx == DefaultBrokerContext: - return setProvider(`typeIdent`, handler) - - for entry in `accessProcIdent`().`zeroArgProvidersFieldName`: - if entry.brokerCtx == brokerCtx: - return err( - "RequestBroker(" & `typeNameLit` & - "): provider already set for broker context " & $brokerCtx - ) - - `accessProcIdent`().`zeroArgProvidersFieldName`.add( - (brokerCtx: brokerCtx, handler: handler) - ) - return ok() - - ) - clearBodyKeyed.add( - quote do: - if `brokerCtxParamIdent` == DefaultBrokerContext: - `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler = - default(`zeroArgProviderName`) - else: - `accessProcIdent`().`zeroArgProvidersFieldName`.keepItIf( - it.brokerCtx != `brokerCtxParamIdent` - ) - ) - case mode - of rbAsync: - result.add( - quote do: - proc request*( - _: typedesc[`typeIdent`] - ): Future[Result[`typeIdent`, string]] {.async: (raises: []).} = - return await request(`typeIdent`, DefaultBrokerContext) - - ) - - result.add( - quote do: - proc request*( - _: typedesc[`typeIdent`], brokerCtx: BrokerContext - ): Future[Result[`typeIdent`, string]] {.async: (raises: []).} = - var provider: `zeroArgProviderName` - if brokerCtx == DefaultBrokerContext: - provider = `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler - else: - for entry in `accessProcIdent`().`zeroArgProvidersFieldName`: - if entry.brokerCtx == brokerCtx: - provider = entry.handler - break - - if provider.isNil(): - if brokerCtx == DefaultBrokerContext: - return err( - "RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered" - ) - return err( - "RequestBroker(" & `typeNameLit` & - "): no provider registered for broker context " & $brokerCtx - ) - - let catchedRes = catch: - await provider() - - if catchedRes.isErr(): - return err( - "RequestBroker(" & `typeNameLit` & "): provider threw exception: " & - catchedRes.error.msg - ) - - let providerRes = catchedRes.get() - if providerRes.isOk(): - let resultValue = providerRes.get() - when compiles(resultValue.isNil()): - if resultValue.isNil(): - return err( - "RequestBroker(" & `typeNameLit` & "): provider returned nil result" - ) - return providerRes - - ) - of rbSync: - result.add( - quote do: - proc request*( - _: typedesc[`typeIdent`] - ): Result[`typeIdent`, string] {.gcsafe, raises: [].} = - return request(`typeIdent`, DefaultBrokerContext) - - ) - - result.add( - quote do: - proc request*( - _: typedesc[`typeIdent`], brokerCtx: BrokerContext - ): Result[`typeIdent`, string] {.gcsafe, raises: [].} = - var provider: `zeroArgProviderName` - if brokerCtx == DefaultBrokerContext: - provider = `accessProcIdent`().`zeroArgProvidersFieldName`[0].handler - else: - for entry in `accessProcIdent`().`zeroArgProvidersFieldName`: - if entry.brokerCtx == brokerCtx: - provider = entry.handler - break - - if provider.isNil(): - if brokerCtx == DefaultBrokerContext: - return err( - "RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered" - ) - return err( - "RequestBroker(" & `typeNameLit` & - "): no provider registered for broker context " & $brokerCtx - ) - - var providerRes: Result[`typeIdent`, string] - try: - providerRes = provider() - except CatchableError as e: - return err( - "RequestBroker(" & `typeNameLit` & "): provider threw exception: " & - e.msg - ) - - if providerRes.isOk(): - let resultValue = providerRes.get() - when compiles(resultValue.isNil()): - if resultValue.isNil(): - return err( - "RequestBroker(" & `typeNameLit` & "): provider returned nil result" - ) - return providerRes - - ) - if not argSig.isNil(): - let argProvidersFieldName = ident("providersWithArgs") - result.add( - quote do: - proc setProvider*( - _: typedesc[`typeIdent`], handler: `argProviderName` - ): Result[void, string] = - if not `accessProcIdent`().`argProvidersFieldName`[0].handler.isNil(): - return err("Provider already set") - `accessProcIdent`().`argProvidersFieldName`[0].handler = handler - return ok() - - ) - - result.add( - quote do: - proc setProvider*( - _: typedesc[`typeIdent`], - brokerCtx: BrokerContext, - handler: `argProviderName`, - ): Result[void, string] = - if brokerCtx == DefaultBrokerContext: - return setProvider(`typeIdent`, handler) - - for entry in `accessProcIdent`().`argProvidersFieldName`: - if entry.brokerCtx == brokerCtx: - return err( - "RequestBroker(" & `typeNameLit` & - "): provider already set for broker context " & $brokerCtx - ) - - `accessProcIdent`().`argProvidersFieldName`.add( - (brokerCtx: brokerCtx, handler: handler) - ) - return ok() - - ) - clearBodyKeyed.add( - quote do: - if `brokerCtxParamIdent` == DefaultBrokerContext: - `accessProcIdent`().`argProvidersFieldName`[0].handler = - default(`argProviderName`) - else: - `accessProcIdent`().`argProvidersFieldName`.keepItIf( - it.brokerCtx != `brokerCtxParamIdent` - ) - ) - let requestParamDefs = cloneParams(argParams) - let argNameIdents = collectParamNames(requestParamDefs) - var formalParams = newTree(nnkFormalParams) - formalParams.add(copyNimTree(returnType)) - formalParams.add( - newTree( - nnkIdentDefs, - ident("_"), - newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)), - newEmptyNode(), - ) - ) - for paramDef in requestParamDefs: - formalParams.add(paramDef) - - let requestPragmas = - case mode - of rbAsync: - quote: - {.async: (raises: []).} - of rbSync: - quote: - {.gcsafe, raises: [].} - - var forwardCall = newCall(ident("request")) - forwardCall.add(copyNimTree(typeIdent)) - forwardCall.add(ident("DefaultBrokerContext")) - for argName in argNameIdents: - forwardCall.add(argName) - - var requestBody = newStmtList() - case mode - of rbAsync: - requestBody.add( - quote do: - return await `forwardCall` - ) - of rbSync: - requestBody.add( - quote do: - return `forwardCall` - ) - - result.add( - newTree( - nnkProcDef, - postfix(ident("request"), "*"), - newEmptyNode(), - newEmptyNode(), - formalParams, - requestPragmas, - newEmptyNode(), - requestBody, - ) - ) - - # Keyed request variant for the argument-based signature. - let requestParamDefsKeyed = cloneParams(argParams) - let argNameIdentsKeyed = collectParamNames(requestParamDefsKeyed) - let providerSymKeyed = genSym(nskVar, "provider") - var formalParamsKeyed = newTree(nnkFormalParams) - formalParamsKeyed.add(copyNimTree(returnType)) - formalParamsKeyed.add( - newTree( - nnkIdentDefs, - ident("_"), - newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)), - newEmptyNode(), - ) - ) - formalParamsKeyed.add( - newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode()) - ) - for paramDef in requestParamDefsKeyed: - formalParamsKeyed.add(paramDef) - - let requestPragmasKeyed = requestPragmas - var providerCallKeyed = newCall(providerSymKeyed) - for argName in argNameIdentsKeyed: - providerCallKeyed.add(argName) - - var requestBodyKeyed = newStmtList() - requestBodyKeyed.add( - quote do: - var `providerSymKeyed`: `argProviderName` - if brokerCtx == DefaultBrokerContext: - `providerSymKeyed` = `accessProcIdent`().`argProvidersFieldName`[0].handler - else: - for entry in `accessProcIdent`().`argProvidersFieldName`: - if entry.brokerCtx == brokerCtx: - `providerSymKeyed` = entry.handler - break - ) - requestBodyKeyed.add( - quote do: - if `providerSymKeyed`.isNil(): - if brokerCtx == DefaultBrokerContext: - return err( - "RequestBroker(" & `typeNameLit` & - "): no provider registered for input signature" - ) - return err( - "RequestBroker(" & `typeNameLit` & - "): no provider registered for broker context " & $brokerCtx - ) - ) - - case mode - of rbAsync: - requestBodyKeyed.add( - quote do: - let catchedRes = catch: - await `providerCallKeyed` - if catchedRes.isErr(): - return err( - "RequestBroker(" & `typeNameLit` & "): provider threw exception: " & - catchedRes.error.msg - ) - - let providerRes = catchedRes.get() - if providerRes.isOk(): - let resultValue = providerRes.get() - when compiles(resultValue.isNil()): - if resultValue.isNil(): - return err( - "RequestBroker(" & `typeNameLit` & "): provider returned nil result" - ) - return providerRes - ) - of rbSync: - requestBodyKeyed.add( - quote do: - var providerRes: Result[`typeIdent`, string] - try: - providerRes = `providerCallKeyed` - except CatchableError as e: - return err( - "RequestBroker(" & `typeNameLit` & "): provider threw exception: " & e.msg - ) - - if providerRes.isOk(): - let resultValue = providerRes.get() - when compiles(resultValue.isNil()): - if resultValue.isNil(): - return err( - "RequestBroker(" & `typeNameLit` & "): provider returned nil result" - ) - return providerRes - ) - - result.add( - newTree( - nnkProcDef, - postfix(ident("request"), "*"), - newEmptyNode(), - newEmptyNode(), - formalParamsKeyed, - requestPragmasKeyed, - newEmptyNode(), - requestBodyKeyed, - ) - ) - - block: - var formalParamsClearKeyed = newTree(nnkFormalParams) - formalParamsClearKeyed.add(newEmptyNode()) - formalParamsClearKeyed.add( - newTree( - nnkIdentDefs, - ident("_"), - newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)), - newEmptyNode(), - ) - ) - formalParamsClearKeyed.add( - newTree(nnkIdentDefs, brokerCtxParamIdent, ident("BrokerContext"), newEmptyNode()) - ) - - result.add( - newTree( - nnkProcDef, - postfix(ident("clearProvider"), "*"), - newEmptyNode(), - newEmptyNode(), - formalParamsClearKeyed, - newEmptyNode(), - newEmptyNode(), - clearBodyKeyed, - ) - ) - - result.add( - quote do: - proc clearProvider*(_: typedesc[`typeIdent`]) = - clearProvider(`typeIdent`, DefaultBrokerContext) - - ) - - when defined(requestBrokerDebug): - echo result.repr - - return result - -macro RequestBroker*(body: untyped): untyped = - ## Default (async) mode. - generateRequestBroker(body, rbAsync) - -macro RequestBroker*(mode: untyped, body: untyped): untyped = - ## Explicit mode selector. - ## Example: - ## RequestBroker(sync): - ## type Foo = object - ## proc signature*(): Result[Foo, string] - generateRequestBroker(body, parseMode(mode)) diff --git a/waku/events/delivery_events.nim b/waku/events/delivery_events.nim index f27f02721..5730335e0 100644 --- a/waku/events/delivery_events.nim +++ b/waku/events/delivery_events.nim @@ -1,4 +1,5 @@ -import waku/waku_core/[message/message, message/digest], waku/common/broker/event_broker +import brokers/event_broker +import waku/waku_core/[message/message, message/digest] EventBroker: type OnFilterSubscribeEvent* = object diff --git a/waku/events/events.nim b/waku/events/events.nim index 46dd4fdd3..5a3c0c748 100644 --- a/waku/events/events.nim +++ b/waku/events/events.nim @@ -1,3 +1,3 @@ -import ./[message_events, delivery_events, health_events, peer_events] +import ./[message_events, delivery_events, health_events, peer_events, lifecycle_events] -export message_events, delivery_events, health_events, peer_events +export message_events, delivery_events, health_events, peer_events, lifecycle_events diff --git a/waku/events/health_events.nim b/waku/events/health_events.nim index 1e6decedb..95912941e 100644 --- a/waku/events/health_events.nim +++ b/waku/events/health_events.nim @@ -1,4 +1,4 @@ -import waku/common/broker/event_broker +import brokers/event_broker import waku/api/types import waku/node/health_monitor/[protocol_health, topic_health] diff --git a/waku/events/message_events.nim b/waku/events/message_events.nim index 677a4a433..b45f91249 100644 --- a/waku/events/message_events.nim +++ b/waku/events/message_events.nim @@ -1,5 +1,5 @@ -import waku/[api/types, waku_core/message, waku_core/topics, common/broker/event_broker] - +import brokers/event_broker +import waku/[api/types, waku_core/message, waku_core/topics] export types EventBroker: diff --git a/waku/events/peer_events.nim b/waku/events/peer_events.nim index dd02841f7..7eed309b3 100644 --- a/waku/events/peer_events.nim +++ b/waku/events/peer_events.nim @@ -1,4 +1,4 @@ -import waku/common/broker/event_broker +import brokers/event_broker import libp2p/switch type WakuPeerEventKind* {.pure.} = enum diff --git a/waku/factory/builder.nim b/waku/factory/builder.nim index 87b0db492..4212cb92d 100644 --- a/waku/factory/builder.nim +++ b/waku/factory/builder.nim @@ -8,15 +8,16 @@ import libp2p/builders, libp2p/nameresolving/nameresolver, libp2p/transports/wstransport, - libp2p/protocols/connectivity/relay/relay + libp2p/protocols/connectivity/relay/relay, + brokers/broker_context + import ../waku_enr, ../discovery/waku_discv5, ../waku_node, ../node/peer_manager, ../common/rate_limit/setting, - ../common/utils/parse_size_units, - ../common/broker/broker_context + ../common/utils/parse_size_units type WakuNodeBuilder* = object # General diff --git a/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index 5954bbe58..96e34eeed 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -14,6 +14,7 @@ import common/logging, common/utils/parse_size_units, waku_enr/capabilities, + persistency/persistency, ], tools/confutils/entry_nodes @@ -136,6 +137,8 @@ type WakuConfBuilder* = object circuitRelayClient: Option[bool] p2pReliability: Option[bool] + localStoragePath: Option[string] + proc init*(T: type WakuConfBuilder): WakuConfBuilder = WakuConfBuilder( dnsDiscoveryConf: DnsDiscoveryConfBuilder.init(), @@ -272,6 +275,9 @@ proc withRelayShardedPeerManagement*( proc withP2pReliability*(b: var WakuConfBuilder, p2pReliability: bool) = b.p2pReliability = some(p2pReliability) +proc withLocalStoragePath*(b: var WakuConfBuilder, localStoragePath: string) = + b.localStoragePath = some(localStoragePath) + proc withExtMultiAddrs*(builder: var WakuConfBuilder, extMultiAddrs: seq[string]) = builder.extMultiAddrs = concat(builder.extMultiAddrs, extMultiAddrs) @@ -719,6 +725,7 @@ proc build*( relayShardedPeerManagement: relayShardedPeerManagement, p2pReliability: builder.p2pReliability.get(false), wakuFlags: wakuFlags, + localStoragePath: builder.localStoragePath.get(DefaultStoragePath), ) ?wakuConf.validate() diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 395841130..6a5567f8c 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -18,6 +18,7 @@ import presto, metrics, metrics/chronos_httpserver, + brokers/broker_context, waku/[ waku_core, waku_node, @@ -30,7 +31,6 @@ import waku_enr/multiaddr, api/types, common/logging, - common/broker/broker_context, node/peer_manager, node/health_monitor, node/waku_metrics, @@ -46,6 +46,7 @@ import factory/node_factory, factory/internal_config, factory/app_callbacks, + persistency/persistency, ], ./waku_conf, ./waku_state_info @@ -393,6 +394,12 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: else: waku[].dynamicBootstrapNodes = dynamicBootstrapNodesRes.get() + ## Initialize persistency singleton instance - we don't need the instance itself here, + ## but this ensures it's initialized before any store job starts. + discard Persistency.instance(conf.localStoragePath).valueOr: + error "Failed to initialize persistency instance", error = $error + return err("Failed to initialize persistency instance: " & $error) + (await startNode(waku.node, waku.conf, waku.dynamicBootstrapNodes)).isOkOr: return err("error while calling startNode: " & $error) @@ -523,6 +530,8 @@ proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} = try: waku.healthMonitor.setOverallHealth(HealthStatus.SHUTTING_DOWN) + Persistency.reset() + if not waku.metricsServer.isNil(): await waku.metricsServer.stop() diff --git a/waku/factory/waku_conf.nim b/waku/factory/waku_conf.nim index 4934faccc..9edc12a44 100644 --- a/waku/factory/waku_conf.nim +++ b/waku/factory/waku_conf.nim @@ -152,6 +152,8 @@ type WakuConf* {.requiresInit.} = ref object p2pReliability*: bool + localStoragePath*: string + proc logConf*(conf: WakuConf) = info "Configuration: Enabled protocols", relay = conf.relay, diff --git a/waku/node/delivery_service/recv_service/recv_service.nim b/waku/node/delivery_service/recv_service/recv_service.nim index 0f077a289..899f80f71 100644 --- a/waku/node/delivery_service/recv_service/recv_service.nim +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -5,6 +5,7 @@ import std/[tables, sequtils, options, sets] import chronos, chronicles, libp2p/utility import ../[subscription_manager] +import brokers/broker_context import waku/[ waku_core, @@ -14,7 +15,6 @@ import waku_core/topics, events/message_events, waku_node, - common/broker/broker_context, ] const StoreCheckPeriod = chronos.minutes(5) ## How often to perform store queries @@ -179,7 +179,7 @@ proc startRecvService*(self: RecvService) = quit(QuitFailure) proc stopRecvService*(self: RecvService) {.async.} = - MessageSeenEvent.dropListener(self.brokerCtx, self.seenMsgListener) + await MessageSeenEvent.dropListener(self.brokerCtx, self.seenMsgListener) if not self.msgCheckerHandler.isNil(): await self.msgCheckerHandler.cancelAndWait() self.msgCheckerHandler = nil diff --git a/waku/node/delivery_service/send_service/delivery_task.nim b/waku/node/delivery_service/send_service/delivery_task.nim index 0ff151f6e..aa1dc17d7 100644 --- a/waku/node/delivery_service/send_service/delivery_task.nim +++ b/waku/node/delivery_service/send_service/delivery_task.nim @@ -1,6 +1,6 @@ import std/[options, times], chronos +import brokers/broker_context import waku/waku_core, waku/api/types, waku/requests/node_requests -import waku/common/broker/broker_context type DeliveryState* {.pure.} = enum Entry diff --git a/waku/node/delivery_service/send_service/lightpush_processor.nim b/waku/node/delivery_service/send_service/lightpush_processor.nim index 40a754757..7a9f65c71 100644 --- a/waku/node/delivery_service/send_service/lightpush_processor.nim +++ b/waku/node/delivery_service/send_service/lightpush_processor.nim @@ -1,11 +1,7 @@ import chronicles, chronos, results import std/options - -import - waku/node/peer_manager, - waku/waku_core, - waku/waku_lightpush/[common, client, rpc], - waku/common/broker/broker_context +import brokers/broker_context +import waku/node/peer_manager, waku/waku_core, waku/waku_lightpush/[common, client, rpc] import ./[delivery_task, send_processor] diff --git a/waku/node/delivery_service/send_service/relay_processor.nim b/waku/node/delivery_service/send_service/relay_processor.nim index 833d15845..e06b664fb 100644 --- a/waku/node/delivery_service/send_service/relay_processor.nim +++ b/waku/node/delivery_service/send_service/relay_processor.nim @@ -1,8 +1,8 @@ import std/options import chronos, chronicles +import brokers/broker_context import waku/[waku_core], waku/waku_lightpush/[common, rpc] import waku/requests/health_requests -import waku/common/broker/broker_context import waku/api/types import ./[delivery_task, send_processor] diff --git a/waku/node/delivery_service/send_service/send_processor.nim b/waku/node/delivery_service/send_service/send_processor.nim index 0108eacd0..3782b9d4e 100644 --- a/waku/node/delivery_service/send_service/send_processor.nim +++ b/waku/node/delivery_service/send_service/send_processor.nim @@ -1,6 +1,6 @@ import chronos +import brokers/broker_context import ./delivery_task -import waku/common/broker/broker_context {.push raises: [].} diff --git a/waku/node/delivery_service/send_service/send_service.nim b/waku/node/delivery_service/send_service/send_service.nim index e6d3a2eda..902f3aa1c 100644 --- a/waku/node/delivery_service/send_service/send_service.nim +++ b/waku/node/delivery_service/send_service/send_service.nim @@ -3,6 +3,7 @@ import std/[sequtils, tables, options] import chronos, chronicles, libp2p/utility +import brokers/broker_context import ./[send_processor, relay_processor, lightpush_processor, delivery_task], ../[subscription_manager], @@ -17,7 +18,6 @@ import waku_lightpush/client, waku_lightpush/callbacks, events/message_events, - common/broker/broker_context, ] logScope: diff --git a/waku/node/delivery_service/subscription_manager.nim b/waku/node/delivery_service/subscription_manager.nim index c34335057..393a61eae 100644 --- a/waku/node/delivery_service/subscription_manager.nim +++ b/waku/node/delivery_service/subscription_manager.nim @@ -1,5 +1,7 @@ import std/[sequtils, sets, tables, options, strutils], chronos, chronicles, results import libp2p/[peerid, peerinfo] +import brokers/broker_context + import waku/[ waku_core, @@ -10,7 +12,6 @@ import waku_filter_v2/common as filter_common, waku_filter_v2/client as filter_client, waku_filter_v2/protocol as filter_protocol, - common/broker/broker_context, events/health_events, events/peer_events, requests/health_requests, @@ -530,7 +531,7 @@ proc stopEdgeFilterLoops(self: SubscriptionManager) {.async: (raises: []).} = if not fut.finished: await fut.cancelAndWait() - WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener) + await WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener) # --------------------------------------------------------------------------- # SubscriptionManager Lifecycle (calls Edge behavior above) diff --git a/waku/node/health_monitor/node_health_monitor.nim b/waku/node/health_monitor/node_health_monitor.nim index c92dc1aaf..c652f7cea 100644 --- a/waku/node/health_monitor/node_health_monitor.nim +++ b/waku/node/health_monitor/node_health_monitor.nim @@ -725,8 +725,10 @@ proc stopHealthMonitor*(hm: NodeHealthMonitor) {.async.} = if not isNil(hm.eventLoopMonitorFut): await hm.eventLoopMonitorFut.cancelAndWait() - WakuPeerEvent.dropListener(hm.node.brokerCtx, hm.peerEventListener) - EventShardTopicHealthChange.dropListener(hm.node.brokerCtx, hm.shardHealthListener) + await WakuPeerEvent.dropListener(hm.node.brokerCtx, hm.peerEventListener) + await EventShardTopicHealthChange.dropListener( + hm.node.brokerCtx, hm.shardHealthListener + ) if not isNil(hm.node.wakuRelay) and not isNil(hm.relayObserver): hm.node.wakuRelay.removeObserver(hm.relayObserver) diff --git a/waku/node/kernel_api/relay.nim b/waku/node/kernel_api/relay.nim index fe46f5bd2..f1b80cf19 100644 --- a/waku/node/kernel_api/relay.nim +++ b/waku/node/kernel_api/relay.nim @@ -16,7 +16,8 @@ import libp2p/builders, libp2p/transports/tcptransport, libp2p/transports/wstransport, - libp2p/utility + libp2p/utility, + brokers/broker_context import waku/[ @@ -29,7 +30,6 @@ import waku_rln_relay, node/waku_node, node/peer_manager, - common/broker/broker_context, events/message_events, ] diff --git a/waku/node/peer_manager/peer_manager.nim b/waku/node/peer_manager/peer_manager.nim index 1d8b55bb5..6602c049b 100644 --- a/waku/node/peer_manager/peer_manager.nim +++ b/waku/node/peer_manager/peer_manager.nim @@ -8,6 +8,9 @@ import chronicles, metrics, libp2p/[multistream, muxers/muxer, nameresolving/nameresolver, peerstore], + brokers/broker_context + +import waku/[ waku_core, waku_relay, @@ -21,7 +24,6 @@ import common/enr, common/callbacks, common/utils/parse_size_units, - common/broker/broker_context, node/health_monitor/online_monitor, ], ./peer_store/peer_storage, diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 7cd334b53..26a2b5a57 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -24,7 +24,9 @@ import libp2p/utility, libp2p/utils/offsettedseq, libp2p/protocols/mix, - libp2p/protocols/mix/mix_protocol + libp2p/protocols/mix/mix_protocol, + brokers/broker_context, + brokers/request_broker import waku/[ @@ -53,8 +55,6 @@ import common/rate_limit/setting, common/callbacks, common/nimchronos, - common/broker/broker_context, - common/broker/request_broker, waku_mix, requests/node_requests, requests/health_requests, diff --git a/waku/persistency/backend_comm.nim b/waku/persistency/backend_comm.nim new file mode 100644 index 000000000..dd7e71297 --- /dev/null +++ b/waku/persistency/backend_comm.nim @@ -0,0 +1,161 @@ +## Cross-thread broker declarations for the persistency library. +## +## One EventBroker (writes, fire-and-forget) and five RequestBrokers (reads +## + acked delete). All in multi-thread (mt) mode: the listener / provider runs on the +## job's storage thread; callers on any thread reach it via the shared +## BrokerContext owned by the Job. +## +## ## Error type, important +## +## nim-brokers' RequestBroker macro hard-codes the response shape as +## `Future[Result[ResponseType, string]]` — the error channel is `string`, +## not our `PersistencyError`. We honour the broker contract here and lift +## back to `PersistencyError` at the public facade (persistency.nim). The +## convention for the broker-level string is `": "` so the +## facade can reconstruct the `PersistencyErrorKind`. +## +## ## Response shapes +## +## The five Kv* types are *response* objects (the value the provider +## returns). Per-request inputs sit on the `signature` proc parameters. + +{.push raises: [].} + +import std/[options, strutils] +import chronos, results +import brokers/[event_broker, request_broker, broker_context] +import brokers/internal/mt_codec +import ./types + +export broker_context + +# ── mt codec overloads for non-POD library types ──────────────────────── +# +# brokers 2.0.0's mtMarshalValue / mtUnmarshalValue handle scalars, enums, +# strings, seqs, arrays, and plain object/tuple recursion -- but they do +# not see through `distinct seq[byte]`, nor do they know how to dispatch +# a variant (case) object. We provide explicit overloads for the types +# that appear in our broker payloads. + +proc mtMarshalValue*( + buf: ptr UncheckedArray[byte], cap: int, value: Key, pos: var int +): bool {.gcsafe.} = + ## Encode a Key as the raw seq[byte] it wraps. + mtMarshalValue(buf, cap, bytes(value), pos) + +proc mtUnmarshalValue*( + buf: ptr UncheckedArray[byte], len: int, value: var Key, pos: var int +): bool {.gcsafe.} = + var s: seq[byte] + if not mtUnmarshalValue(buf, len, s, pos): + return false + value = Key(s) + return true + +proc mtMarshalValue*( + buf: ptr UncheckedArray[byte], cap: int, value: TxOp, pos: var int +): bool {.gcsafe.} = + ## TxOp is a case object: write the discriminator, then only the + ## fields that belong to the active branch. + if not mtMarshalValue(buf, cap, value.category, pos): + return false + if not mtMarshalValue(buf, cap, value.key, pos): + return false + let kind = uint8(ord(value.kind)) + if not mtMarshalValue(buf, cap, kind, pos): + return false + case value.kind + of txPut: + if not mtMarshalValue(buf, cap, value.payload, pos): + return false + of txDelete: + discard + return true + +proc mtUnmarshalValue*( + buf: ptr UncheckedArray[byte], len: int, value: var TxOp, pos: var int +): bool {.gcsafe.} = + var + category: string + key: Key + kindByte: uint8 + if not mtUnmarshalValue(buf, len, category, pos): + return false + if not mtUnmarshalValue(buf, len, key, pos): + return false + if not mtUnmarshalValue(buf, len, kindByte, pos): + return false + case TxOpKind(kindByte) + of txPut: + var payload: seq[byte] + if not mtUnmarshalValue(buf, len, payload, pos): + return false + value = TxOp(category: category, key: key, kind: txPut, payload: payload) + of txDelete: + value = TxOp(category: category, key: key, kind: txDelete) + return true + +EventBroker(mt): + type PersistEvent* = object + ops*: seq[TxOp] + +RequestBroker(mt): + type KvGet* = object + value*: Option[seq[byte]] + + proc signature*(category: string, key: Key): Future[Result[KvGet, string]] {.async.} + +RequestBroker(mt): + type KvExists* = object + value*: bool + + proc signature*( + category: string, key: Key + ): Future[Result[KvExists, string]] {.async.} + +RequestBroker(mt): + type KvScan* = object + rows*: seq[KvRow] + + proc signature*( + category: string, range: KeyRange, reverse: bool + ): Future[Result[KvScan, string]] {.async.} + +RequestBroker(mt): + type KvCount* = object + n*: int + + proc signature*( + category: string, range: KeyRange + ): Future[Result[KvCount, string]] {.async.} + +RequestBroker(mt): + type KvDelete* = object + existed*: bool + + proc signature*( + category: string, key: Key + ): Future[Result[KvDelete, string]] {.async.} + +# ── string<->PersistencyError boundary helpers ────────────────────────── + +const ErrSep = ": " + +proc encodeErr*(e: PersistencyError): string = + ## Encode a PersistencyError into the broker's string channel. The facade + ## decodes via `decodeErr`. + $e.kind & ErrSep & e.msg + +proc decodeErr*(s: string): PersistencyError = + ## Inverse of encodeErr. Falls back to peBackend if the prefix is missing. + let idx = s.find(ErrSep) + if idx < 0: + return persistencyErr(peBackend, s) + let head = s[0 ..< idx] + let tail = s[idx + ErrSep.len .. ^1] + for k in PersistencyErrorKind: + if $k == head: + return persistencyErr(k, tail) + persistencyErr(peBackend, s) + +{.pop.} diff --git a/waku/persistency/backend_sqlite.nim b/waku/persistency/backend_sqlite.nim new file mode 100644 index 000000000..6851febc1 --- /dev/null +++ b/waku/persistency/backend_sqlite.nim @@ -0,0 +1,247 @@ +## Synchronous SQLite backend for the persistency library. +## +## Plain procs against a SqliteDatabase connection. Phase 3 wraps these in +## per-job storage threads driven by brokers; phase 2 verifies the SQL +## itself against an in-memory database. + +import std/options +import results, sqlite3_abi +import ../common/databases/[common, db_sqlite] +import ./[types, schema] + +type + KvBackend* = ref object + db*: SqliteDatabase + putStmt: SqliteStmt[(seq[byte], seq[byte], seq[byte]), void] + deleteStmt: SqliteStmt[(seq[byte], seq[byte]), void] + + RowHandler = proc(s: ptr sqlite3_stmt) {.gcsafe, raises: [].} + +proc toErr(msg: string): PersistencyError {.inline.} = + persistencyErr(peBackend, msg) + +proc catBytes(category: string): seq[byte] = + var buf = newSeq[byte](category.len) + for i, c in category: + buf[i] = byte(c) + return buf + +proc keyBytes(key: Key): seq[byte] {.inline.} = + bytes(key) + +proc readBlob(s: ptr sqlite3_stmt, col: cint): seq[byte] = + let n = sqlite3_column_bytes(s, col) + var buf = newSeq[byte](n) + if n > 0: + let src = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, col)) + for i in 0 ..< n: + buf[i] = src[i] + return buf + +proc bindBlob(s: ptr sqlite3_stmt, n: cint, val: seq[byte]): cint = + if val.len > 0: + sqlite3_bind_blob(s, n, unsafeAddr val[0], val.len.cint, SQLITE_TRANSIENT) + else: + sqlite3_bind_blob(s, n, nil, 0.cint, SQLITE_TRANSIENT) + +proc runRead( + db: SqliteDatabase, sql: string, params: openArray[seq[byte]], onRow: RowHandler +): Result[void, PersistencyError] = + var s: ptr sqlite3_stmt + let rc = sqlite3_prepare_v2(db.env, sql.cstring, sql.len.cint, addr s, nil) + if rc != SQLITE_OK: + return err(toErr("prepare: " & $sqlite3_errstr(rc))) + defer: + discard sqlite3_finalize(s) + + for i, p in params: + let bc = bindBlob(s, cint(i + 1), p) + if bc != SQLITE_OK: + return err(toErr("bind: " & $sqlite3_errstr(bc))) + + while true: + let v = sqlite3_step(s) + case v + of SQLITE_ROW: + onRow(s) + of SQLITE_DONE: + break + else: + return err(toErr("step: " & $sqlite3_errstr(v))) + return ok() + +proc prepareStatements(b: KvBackend): DatabaseResult[void] = + b.putStmt = ?b.db.prepareStmt( + "INSERT OR REPLACE INTO kv(category, key, payload) VALUES (?, ?, ?);", + (seq[byte], seq[byte], seq[byte]), + void, + ) + b.deleteStmt = ?b.db.prepareStmt( + "DELETE FROM kv WHERE category = ? AND key = ?;", (seq[byte], seq[byte]), void + ) + return ok() + +proc openBackend*(path: string): Result[KvBackend, PersistencyError] = + let dbRes = SqliteDatabase.new(path) + if dbRes.isErr: + return err(toErr("open " & path & " failed: " & dbRes.error)) + let db = dbRes.get() + + applyPragmas(db).isOkOr: + return err(toErr(error)) + ensureSchema(db).isOkOr: + return err(toErr(error)) + + let b = KvBackend(db: db) + prepareStatements(b).isOkOr: + return err(toErr(error)) + return ok(b) + +proc openBackendInMemory*(): Result[KvBackend, PersistencyError] = + ## Convenience for tests. + let dbRes = SqliteDatabase.new(":memory:") + if dbRes.isErr: + return err(toErr("open :memory: failed: " & dbRes.error)) + let db = dbRes.get() + + applyPragmas(db).isOkOr: + return err(toErr(error)) + ensureSchema(db).isOkOr: + return err(toErr(error)) + + let b = KvBackend(db: db) + prepareStatements(b).isOkOr: + return err(toErr(error)) + return ok(b) + +proc close*(b: KvBackend) = + if b.db != nil: + dispose(b.putStmt) + dispose(b.deleteStmt) + b.db.close() + b.db = nil + +proc applyOne(b: KvBackend, op: TxOp): Result[void, PersistencyError] = + case op.kind + of txPut: + let r = b.putStmt.exec((catBytes(op.category), keyBytes(op.key), op.payload)) + if r.isErr: + return err(toErr("put failed: " & r.error)) + of txDelete: + let r = b.deleteStmt.exec((catBytes(op.category), keyBytes(op.key))) + if r.isErr: + return err(toErr("delete failed: " & r.error)) + return ok() + +proc execSql(b: KvBackend, sql: string): Result[void, PersistencyError] = + let r = b.db.query(sql, NoopRowHandler) + if r.isErr: + return err(toErr(sql & ": " & r.error)) + return ok() + +proc applyOps*(b: KvBackend, ops: openArray[TxOp]): Result[void, PersistencyError] = + ## Single op = auto-commit. Multiple ops = BEGIN IMMEDIATE / COMMIT, with + ## ROLLBACK on first failure. This is the single source of truth for write + ## SQL — Phase 3's PersistEvent listener calls straight into here. + if ops.len == 0: + return ok() + if ops.len == 1: + return b.applyOne(ops[0]) + + ?b.execSql("BEGIN IMMEDIATE;") + for op in ops: + let r = b.applyOne(op) + if r.isErr: + discard b.execSql("ROLLBACK;") + return r + ?b.execSql("COMMIT;") + return ok() + +proc getOne*( + b: KvBackend, category: string, key: Key +): Result[Option[seq[byte]], PersistencyError] = + var found: Option[seq[byte]] = none(seq[byte]) + proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} = + found = some(readBlob(rs, 0.cint)) + + ?b.db.runRead( + "SELECT payload FROM kv WHERE category = ? AND key = ? LIMIT 1;", + [catBytes(category), keyBytes(key)], + onRow, + ) + return ok(found) + +proc existsOne*( + b: KvBackend, category: string, key: Key +): Result[bool, PersistencyError] = + var present = false + proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} = + present = true + + ?b.db.runRead( + "SELECT 1 FROM kv WHERE category = ? AND key = ? LIMIT 1;", + [catBytes(category), keyBytes(key)], + onRow, + ) + return ok(present) + +proc deleteOne*( + b: KvBackend, category: string, key: Key +): Result[bool, PersistencyError] = + ## Returns true if a row was actually removed. + let existed = ?b.existsOne(category, key) + if not existed: + return ok(false) + let r = b.deleteStmt.exec((catBytes(category), keyBytes(key))) + if r.isErr: + return err(toErr("delete: " & r.error)) + return ok(true) + +proc scanRange*( + b: KvBackend, category: string, range: KeyRange, reverse = false +): Result[seq[KvRow], PersistencyError] = + let openEnded = bytes(range.stop).len == 0 + let direction = if reverse: "DESC" else: "ASC" + let sql = + if openEnded: + "SELECT key, payload FROM kv WHERE category = ? AND key >= ? ORDER BY key " & + direction & ";" + else: + "SELECT key, payload FROM kv WHERE category = ? AND key >= ? AND key < ? ORDER BY key " & + direction & ";" + + var rows: seq[KvRow] = @[] + proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} = + let k = readBlob(rs, 0.cint) + let p = readBlob(rs, 1.cint) + rows.add((rawKey(k), p)) + + if openEnded: + ?b.db.runRead(sql, [catBytes(category), keyBytes(range.start)], onRow) + else: + ?b.db.runRead( + sql, [catBytes(category), keyBytes(range.start), keyBytes(range.stop)], onRow + ) + return ok(rows) + +proc countRange*( + b: KvBackend, category: string, range: KeyRange +): Result[int, PersistencyError] = + let openEnded = bytes(range.stop).len == 0 + let sql = + if openEnded: + "SELECT COUNT(*) FROM kv WHERE category = ? AND key >= ?;" + else: + "SELECT COUNT(*) FROM kv WHERE category = ? AND key >= ? AND key < ?;" + + var n: int64 = 0 + proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} = + n = sqlite3_column_int64(rs, 0.cint) + + if openEnded: + ?b.db.runRead(sql, [catBytes(category), keyBytes(range.start)], onRow) + else: + ?b.db.runRead( + sql, [catBytes(category), keyBytes(range.start), keyBytes(range.stop)], onRow + ) + return ok(int(n)) diff --git a/waku/persistency/backend_thread.nim b/waku/persistency/backend_thread.nim new file mode 100644 index 000000000..e32e5c209 --- /dev/null +++ b/waku/persistency/backend_thread.nim @@ -0,0 +1,271 @@ +## Internal per-job storage thread. +## +## Exposes two operations to ``persistency.nim``: +## * ``startStorageThread(ctx, dbPath)`` — spawn one worker, block until +## it signals ready (or error). Returns a ``JobRuntime``. +## * ``stopStorageThread(rt)`` — signal shutdown, join, free. +## +## The worker: +## 1. installs the supplied BrokerContext on its threadvar +## 2. opens the SQLite backend (creating the file + schema if absent) +## 3. registers the PersistEvent listener and the 5 RequestBroker +## providers under that context +## 4. runs the chronos event loop until shutdown is signalled +## 5. clears providers + listeners, closes the backend +## +## The arg struct lives in shared memory (``allocShared0``). The dbPath is +## carried as a shared cstring buffer rather than a Nim string to avoid +## refc ref-count traffic across threads. The arg is freed by +## ``stopStorageThread`` after ``joinThread`` returns. + +import std/[options, os] +import std/atomics # std/concurrency/atomics is the same module in Nim 2.2 +import chronos, chronicles, results +import brokers/[event_broker, request_broker, broker_context] +import ./[types, backend_comm, backend_sqlite] + +export broker_context, backend_comm + +logScope: + topics = "persistency thread" + +type + ReadyState {.pure.} = enum + Pending = 0 + Ready = 1 + Error = 2 + + StorageThreadArg = object + ctx: BrokerContext + dbPath: cstring ## allocShared0'd; freed in closeJob + dbPathLen: int ## bytes including the trailing NUL + shutdownFlag: Atomic[int] + readyFlag: Atomic[int] ## values from ReadyState + errBuf: array[256, char] ## last error message, NUL-terminated + + StorageThread = Thread[ptr StorageThreadArg] + +# ── arg helpers ───────────────────────────────────────────────────────── + +proc allocArg(ctx: BrokerContext, dbPath: string): ptr StorageThreadArg = + let arg = cast[ptr StorageThreadArg](allocShared0(sizeof(StorageThreadArg))) + arg.ctx = ctx + arg.dbPathLen = dbPath.len + 1 + arg.dbPath = cast[cstring](allocShared0(arg.dbPathLen)) + if dbPath.len > 0: + copyMem(arg.dbPath, unsafeAddr dbPath[0], dbPath.len) + return arg + +proc freeArg(a: ptr StorageThreadArg) = + if a.isNil(): + return + if a.dbPath != nil: + deallocShared(a.dbPath) + deallocShared(a) + +proc recordErr(a: ptr StorageThreadArg, msg: string) = + let n = min(msg.len, a.errBuf.len - 1) + for i in 0 ..< n: + a.errBuf[i] = msg[i] + a.errBuf[n] = '\0' + a.readyFlag.store(int(ReadyState.Error), moRelease) + +proc errMsg(a: ptr StorageThreadArg): string = + $cast[cstring](a.errBuf[0].addr) + +# ── provider closures ─────────────────────────────────────────────────── + +proc encode(e: PersistencyError): string = + encodeErr(e) + +template unwrapErr(r: untyped): string = + ## Disambiguates Result's `error` accessor from chronicles' `error` macro + ## by binding through an explicitly-typed local before stringifying. + block: + let pe: PersistencyError = r.error() + encode(pe) + +proc registerProviders(backend: KvBackend, ctx: BrokerContext): Result[void, string] = + ## Wires the 5 RequestBroker providers + the PersistEvent listener. + ## All closures capture `backend` by reference (it lives for the entire + ## thread lifetime). + + proc onGet(category: string, key: Key): Future[Result[KvGet, string]] {.async.} = + let r = backend.getOne(category, key) + if r.isErr: + return err(unwrapErr(r)) + return ok(KvGet(value: r.get())) + + proc onExists( + category: string, key: Key + ): Future[Result[KvExists, string]] {.async.} = + let r = backend.existsOne(category, key) + if r.isErr: + return err(unwrapErr(r)) + return ok(KvExists(value: r.get())) + + proc onScan( + category: string, range: KeyRange, reverse: bool + ): Future[Result[KvScan, string]] {.async.} = + let r = backend.scanRange(category, range, reverse) + if r.isErr: + return err(unwrapErr(r)) + return ok(KvScan(rows: r.get())) + + proc onCount( + category: string, range: KeyRange + ): Future[Result[KvCount, string]] {.async.} = + let r = backend.countRange(category, range) + if r.isErr: + return err(unwrapErr(r)) + return ok(KvCount(n: r.get())) + + proc onDelete( + category: string, key: Key + ): Future[Result[KvDelete, string]] {.async.} = + let r = backend.deleteOne(category, key) + if r.isErr: + return err(unwrapErr(r)) + return ok(KvDelete(existed: r.get())) + + # PersistEvent listener — fire-and-forget; we log on backend failure + # because the caller has no return channel. + proc onPersist(ev: PersistEvent): Future[void] {.async: (raises: []).} = + let r = backend.applyOps(ev.ops) + if r.isErr: + let pe: PersistencyError = r.error() + error "PersistEvent applyOps failed", + error = pe.msg, kind = $pe.kind, opCount = ev.ops.len + + KvGet.setProvider(ctx, onGet).isOkOr: + return err("KvGet.setProvider: " & error) + + let existsRes = KvExists.setProvider(ctx, onExists) + if existsRes.isErr: + return err("KvExists.setProvider: " & existsRes.error()) + + let scanRes = KvScan.setProvider(ctx, onScan) + if scanRes.isErr: + return err("KvScan.setProvider: " & scanRes.error()) + + let countRes = KvCount.setProvider(ctx, onCount) + if countRes.isErr: + return err("KvCount.setProvider: " & countRes.error()) + + let delRes = KvDelete.setProvider(ctx, onDelete) + if delRes.isErr: + return err("KvDelete.setProvider: " & delRes.error()) + + let listenRes = PersistEvent.listen(ctx, onPersist) + if listenRes.isErr: + return err("PersistEvent.listen: " & listenRes.error()) + + return ok() + +proc clearProviders(ctx: BrokerContext) = + KvGet.clearProvider(ctx) + KvExists.clearProvider(ctx) + KvScan.clearProvider(ctx) + KvCount.clearProvider(ctx) + KvDelete.clearProvider(ctx) + PersistEvent.dropAllListeners(ctx) + +# ── thread proc ───────────────────────────────────────────────────────── + +proc storageThreadMain(arg: ptr StorageThreadArg) {.thread.} = + ## Worker thread entrypoint. Errors during setup are surfaced via + ## arg.errBuf + readyFlag=ReadyState.Error; the spawning thread checks both. + + setThreadBrokerContext(arg.ctx) + + let path = $arg.dbPath + + let backendRes = + try: + openBackend(path) + except CatchableError as e: + arg.recordErr("openBackend raised: " & e.msg) + return + if backendRes.isErr: + arg.recordErr("openBackend: " & backendRes.error.msg) + return + let backend = backendRes.get() + + let regRes = + try: + registerProviders(backend, arg.ctx) + except CatchableError as e: + backend.close() + arg.recordErr("registerProviders raised: " & e.msg) + return + if regRes.isErr: + backend.close() + arg.recordErr(regRes.error) + return + + arg.readyFlag.store(int(ReadyState.Ready), moRelease) + + proc awaitShutdown() {.async.} = + while arg.shutdownFlag.load(moAcquire) != 1: + try: + await sleepAsync(milliseconds(10)) + except CatchableError: + discard + + try: + waitFor awaitShutdown() + except CatchableError as e: + error "storage thread loop crashed", err = e.msg + + clearProviders(arg.ctx) + backend.close() + +# ── lifecycle ─────────────────────────────────────────────────────────── + +type JobRuntime* = ref object + ## Opaque per-job runtime owned by `persistency.nim`. Holds the typed + ## Thread handle + shared arg pointer so closeJob can shut the worker + ## down. Created by `startStorageThread` and torn down by + ## `stopStorageThread`. + arg*: ptr StorageThreadArg + thread*: StorageThread + +proc startStorageThread*( + ctx: BrokerContext, dbPath: string +): Result[JobRuntime, PersistencyError] = + ## Spawn a storage worker for one job. Blocks until the worker either + ## signals ready (returns the runtime) or signals error (joins, frees, + ## returns peBackend with the worker's error message). + let arg = allocArg(ctx, dbPath) + arg.shutdownFlag.store(0, moRelease) + arg.readyFlag.store(int(ReadyState.Pending), moRelease) + + var rt = JobRuntime(arg: arg) + try: + createThread(rt.thread, storageThreadMain, arg) + except ResourceExhaustedError as e: + freeArg(arg) + return err(persistencyErr(peBackend, "createThread: " & e.msg)) + + # Spin-wait for ready or error. The thread does its setup synchronously + # before signaling, so this is bounded by SQLite open time. + while true: + let s = arg.readyFlag.load(moAcquire) + if s == int(ReadyState.Ready): + return ok(rt) + if s == int(ReadyState.Error): + let msg = errMsg(arg) + joinThread(rt.thread) + freeArg(arg) + return err(persistencyErr(peBackend, msg)) + sleep(1) + +proc stopStorageThread*(rt: JobRuntime) = + ## Signal shutdown, join the worker, free the shared arg. Idempotent in + ## the sense that it tolerates a nil arg (already stopped). + if rt == nil or rt.arg == nil: + return + rt.arg.shutdownFlag.store(1, moRelease) + joinThread(rt.thread) + freeArg(rt.arg) + rt.arg = nil diff --git a/waku/persistency/keys.nim b/waku/persistency/keys.nim new file mode 100644 index 000000000..6a3199b8c --- /dev/null +++ b/waku/persistency/keys.nim @@ -0,0 +1,180 @@ +## Composite-key encoding. +## +## Keys are byte-wise lexicographically comparable so SQLite's BLOB +## ordering reproduces tuple ordering of the original components. Each +## component contributes a self-delimiting, sort-stable byte sequence +## through an `encodePart` overload; the generic fallback recurses through +## `tuple | object` fields, so any user type whose fields are themselves +## encodable can be used as a key part without ceremony. +## +## ## Encoding by type +## +## | Nim type | Bytes emitted | +## |-------------------------|------------------------------------------------------------------| +## | `string`, `openArray[byte]` | 2-byte BE length prefix + payload bytes (max 65535 bytes) | +## | `int64`, `int`, .. | XOR with 0x8000_0000_0000_0000 then 8-byte BE (sign-flip) | +## | `uint64`, `uint32`, .. | 8-byte BE | +## | `bool` | 1 byte (0/1) | +## | `byte`, `char` | 1 byte | +## | `enum E` | sign-flipped 8-byte BE of `ord(v).int64` | +## | `Key` | raw bytes (lets you embed a pre-built key inside another) | +## | `tuple | object` | each field encoded in declaration order, concatenated | +## +## ## Sort-order caveats +## +## - Length-prefixed strings sort by **length first, then byte order**. For +## uniform-length components (channel ids, hashes) this is identical to +## natural lex order; for variable-length text it is not. +## - `int64.low < -1 < 0 < 1 < int64.high` after byte comparison thanks to +## the sign flip. +## - Tuple/object ordering is component-major: field 0 dominates field 1 +## dominates field 2, like a multi-column ORDER BY. +## +## ## Building keys +## +## `key(...)` is a variadic macro that calls `encodePart` per argument. It +## accepts mixed types in one call: +## +## ```nim +## let k = key("channel-42", 1'i64) +## let k2 = key("channel-42", (epoch: 1'i64, seqNum: 7'u64)) +## let k3 = key(myEnumValue, myObject) +## ``` +## +## For a single value, `toKey(v)` is the simpler form (same semantics). + +{.push raises: [].} + +import std/macros +import ./types + +const + StringLenMax* = 0xFFFF + SignFlip = 0x8000_0000_0000_0000'u64 + +# ── Low-level byte helpers ────────────────────────────────────────────── + +proc appendBE16(buf: var seq[byte], v: uint16) = + buf.add(byte((v shr 8) and 0xFF'u16)) + buf.add(byte(v and 0xFF'u16)) + +proc appendBE64(buf: var seq[byte], v: uint64) = + for shift in countdown(56, 0, 8): + buf.add(byte((v shr shift) and 0xFF'u64)) + +# ── encodePart: primitives ────────────────────────────────────────────── + +proc encodePart*(dest: var seq[byte], s: string) = + doAssert s.len <= StringLenMax, "string component exceeds 65535 bytes" + appendBE16(dest, uint16(s.len)) + for c in s: + dest.add(byte(c)) + +proc encodePart*(dest: var seq[byte], raw: openArray[byte]) = + doAssert raw.len <= StringLenMax, "byte component exceeds 65535 bytes" + appendBE16(dest, uint16(raw.len)) + for b in raw: + dest.add(b) + +proc encodePart*(dest: var seq[byte], i: int64) = + appendBE64(dest, cast[uint64](i) xor SignFlip) + +proc encodePart*(dest: var seq[byte], u: uint64) = + appendBE64(dest, u) + +proc encodePart*(dest: var seq[byte], i: int) {.inline.} = + encodePart(dest, i.int64) + +proc encodePart*(dest: var seq[byte], i: int32) {.inline.} = + encodePart(dest, i.int64) + +proc encodePart*(dest: var seq[byte], i: int16) {.inline.} = + encodePart(dest, i.int64) + +proc encodePart*(dest: var seq[byte], i: int8) {.inline.} = + encodePart(dest, i.int64) + +proc encodePart*(dest: var seq[byte], u: uint32) {.inline.} = + encodePart(dest, u.uint64) + +proc encodePart*(dest: var seq[byte], u: uint16) {.inline.} = + encodePart(dest, u.uint64) + +proc encodePart*(dest: var seq[byte], b: bool) = + dest.add(if b: 1'u8 else: 0'u8) + +proc encodePart*(dest: var seq[byte], b: byte) = + dest.add(b) + +proc encodePart*(dest: var seq[byte], c: char) = + dest.add(byte(c)) + +proc encodePart*(dest: var seq[byte], k: Key) = + ## Embed an already-encoded Key (e.g. a pre-built prefix) verbatim. + for b in bytes(k): + dest.add(b) + +# ── encodePart: generic structural fallback ───────────────────────────── + +proc encodePart*[E: enum](dest: var seq[byte], v: E) {.inline.} = + encodePart(dest, int64(ord(v))) + +proc encodePart*[T: tuple | object](dest: var seq[byte], v: T) = + ## Walks the type's fields in declaration order. Each field must itself + ## have an `encodePart` overload (primitive, Key, or another struct). + for f in fields(v): + encodePart(dest, f) + +# ── Public Key constructors ───────────────────────────────────────────── + +proc add*[T](k: var Key, v: T) = + ## In-place key extension. Equivalent to writing `encodePart` against the + ## underlying byte buffer. + var buf = seq[byte](k) + encodePart(buf, v) + k = Key(buf) + +proc toKey*[T](v: T): Key = + ## Single-value Key constructor. Equivalent to `key(v)`. + var buf: seq[byte] = @[] + encodePart(buf, v) + return Key(buf) + +macro key*(parts: varargs[typed]): Key = + ## Variadic Key builder. Accepts any mix of types for which `encodePart` + ## resolves -- including tuples and objects via the structural fallback. + ## + ## ```nim + ## key() # empty Key + ## key("ch", 1'i64) # 2-component + ## key("ch", (1'i64, 7'u64)) # nested tuple flattens + ## ``` + let bufSym = genSym(nskVar, "keyBuf") + var body = newStmtList() + body.add quote do: + var `bufSym`: seq[byte] = @[] + for p in parts: + body.add quote do: + encodePart(`bufSym`, `p`) + body.add quote do: + Key(`bufSym`) + return newBlockStmt(body) + +# ── Range helpers ─────────────────────────────────────────────────────── + +proc prefixRange*(prefix: Key): KeyRange = + ## Build [prefix, prefix++) — a half-open range that captures every key + ## starting with `prefix`. If `prefix` is all 0xFF, the upper bound is + ## empty (open-ended); the backend treats `stop.len == 0` as "no upper + ## bound". + var stop = bytes(prefix) + var i = stop.len - 1 + while i >= 0: + if stop[i] != 0xFF'u8: + stop[i] = stop[i] + 1'u8 + stop.setLen(i + 1) + return KeyRange(start: prefix, stop: Key(stop)) + dec i + return KeyRange(start: prefix, stop: Key(@[])) + +{.pop.} diff --git a/waku/persistency/payload.nim b/waku/persistency/payload.nim new file mode 100644 index 000000000..222de4177 --- /dev/null +++ b/waku/persistency/payload.nim @@ -0,0 +1,53 @@ +## Generic payload encoding. +## +## Symmetric with `keys.nim`: reuses the same `encodePart` family so any +## Nim type composable from primitives + tuples/objects can be turned +## into a `seq[byte]` for storage. Unlike keys, payloads do **not** need +## byte-wise lex order — but using the same encoder keeps the system +## small. If a tenant needs a different on-disk format (CBOR, protobuf, +## SSZ, ...) they can write their own `toPayload` overload or pass an +## already-encoded `seq[byte]` to `persistPut`. +## +## ```nim +## # Primitives: +## let p1 = payload("hello") # length-prefixed string bytes +## let p2 = payload(42'i64) # 8 bytes, sign-flipped BE +## +## # Composites: +## type Msg = object +## sender: string +## epoch: int64 +## body: seq[byte] +## let p3 = toPayload(Msg(sender: "alice", epoch: 7, body: @[1'u8, 2, 3])) +## +## # Variadic when you want multiple values back-to-back: +## let p4 = payload("v1", 1'i64, body) +## ``` + +{.push raises: [].} + +import std/macros +import ./keys + +export keys.encodePart + +proc toPayload*[T](v: T): seq[byte] = + ## Single-value payload constructor. Equivalent to `payload(v)`. + var buf: seq[byte] = @[] + encodePart(buf, v) + return buf + +macro payload*(parts: varargs[typed]): seq[byte] = + ## Variadic payload builder. Same encoder as `key(...)`; only the return + ## type differs. + let bufSym = genSym(nskVar, "payloadBuf") + var body = newStmtList() + body.add quote do: + var `bufSym`: seq[byte] = @[] + for p in parts: + body.add quote do: + encodePart(`bufSym`, `p`) + body.add bufSym + return newBlockStmt(body) + +{.pop.} diff --git a/waku/persistency/persistency.nim b/waku/persistency/persistency.nim new file mode 100644 index 000000000..916f3ac8b --- /dev/null +++ b/waku/persistency/persistency.nim @@ -0,0 +1,433 @@ +## Public facade and main driver types for the persistency library. +## +## ``Persistency`` is the per-root coordinator; one instance owns one +## directory and any number of named jobs. ``Job`` is the per-job handle: +## one tenant, one DB file, one worker thread, one BrokerContext. +## +## ## Two ways to drive a job +## +## **By Job ref** — capture the handle from `openJob` and call methods on +## it. Cheapest, no map lookup per call: +## +## ```nim +## let p = Persistency.instance("/var/lib/wakustore").get() +## let j = p.openJob("alpha").get() +## await j.persistPut("msg", k, payload) +## let v = await j.get("msg", k) +## ``` +## +## **By job id string** — useful when the caller doesn't want to thread +## the ``Job`` ref around (config-driven services, RPC dispatchers). The +## Job must still have been opened previously; the string-form procs look +## it up in `Persistency.jobs`: +## +## ```nim +## discard p.openJob("alpha") +## await p.persistPut("alpha", "msg", k, payload) # logs and resolves if not open +## let v = await p.get("alpha", "msg", k) # Result, peJobNotFound if missing +## ``` +## +## ## Drain semantics +## +## Writes return a ``Future[void]`` that resolves once the PersistEvent +## has been pushed onto the worker thread's channel — **not** once the +## SQL has run. The listener is still fire-and-forget on the SQL side, so +## a read issued immediately after an awaited write is still racy by +## design in v1. To bridge the race: +## * use ``deleteAcked`` (it round-trips through the read path), or +## * poll ``exists`` until it returns true, or +## * yield with ``await sleepAsync(...)``. + +{.push raises: [].} + +import std/[locks, options, os, sequtils, tables] +import chronos, chronicles, results +import brokers/[event_broker, request_broker, broker_context] +import ./[types, keys, payload, backend_comm, backend_thread] + +export types, keys, payload + +logScope: + topics = "persistency" + +const DefaultStoragePath* = "./data" + +# ── Driver types ──────────────────────────────────────────────────────── + +type + Job* = ref object + ## Per-job handle. Owns its BrokerContext and the worker thread that + ## services it. Created and torn down via `Persistency.openJob` / + ## `Persistency.closeJob`. + id*: string + context*: BrokerContext + runtime: JobRuntime ## internal — managed by openJob/closeJob + running*: bool + + Persistency* = ref object + ## Per-root coordinator. One Persistency instance manages a directory + ## of per-job SQLite files at ``rootDir/.db``. + rootDir*: string + jobs*: Table[string, Job] + +# ── Singleton state ───────────────────────────────────────────────────── +# +# Persistency is a process-wide singleton: one rootDir at a time. The +# `instance` factory is the only public constructor; `new` below is +# private and skips the singleton bookkeeping (used internally and never +# called twice with conflicting rootDirs). + +var + gPersistency {.global.}: Persistency + gPersistencyLock {.global.}: Lock + +once: + gPersistencyLock.initLock() + +# ── Lifecycle ─────────────────────────────────────────────────────────── + +proc dbPathFor(p: Persistency, jobId: string): string = + p.rootDir / (jobId & ".db") + +proc new(T: type Persistency, rootDir: string): Result[T, PersistencyError] = + ## Private. Build a Persistency value without touching the singleton + ## slot. Validates ``rootDir`` but does **not** create it — directory + ## materialisation is deferred to the first ``openJob`` call. Semantics: + ## + ## * If ``rootDir`` is empty, returns ``peInvalidArgument``. + ## * If ``rootDir`` exists and is a directory, accept it. + ## * If ``rootDir`` exists but is not a directory, returns + ## ``peInvalidArgument``. + ## * If ``rootDir`` does not exist, walk up the parent chain: the first + ## existing ancestor must be a directory; otherwise returns + ## ``peInvalidArgument``. This catches "obviously broken" paths early + ## without actually touching the filesystem. + if rootDir.len == 0: + return err(persistencyErr(peInvalidArgument, "rootDir is empty")) + if fileExists(rootDir) and not dirExists(rootDir): + return err( + persistencyErr( + peInvalidArgument, "rootDir exists and is not a directory: " & rootDir + ) + ) + if not dirExists(rootDir): + var parent = parentDir(rootDir) + while parent.len > 0 and not dirExists(parent): + if fileExists(parent): + return err( + persistencyErr( + peInvalidArgument, + "rootDir ancestor exists and is not a directory: " & parent, + ) + ) + parent = parentDir(parent) + return ok(T(rootDir: rootDir, jobs: initTable[string, Job]())) + +proc ensureRootDir(p: Persistency): Result[void, PersistencyError] = + ## Materialise ``rootDir`` on demand. Idempotent; called from + ## ``openJob`` so an unused Persistency leaves no directory behind. + if dirExists(p.rootDir): + return ok() + try: + createDir(p.rootDir) + except OSError, IOError: + return + err(persistencyErr(peBackend, "createDir failed: " & getCurrentExceptionMsg())) + return ok() + +proc reset*(T: type Persistency) {.gcsafe.} = + ## Tear down the singleton: close every open job, clear the Teardown + ## provider, and free the slot so a subsequent ``Persistency.instance`` + ## starts fresh. Idempotent. Tests use this in `defer`;. + {.cast(gcsafe).}: + acquire(gPersistencyLock) + defer: + release(gPersistencyLock) + if gPersistency != nil: + let p = gPersistency + gPersistency = nil + p.close() + +proc instance*( + T: type Persistency, rootDir: string +): Result[T, PersistencyError] {.gcsafe.} = + ## Get-or-init the process-wide Persistency singleton. + ## + ## * First call: validates ``rootDir`` (without creating it) and + ## registers the Teardown handler. The directory itself is created + ## lazily by the first ``openJob`` call, so a Persistency that never + ## opens a job leaves no filesystem footprint. + ## * Later calls with the same ``rootDir``: returns the live instance + ## (idempotent). + ## * Later calls with a different ``rootDir``: returns + ## ``peInvalidArgument`` — the singleton can only be re-targeted via + ## ``Persistency.reset`` (or by the Teardown shutdown flow). + {.cast(gcsafe).}: + acquire(gPersistencyLock) + defer: + release(gPersistencyLock) + + if gPersistency != nil: + if gPersistency.rootDir == rootDir: + return ok(gPersistency) + return err( + persistencyErr( + peInvalidArgument, + "Persistency already initialised with rootDir " & gPersistency.rootDir & + "; cannot re-init with " & rootDir, + ) + ) + + let p = ?Persistency.new(rootDir) + gPersistency = p + return ok(p) + +proc instance*(T: type Persistency): Result[T, PersistencyError] {.gcsafe.} = + ## No-args form: succeeds only if the singleton is already initialised. + ## Use this from services that must not be the first to touch + ## persistency. + {.cast(gcsafe).}: + acquire(gPersistencyLock) + defer: + release(gPersistencyLock) + if gPersistency.isNil: + return err(persistencyErr(peClosed, "Persistency not initialised")) + return ok(gPersistency) + +proc openJob*(p: Persistency, jobId: string): Result[Job, PersistencyError] = + ## Open-or-create a job under this Persistency. + ## + ## * If the job is already open in this process, the existing ``Job`` + ## ref is returned (idempotent). + ## * Otherwise ``rootDir`` is materialised on demand (created with + ## missing parents on first use; no-op on subsequent calls), a worker + ## thread is spawned, and the SQLite file at + ## ``/.db`` is opened. If the file does not exist it + ## is created and the schema initialised; if it already exists it is + ## reopened in place and its data is preserved. + let existing = p.jobs.getOrDefault(jobId, nil) + if existing != nil: + return ok(existing) + + ?p.ensureRootDir() + + let ctx = NewBrokerContext() + let rt = ?startStorageThread(ctx, dbPathFor(p, jobId)) + let job = Job(id: jobId, context: ctx, runtime: rt, running: true) + p.jobs[jobId] = job + return ok(job) + +proc closeJob*(p: Persistency, jobId: string) = + ## Stop the worker, join its thread, and forget the job. No-op if the + ## job isn't open. + let job = p.jobs.getOrDefault(jobId, nil) + if job == nil: + return + stopStorageThread(job.runtime) + job.runtime = nil + job.running = false + p.jobs.del(jobId) + +proc close*(p: Persistency) = + ## Close every open job. Idempotent. + var ids: seq[string] + for id in p.jobs.keys: + ids.add(id) + for id in ids: + p.closeJob(id) + +proc dropJob*(p: Persistency, jobId: string) = + ## Close the job if open, then delete its DB file (plus -wal / -shm + ## sidecars). Best-effort: a missing file is not an error. + p.closeJob(jobId) + let path = dbPathFor(p, jobId) + for suffix in ["", "-wal", "-shm"]: + try: + removeFile(path & suffix) + except OSError, IOError: + discard + +# ── String lookup ─────────────────────────────────────────────────────── + +proc job*(p: Persistency, jobId: string): Result[Job, PersistencyError] = + ## Look up an already-open job. Returns ``peJobNotFound`` if no such + ## job has been opened (``openJob`` first). + let j = p.jobs.getOrDefault(jobId, nil) + if j != nil: + return ok(j) + else: + return err(persistencyErr(peJobNotFound, "no open job with id: " & jobId)) + +proc `[]`*(p: Persistency, jobId: string): Job {.raises: [KeyError].} = + ## Subscript sugar for `job` — raises ``KeyError`` if the job isn't + ## open. Prefer `job(p, id)` when you want a typed error. + p.jobs[jobId] + +proc hasJob*(p: Persistency, jobId: string): bool {.inline.} = + p.jobs.hasKey(jobId) + +# ── Writes (fire-and-forget) — Job form ───────────────────────────────── + +proc persist*(t: Job, ops: seq[TxOp]): Future[void] {.async.} = + ## Emit a batched persist event. The handler treats >1 ops as a single + ## BEGIN IMMEDIATE/COMMIT transaction (see backend_sqlite.applyOps). + await PersistEvent.emit(t.context, PersistEvent(ops: ops)) + +proc persist*(t: Job, op: TxOp): Future[void] {.async.} = + await persist(t, @[op]) + +proc persistPut*( + t: Job, category: string, key: Key, payload: seq[byte] +): Future[void] {.async.} = + await persist(t, TxOp(category: category, key: key, kind: txPut, payload: payload)) + +proc persistDelete*(t: Job, category: string, key: Key): Future[void] {.async.} = + await persist(t, TxOp(category: category, key: key, kind: txDelete)) + +proc persistEncoded*[T]( + t: Job, category: string, key: Key, value: T +): Future[void] {.async.} = + ## Convenience: encode `value` via `toPayload` and put it. Use the raw + ## `persistPut(..., seq[byte])` form when you already have bytes + ## (e.g. an externally-produced CBOR blob). + await persistPut(t, category, key, toPayload(value)) + +# ── Writes (fire-and-forget) — string-lookup form ─────────────────────── +# +# These look up the Job by id and dispatch. If the job isn't open we log +# a warning and drop the write — consistent with the fire-and-forget +# contract; the caller has no return channel to inspect. + +proc jobOrWarn(p: Persistency, jobId: string): Job = + ## Lookup helper for the fire-and-forget write paths. Returns nil and + ## logs a warning if the job isn't open. Isolated as a non-generic proc + ## so chronicles' `warn` macro expands cleanly (it doesn't, when called + ## from inside a generic proc's body). + let job = p.jobs.getOrDefault(jobId, nil) + if job.isNil(): + warn "persistency: write dropped, job not open", jobId + return job + +template withJobOrWarn(p: Persistency, jobId: string, j, body: untyped) = + let `j` = p.jobOrWarn(jobId) + if not `j`.isNil(): + body + +proc persist*(p: Persistency, jobId: string, ops: seq[TxOp]): Future[void] {.async.} = + let j = p.jobOrWarn(jobId) + if not j.isNil(): + await j.persist(ops) + +proc persist*(p: Persistency, jobId: string, op: TxOp): Future[void] {.async.} = + await p.persist(jobId, @[op]) + +proc persistPut*( + p: Persistency, jobId: string, category: string, key: Key, payload: seq[byte] +): Future[void] {.async.} = + let j = p.jobOrWarn(jobId) + if not j.isNil(): + await j.persistPut(category, key, payload) + +proc persistDelete*( + p: Persistency, jobId: string, category: string, key: Key +): Future[void] {.async.} = + let j = p.jobOrWarn(jobId) + if not j.isNil(): + await j.persistDelete(category, key) + +proc persistEncoded*[T]( + p: Persistency, jobId: string, category: string, key: Key, value: T +): Future[void] {.async.} = + let j = p.jobOrWarn(jobId) + if not j.isNil(): + await j.persistEncoded(category, key, value) + +# ── Reads (async, typed errors) — Job form ────────────────────────────── + +template liftErr(s: string): PersistencyError = + decodeErr(s) + +proc get*( + t: Job, category: string, key: Key +): Future[Result[Option[seq[byte]], PersistencyError]] {.async.} = + let r = (await KvGet.request(t.context, category, key)).valueOr: + return err(liftErr(error)) + return ok(r.value) + +proc exists*( + t: Job, category: string, key: Key +): Future[Result[bool, PersistencyError]] {.async.} = + let r = (await KvExists.request(t.context, category, key)).valueOr: + return err(liftErr(error)) + return ok(r.value) + +proc scan*( + t: Job, category: string, range: KeyRange, reverse = false +): Future[Result[seq[KvRow], PersistencyError]] {.async.} = + let r = (await KvScan.request(t.context, category, range, reverse)).valueOr: + return err(liftErr(error)) + return ok(r.rows) + +proc scanPrefix*( + t: Job, category: string, prefix: Key, reverse = false +): Future[Result[seq[KvRow], PersistencyError]] {.async.} = + let rng = prefixRange(prefix) + let r = (await KvScan.request(t.context, category, rng, reverse)).valueOr: + return err(liftErr(error)) + return ok(r.rows) + +proc count*( + t: Job, category: string, range: KeyRange +): Future[Result[int, PersistencyError]] {.async.} = + let r = (await KvCount.request(t.context, category, range)).valueOr: + return err(liftErr(error)) + return ok(r.n) + +proc deleteAcked*( + t: Job, category: string, key: Key +): Future[Result[bool, PersistencyError]] {.async.} = + ## Goes through the read path so the caller learns whether a row was + ## actually removed. + let r = (await KvDelete.request(t.context, category, key)).valueOr: + return err(liftErr(error)) + return ok(r.existed) + +# ── Reads (async, typed errors) — string-lookup form ──────────────────── + +proc get*( + p: Persistency, jobId: string, category: string, key: Key +): Future[Result[Option[seq[byte]], PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.get(category, key) + +proc exists*( + p: Persistency, jobId: string, category: string, key: Key +): Future[Result[bool, PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.exists(category, key) + +proc scan*( + p: Persistency, jobId: string, category: string, range: KeyRange, reverse = false +): Future[Result[seq[KvRow], PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.scan(category, range, reverse) + +proc scanPrefix*( + p: Persistency, jobId: string, category: string, prefix: Key, reverse = false +): Future[Result[seq[KvRow], PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.scanPrefix(category, prefix, reverse) + +proc count*( + p: Persistency, jobId: string, category: string, range: KeyRange +): Future[Result[int, PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.count(category, range) + +proc deleteAcked*( + p: Persistency, jobId: string, category: string, key: Key +): Future[Result[bool, PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.deleteAcked(category, key) + +{.pop.} diff --git a/waku/persistency/schema.nim b/waku/persistency/schema.nim new file mode 100644 index 000000000..1d13014f2 --- /dev/null +++ b/waku/persistency/schema.nim @@ -0,0 +1,58 @@ +## SQL schema and pragma setup for the persistency library. +## +## Single uniform schema per job DB file: +## kv(category BLOB, key BLOB, payload BLOB) PRIMARY KEY (category, key) +## WITHOUT ROWID +## +## category is declared BLOB (not TEXT) so it round-trips via the existing +## sqlite3_abi binding helpers (which do not yet expose bind_text). SQLite +## compares BLOBs byte-wise, which is exactly the ordering we want. + +{.push raises: [].} + +import results +import ../common/databases/[common, db_sqlite] + +const + PersistencyUserVersion* = 1'i64 + + CreateKvTableSql* = """ + CREATE TABLE IF NOT EXISTS kv ( + category BLOB NOT NULL, + key BLOB NOT NULL, + payload BLOB NOT NULL, + PRIMARY KEY (category, key) + ) WITHOUT ROWID; + """ + + ApplyPragmasSql* = """ + PRAGMA synchronous = NORMAL; + PRAGMA temp_store = MEMORY; + PRAGMA busy_timeout = 5000; + PRAGMA foreign_keys = OFF; + """ + +proc applyPragmas*(db: SqliteDatabase): DatabaseResult[void] = + ## Apply the connection-level pragmas. journal_mode=WAL is already set by + ## SqliteDatabase.new. + for stmt in [ + "PRAGMA synchronous = NORMAL;", "PRAGMA temp_store = MEMORY;", + "PRAGMA busy_timeout = 5000;", "PRAGMA foreign_keys = OFF;", + ]: + db.query(stmt, NoopRowHandler).isOkOr: + return err("pragma failed: " & stmt & ": " & error) + return ok() + +proc ensureSchema*(db: SqliteDatabase): DatabaseResult[void] = + db.query(CreateKvTableSql, NoopRowHandler).isOkOr: + return err("create kv table failed: " & error) + + let userVersion = ?db.getUserVersion() + if userVersion == 0: + ?db.setUserVersion(PersistencyUserVersion) + elif userVersion != PersistencyUserVersion: + return err( + "incompatible persistency user_version: got " & $userVersion & ", expected " & + $PersistencyUserVersion + ) + return ok() diff --git a/waku/persistency/types.nim b/waku/persistency/types.nim new file mode 100644 index 000000000..4c4c2de3f --- /dev/null +++ b/waku/persistency/types.nim @@ -0,0 +1,81 @@ +## Core types for the logos-delivery persistency library. +## +## The library is backend-neutral CRUD: jobs own their domain ports and +## map them onto the primitives exposed in persistency.nim. See +## persistency.nim for the public facade and brokers.nim for the +## cross-thread plumbing. + +{.push raises: [].} + +type + Key* = distinct seq[byte] + + KeyRange* = object + start*: Key + stop*: Key ## exclusive; an empty `stop` means "no upper bound" + + KvRow* = tuple[key: Key, payload: seq[byte]] + + TxOpKind* = enum + txPut + txDelete + + TxOp* = object + category*: string + key*: Key + case kind*: TxOpKind + of txPut: + payload*: seq[byte] + of txDelete: + discard + + PersistencyErrorKind* = enum + peBackend + peClosed + peInvalidArgument + peTimeout + peJobNotFound + + PersistencyError* = object + kind*: PersistencyErrorKind + msg*: string + backendCode*: int + +proc bytes*(k: Key): lent seq[byte] {.inline.} = + seq[byte](k) + +proc len*(k: Key): int {.inline.} = + seq[byte](k).len + +proc `==`*(a, b: Key): bool {.inline.} = + seq[byte](a) == seq[byte](b) + +proc `<`*(a, b: Key): bool = + let ab = seq[byte](a) + let bb = seq[byte](b) + let n = min(ab.len, bb.len) + for i in 0 ..< n: + if ab[i] != bb[i]: + return ab[i] < bb[i] + return ab.len < bb.len + +proc `<=`*(a, b: Key): bool {.inline.} = + a == b or a < b + +proc rawKey*(b: openArray[byte]): Key = + var s = newSeq[byte](b.len) + for i, v in b: + s[i] = v + return Key(s) + +proc rawKey*(b: sink seq[byte]): Key {.inline.} = + Key(b) + +proc persistencyErr*( + kind: PersistencyErrorKind, msg: string, backendCode = 0 +): PersistencyError {.inline.} = + PersistencyError(kind: kind, msg: msg, backendCode: backendCode) + +proc `$`*(e: PersistencyError): string = + "PersistencyError(" & $e.kind & ": " & e.msg & + (if e.backendCode != 0: ", code=" & $e.backendCode else: "") & ")" diff --git a/waku/requests/health_requests.nim b/waku/requests/health_requests.nim index c3a0ce286..d48b3278f 100644 --- a/waku/requests/health_requests.nim +++ b/waku/requests/health_requests.nim @@ -1,4 +1,4 @@ -import waku/common/broker/request_broker +import brokers/request_broker import waku/api/types import waku/node/health_monitor/[protocol_health, topic_health, health_report] diff --git a/waku/requests/node_requests.nim b/waku/requests/node_requests.nim index a4ccc6de4..93c6b1159 100644 --- a/waku/requests/node_requests.nim +++ b/waku/requests/node_requests.nim @@ -1,5 +1,5 @@ import std/options -import waku/common/broker/[request_broker, multi_request_broker] +import brokers/[request_broker, multi_request_broker] import waku/waku_core/[topics] RequestBroker(sync): diff --git a/waku/requests/rln_requests.nim b/waku/requests/rln_requests.nim index 8b61f9fcd..ffd747bed 100644 --- a/waku/requests/rln_requests.nim +++ b/waku/requests/rln_requests.nim @@ -1,4 +1,5 @@ -import waku/common/broker/request_broker, waku/waku_core/message/message +import brokers/request_broker +import waku/waku_core/message/message RequestBroker: type RequestGenerateRlnProof* = object diff --git a/waku/waku_filter_v2/client.nim b/waku/waku_filter_v2/client.nim index 265bf5e7b..7798f41b7 100644 --- a/waku/waku_filter_v2/client.nim +++ b/waku/waku_filter_v2/client.nim @@ -8,10 +8,11 @@ import chronos, libp2p/protocols/protocol, bearssl/rand, - stew/byteutils + stew/byteutils, + brokers/broker_context + import - waku/ - [node/peer_manager, waku_core, events/delivery_events, common/broker/broker_context], + waku/[node/peer_manager, waku_core, events/delivery_events], ./common, ./protocol_metrics, ./rpc_codec, diff --git a/waku/waku_relay/protocol.nim b/waku/waku_relay/protocol.nim index e7b2c99cb..d0b1ddb48 100644 --- a/waku/waku_relay/protocol.nim +++ b/waku/waku_relay/protocol.nim @@ -16,7 +16,8 @@ import libp2p/protocols/pubsub/gossipsub, libp2p/protocols/pubsub/rpc/messages, libp2p/stream/connection, - libp2p/switch + libp2p/switch, + brokers/broker_context import waku/waku_core, @@ -24,7 +25,6 @@ import waku/requests/health_requests, waku/events/health_events, ./message_id, - waku/common/broker/broker_context, waku/events/peer_events from waku/waku_core/codecs import WakuRelayCodec @@ -526,7 +526,7 @@ method stop*(w: WakuRelay) {.async: (raises: []).} = info "stop" await procCall GossipSub(w).stop() - WakuPeerEvent.dropListener(w.brokerCtx, w.peerEventListener) + await WakuPeerEvent.dropListener(w.brokerCtx, w.peerEventListener) if not w.topicHealthLoopHandle.isNil(): await w.topicHealthLoopHandle.cancelAndWait() diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index ac128b5bc..7c36300b2 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -13,7 +13,9 @@ import libp2p/protocols/pubsub/rpc/messages, libp2p/protocols/pubsub/pubsub, results, - stew/[byteutils, arrayops] + stew/[byteutils, arrayops], + brokers/broker_context + import ./group_manager, ./rln, @@ -30,7 +32,6 @@ import waku_core, requests/rln_requests, waku_keystore, - common/broker/broker_context, ] logScope: From e7142110a310304cf9a77f4e477b3ff613893b8a Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Wed, 20 May 2026 21:53:08 +0100 Subject: [PATCH 149/155] feat(node-info): expose MixPubKey as node info item (#3893) Adds NodeInfoId.MyMixPubKey, returning the node's mix public key as 0x-prefixed hex via the existing debug API. Returns an empty string when the mix protocol is not mounted. Co-authored-by: Claude Opus 4.7 (1M context) --- waku/factory/waku_state_info.nim | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/waku/factory/waku_state_info.nim b/waku/factory/waku_state_info.nim index 397b90d6d..5796e04f5 100644 --- a/waku/factory/waku_state_info.nim +++ b/waku/factory/waku_state_info.nim @@ -5,7 +5,7 @@ ## accessible through the debug API. import std/[tables, sequtils, strutils] -import metrics, eth/p2p/discoveryv5/enr, libp2p/peerid +import metrics, eth/p2p/discoveryv5/enr, libp2p/peerid, stew/byteutils import waku/[waku_node, net/bound_ports] type @@ -16,6 +16,7 @@ type MyENR MyPeerId MyBoundPorts + MyMixPubKey WakuStateInfo* {.requiresInit.} = object node: WakuNode @@ -46,6 +47,11 @@ proc getNodeInfoItem*(self: WakuStateInfo, infoItemId: NodeInfoId): string = return $PeerId(self.node.peerId()) of NodeInfoId.MyBoundPorts: return $self.node.ports + of NodeInfoId.MyMixPubKey: + ## Empty when the mix protocol is not mounted on this node. + if self.node.wakuMix.isNil(): + return "" + return self.node.wakuMix.pubKey.to0xHex() else: return "unknown info item id" From c6e448a0bab95b61c85a224a91bb6a5f0d20002a Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Wed, 20 May 2026 21:57:14 +0100 Subject: [PATCH 150/155] fix: real getNodeInfo Version in Nix/lgpm builds (#3889) * fix(node-info): real Version + new Commit in Nix/lgpm builds getNodeInfo Version returned "n/a" on Nix-built libs (and lgpm releases built from the flake) because nix/default.nix never passed -d:git_version. A flake sandbox has no .git, so git describe is impossible; derive the semver from waku.nimble's version field plus the flake short commit, and expose the full commit SHA via a new Commit node info id. - waku_state_info: add Commit to NodeInfoId + dispatch git_commit - waku_node: add git_commit {.strdefine.} (default "n/a") - node start logs ("Starting Waku node" / "Running nwaku node") now print commit = git_commit alongside version - Makefile: inject -d:git_commit (full SHA), mirrors docker label - nix/default.nix: accept gitVersion/gitCommit, pass as nim defines - flake.nix: gitVersion = -g, gitCommit = rev - CI version-check (PR only): ancestor-aware `git describe --tags --abbrev=0` vs PR HEAD, base-version compare, so waku.nimble is kept current early and a new tag never breaks in-flight PRs - release-assets.yml: gate build/upload on a verify-version job asserting tag base == waku.nimble (RC tags allowed), so a mismatched tag publishes no artifacts - docs: prepare_release.md explains the bump-before-tag requirement Refs: status-im/infra-logos#4 Closes: logos-messaging/logos-delivery#3884 Co-Authored-By: Claude Opus 4.7 (1M context) * docs: simplify * chore: update version in waku.nimble * fix(node-info): remove Commit node info field Drop the newly added Commit (full SHA) node info id and its git_commit compile-time define plumbing across Makefile, flake.nix and nix/default.nix; revert the start/run log lines to version only. The PR now solely fixes the getNodeInfo Version regression. Co-Authored-By: Claude Opus 4.7 (1M context) * chore(nix): align git_version format closer to Makefile Adds the `v` prefix and uses a 6-char SHA so Nix-built nodes report e.g. `v0.38.1-g52e980`, matching the shape of `git describe --abbrev=6 --always --tags` aside from the unreachable commit-count segment (tag metadata isn't exposed through the flake input protocol). Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .github/ISSUE_TEMPLATE/prepare_release.md | 2 +- .github/workflows/release-assets.yml | 28 +++++++++++++ .github/workflows/version-check.yml | 49 +++++++++++++++++++++++ flake.nix | 15 +++++++ nix/default.nix | 4 +- waku.nimble | 2 +- 6 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/version-check.yml diff --git a/.github/ISSUE_TEMPLATE/prepare_release.md b/.github/ISSUE_TEMPLATE/prepare_release.md index de67b3eaf..3c2e2f729 100644 --- a/.github/ISSUE_TEMPLATE/prepare_release.md +++ b/.github/ISSUE_TEMPLATE/prepare_release.md @@ -18,7 +18,7 @@ For detailed info on the release process refer to https://github.com/logos-messa All items below are to be completed by the owner of the given release. - [ ] Create release branch with major and minor only ( e.g. release/v0.X ) if it doesn't exist. -- [ ] Update the `version` field in `waku.nimble` to match the release version (e.g. `version = "0.X.0"`). +- [ ] Update the `version` field in `waku.nimble` to match the release version (e.g. `version = "0.X.0"`) **and merge it before assigning any tag** - the `release-assets` workflow gates artifact build/upload. - [ ] Assign release candidate tag to the release branch HEAD (e.g. `v0.X.0-rc.0`, `v0.X.0-rc.1`, ... `v0.X.0-rc.N`). - [ ] Generate and edit release notes in CHANGELOG.md. diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml index fc1f819d9..77862d11b 100644 --- a/.github/workflows/release-assets.yml +++ b/.github/workflows/release-assets.yml @@ -11,7 +11,35 @@ env: NPROC: 2 jobs: + # Release gate: the pushed tag MUST exactly match waku.nimble's version, + # so every published artifact reports the correct getNodeInfo Version. + # CI cannot reject/remove a tag, so we gate artifact build & upload on + # this instead: a mismatched tag yields no released artifacts. + verify-version: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Assert pushed tag equals waku.nimble version + if: startsWith(github.ref, 'refs/tags/') + run: | + set -euo pipefail + NIMBLE_VERSION=$(grep -m1 '^version = ' waku.nimble | sed -E 's/version = "([^"]+)"/\1/') + # Strip leading v and any prerelease suffix (e.g. v0.38.0-rc.1 -> + # 0.38.0) so release-candidate tags build against the same + # waku.nimble version as the final tag. + TAG_VERSION="${GITHUB_REF_NAME#v}" + BASE_VERSION="${TAG_VERSION%%-*}" + echo "tag: ${GITHUB_REF_NAME} (base ${BASE_VERSION})" + echo "waku.nimble version: ${NIMBLE_VERSION}" + if [ "${BASE_VERSION}" != "${NIMBLE_VERSION}" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} (base ${BASE_VERSION}) does not match" + echo "::error::waku.nimble version (${NIMBLE_VERSION}). Bump waku.nimble before tagging." + exit 1 + fi + echo "OK: tag base matches waku.nimble." + build-and-upload: + needs: verify-version strategy: matrix: os: [ubuntu-22.04, macos-15] diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 000000000..e3ad958ba --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,49 @@ +name: version check +permissions: + contents: read + +on: + pull_request: + branches: [master] + +jobs: + # PR check: waku.nimble version must be >= the nearest tag reachable from + # this branch (`git describe --tags --abbrev=0`, i.e. ancestor-aware). + # Because we check out the PR HEAD (not the simulated merge ref), a branch + # that predates a release tag does not see that tag in its history, so a + # newly pushed tag does NOT break in-flight PRs. Once the branch merges/ + # rebases past the tag, the bump is then enforced. This keeps waku.nimble + # fixed as early as possible, independent of whether a release is cut. + # The exact tag==nimble guarantee at release time lives in + # release-assets.yml, which gates artifact publishing on it. + nimble-not-behind-tag: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + - name: Compare waku.nimble version with nearest ancestor tag + run: | + set -euo pipefail + NIMBLE_VERSION=$(grep -m1 '^version = ' waku.nimble | sed -E 's/version = "([^"]+)"/\1/') + # Nearest tag reachable from HEAD; --abbrev=0 drops the --g + # suffix so we get the bare tag (e.g. v0.38.0). + BASE_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + BASE_TAG=${BASE_TAG#v} + # Compare on the base version, ignoring any -rc.N prerelease suffix. + BASE_TAG=${BASE_TAG%%-*} + echo "waku.nimble version: ${NIMBLE_VERSION}" + echo "ancestor git tag: ${BASE_TAG:-}" + if [ -z "${BASE_TAG}" ]; then + echo "No ancestor release tag; skipping." + exit 0 + fi + # lowest of the two by version sort must be the tag => nimble >= tag + LOWEST=$(printf '%s\n%s\n' "${NIMBLE_VERSION}" "${BASE_TAG}" | sort -V | head -1) + if [ "${LOWEST}" != "${BASE_TAG}" ] && [ "${NIMBLE_VERSION}" != "${BASE_TAG}" ]; then + echo "::error::waku.nimble version (${NIMBLE_VERSION}) is behind its" + echo "::error::ancestor git tag (v${BASE_TAG}). Bump 'version' in waku.nimble." + exit 1 + fi + echo "OK: waku.nimble is not behind its ancestor tag." diff --git a/flake.nix b/flake.nix index 50b6dc0b5..b99eff6cd 100644 --- a/flake.nix +++ b/flake.nix @@ -36,6 +36,20 @@ forAllSystems = nixpkgs.lib.genAttrs systems; + lib = nixpkgs.lib; + + # Single source of truth for the semver: the `version` field of + # waku.nimble. Kept in sync with git tags by the version-check CI. + nimbleVersion = + let line = lib.findFirst (l: lib.hasPrefix "version = " l) + "version = \"unknown\"" + (lib.splitString "\n" (builtins.readFile ./waku.nimble)); + in lib.removeSuffix "\"" (lib.removePrefix "version = \"" line); + + # A flake sandbox has no .git, so `git describe` is impossible; the + # commit comes from the flake metadata instead. + shortRev = self.shortRev or self.dirtyShortRev or "dirty"; + nimbleOverlay = final: prev: { nimble = prev.nimble.overrideAttrs (_: { version = "0.22.3"; @@ -60,6 +74,7 @@ inherit pkgs; src = ./.; zerokitRln = zerokit.packages.${system}.rln; + gitVersion = "v${nimbleVersion}-g${builtins.substring 0 6 shortRev}"; }; in { inherit liblogosdelivery; diff --git a/nix/default.nix b/nix/default.nix index a9ea0f598..7b7989e1a 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,6 +1,7 @@ { pkgs , src , zerokitRln +, gitVersion ? "n/a" , enablePostgres ? true , enableNimDebugDlOpen ? true , chroniclesLogLevel ? null @@ -10,7 +11,8 @@ let deps = import ./deps.nix { inherit pkgs; }; nimDefineArgs = pkgs.lib.concatStringsSep " \\\n " ( - [ "--define:disable_libbacktrace" ] + [ "--define:disable_libbacktrace" + "--define:git_version=${gitVersion}" ] ++ pkgs.lib.optional enablePostgres "--define:postgres" ++ pkgs.lib.optional enableNimDebugDlOpen "--define:nimDebugDlOpen" ++ pkgs.lib.optional (chroniclesLogLevel != null) diff --git a/waku.nimble b/waku.nimble index 57ce858a4..da5b87eb6 100644 --- a/waku.nimble +++ b/waku.nimble @@ -4,7 +4,7 @@ import os mode = ScriptMode.Verbose ### Package -version = "0.37.4" +version = "0.38.1" author = "Status Research & Development GmbH" description = "Waku, Private P2P Messaging for Resource-Restricted Devices" license = "MIT or Apache License 2.0" From eb1891dc0e470dfa9612158728298b358077b907 Mon Sep 17 00:00:00 2001 From: Darshan <35736874+darshankabariya@users.noreply.github.com> Date: Thu, 21 May 2026 17:31:03 +0530 Subject: [PATCH 151/155] feat: migrate to zerokit v2.0.2 (#3868) --- Makefile | 2 +- flake.lock | 6 +- flake.nix | 38 +- scripts/build_rln.sh | 12 +- .../waku_lightpush_legacy/lightpush_utils.nim | 5 +- tests/waku_rln_relay/rln/buffer_utils.nim | 15 +- .../waku_rln_relay/rln/test_rln_interface.nim | 37 +- tests/waku_rln_relay/rln/test_wrappers.nim | 117 +---- .../test_rln_group_manager_onchain.nim | 86 +++- tests/waku_rln_relay/test_waku_rln_relay.nim | 45 +- vendor/zerokit | 2 +- waku/waku_rln_relay/constants.nim | 2 - waku/waku_rln_relay/conversion_utils.nim | 42 -- .../group_manager/on_chain/group_manager.nim | 104 +--- waku/waku_rln_relay/protocol_metrics.nim | 2 +- waku/waku_rln_relay/rln/rln_interface.nim | 484 +++++++++++++----- waku/waku_rln_relay/rln/wrappers.nim | 435 +++++++++++----- 17 files changed, 889 insertions(+), 545 deletions(-) diff --git a/Makefile b/Makefile index be9e14027..f147c5e7e 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,7 @@ deps: | nimble .PHONY: librln LIBRLN_BUILDDIR := $(CURDIR)/vendor/zerokit -LIBRLN_VERSION := v0.9.0 +LIBRLN_VERSION := v2.0.2 ifeq ($(detected_OS),Windows) LIBRLN_FILE ?= rln.lib diff --git a/flake.lock b/flake.lock index 9b5db728d..8d0db9269 100644 --- a/flake.lock +++ b/flake.lock @@ -72,17 +72,15 @@ "rust-overlay": "rust-overlay_2" }, "locked": { - "lastModified": 1771279884, - "narHash": "sha256-tzkQPwSl4vPTUo1ixHh6NCENjsBDroMKTjifg2q8QX8=", "owner": "vacp2p", "repo": "zerokit", - "rev": "53b18098e6d5d046e3eb1ac338a8f4f651432477", + "rev": "5e64cb8822bee65eed6cf459f95ae72b80c6ba63", "type": "github" }, "original": { "owner": "vacp2p", "repo": "zerokit", - "rev": "53b18098e6d5d046e3eb1ac338a8f4f651432477", + "rev": "5e64cb8822bee65eed6cf459f95ae72b80c6ba63", "type": "github" } } diff --git a/flake.nix b/flake.nix index b99eff6cd..8012fc970 100644 --- a/flake.nix +++ b/flake.nix @@ -21,7 +21,10 @@ # External flake input: Zerokit pinned to a specific commit. # Update the rev here when a new zerokit version is needed. zerokit = { - url = "github:vacp2p/zerokit/53b18098e6d5d046e3eb1ac338a8f4f651432477"; + # Pinned to v2.0.2 (5e64cb8822bee65eed6cf459f95ae72b80c6ba63) to match + # the vendor/zerokit submodule. Keep these two in sync: the nix build + # links librln from this input, the Makefile build from the submodule. + url = "github:vacp2p/zerokit/5e64cb8822bee65eed6cf459f95ae72b80c6ba63"; inputs.nixpkgs.follows = "nixpkgs"; }; }; @@ -70,10 +73,41 @@ packages = forAllSystems (system: let pkgs = pkgsFor system; + + # zerokit's nix/default.nix hardcodes a cargoHash that is stale for + # our pinned nixpkgs on a cold runner (the status.im substituter is + # untrusted here, so the cargo-vendor FOD is recomputed). v2.0.2 did + # NOT fix this for consumers — its committed hash is the old v2.0.1 + # value while v2.0.2's Cargo.lock changed. Rebuild librln here from + # the pinned zerokit source with the correct cargoHash. Keep the + # version + cargoHash in sync with the zerokit input rev. + rustToolchain = pkgs.rust-bin.stable.latest.default; + zerokitRln = pkgs.rustPlatform.buildRustPackage { + pname = "zerokit"; + version = "2.0.2"; + src = zerokit; + cargo = rustToolchain; + rustc = rustToolchain; + cargoHash = "sha256-PNwEdZLgGQPqQDrEK2hsQtSybVfBbD6xn4K47fPFJUU="; + nativeBuildInputs = [ pkgs.rust-cbindgen ]; + doCheck = false; + buildPhase = '' + export CARGO_HOME=$TMPDIR/cargo + cargo build --lib --release --manifest-path rln/Cargo.toml + ''; + installPhase = '' + set -eu + mkdir -p $out/lib $out/include + find target -type f -name 'librln.*' -not -path '*/deps/*' \ + -exec cp -v '{}' "$out/lib/" \; + cbindgen ./rln -l c > "$out/include/rln.h" + ''; + }; + liblogosdelivery = pkgs.callPackage ./nix/default.nix { inherit pkgs; src = ./.; - zerokitRln = zerokit.packages.${system}.rln; + inherit zerokitRln; gitVersion = "v${nimbleVersion}-g${builtins.substring 0 6 shortRev}"; }; in { diff --git a/scripts/build_rln.sh b/scripts/build_rln.sh index b36ebe807..35b5b8953 100755 --- a/scripts/build_rln.sh +++ b/scripts/build_rln.sh @@ -33,8 +33,16 @@ if [[ "v${submodule_version}" != "${rln_version}" ]]; then exit 1 fi -# Build rln from source -cargo build --release -p rln --manifest-path "${build_dir}/rln/Cargo.toml" +# Build rln from source. +# `stateless` feature: logos-delivery does not maintain a local Merkle tree +# (post-PR #3312); the contract is the source of truth and the path is fetched +# via getMerkleProof(index). The stateless build compiles out tree code. +# +# --no-default-features is required because zerokit's default features include +# `pmtree-ft` (a Merkle tree backend); `stateless` and any Merkle-tree feature +# are mutually exclusive (rln/src/lib.rs:32 compile_error). +cargo build --release -p rln --manifest-path "${build_dir}/rln/Cargo.toml" \ + --no-default-features --features stateless cp "${build_dir}/target/release/librln.a" "${output_filename}" echo "Successfully built ${output_filename}" diff --git a/tests/waku_lightpush_legacy/lightpush_utils.nim b/tests/waku_lightpush_legacy/lightpush_utils.nim index 11c4bf929..d5602173a 100644 --- a/tests/waku_lightpush_legacy/lightpush_utils.nim +++ b/tests/waku_lightpush_legacy/lightpush_utils.nim @@ -1,6 +1,9 @@ {.used.} -import std/options, chronos, libp2p/crypto/crypto +import std/options, chronos, chronicles, libp2p/crypto/crypto + +logScope: + topics = "test waku_lightpush_legacy" import waku/node/peer_manager, diff --git a/tests/waku_rln_relay/rln/buffer_utils.nim b/tests/waku_rln_relay/rln/buffer_utils.nim index e38cc5c17..a5ef921f1 100644 --- a/tests/waku_rln_relay/rln/buffer_utils.nim +++ b/tests/waku_rln_relay/rln/buffer_utils.nim @@ -1,11 +1,4 @@ -import waku/waku_rln_relay/rln/rln_interface - -proc `==`*(a: Buffer, b: seq[uint8]): bool = - if a.len != uint(b.len): - return false - - let bufferArray = cast[ptr UncheckedArray[uint8]](a.ptr) - for i in 0 ..< b.len: - if bufferArray[i] != b[i]: - return false - return true +# buffer_utils.nim — intentionally empty. +# The v0.9 Buffer type and toBuffer helper were removed in the zerokit v2.0.1 +# migration. This file is kept as a placeholder so that any future test imports +# do not break the build; the content that was here is no longer needed. diff --git a/tests/waku_rln_relay/rln/test_rln_interface.nim b/tests/waku_rln_relay/rln/test_rln_interface.nim index 7aedf587f..7b8ea3878 100644 --- a/tests/waku_rln_relay/rln/test_rln_interface.nim +++ b/tests/waku_rln_relay/rln/test_rln_interface.nim @@ -1,17 +1,36 @@ -import testutils/unittests +import testutils/unittests, results -import waku/waku_rln_relay/rln/rln_interface, ./buffer_utils +import waku/waku_rln_relay/rln/rln_interface +import waku/waku_rln_relay/rln/wrappers -suite "Buffer": - suite "toBuffer": +suite "Vec_uint8": + suite "toVecUint8": test "valid": # Given let bytes: seq[byte] = @[0x01, 0x02, 0x03] - # When - let buffer = bytes.toBuffer() + # When — wrap as a Vec_uint8 view then read the bytes back + var vec = toVecUint8(bytes) + let roundtrip = vecToSeq(vec) - # Then - let expectedBuffer: seq[uint8] = @[1, 2, 3] + # Then — byte values are preserved check: - buffer == expectedBuffer + roundtrip == bytes + +suite "RlnConfig": + suite "createRLNInstance": + test "ok": + # When we create the RLN instance (stateless build — no tree_depth arg) + let rlnRes = createRLNInstance() + + # Then it succeeds + check: + rlnRes.isOk() + + test "default": + # When we create the RLN instance + let rlnRes = createRLNInstance() + + # Then it succeeds + check: + rlnRes.isOk() diff --git a/tests/waku_rln_relay/rln/test_wrappers.nim b/tests/waku_rln_relay/rln/test_wrappers.nim index 29e24aae5..8cd9251c0 100644 --- a/tests/waku_rln_relay/rln/test_wrappers.nim +++ b/tests/waku_rln_relay/rln/test_wrappers.nim @@ -1,37 +1,6 @@ -import - std/options, - testutils/unittests, - chronicles, - chronos, - eth/keys, - bearssl, - stew/[results], - metrics, - metrics/chronos_httpserver +import testutils/unittests, results -import - waku/waku_rln_relay, - waku/waku_rln_relay/rln, - waku/waku_rln_relay/rln/wrappers, - ./waku_rln_relay_utils, - ../../testlib/[simple_mock, assertions], - ../../waku_keystore/utils, - ../../testlib/testutils - -from std/times import epochTime - -const Empty32Array = default(array[32, byte]) - -proc valid(x: seq[byte]): bool = - if x.len != 32: - error "Length should be 32", length = x.len - return false - - if x == Empty32Array: - error "Should not be empty array", array = x - return false - - return true +import waku/waku_rln_relay/rln, waku/waku_rln_relay/rln/wrappers, ./waku_rln_relay_utils suite "membershipKeyGen": test "ok": @@ -41,60 +10,20 @@ suite "membershipKeyGen": # Then it contains valid identity credentials let identityCredentials = identityCredentialsRes.get() + proc nonEmpty(x: seq[byte]): bool = + x.len == 32 and x != newSeq[byte](32) + check: - identityCredentials.idTrapdoor.valid() - identityCredentials.idNullifier.valid() - identityCredentials.idSecretHash.valid() - identityCredentials.idCommitment.valid() - - test "done is false": - # Given the key_gen function fails - let backup = key_gen - mock(key_gen): - proc keyGenMock(ctx: ptr RLN, output_buffer: ptr Buffer): bool = - return false - - keyGenMock - - # When we generate the membership keys - let identityCredentialsRes = membershipKeyGen() - - # Then it fails - check: - identityCredentialsRes.error() == "error in key generation" - - # Cleanup - mock(key_gen): - backup - - test "generatedKeys length is not 128": - # Given the key_gen function succeeds with wrong values - let backup = key_gen - mock(key_gen): - proc keyGenMock(ctx: ptr RLN, output_buffer: ptr Buffer): bool = - echo "# RUNNING MOCK" - output_buffer.len = 0 - output_buffer.ptr = cast[ptr uint8](newSeq[byte](0)) - return true - - keyGenMock - - # When we generate the membership keys - let identityCredentialsRes = membershipKeyGen() - - # Then it fails - check: - identityCredentialsRes.error() == "keysBuffer is of invalid length" - - # Cleanup - mock(key_gen): - backup + identityCredentials.idTrapdoor.nonEmpty() + identityCredentials.idNullifier.nonEmpty() + identityCredentials.idSecretHash.nonEmpty() + identityCredentials.idCommitment.nonEmpty() suite "RlnConfig": suite "createRLNInstance": test "ok": - # When we create the RLN instance - let rlnRes: RLNResult = createRLNInstance(15) + # When we create the RLN instance (stateless build — no tree_depth arg) + let rlnRes = createRLNInstance() # Then it succeeds check: @@ -102,30 +31,8 @@ suite "RlnConfig": test "default": # When we create the RLN instance - let rlnRes: RLNResult = createRLNInstance() + let rlnRes = createRLNInstance() # Then it succeeds check: rlnRes.isOk() - - test "new_circuit fails": - # Given the new_circuit function fails - let backup = new_circuit - mock(new_circuit): - proc newCircuitMock( - tree_height: uint, input_buffer: ptr Buffer, ctx: ptr (ptr RLN) - ): bool = - return false - - newCircuitMock - - # When we create the RLN instance - let rlnRes: RLNResult = createRLNInstance(15) - - # Then it fails - check: - rlnRes.error() == "error in parameters generation" - - # Cleanup - mock(new_circuit): - backup diff --git a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim index 29da94129..6b5b81532 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -3,7 +3,7 @@ {.push raises: [].} import - std/[options, sequtils, deques, random, locks, osproc], + std/[options, sequtils, deques, random, locks, osproc, algorithm], results, stew/byteutils, testutils/unittests, @@ -253,6 +253,9 @@ suite "Onchain group manager": manager.merkleProofCache = newSeq[byte](640) for i in 0 ..< 640: manager.merkleProofCache[i] = byte(rand(255)) + # chunk[0] becomes the MSB after reversal in group_manager; must be < 0x30 + for i in 0 ..< 20: + manager.merkleProofCache[i * 32] = 0 let messageBytes = "Hello".toBytes() @@ -335,6 +338,9 @@ suite "Onchain group manager": manager.merkleProofCache = newSeq[byte](640) for i in 0 ..< 640: manager.merkleProofCache[i] = byte(rand(255)) + # chunk[0] becomes the MSB after reversal in group_manager; must be < 0x30 + for i in 0 ..< 20: + manager.merkleProofCache[i * 32] = 0 let epoch = default(Epoch) info "epoch in bytes", epochHex = epoch.inHex() @@ -419,3 +425,81 @@ suite "Onchain group manager": check: isReady == true + + test "proof roundtrip: generateRlnProofWithWitness -> verifyRlnProof": + ## Smoke test: proof gen -> wire serialize -> deserialize -> ffi_verify_with_roots. + let credentials = generateCredentials() + + (waitFor manager.init()).isOkOr: + raiseAssert $error + + (waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr: + assert false, "register failed: " & error + + discard waitFor manager.updateRoots() + let roots = manager.validRoots.items().toSeq() + require: + roots.len > 0 + + let proofElements = (waitFor manager.fetchMerkleProofElements()).valueOr: + raiseAssert "fetchMerkleProofElements failed: " & error + + let signal = "Hello, RLN!".toBytes() + let epoch = default(Epoch) + + # Build RLNWitnessInput the same way group_manager.generateProof does. + var pathElements = newSeq[byte]() + for i in 0 ..< proofElements.len div 32: + pathElements.add(proofElements[i * 32 .. (i + 1) * 32 - 1].reversed()) + + let xCfr = hashToFieldLe(signal).valueOr: + raiseAssert "hashToFieldLe failed: " & error + defer: + ffi_cfr_free(xCfr) + let x = cfrToBytesLe(xCfr).valueOr: + raiseAssert "cfrToBytesLe failed: " & error + + let extNullifier = generateExternalNullifier(epoch, DefaultRlnIdentifier).valueOr: + raiseAssert "generateExternalNullifier failed: " & error + + let witness = RLNWitnessInput( + identity_secret: seqToField(credentials.idSecretHash), + user_message_limit: uint64ToField(uint64(UserMessageLimit(20))), + message_id: uint64ToField(uint64(MessageId(1))), + path_elements: pathElements, + identity_path_index: uint64ToIndex(manager.membershipIndex.get(), 20), + x: x, + external_nullifier: extNullifier, + ) + + # Step 1: generate proof via the FFI wrapper + let proof = generateRlnProofWithWitness( + manager.rlnInstance, witness, epoch, DefaultRlnIdentifier + ).valueOr: + raiseAssert "generateRlnProofWithWitness failed: " & error + + let zeroField = default(array[32, byte]) + check: + proof.merkleRoot != zeroField + proof.nullifier != zeroField + + # Step 2: serialize -> deserialize -> verify (the actual roundtrip) + let verified = verifyRlnProof(manager.rlnInstance, proof, signal, roots).valueOr: + raiseAssert "verifyRlnProof failed: " & error + check verified == true + + # Step 3: wrong signal -> x mismatch -> false + let wrongSignalVerified = verifyRlnProof( + manager.rlnInstance, proof, "wrong".toBytes(), roots + ).valueOr: + raiseAssert "verifyRlnProof (wrong signal) failed: " & error + check wrongSignalVerified == false + + # Step 4: bad root -> root not in set -> false + # byte[31] in LE is the MSB; 0x01 < 0x30 so this is a canonical field element. + var badRoot: MerkleNode + for i in 0 ..< 32: + badRoot[i] = 0x01 + let badRootVerified = verifyRlnProof(manager.rlnInstance, proof, signal, @[badRoot]).valueOr: + raiseAssert "verifyRlnProof (bad root) failed: " & error + check badRootVerified == false diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index 099226b76..7694b8112 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -1,7 +1,7 @@ {.used.} import - std/[options, os, sequtils, tempfiles, strutils, osproc], + std/[options, os, sequtils, tempfiles, strutils, osproc, algorithm], stew/byteutils, testutils/unittests, chronos, @@ -36,23 +36,16 @@ suite "Waku rln relay": teardown: stopAnvil(anvilProc) - test "key_gen Nim Wrappers": - let merkleDepth: csize_t = 20 + test "ffi_extended_key_gen raw FFI": + # When we call the raw key-generation FFI + var vec = ffi_extended_key_gen() - # keysBufferPtr will hold the generated identity credential i.e., id trapdoor, nullifier, secret hash and commitment - var keysBuffer: Buffer - let - keysBufferPtr = addr(keysBuffer) - done = key_gen(keysBufferPtr, true) - require: - # check whether the keys are generated successfully - done - - let generatedKeys = cast[ptr array[4 * 32, byte]](keysBufferPtr.`ptr`)[] + # Then it returns exactly 4 field elements + # (idTrapdoor, idNullifier, idSecretHash, idCommitment — each 32 bytes) + defer: + ffi_vec_cfr_free(vec) check: - # the id trapdoor, nullifier, secert hash and commitment together are 4*32 bytes - generatedKeys.len == 4 * 32 - info "generated keys: ", generatedKeys + int(ffi_vec_cfr_len(addr vec)) == 4 test "membership Key Generation": let idCredentialsRes = membershipKeyGen() @@ -80,18 +73,22 @@ suite "Waku rln relay": rlnInstance.isOk() let rln = rlnInstance.get() - # prepare the input - let msg = @[ - "126f4c026cd731979365f79bd345a46d673c5a3f6f588bdc718e6356d02b6fdc".toBytes(), - "1f0e5db2b69d599166ab16219a97b82b662085c93220382b39f9f911d3b943b1".toBytes(), - ] + # prepare the input — hex-decoded then reversed to little-endian field elements + let + left = hexToSeqByte( + "126f4c026cd731979365f79bd345a46d673c5a3f6f588bdc718e6356d02b6fdc" + ) + .reversed() + right = hexToSeqByte( + "1f0e5db2b69d599166ab16219a97b82b662085c93220382b39f9f911d3b943b1" + ) + .reversed() - let hashRes = poseidon(msg) + let hashRes = poseidon(left, right) - # Value taken from zerokit check: hashRes.isOk() - "28a15a991fe3d2a014485c7fa905074bfb55c0909112f865ded2be0a26a932c3" == + "180543bc9afb81d9c2282df9c9946f87b4596cf6d3fec2cc32b6637427685353" == hashRes.get().inHex() test "RateLimitProof Protobuf encode/init test": diff --git a/vendor/zerokit b/vendor/zerokit index a4bb3feb5..5e64cb882 160000 --- a/vendor/zerokit +++ b/vendor/zerokit @@ -1 +1 @@ -Subproject commit a4bb3feb5054e6fd24827adf204493e6e173437b +Subproject commit 5e64cb8822bee65eed6cf459f95ae72b80c6ba63 diff --git a/waku/waku_rln_relay/constants.nim b/waku/waku_rln_relay/constants.nim index 3e4757537..8532abaaa 100644 --- a/waku/waku_rln_relay/constants.nim +++ b/waku/waku_rln_relay/constants.nim @@ -25,8 +25,6 @@ const # the size of poseidon hash output as the number hex digits HashHexSize* = int(HashBitSize / 4) -const DefaultRlnTreePath* = "rln_tree.db" - const # pre-processed "rln/waku-rln-relay/v2.0.0" to array[32, byte] DefaultRlnIdentifier*: RlnIdentifier = [ diff --git a/waku/waku_rln_relay/conversion_utils.nim b/waku/waku_rln_relay/conversion_utils.nim index 4a168ebeb..fc130621b 100644 --- a/waku/waku_rln_relay/conversion_utils.nim +++ b/waku/waku_rln_relay/conversion_utils.nim @@ -75,48 +75,6 @@ proc serialize*( ) return output -proc serialize*(witness: RLNWitnessInput): seq[byte] = - ## Serializes the RLN witness into a byte array following zerokit's expected format. - ## The serialized format includes: - ## - identity_secret (32 bytes, little-endian with zero padding) - ## - user_message_limit (32 bytes, little-endian with zero padding) - ## - message_id (32 bytes, little-endian with zero padding) - ## - merkle tree depth (8 bytes, little-endian) = path_elements.len / 32 - ## - path_elements (each 32 bytes, ordered bottom-to-top) - ## - merkle tree depth again (8 bytes, little-endian) - ## - identity_path_index (sequence of bits as bytes, 0 = left, 1 = right) - ## - x (32 bytes, little-endian with zero padding) - ## - external_nullifier (32 bytes, little-endian with zero padding) - var buffer: seq[byte] - buffer.add(@(witness.identity_secret)) - buffer.add(@(witness.user_message_limit)) - buffer.add(@(witness.message_id)) - buffer.add(toBytes(uint64(witness.path_elements.len / 32), Endianness.littleEndian)) - for element in witness.path_elements: - buffer.add(element) - buffer.add(toBytes(uint64(witness.path_elements.len / 32), Endianness.littleEndian)) - buffer.add(witness.identity_path_index) - buffer.add(@(witness.x)) - buffer.add(@(witness.external_nullifier)) - return buffer - -proc serialize*(proof: RateLimitProof, data: openArray[byte]): seq[byte] = - ## a private proc to convert RateLimitProof and data to a byte seq - ## this conversion is used in the proof verification proc - ## [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> | signal_len<8> | signal ] - let lenPrefMsg = encodeLengthPrefix(@data) - var proofBytes = concat( - @(proof.proof), - @(proof.merkleRoot), - @(proof.externalNullifier), - @(proof.shareX), - @(proof.shareY), - @(proof.nullifier), - lenPrefMsg, - ) - - return proofBytes - # Serializes a sequence of MerkleNodes proc serialize*(roots: seq[MerkleNode]): seq[byte] = var rootsBytes: seq[byte] = @[] diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 38c533029..02317a056 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -11,7 +11,7 @@ import stint, json, std/[strutils, tables, algorithm, strformat], - stew/[byteutils, arrayops], + stew/byteutils, sequtils import @@ -327,7 +327,7 @@ proc getRootFromProofAndIndex( # it's currently not used anywhere, but can be used to verify the root from the proof and index # Compute leaf hash from idCommitment and messageLimit let messageLimitField = uint64ToField(g.userMessageLimit.get()) - var hash = poseidon(@[g.idCredentials.get().idCommitment, @messageLimitField]).valueOr: + var hash = poseidon(g.idCredentials.get().idCommitment, @messageLimitField).valueOr: return err("Failed to compute leaf hash: " & error) for i in 0 ..< bits.len: @@ -335,9 +335,9 @@ proc getRootFromProofAndIndex( let hashRes = if bits[i] == 0: - poseidon(@[@hash, sibling]) + poseidon(@hash, sibling) else: - poseidon(@[sibling, @hash]) + poseidon(sibling, @hash) hash = hashRes.valueOr: return err("Failed to compute poseidon hash: " & error) @@ -373,7 +373,12 @@ method generateProof*( let chunk = g.merkleProofCache[i * 32 .. (i + 1) * 32 - 1] path_elements.add(chunk.reversed()) - let x = keccak.keccak256.digest(data) + let xCfr = hashToFieldLe(data).valueOr: + return err("Failed to hash signal to field: " & error) + defer: + ffi_cfr_free(xCfr) + let x = cfrToBytesLe(xCfr).valueOr: + return err("Failed to serialize signal hash: " & error) let extNullifier = generateExternalNullifier(epoch, rlnIdentifier).valueOr: return err("Failed to compute external nullifier: " & error) @@ -388,57 +393,8 @@ method generateProof*( external_nullifier: extNullifier, ) - let serializedWitness = serialize(witness) - - var input_witness_buffer = toBuffer(serializedWitness) - - # Generate the proof using the zerokit API - var output_witness_buffer: Buffer - let witness_success = generate_proof_with_witness( - g.rlnInstance, addr input_witness_buffer, addr output_witness_buffer - ) - - if not witness_success: - return err("Failed to generate proof") - - # Parse the proof into a RateLimitProof object - var proofValue = cast[ptr array[320, byte]](output_witness_buffer.`ptr`) - let proofBytes: array[320, byte] = proofValue[] - - ## Parse the proof as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ] - let - proofOffset = 128 - rootOffset = proofOffset + 32 - externalNullifierOffset = rootOffset + 32 - shareXOffset = externalNullifierOffset + 32 - shareYOffset = shareXOffset + 32 - nullifierOffset = shareYOffset + 32 - - var - zkproof: ZKSNARK - proofRoot, shareX, shareY: MerkleNode - externalNullifier: ExternalNullifier - nullifier: Nullifier - - discard zkproof.copyFrom(proofBytes[0 .. proofOffset - 1]) - discard proofRoot.copyFrom(proofBytes[proofOffset .. rootOffset - 1]) - discard - externalNullifier.copyFrom(proofBytes[rootOffset .. externalNullifierOffset - 1]) - discard shareX.copyFrom(proofBytes[externalNullifierOffset .. shareXOffset - 1]) - discard shareY.copyFrom(proofBytes[shareXOffset .. shareYOffset - 1]) - discard nullifier.copyFrom(proofBytes[shareYOffset .. nullifierOffset - 1]) - - # Create the RateLimitProof object - let output = RateLimitProof( - proof: zkproof, - merkleRoot: proofRoot, - externalNullifier: externalNullifier, - epoch: epoch, - rlnIdentifier: rlnIdentifier, - shareX: shareX, - shareY: shareY, - nullifier: nullifier, - ) + let output = generateRlnProofWithWitness(g.rlnInstance, witness, epoch, rlnIdentifier).valueOr: + return err("Failed to generate proof: " & error) info "Proof generated successfully", proof = output @@ -449,34 +405,12 @@ method generateProof*( method verifyProof*( g: OnchainGroupManager, input: seq[byte], proof: RateLimitProof ): GroupManagerResult[bool] {.gcsafe.} = - ## -- Verifies an RLN rate-limit proof against the set of valid Merkle roots -- - - var normalizedProof = proof - - let externalNullifier = generateExternalNullifier(proof.epoch, proof.rlnIdentifier).valueOr: - return err("Failed to compute external nullifier: " & error) - normalizedProof.externalNullifier = externalNullifier - - let proofBytes = serialize(normalizedProof, input) - let proofBuffer = proofBytes.toBuffer() - - let rootsBytes = serialize(g.validRoots.items().toSeq()) - let rootsBuffer = rootsBytes.toBuffer() - - var validProof: bool # out-param - let ffiOk = verify_with_roots( - g.rlnInstance, # RLN context created at init() - addr proofBuffer, # (proof + signal) - addr rootsBuffer, # valid Merkle roots - addr validProof # will be set by the FFI call - , - ) - - if not ffiOk: - return err("could not verify the proof") - else: - info "Proof verified successfully" + let validProof = verifyRlnProof( + g.rlnInstance, proof, input, g.validRoots.items().toSeq() + ).valueOr: + return err("could not verify the proof: " & error) + info "Proof verified", isValid = validProof return ok(validProof) method onRegister*(g: OnchainGroupManager, cb: OnRegisterCallback) {.gcsafe.} = @@ -623,6 +557,10 @@ method stop*(g: OnchainGroupManager): Future[void] {.async, gcsafe.} = g.ethRpc.get().ondisconnect = nil await g.ethRpc.get().close() + if not g.rlnInstance.isNil: + ffi_rln_free(g.rlnInstance) + g.rlnInstance = nil + g.initialized = false method isReady*(g: OnchainGroupManager): Future[bool] {.async.} = diff --git a/waku/waku_rln_relay/protocol_metrics.nim b/waku/waku_rln_relay/protocol_metrics.nim index 1551f022e..2cea329fe 100644 --- a/waku/waku_rln_relay/protocol_metrics.nim +++ b/waku/waku_rln_relay/protocol_metrics.nim @@ -56,7 +56,7 @@ declarePublicGauge( ) declarePublicGauge( waku_rln_membership_insertion_duration_seconds, - "time taken to insert a new member into the local merkle tree", + "time taken to process a new membership registration", ) declarePublicGauge( waku_rln_membership_credentials_import_duration_seconds, diff --git a/waku/waku_rln_relay/rln/rln_interface.nim b/waku/waku_rln_relay/rln/rln_interface.nim index 0bb0ef6b0..612d1a2cc 100644 --- a/waku/waku_rln_relay/rln/rln_interface.nim +++ b/waku/waku_rln_relay/rln/rln_interface.nim @@ -1,168 +1,378 @@ -## Nim wrappers for the functions defined in librln +## Nim wrappers for librln (zerokit v2.0.2, safer-ffi typed handles). +## +## Built against the `stateless` zerokit feature: tree-mutation FFI is not +## bound here because logos-delivery does not maintain a local Merkle tree +## (post-PR #3312); the WakuRlnV2 contract is the source of truth and the +## per-index Merkle path is fetched via getMerkleProof(index). +## +## Memory model: every CResult.err must be checked with `hasError` and +## consumed via `consumeError`. Every CFr / Vec_CFr / Vec_uint8 returned by +## the FFI owns memory the caller must release with the corresponding +## ffi_*_free. Use `defer:` immediately after acquisition. +## +## Wire format (v2.0.2 single-message-id): +## RLNProof: [ 0x00 | proof<128> | RLNProofValues(0x00) ] +## RLNProofValues: [ 0x00 | root<32> | external_nullifier<32> | +## x<32> | y<32> | nullifier<32> ] +## Total RLNProof byte size: 1 + 128 + 1 + 5*32 = 290 bytes. + +import results import ../protocol_types -{.push raises: [].} +{.push raises: [], gcsafe.} -## Buffer struct is taken from -# https://github.com/celo-org/celo-threshold-bls-rs/blob/master/crates/threshold-bls-ffi/src/ffi.rs -type Buffer* = object - `ptr`*: ptr uint8 - len*: uint +# --- Types ------------------------------------------------------------------ -proc toBuffer*(x: openArray[byte]): Buffer = - ## converts the input to a Buffer object - ## the Buffer object is used to communicate data with the rln lib - var temp = @x - let baseAddr = cast[pointer](x) - let output = Buffer(`ptr`: cast[ptr uint8](baseAddr), len: uint(temp.len)) - return output +type + CSize = csize_t -###################################################################### -## RLN Zerokit module APIs -###################################################################### + CFr* = object ## opaque ark_bn254::Fr handle + FFI_RLNProof* = object + FFI_RLNPartialProof* = object + FFI_RLNWitnessInput* = object + FFI_RLNPartialWitnessInput* = object + FFI_RLNProofValues* = object -#-------------------------------- zkSNARKs operations ----------------------------------------- -proc key_gen*( - output_buffer: ptr Buffer, is_little_endian: bool -): bool {.importc: "extended_key_gen".} + Vec_CFr* = object + dataPtr*: ptr CFr + len*: CSize + cap*: CSize -## generates identity trapdoor, identity nullifier, identity secret hash and id commitment tuple serialized inside output_buffer as | identity_trapdoor<32> | identity_nullifier<32> | identity_secret_hash<32> | id_commitment<32> | -## identity secret hash is the poseidon hash of [identity_trapdoor, identity_nullifier] -## id commitment is the poseidon hash of the identity secret hash -## the return bool value indicates the success or failure of the operation + Vec_uint8* = object + dataPtr*: ptr uint8 + len*: CSize + cap*: CSize -proc seeded_key_gen*( - input_buffer: ptr Buffer, output_buffer: ptr Buffer, is_little_endian: bool -): bool {.importc: "seeded_extended_key_gen".} + # CResult variants — safer-ffi lowers Result to a struct of + # (ok: T-or-null, err: Vec_uint8-or-null). Exactly one is populated. + CBoolResult* = object + ok*: bool + err*: Vec_uint8 -## generates identity trapdoor, identity nullifier, identity secret hash and id commitment tuple serialized inside output_buffer as | identity_trapdoor<32> | identity_nullifier<32> | identity_secret_hash<32> | id_commitment<32> | using ChaCha20 -## seeded with an arbitrary long seed serialized in input_buffer -## The input seed provided by the user is hashed using Keccak256 before being passed to ChaCha20 as seed. -## identity secret hash is the poseidon hash of [identity_trapdoor, identity_nullifier] -## id commitment is the poseidon hash of the identity secret hash -# use_little_endian: if true, uses big or little endian for serialization (default: true) -## the return bool value indicates the success or failure of the operation + CResultRLNPtrVecU8* = object + ok*: ptr RLN + err*: Vec_uint8 -proc generate_proof*( - ctx: ptr RLN, input_buffer: ptr Buffer, output_buffer: ptr Buffer -): bool {.importc: "generate_rln_proof".} + CResultCFrPtrVecU8* = object + ok*: ptr CFr + err*: Vec_uint8 -## rln-v2 -## input_buffer has to be serialized as [ identity_secret<32> | identity_index<8> | user_message_limit<32> | message_id<32> | external_nullifier<32> | signal_len<8> | signal ] -## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ] -## rln-v1 -## input_buffer has to be serialized as [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal ] -## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> ] -## integers wrapped in <> indicate value sizes in bytes -## the return bool value indicates the success or failure of the operation + CResultProofPtrVecU8* = object + ok*: ptr FFI_RLNProof + err*: Vec_uint8 -proc generate_proof_with_witness*( - ctx: ptr RLN, input_buffer: ptr Buffer, output_buffer: ptr Buffer -): bool {.importc: "generate_rln_proof_with_witness".} + CResultPartialProofPtrVecU8* = object + ok*: ptr FFI_RLNPartialProof + err*: Vec_uint8 -## rln-v2 -## "witness" term refer to collection of secret inputs with proper serialization -## input_buffer has to be serialized as [ identity_secret<32> | user_message_limit<32> | message_id<32> | path_elements> | identity_path_index> | x<32> | external_nullifier<32> ] -## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ] -## rln-v1 -## input_buffer has to be serialized as [ id_key<32> | path_elements> | identity_path_index> | x<32> | epoch<32> | rln_identifier<32> ] -## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> ] -## integers wrapped in <> indicate value sizes in bytes -## path_elements and identity_path_index serialize a merkle proof and are vectors of elements of 32 and 1 bytes respectively -## the return bool value indicates the success or failure of the operation + CResultWitnessInputPtrVecU8* = object + ok*: ptr FFI_RLNWitnessInput + err*: Vec_uint8 -proc verify*( - ctx: ptr RLN, proof_buffer: ptr Buffer, proof_is_valid_ptr: ptr bool -): bool {.importc: "verify_rln_proof".} + CResultPartialWitnessInputPtrVecU8* = object + ok*: ptr FFI_RLNPartialWitnessInput + err*: Vec_uint8 -## rln-v2 -## proof_buffer has to be serialized as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> | signal_len<8> | signal ] -## rln-v1 -## ## proof_buffer has to be serialized as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> | signal_len<8> | signal ] -## the return bool value indicates the success or failure of the call to the verify function -## the verification of the zk proof is available in proof_is_valid_ptr, where a value of true indicates success and false a failure + CResultVecCFrVecU8* = object + ok*: Vec_CFr + err*: Vec_uint8 -proc verify_with_roots*( - ctx: ptr RLN, - proof_buffer: ptr Buffer, - roots_buffer: ptr Buffer, - proof_is_valid_ptr: ptr bool, -): bool {.importc: "verify_with_roots".} + CResultVecU8VecU8* = object + ok*: Vec_uint8 + err*: Vec_uint8 -## rln-v2 -## proof_buffer has to be serialized as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> | signal_len<8> | signal ] -## rln-v1 -## proof_buffer has to be serialized as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> | signal_len<8> | signal ] -## roots_buffer contains the concatenation of 32 bytes long serializations in little endian of root values -## the return bool value indicates the success or failure of the call to the verify function -## the verification of the zk proof is available in proof_is_valid_ptr, where a value of true indicates success and false a failure +const + FieldElementSize* = 32 + ZksnarkProofSize* = 128 + ## Single-message-id serialized RLNProof size: outer version + proof + ## + inner RLNProofValues (inner version + 5 field elements). + RlnProofWireSize* = 1 + ZksnarkProofSize + 1 + 5 * FieldElementSize -proc zk_prove*( - ctx: ptr RLN, input_buffer: ptr Buffer, output_buffer: ptr Buffer -): bool {.importc: "prove".} +# FFI declarations — source of truth: vendor/zerokit/rln/src/ffi/{ffi_rln,ffi_utils}.rs -## Computes the zkSNARK proof and stores it in output_buffer for input values stored in input_buffer -## rln-v2 -## input_buffer is serialized as input_data as [ identity_secret<32> | user_message_limit<32> | message_id<32> | path_elements> | identity_path_index> | x<32> | external_nullifier<32> ] -## rln-v1 -## input_buffer is serialized as input_data as [ id_key<32> | path_elements> | identity_path_index> | x<32> | epoch<32> | rln_identifier<32> ] -## output_buffer holds the proof data and should be parsed as [ proof<128> ] -## path_elements and indentity_path elements serialize a merkle proof for id_key and are vectors of elements of 32 and 1 bytes, respectively (not. Vec<>). -## x is the x coordinate of the Shamir's secret share for which the proof is computed -## epoch is the input epoch (equivalently, the nullifier) -## the return bool value indicates the success or failure of the operation +# --- RLN instance lifecycle (stateless variants) -------------------------- -proc zk_verify*( - ctx: ptr RLN, proof_buffer: ptr Buffer, proof_is_valid_ptr: ptr bool -): bool {.importc: "verify".} +proc ffi_rln_new*(): CResultRLNPtrVecU8 {.importc: "ffi_rln_new", cdecl.} -## Verifies the zkSNARK proof passed in proof_buffer -## input_buffer is serialized as input_data as [ proof<128> ] -## the verification of the zk proof is available in proof_is_valid_ptr, where a value of true indicates success and false a failure -## the return bool value indicates the success or failure of the operation +proc ffi_rln_new_with_params*( + zkey_data: ptr Vec_uint8, graph_data: ptr Vec_uint8 +): CResultRLNPtrVecU8 {.importc: "ffi_rln_new_with_params", cdecl.} -#-------------------------------- Common procedures ------------------------------------------- -# stateful version -proc new_circuit*( - tree_depth: uint, input_buffer: ptr Buffer, ctx: ptr (ptr RLN) -): bool {.importc: "new".} +proc ffi_rln_free*(rln: ptr RLN) {.importc: "ffi_rln_free", cdecl.} -## creates an instance of rln object as defined by the zerokit RLN lib -## input_buffer contains a serialization of the path where the circuit resources can be found (.r1cs, .wasm, .zkey and optionally the verification_key.json) -## ctx holds the final created rln object -## the return bool value indicates the success or failure of the operation +# --- Keygen --------------------------------------------------------------- -# stateless version -proc new_circuit*(ctx: ptr (ptr RLN)): bool {.importc: "new".} +proc ffi_extended_key_gen*(): Vec_CFr {.importc: "ffi_extended_key_gen", cdecl.} -proc new_circuit_from_data*( - zkey_buffer: ptr Buffer, graph_buffer: ptr Buffer, ctx: ptr (ptr RLN) -): bool {.importc: "new_with_params".} +proc ffi_seeded_extended_key_gen*( + seed: ptr Vec_uint8 +): Vec_CFr {.importc: "ffi_seeded_extended_key_gen", cdecl.} -## creates an instance of rln object as defined by the zerokit RLN lib by passing the required inputs as byte arrays -## zkey_buffer contains the bytes read from the .zkey proving key -## graph_buffer contains the bytes read from the graph data file -## ctx holds the final created rln object -## the return bool value indicates the success or failure of the operation +# --- Witness construction ------------------------------------------------- -#-------------------------------- Hashing utils ------------------------------------------- +proc ffi_rln_witness_input_new*( + identity_secret: ptr CFr, + user_message_limit: ptr CFr, + message_id: ptr CFr, + path_elements: ptr Vec_CFr, + identity_path_index: ptr Vec_uint8, + x: ptr CFr, + external_nullifier: ptr CFr, +): CResultWitnessInputPtrVecU8 {.importc: "ffi_rln_witness_input_new", cdecl.} -proc sha256*( - input_buffer: ptr Buffer, output_buffer: ptr Buffer, is_little_endian: bool -): bool {.importc: "hash".} +proc ffi_rln_witness_input_free*( + witness: ptr FFI_RLNWitnessInput +) {.importc: "ffi_rln_witness_input_free", cdecl.} -## it hashes (sha256) the plain text supplied in inputs_buffer and then maps it to a field element -## this proc is used to map arbitrary signals to field element for the sake of proof generation -## inputs_buffer holds the hash input as a byte seq -## the hash output is generated and populated inside output_buffer -## the output_buffer contains 32 bytes hash output +proc ffi_rln_partial_witness_input_new*( + identity_secret: ptr CFr, + user_message_limit: ptr CFr, + path_elements: ptr Vec_CFr, + identity_path_index: ptr Vec_uint8, +): CResultPartialWitnessInputPtrVecU8 {. + importc: "ffi_rln_partial_witness_input_new", cdecl +.} -proc poseidon*( - input_buffer: ptr Buffer, output_buffer: ptr Buffer, is_little_endian: bool -): bool {.importc: "poseidon_hash".} +proc ffi_rln_partial_witness_input_free*( + witness: ptr FFI_RLNPartialWitnessInput +) {.importc: "ffi_rln_partial_witness_input_free", cdecl.} -## it hashes (poseidon) the plain text supplied in inputs_buffer -## this proc is used to compute the identity secret hash, and external nullifier -## inputs_buffer holds the hash input as a byte seq -## the hash output is generated and populated inside output_buffer -## the output_buffer contains 32 bytes hash output +# --- Proof generation ----------------------------------------------------- +# safer-ffi's repr_c::Box lands on the Nim side as `ptr ptr T`. Call sites +# pass `addr handle` where `handle` is `ptr T`. + +proc ffi_generate_rln_proof*( + rln: ptr ptr RLN, witness: ptr ptr FFI_RLNWitnessInput +): CResultProofPtrVecU8 {.importc: "ffi_generate_rln_proof", cdecl.} + +proc ffi_generate_partial_zk_proof*( + rln: ptr ptr RLN, partial_witness: ptr ptr FFI_RLNPartialWitnessInput +): CResultPartialProofPtrVecU8 {.importc: "ffi_generate_partial_zk_proof", cdecl.} + +proc ffi_finish_rln_proof*( + rln: ptr ptr RLN, + partial_proof: ptr ptr FFI_RLNPartialProof, + witness: ptr ptr FFI_RLNWitnessInput, +): CResultProofPtrVecU8 {.importc: "ffi_finish_rln_proof", cdecl.} + +# --- Verification --------------------------------------------------------- + +proc ffi_verify_with_roots*( + rln: ptr ptr RLN, proof: ptr ptr FFI_RLNProof, roots: ptr Vec_CFr, x: ptr CFr +): CBoolResult {.importc: "ffi_verify_with_roots", cdecl.} + +# --- Proof serialization -------------------------------------------------- + +proc ffi_rln_proof_to_bytes_le*( + proof: ptr ptr FFI_RLNProof +): CResultVecU8VecU8 {.importc: "ffi_rln_proof_to_bytes_le", cdecl.} + +proc ffi_bytes_le_to_rln_proof*( + bytes: ptr Vec_uint8 +): CResultProofPtrVecU8 {.importc: "ffi_bytes_le_to_rln_proof", cdecl.} + +# v2.0.2: construct an RLNProof directly from its field elements (single +# message-id variant), avoiding the manual 290-byte wire layout. +proc ffi_rln_proof_new*( + groth16Bytes: ptr Vec_uint8, + root: ptr CFr, + externalNullifier: ptr CFr, + x: ptr CFr, + y: ptr CFr, + nullifier: ptr CFr, +): CResultProofPtrVecU8 {.importc: "ffi_rln_proof_new", cdecl.} + +proc ffi_rln_proof_free*(p: ptr FFI_RLNProof) {.importc: "ffi_rln_proof_free", cdecl.} + +proc ffi_rln_partial_proof_to_bytes_le*( + partial_proof: ptr ptr FFI_RLNPartialProof +): CResultVecU8VecU8 {.importc: "ffi_rln_partial_proof_to_bytes_le", cdecl.} + +proc ffi_bytes_le_to_rln_partial_proof*( + bytes: ptr Vec_uint8 +): CResultPartialProofPtrVecU8 {.importc: "ffi_bytes_le_to_rln_partial_proof", cdecl.} + +proc ffi_rln_partial_proof_free*( + p: ptr FFI_RLNPartialProof +) {.importc: "ffi_rln_partial_proof_free", cdecl.} + +# --- Proof values (extract root / x / y / nullifier from a proof) --------- + +proc ffi_rln_proof_get_values*( + proof: ptr ptr FFI_RLNProof +): ptr FFI_RLNProofValues {.importc: "ffi_rln_proof_get_values", cdecl.} + +proc ffi_rln_proof_values_get_root*( + pv: ptr ptr FFI_RLNProofValues +): ptr CFr {.importc: "ffi_rln_proof_values_get_root", cdecl.} + +proc ffi_rln_proof_values_get_x*( + pv: ptr ptr FFI_RLNProofValues +): ptr CFr {.importc: "ffi_rln_proof_values_get_x", cdecl.} + +proc ffi_rln_proof_values_get_external_nullifier*( + pv: ptr ptr FFI_RLNProofValues +): ptr CFr {.importc: "ffi_rln_proof_values_get_external_nullifier", cdecl.} + +proc ffi_rln_proof_values_get_y*( + pv: ptr ptr FFI_RLNProofValues +): CResultCFrPtrVecU8 {.importc: "ffi_rln_proof_values_get_y", cdecl.} + +proc ffi_rln_proof_values_get_nullifier*( + pv: ptr ptr FFI_RLNProofValues +): CResultCFrPtrVecU8 {.importc: "ffi_rln_proof_values_get_nullifier", cdecl.} + +proc ffi_rln_proof_values_free*( + pv: ptr FFI_RLNProofValues +) {.importc: "ffi_rln_proof_values_free", cdecl.} + +# --- Slashing ------------------------------------------------------------- + +proc ffi_compute_id_secret*( + share1_x: ptr CFr, share1_y: ptr CFr, share2_x: ptr CFr, share2_y: ptr CFr +): CResultCFrPtrVecU8 {.importc: "ffi_compute_id_secret", cdecl.} + +# --- Primitives: CFr ------------------------------------------------------ + +proc ffi_cfr_zero*(): ptr CFr {.importc: "ffi_cfr_zero", cdecl.} + +proc ffi_cfr_to_bytes_le*( + cfr: ptr CFr +): Vec_uint8 {.importc: "ffi_cfr_to_bytes_le", cdecl.} + +proc ffi_bytes_le_to_cfr*( + bytes: ptr Vec_uint8 +): CResultCFrPtrVecU8 {.importc: "ffi_bytes_le_to_cfr", cdecl.} + +proc ffi_cfr_free*(cfr: ptr CFr) {.importc: "ffi_cfr_free", cdecl.} + +# --- Primitives: Vec_CFr -------------------------------------------------- + +proc ffi_vec_cfr_new*(capacity: CSize): Vec_CFr {.importc: "ffi_vec_cfr_new", cdecl.} + +proc ffi_vec_cfr_push*( + v: ptr Vec_CFr, cfr: ptr CFr +) {.importc: "ffi_vec_cfr_push", cdecl.} + +proc ffi_vec_cfr_len*(v: ptr Vec_CFr): CSize {.importc: "ffi_vec_cfr_len", cdecl.} + +proc ffi_vec_cfr_get*( + v: ptr Vec_CFr, i: CSize +): ptr CFr {.importc: "ffi_vec_cfr_get", cdecl.} + +proc ffi_vec_cfr_free*(v: Vec_CFr) {.importc: "ffi_vec_cfr_free", cdecl.} + +# --- Primitives: Vec_uint8 ------------------------------------------------ + +proc ffi_vec_u8_free*(v: Vec_uint8) {.importc: "ffi_vec_u8_free", cdecl.} + +proc ffi_c_string_free*(s: Vec_uint8) {.importc: "ffi_c_string_free", cdecl.} + +# --- Hash helpers --------------------------------------------------------- + +proc ffi_hash_to_field_le*( + input: ptr Vec_uint8 +): ptr CFr {.importc: "ffi_hash_to_field_le", cdecl.} + +proc ffi_poseidon_hash_pair*( + a: ptr CFr, b: ptr CFr +): ptr CFr {.importc: "ffi_poseidon_hash_pair", cdecl.} + +# --- Memory-hygiene helpers ------------------------------------------------- + +proc hasError*(data: Vec_uint8): bool = + not data.dataPtr.isNil + +proc asString*(data: Vec_uint8): string = + if data.dataPtr.isNil or data.len == 0: + return "" + result = newString(int(data.len)) + copyMem(addr result[0], data.dataPtr, int(data.len)) + +proc consumeError*(prefix: string, data: Vec_uint8): string = + ## Read an error string out of a Rust-owned Vec_uint8 AND free it. + let msg = asString(data) + if hasError(data): + ffi_c_string_free(data) + if prefix.len == 0: + msg + elif msg.len == 0: + prefix + else: + prefix & msg + +proc toVecUint8*(data: openArray[byte]): Vec_uint8 = + ## Wrap Nim-owned bytes as a Vec_uint8 view. NOTE: the resulting Vec_uint8 + ## must NOT be passed to ffi_vec_u8_free — Nim retains ownership. + if data.len == 0: + return Vec_uint8(dataPtr: nil, len: 0, cap: 0) + Vec_uint8( + dataPtr: cast[ptr uint8](unsafeAddr data[0]), + len: CSize(data.len), + cap: CSize(data.len), + ) + +proc vecToSeq*(data: Vec_uint8): seq[byte] = + result = newSeq[byte](int(data.len)) + if result.len > 0: + copyMem(addr result[0], data.dataPtr, result.len) + +proc seqToFixed32*(data: openArray[byte]): RlnRelayResult[array[32, byte]] = + if data.len != FieldElementSize: + return err("Expected 32 bytes, got " & $data.len) + var output: array[32, byte] + copyMem(addr output[0], unsafeAddr data[0], FieldElementSize) + ok(output) + +proc cfrToBytesLe*(cfr: ptr CFr): RlnRelayResult[array[32, byte]] = + let bytes = ffi_cfr_to_bytes_le(cfr) + defer: + ffi_vec_u8_free(bytes) + if int(bytes.len) != FieldElementSize: + return err("Invalid field byte length: " & $bytes.len) + seqToFixed32(vecToSeq(bytes)) + +proc bytesToCfrLe*(data: openArray[byte]): RlnRelayResult[ptr CFr] = + ## Allocate a ptr CFr from raw bytes. Caller MUST ffi_cfr_free(x). + var vec = toVecUint8(data) + let res = ffi_bytes_le_to_cfr(addr vec) + if not res.ok.isNil: + return ok(res.ok) + err(consumeError("Failed to convert bytes to field: ", res.err)) + +proc cfrResultToBytes*( + res: CResultCFrPtrVecU8, prefix: string +): RlnRelayResult[array[32, byte]] = + ## Consume a CResultCFrPtrVecU8: read bytes if ok, free the CFr, or + ## propagate the error (also freeing the error string). + if res.ok.isNil: + return err(consumeError(prefix, res.err)) + defer: + ffi_cfr_free(res.ok) + cfrToBytesLe(res.ok) + +proc hashToFieldLe*(data: openArray[byte]): RlnRelayResult[ptr CFr] = + ## Caller MUST ffi_cfr_free the returned ptr. + var vec = toVecUint8(data) + let cfr = ffi_hash_to_field_le(addr vec) + if cfr.isNil: + return err("Failed to hash to field") + ok(cfr) + +proc poseidonPairLe*(a, b: openArray[byte]): RlnRelayResult[array[32, byte]] = + ## Poseidon hash of exactly two 32-byte field elements (little-endian). + ## zerokit v2 FFI only exposes pair-input Poseidon; unary is not supported. + let aPtr = bytesToCfrLe(a).valueOr: + return err(error) + defer: + ffi_cfr_free(aPtr) + let bPtr = bytesToCfrLe(b).valueOr: + return err(error) + defer: + ffi_cfr_free(bPtr) + let cfr = ffi_poseidon_hash_pair(aPtr, bPtr) + if cfr.isNil: + return err("Poseidon hash failed") + defer: + ffi_cfr_free(cfr) + cfrToBytesLe(cfr) diff --git a/waku/waku_rln_relay/rln/wrappers.nim b/waku/waku_rln_relay/rln/wrappers.nim index f6f001d70..4fc8c1542 100644 --- a/waku/waku_rln_relay/rln/wrappers.nim +++ b/waku/waku_rln_relay/rln/wrappers.nim @@ -1,140 +1,149 @@ -import std/json -import - chronicles, - options, - eth/keys, - stew/[arrayops, byteutils, endians2], - stint, - results, - std/[sequtils, strutils, tables], - nimcrypto/keccak as keccak +import chronicles, eth/keys, stew/[arrayops, endians2], stint, results import ./rln_interface, ../conversion_utils, ../protocol_types, ../protocol_metrics import ../../waku_core, ../../waku_keystore +{.push raises: [], gcsafe.} + logScope: topics = "waku rln_relay ffi" -proc membershipKeyGen*(): RlnRelayResult[IdentityCredential] = - ## generates a IdentityCredential that can be used for the registration into the rln membership contract - ## Returns an error if the key generation fails +# Forward decl; body defined below. +proc generateExternalNullifier*( + epoch: Epoch, rlnIdentifier: RlnIdentifier +): RlnRelayResult[ExternalNullifier] - # keysBufferPtr will hold the generated identity tuple i.e., trapdoor, nullifier, secret hash and commitment - var - keysBuffer: Buffer - keysBufferPtr = addr(keysBuffer) - done = key_gen(keysBufferPtr, true) +proc toRootVec(validRoots: seq[MerkleNode]): RlnRelayResult[Vec_CFr] = + ## Caller MUST ffi_vec_cfr_free the returned Vec_CFr. + var roots = ffi_vec_cfr_new(csize_t(validRoots.len)) + for root in validRoots: + let cfr = bytesToCfrLe(root).valueOr: + ffi_vec_cfr_free(roots) + return err("failed call to bytesToCfrLe in toRootVec: " & error) + ffi_vec_cfr_push(addr roots, cfr) + ffi_cfr_free(cfr) + ok(roots) - # check whether the keys are generated successfully - if (done == false): - return err("error in key generation") +proc proofPtrToRateLimitProof( + proofPtr: ptr FFI_RLNProof, epoch: Epoch, rlnIdentifier: RlnIdentifier +): RlnRelayResult[RateLimitProof] = + var proofHandle = proofPtr + let proofBytesRes = ffi_rln_proof_to_bytes_le(addr proofHandle) + if hasError(proofBytesRes.err): + return err(consumeError("Failed to serialize proof: ", proofBytesRes.err)) + defer: + ffi_vec_u8_free(proofBytesRes.ok) - if (keysBuffer.len != 4 * 32): - return err("keysBuffer is of invalid length") + let serialized = vecToSeq(proofBytesRes.ok) + if serialized.len < RlnProofWireSize: + return err("Serialized proof too short: " & $serialized.len) - var generatedKeys = cast[ptr array[4 * 32, byte]](keysBufferPtr.`ptr`)[] - # the public and secret keys together are 64 bytes + let proofValues = ffi_rln_proof_get_values(addr proofHandle) + if proofValues.isNil(): + return err("Failed to extract proof values") + defer: + ffi_rln_proof_values_free(proofValues) - # TODO define a separate proc to decode the generated keys to the secret and public components - var - idTrapdoor: array[32, byte] - idNullifier: array[32, byte] - idSecretHash: array[32, byte] - idCommitment: array[32, byte] - for (i, x) in idTrapdoor.mpairs: - x = generatedKeys[i + 0 * 32] - for (i, x) in idNullifier.mpairs: - x = generatedKeys[i + 1 * 32] - for (i, x) in idSecretHash.mpairs: - x = generatedKeys[i + 2 * 32] - for (i, x) in idCommitment.mpairs: - x = generatedKeys[i + 3 * 32] + var output: RateLimitProof + output.epoch = epoch + output.rlnIdentifier = rlnIdentifier - var identityCredential = IdentityCredential( - idTrapdoor: @idTrapdoor, - idNullifier: @idNullifier, - idSecretHash: @idSecretHash, - idCommitment: @idCommitment, + # zkSNARK bytes: skip the leading version byte, take 128. + copyMem(addr output.proof[0], unsafeAddr serialized[1], ZksnarkProofSize) + + var pvHandle = proofValues + + let rootPtr = ffi_rln_proof_values_get_root(addr pvHandle) + if rootPtr.isNil(): + return err("Failed to read proof root") + defer: + ffi_cfr_free(rootPtr) + output.merkleRoot = cfrToBytesLe(rootPtr).valueOr: + return + err("failed call to cfrToBytesLe (root) in proofPtrToRateLimitProof: " & error) + + let xPtr = ffi_rln_proof_values_get_x(addr pvHandle) + if xPtr.isNil(): + return err("Failed to read proof x") + defer: + ffi_cfr_free(xPtr) + output.shareX = cfrToBytesLe(xPtr).valueOr: + return + err("failed call to cfrToBytesLe (shareX) in proofPtrToRateLimitProof: " & error) + + let yRes = ffi_rln_proof_values_get_y(addr pvHandle) + output.shareY = cfrResultToBytes(yRes, "Failed to read proof y: ").valueOr: + return err(error) + + let nullifierRes = ffi_rln_proof_values_get_nullifier(addr pvHandle) + output.nullifier = cfrResultToBytes(nullifierRes, "Failed to read proof nullifier: ").valueOr: + return err(error) + + let extNullPtr = ffi_rln_proof_values_get_external_nullifier(addr pvHandle) + if extNullPtr.isNil(): + return err("Failed to read proof external nullifier") + defer: + ffi_cfr_free(extNullPtr) + output.externalNullifier = cfrToBytesLe(extNullPtr).valueOr: + return err( + "failed call to cfrToBytesLe (externalNullifier) in proofPtrToRateLimitProof: " & + error + ) + + ok(output) + +proc parseCredentialVec(vec: var Vec_CFr): RlnRelayResult[IdentityCredential] = + ## Vec_CFr order: idTrapdoor, idNullifier, idSecretHash, idCommitment. + if int(ffi_vec_cfr_len(addr vec)) != 4: + return err("Unexpected credential element count") + + template readField(idx: int): seq[byte] = + let f = ffi_vec_cfr_get(addr vec, csize_t(idx)) + if f.isNil(): + return err("Missing credential field from zerokit") + let bytes = cfrToBytesLe(f).valueOr: + return err("failed call to cfrToBytesLe in parseCredentialVec: " & error) + @bytes + + let idTrapdoor = readField(0) + let idNullifier = readField(1) + let idSecretHash = readField(2) + let idCommitment = readField(3) + + return ok( + IdentityCredential( + idTrapdoor: idTrapdoor, + idNullifier: idNullifier, + idSecretHash: idSecretHash, + idCommitment: idCommitment, + ) ) - return ok(identityCredential) - -type RlnTreeConfig = ref object of RootObj - cache_capacity: int - mode: string - compression: bool - flush_every_ms: int - -type RlnConfig = ref object of RootObj - resources_folder: string - tree_config: RlnTreeConfig - -proc `%`(c: RlnConfig): JsonNode = - ## wrapper around the generic JObject constructor. - ## We don't need to have a separate proc for the tree_config field - let tree_config = %{ - "cache_capacity": %c.tree_config.cache_capacity, - "mode": %c.tree_config.mode, - "compression": %c.tree_config.compression, - "flush_every_ms": %c.tree_config.flush_every_ms, - } - return %[("resources_folder", %c.resources_folder), ("tree_config", %tree_config)] +proc membershipKeyGen*(): RlnRelayResult[IdentityCredential] = + var vec = ffi_extended_key_gen() + defer: + ffi_vec_cfr_free(vec) + parseCredentialVec(vec) proc createRLNInstanceLocal(): RLNResult = - ## generates an instance of RLN - ## An RLN instance supports both zkSNARKs logics and Merkle tree data structure and operations - ## Returns an error if the instance creation fails - - let rln_config = RlnConfig( - resources_folder: "tree_height_/", - tree_config: RlnTreeConfig( - cache_capacity: 15_000, - mode: "high_throughput", - compression: false, - flush_every_ms: 500, - ), - ) - - var serialized_rln_config = $(%rln_config) - - var - rlnInstance: ptr RLN - merkleDepth: csize_t = uint(20) - configBuffer = - serialized_rln_config.toOpenArrayByte(0, serialized_rln_config.high).toBuffer() - - # create an instance of RLN - let res = new_circuit(merkleDepth, addr configBuffer, addr rlnInstance) - # check whether the circuit parameters are generated successfully - if (res == false): - info "error in parameters generation" - return err("error in parameters generation") - return ok(rlnInstance) + ## Creates a stateless RLN instance (no local Merkle tree). + let res = ffi_rln_new() + if res.ok.isNil(): + let msg = consumeError("error in parameters generation: ", res.err) + info "error in parameters generation", err = msg + return err(msg) + ok(res.ok) proc createRLNInstance*(): RLNResult = - ## Wraps the rln instance creation for metrics - ## Returns an error if the instance creation fails + ## Wraps createRLNInstanceLocal with metrics timing. var res: RLNResult waku_rln_instance_creation_duration_seconds.nanosecondTime: res = createRLNInstanceLocal() return res -proc poseidon*(data: seq[seq[byte]]): RlnRelayResult[array[32, byte]] = - ## a thin layer on top of the Nim wrapper of the poseidon hasher - var inputBytes = serialize(data) - var - hashInputBuffer = inputBytes.toBuffer() - outputBuffer: Buffer # will holds the hash output - - let hashSuccess = poseidon(addr hashInputBuffer, addr outputBuffer, true) - - # check whether the hash call is done successfully - if not hashSuccess: - return err("error in poseidon hash") - - let output = cast[ptr array[32, byte]](outputBuffer.`ptr`)[] - - return ok(output) +proc poseidon*(left, right: seq[byte]): RlnRelayResult[array[32, byte]] = + ## Poseidon hash of exactly 2 inputs; zerokit v2 FFI only exposes the pair variant. + poseidonPairLe(left, right) proc toLeaf*(rateCommitment: RateCommitment): RlnRelayResult[seq[byte]] = let idCommitment = rateCommitment.idCommitment @@ -147,7 +156,7 @@ proc toLeaf*(rateCommitment: RateCommitment): RlnRelayResult[seq[byte]] = return err( "could not convert the user message limit to bytes: " & getCurrentExceptionMsg() ) - let leaf = poseidon(@[@idCommitment, @userMessageLimit]).valueOr: + let leaf = poseidon(@idCommitment, @userMessageLimit).valueOr: return err("could not convert the rate commitment to a leaf") var retLeaf = newSeq[byte](leaf.len) for i in 0 ..< leaf.len: @@ -165,11 +174,24 @@ proc toLeaves*(rateCommitments: seq[RateCommitment]): RlnRelayResult[seq[seq[byt proc generateExternalNullifier*( epoch: Epoch, rlnIdentifier: RlnIdentifier ): RlnRelayResult[ExternalNullifier] = - let epochHash = keccak.keccak256.digest(@(epoch)) - let rlnIdentifierHash = keccak.keccak256.digest(@(rlnIdentifier)) - let externalNullifier = poseidon(@[@(epochHash), @(rlnIdentifierHash)]).valueOr: - return err("Failed to compute external nullifier: " & error) - return ok(externalNullifier) + ## externalNullifier = Poseidon(H(epoch), H(rlnIdentifier)); H = ffi_hash_to_field_le. + let epochFr = hashToFieldLe(@epoch).valueOr: + return err("Failed to hash epoch to field: " & error) + defer: + ffi_cfr_free(epochFr) + let rlnIdFr = hashToFieldLe(@rlnIdentifier).valueOr: + return err("Failed to hash rlnIdentifier to field: " & error) + defer: + ffi_cfr_free(rlnIdFr) + let cfr = ffi_poseidon_hash_pair(epochFr, rlnIdFr) + if cfr.isNil(): + return err("Failed to compute external nullifier") + defer: + ffi_cfr_free(cfr) + cfrToBytesLe(cfr).mapErr( + proc(e: string): string = + "Failed to serialize external nullifier: " & e + ) proc extractMetadata*(proof: RateLimitProof): RlnRelayResult[ProofMetadata] = let externalNullifier = generateExternalNullifier(proof.epoch, proof.rlnIdentifier).valueOr: @@ -182,3 +204,178 @@ proc extractMetadata*(proof: RateLimitProof): RlnRelayResult[ProofMetadata] = externalNullifier: externalNullifier, ) ) + +proc buildPathElementsVec( + pathElements: seq[byte], depth: int +): RlnRelayResult[Vec_CFr] = + ## Caller MUST ffi_vec_cfr_free the returned Vec_CFr. + var vec = ffi_vec_cfr_new(csize_t(depth)) + for i in 0 ..< depth: + let start = i * FieldElementSize + let element = bytesToCfrLe( + pathElements.toOpenArray(start, start + FieldElementSize - 1) + ).valueOr: + ffi_vec_cfr_free(vec) + return err( + "failed call to bytesToCfrLe (path element) in buildPathElementsVec: " & error + ) + ffi_vec_cfr_push(addr vec, element) + ffi_cfr_free(element) + ok(vec) + +proc buildWitnessInput( + witness: RLNWitnessInput +): RlnRelayResult[ptr FFI_RLNWitnessInput] = + ## ffi_rln_witness_input_new copies all inputs, so the intermediate CFrs/vecs + ## are freed here. Caller MUST ffi_rln_witness_input_free the returned handle. + let depth = witness.identity_path_index.len + if witness.path_elements.len != depth * FieldElementSize: + return err( + "Invalid Merkle path: expected " & $(depth * FieldElementSize) & " bytes for " & + $depth & " levels, got " & $witness.path_elements.len + ) + + var pathElementsVec = buildPathElementsVec(witness.path_elements, depth).valueOr: + return err("failed call to buildPathElementsVec in buildWitnessInput: " & error) + defer: + ffi_vec_cfr_free(pathElementsVec) + + var pathIndexVec = toVecUint8(witness.identity_path_index) + + let identitySecret = bytesToCfrLe(witness.identity_secret).valueOr: + return err( + "failed call to bytesToCfrLe (identity_secret) in buildWitnessInput: " & error + ) + defer: + ffi_cfr_free(identitySecret) + let userLimit = bytesToCfrLe(witness.user_message_limit).valueOr: + return err( + "failed call to bytesToCfrLe (user_message_limit) in buildWitnessInput: " & error + ) + defer: + ffi_cfr_free(userLimit) + let messageIdFr = bytesToCfrLe(witness.message_id).valueOr: + return + err("failed call to bytesToCfrLe (message_id) in buildWitnessInput: " & error) + defer: + ffi_cfr_free(messageIdFr) + let xFr = bytesToCfrLe(witness.x).valueOr: + return err("failed call to bytesToCfrLe (x) in buildWitnessInput: " & error) + defer: + ffi_cfr_free(xFr) + let externalNullifierFr = bytesToCfrLe(witness.external_nullifier).valueOr: + return err( + "failed call to bytesToCfrLe (external_nullifier) in buildWitnessInput: " & error + ) + defer: + ffi_cfr_free(externalNullifierFr) + + let witnessRes = ffi_rln_witness_input_new( + identitySecret, + userLimit, + messageIdFr, + addr pathElementsVec, + addr pathIndexVec, + xFr, + externalNullifierFr, + ) + if witnessRes.ok.isNil(): + return err( + consumeError("Failed to create witness in buildWitnessInput: ", witnessRes.err) + ) + return ok(witnessRes.ok) + +proc generateRlnProofWithWitness*( + rlnInstance: ptr RLN, + witness: RLNWitnessInput, + epoch: Epoch, + rlnIdentifier: RlnIdentifier, +): RlnRelayResult[RateLimitProof] = + let witnessHandle = buildWitnessInput(witness).valueOr: + return + err("failed call to buildWitnessInput in generateRlnProofWithWitness: " & error) + defer: + ffi_rln_witness_input_free(witnessHandle) + + var ctx = rlnInstance + var wh = witnessHandle + let proofRes = ffi_generate_rln_proof(addr ctx, addr wh) + if proofRes.ok.isNil(): + return err(consumeError("Failed to generate RLN proof: ", proofRes.err)) + defer: + ffi_rln_proof_free(proofRes.ok) + + return proofPtrToRateLimitProof(proofRes.ok, epoch, rlnIdentifier) + +proc buildRlnProof( + proof: RateLimitProof, externalNullifier: ExternalNullifier +): RlnRelayResult[ptr FFI_RLNProof] = + ## ffi_rln_proof_new copies all inputs, so the intermediate CFrs are freed + ## here. Caller MUST ffi_rln_proof_free the returned handle. + var groth16Vec = toVecUint8(proof.proof) + let rootFr = bytesToCfrLe(proof.merkleRoot).valueOr: + return err("failed call to bytesToCfrLe (root) in buildRlnProof: " & error) + defer: + ffi_cfr_free(rootFr) + let extNullFr = bytesToCfrLe(externalNullifier).valueOr: + return + err("failed call to bytesToCfrLe (externalNullifier) in buildRlnProof: " & error) + defer: + ffi_cfr_free(extNullFr) + let shareXFr = bytesToCfrLe(proof.shareX).valueOr: + return err("failed call to bytesToCfrLe (shareX) in buildRlnProof: " & error) + defer: + ffi_cfr_free(shareXFr) + let shareYFr = bytesToCfrLe(proof.shareY).valueOr: + return err("failed call to bytesToCfrLe (shareY) in buildRlnProof: " & error) + defer: + ffi_cfr_free(shareYFr) + let nullifierFr = bytesToCfrLe(proof.nullifier).valueOr: + return err("failed call to bytesToCfrLe (nullifier) in buildRlnProof: " & error) + defer: + ffi_cfr_free(nullifierFr) + + let proofRes = ffi_rln_proof_new( + addr groth16Vec, rootFr, extNullFr, shareXFr, shareYFr, nullifierFr + ) + if proofRes.ok.isNil(): + return + err(consumeError("Failed to build RLN proof in buildRlnProof: ", proofRes.err)) + return ok(proofRes.ok) + +proc verifyRlnProof*( + rlnInstance: ptr RLN, + proof: RateLimitProof, + signal: openArray[byte], + validRoots: seq[MerkleNode], +): RlnRelayResult[bool] = + if validRoots.len == 0: + return err("verifyRlnProof requires at least one valid root (stateless mode)") + + # externalNullifier isn't a protobuf wire field, so a received proof has it + # zeroed; recompute from epoch + rlnIdentifier. + let externalNullifier = generateExternalNullifier(proof.epoch, proof.rlnIdentifier).valueOr: + return err("failed call to generateExternalNullifier in verifyRlnProof: " & error) + + let proofHandlePtr = buildRlnProof(proof, externalNullifier).valueOr: + return err("failed call to buildRlnProof in verifyRlnProof: " & error) + defer: + ffi_rln_proof_free(proofHandlePtr) + + let xFr = hashToFieldLe(signal).valueOr: + return err("failed call to hashToFieldLe (signal) in verifyRlnProof: " & error) + defer: + ffi_cfr_free(xFr) + + var roots = toRootVec(validRoots).valueOr: + return err("failed call to toRootVec in verifyRlnProof: " & error) + defer: + ffi_vec_cfr_free(roots) + + var ctx = rlnInstance + var proofHandle = proofHandlePtr + let verifyRes = ffi_verify_with_roots(addr ctx, addr proofHandle, addr roots, xFr) + # zerokit FFI quirk: err is non-nil for all failures; free it and return the bool. + if hasError(verifyRes.err): + ffi_c_string_free(verifyRes.err) + return ok(verifyRes.ok) From 29a77dcf4d7bbabf3fc80b8fd5abc7b8af68246d Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Thu, 21 May 2026 18:29:33 +0100 Subject: [PATCH 152/155] feat: add logos.test fleet preset (#3900) Co-authored-by: Claude Opus 4.7 (1M context) --- tests/api/test_node_conf.nim | 16 ++++++++++++++++ tools/confutils/cli_args.nim | 4 +++- waku/factory/networks_config.nim | 29 +++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index e171c5207..8798c5cc5 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -219,6 +219,22 @@ suite "WakuNodeConf - preset integration": check: wakuConf.clusterId == 2 + test "LogosTest preset applies LogosTestConf": + ## Given + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.preset = "logostest" + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.clusterId == 2 + test "Invalid preset returns error": ## Given var conf = defaultWakuNodeConf().valueOr: diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 183de3b80..f965c3a06 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -165,7 +165,7 @@ type WakuNodeConf* = object preset* {. desc: - "Network preset to use. 'twn' is The RLN-protected Waku Network (cluster 1). 'logos.dev' is the Logos Dev Network (cluster 2). Overrides other values.", + "Network preset to use. 'twn' is The RLN-protected Waku Network (cluster 1). 'logos.dev' is the Logos Dev Network (cluster 2). 'logos.test' is the Logos Test Network (cluster 2). Overrides other values.", defaultValue: "", name: "preset" .}: string @@ -947,6 +947,8 @@ proc toNetworkConf( ok(some(NetworkConf.TheWakuNetworkConf())) of "logos.dev", "logosdev": ok(some(NetworkConf.LogosDevConf())) + of "logos.test", "logostest": + ok(some(NetworkConf.LogosTestConf())) else: err("Invalid --preset value passed: " & lcPreset) diff --git a/waku/factory/networks_config.nim b/waku/factory/networks_config.nim index d9c0cf879..488f58464 100644 --- a/waku/factory/networks_config.nim +++ b/waku/factory/networks_config.nim @@ -92,6 +92,35 @@ proc LogosDevConf*(T: type NetworkConf): NetworkConf = ], ) +# cluster-id=2 (Logos Test Network) +# Cluster configuration for the Logos Test Network. +proc LogosTestConf*(T: type NetworkConf): NetworkConf = + const ZeroChainId = 0'u256 + return NetworkConf( + maxMessageSize: "150KiB", + clusterId: 2, + rlnRelay: false, + rlnRelayEthContractAddress: "", + rlnRelayDynamic: false, + rlnRelayChainId: ZeroChainId, + rlnEpochSizeSec: 0, + rlnRelayUserMessageLimit: 0, + shardingConf: ShardingConf(kind: AutoSharding, numShardsInCluster: 8), + enableKadDiscovery: true, + mix: true, + p2pReliability: true, + discv5Discovery: true, + discv5BootstrapNodes: @[], + entryNodes: @[ + "/dns4/node-01.do-ams3.logos.test.status.im/tcp/30303/p2p/16Uiu2HAmQ9X2xDfPG3uL77V9piYDhjq14JhKCtcmNYsTMKNqrKCj", + "/dns4/node-02.do-ams3.logos.test.status.im/tcp/30303/p2p/16Uiu2HAmB8NYprrfQrgWVzsJtYWkfjsXbmJEGNMG6othXsQ53BwG", + "/dns4/node-01.gc-us-central1-a.logos.test.status.im/tcp/30303/p2p/16Uiu2HAmF8WtwGPmeGHgYAX2277jHgy5cW9F7zsB8EqUjBZQAZQ3", + "/dns4/node-02.gc-us-central1-a.logos.test.status.im/tcp/30303/p2p/16Uiu2HAmUuXhUW9bdJpzN1kfDziFiUZo4bszTk66cvr7uuyCHXR7", + "/dns4/node-01.ac-cn-hongkong-c.logos.test.status.im/tcp/30303/p2p/16Uiu2HAmL3oU95jh1BZHozn3uNhx8HEneirgr8M1jEAapzXGDqRF", + "/dns4/node-02.ac-cn-hongkong-c.logos.test.status.im/tcp/30303/p2p/16Uiu2HAm28CoBZjpyxsanC8tQpbvZ7bZJnVYuB1EgFzb571qpWsV", + ], + ) + proc validateShards*( shardingConf: ShardingConf, shards: seq[uint16] ): Result[void, string] = From 5ff734aa569cdd1a5b37102f18bf7130ce58fb25 Mon Sep 17 00:00:00 2001 From: Jakub Date: Thu, 21 May 2026 22:46:10 +0200 Subject: [PATCH 153/155] chore: simplify zerokit cargoHash fix (#3899) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replacing the whole package is unnecessary. Just hash can be fixed. Signed-off-by: Jakub Sokołowski --- flake.nix | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/flake.nix b/flake.nix index 8012fc970..5701d97a3 100644 --- a/flake.nix +++ b/flake.nix @@ -74,35 +74,14 @@ let pkgs = pkgsFor system; - # zerokit's nix/default.nix hardcodes a cargoHash that is stale for - # our pinned nixpkgs on a cold runner (the status.im substituter is - # untrusted here, so the cargo-vendor FOD is recomputed). v2.0.2 did - # NOT fix this for consumers — its committed hash is the old v2.0.1 - # value while v2.0.2's Cargo.lock changed. Rebuild librln here from - # the pinned zerokit source with the correct cargoHash. Keep the - # version + cargoHash in sync with the zerokit input rev. - rustToolchain = pkgs.rust-bin.stable.latest.default; - zerokitRln = pkgs.rustPlatform.buildRustPackage { - pname = "zerokit"; - version = "2.0.2"; - src = zerokit; - cargo = rustToolchain; - rustc = rustToolchain; - cargoHash = "sha256-PNwEdZLgGQPqQDrEK2hsQtSybVfBbD6xn4K47fPFJUU="; - nativeBuildInputs = [ pkgs.rust-cbindgen ]; - doCheck = false; - buildPhase = '' - export CARGO_HOME=$TMPDIR/cargo - cargo build --lib --release --manifest-path rln/Cargo.toml - ''; - installPhase = '' - set -eu - mkdir -p $out/lib $out/include - find target -type f -name 'librln.*' -not -path '*/deps/*' \ - -exec cp -v '{}' "$out/lib/" \; - cbindgen ./rln -l c > "$out/include/rln.h" - ''; - }; + # HACK: Fix for stale cargoHash in 2.0.2 release. + zerokitRln = zerokit.packages.${system}.rln.overrideAttrs (old: { + cargoDeps = old.cargoDeps.overrideAttrs (oldCargoDeps: { + vendorStaging = oldCargoDeps.vendorStaging.overrideAttrs (_: { + outputHash = "sha256-PNwEdZLgGQPqQDrEK2hsQtSybVfBbD6xn4K47fPFJUU="; + }); + }); + }); liblogosdelivery = pkgs.callPackage ./nix/default.nix { inherit pkgs; From bdd562ecc6ceac739f7cbcbc34d0e84d8e9e0952 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Fri, 22 May 2026 11:51:52 +0100 Subject: [PATCH 154/155] feat(nix): expose cargoHash-corrected librln as packages.rln (#3902) PR #3899 fixes zerokit v2.0.2's stale cargoHash, but only via an internal `let` binding consumed by liblogosdelivery. Downstream consumers (e.g. logos-delivery-module) that need librln still pull zerokit's rln package directly and hit the stale hash. Expose that corrected derivation as `packages..rln` so consumers can bundle the exact same librln this build links, instead of overriding the cargoHash themselves. Co-authored-by: Claude Opus 4.7 (1M context) --- flake.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flake.nix b/flake.nix index 5701d97a3..077668b9a 100644 --- a/flake.nix +++ b/flake.nix @@ -91,6 +91,11 @@ }; in { inherit liblogosdelivery; + # Expose the cargoHash-corrected librln so downstream consumers + # (e.g. logos-delivery-module) bundle the exact same librln this + # build links, instead of pulling zerokit's rln directly — whose + # committed cargoHash is stale for v2.0.2 (see zerokitRln above). + rln = zerokitRln; default = liblogosdelivery; } ); From 04ef12ccf36b11c7208aac672dfb7b3d051f49f9 Mon Sep 17 00:00:00 2001 From: Tanya S <120410716+stubbsta@users.noreply.github.com> Date: Fri, 22 May 2026 13:23:58 +0200 Subject: [PATCH 155/155] Update and Improve READMEs (#3894) * Update READMEs for new nimble build system * Move recommended nim versions out of prerequisites --- README.md | 42 +++++++++++++++++++++--------------------- waku/README.md | 18 ++++++++---------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 8833ae131..f227ea483 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ This repository implements a set of libp2p protocols aimed to bring private communications. -- Nim implementation of [these specs](https://github.com/vacp2p/rfc-index/tree/main/waku). +- Nim implementation of [these specs](https://github.com/logos-co/logos-lips/tree/master/docs/messaging). - C library that exposes the implemented protocols. -- CLI application that allows you to run an lmn node. +- CLI application that allows you to run a logos-delivery node. - Examples. - Various tests of above. @@ -17,15 +17,20 @@ For more details see the [source code](waku/README.md) These instructions are generic. For more detailed instructions, see the source code above. +Recommended and tested toolchain versions (these are installed when you follow the build instructions below): +- Nim 2.2.4 +- Nimble 0.22.3 + ### Prerequisites -The standard developer tools, including a C compiler, GNU Make, Bash, and Git. More information on these installations can be found [here](https://docs.waku.org/guides/nwaku/build-source#install-dependencies). +The standard developer tools, including a C compiler, GNU Make, Bash, and Git. > In some distributions (Fedora linux for example), you may need to install `which` utility separately. Nimbus build system is relying on it. You'll also need an installation of Rust and its toolchain (specifically `rustc` and `cargo`). The easiest way to install these, is using `rustup`: +Rust: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` @@ -33,8 +38,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ### Wakunode ```bash -# The first `make` invocation will update all Git submodules. -# You'll run `make update` after each `git pull` in the future to keep those submodules updated. +# The first `make` invocation will initialize the local dependency state. make wakunode2 # Build with custom compilation flags. Do not use NIM_PARAMS unless you know what you are doing. @@ -48,12 +52,12 @@ make wakunode2 NIMFLAGS="-d:chronicles_colors:none -d:disableMarchNative" ./build/wakunode2 --help ``` To join the network, you need to know the address of at least one bootstrap node. -Please refer to the [Waku README](https://github.com/waku-org/nwaku/blob/master/waku/README.md) for more information. +Please refer to the [Waku README](https://github.com/logos-messaging/logos-delivery/blob/master/waku/README.md) for more information. For more on how to run `wakunode2`, refer to: -- [Run using binaries](https://docs.waku.org/guides/nwaku/build-source) -- [Run using docker](https://docs.waku.org/guides/nwaku/run-docker) -- [Run using docker-compose](https://docs.waku.org/guides/nwaku/run-docker-compose) +- [Run using binaries](https://docs.waku.org/run-node/build-source) +- [Run using docker](https://docs.waku.org/run-node/run-docker) +- [Run using docker-compose](https://docs.waku.org/run-node/run-docker-compose) #### Issues ##### WSL @@ -104,13 +108,9 @@ If `wakunode2.exe` isn't generated: This repository is bundled with a Nim runtime that includes the necessary dependencies for the project. Before you can utilize the runtime you'll need to build the project, as detailed in a previous section. -This will generate a `vendor` directory containing various dependencies, including the `nimbus-build-system` which has the bundled nim runtime. +This will generate a `nimbledeps/pkgs2` directory containing various dependencies. -After successfully building the project, you may bring the bundled runtime into scope by running: -```bash -source env.sh -``` -If everything went well, you should see your prompt suffixed with `[Nimbus env]$`. Now you can run `nim` commands as usual. +If everything went well, you should see your prompt suffixed with `[SuccessX]`. Now you can run `nim` commands as usual. ### Test Suite @@ -144,7 +144,7 @@ make test/tests/common/test_enr_builder.nim ``` ### Testing against `js-waku` -Refer to [js-waku repo](https://github.com/waku-org/js-waku/tree/master/packages/tests) for instructions. +Refer to [logos-delivery-js repo](https://github.com/logos-messaging/logos-delivery-js/tree/master/packages/tests) for instructions. ## Formatting @@ -175,14 +175,14 @@ Different tools and their corresponding how-to guides can be found in the `tools ### Bugs, Questions & Features -For an inquiry, or if you would like to propose new features, feel free to [open a general issue](https://github.com/waku-org/nwaku/issues/new). +For an inquiry, or if you would like to propose new features, feel free to [open a general issue](https://github.com/logos-messaging/logos-delivery/issues/new). -For bug reports, please [tag your issue with the `bug` label](https://github.com/waku-org/nwaku/issues/new). +For bug reports, please [tag your issue with the `bug` label](https://github.com/logos-messaging/logos-delivery/issues/new). -If you believe the reported issue requires critical attention, please [use the `critical` label](https://github.com/waku-org/nwaku/issues/new?labels=critical,bug) to assist with triaging. +If you believe the reported issue requires critical attention, please [use the `critical` label](https://github.com/logos-messaging/logos-delivery/issues/new?labels=critical,bug) to assist with triaging. -To get help, or participate in the conversation, join the [Waku Discord](https://discord.waku.org/) server. +To get help, or participate in the conversation, join the [Logos Discord](https://discord.gg/logosnetwork) server. ### Docs -* [REST API Documentation](https://waku-org.github.io/waku-rest-api/) +* [REST API Documentation](https://logos-messaging.github.io/logos-delivery-rest-api/) \ No newline at end of file diff --git a/waku/README.md b/waku/README.md index ed3887a09..d9f160cb5 100644 --- a/waku/README.md +++ b/waku/README.md @@ -45,15 +45,16 @@ Setting up a `wakunode2` on the smallest [digital ocean](https://docs.digitaloce make test ``` -To run a specific test. +To run a specific test file or test case: ```bash -# Get a shell with the right environment variables set -./env.sh bash -# Run a specific test -nim c -r ./tests/test_waku_filter_legacy.nim +# Run all tests in a specific file +make test tests/waku_filter_v2/test_waku_filter.nim + +# Run a specific test case within a file +make test tests/waku_filter_v2/test_waku_filter.nim "specific test name" ``` -You can also alter compile options. For example, if you want a less verbose output you can do the following. For more, refer to the [compiler flags](https://nim-lang.org/docs/nimc.html#compiler-usage) and [chronicles documentation](https://github.com/status-im/nim-chronicles#compile-time-configuration). +Alternatively, you can invoke the Nim compiler directly. For more on available flags, refer to the [compiler flags](https://nim-lang.org/docs/nimc.html#compiler-usage) and [chronicles documentation](https://github.com/status-im/nim-chronicles#compile-time-configuration). ```bash nim c -r -d:chronicles_log_level=WARN --verbosity=0 --hints=off ./tests/waku_filter_v2/test_waku_filter.nim @@ -231,7 +232,4 @@ However, they can be used for local testing purposes: mkdir -p ./ssl_dir/ openssl req -x509 -newkey rsa:4096 -keyout ./ssl_dir/key.pem -out ./ssl_dir/cert.pem -sha256 -nodes wakunode2 --websocket-secure-support=true --websocket-secure-key-path="./ssl_dir/key.pem" --websocket-secure-cert-path="./ssl_dir/cert.pem" -``` - - - +``` \ No newline at end of file

_gN@u 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 OUH9b49e}-tEfu9RH*{E#fa7nNxD+!ba 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