merge roots and merkle cache syncing

This commit is contained in:
stubbsta 2026-06-05 15:31:33 +02:00
commit ffde39f8b1
No known key found for this signature in database
77 changed files with 1904 additions and 1327 deletions

View File

@ -89,12 +89,15 @@ jobs:
path: | path: |
nimbledeps/ nimbledeps/
nimble.paths nimble.paths
key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} key: ${{ runner.os }}-nimbledeps-v2-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }}
- name: Install nimble deps - name: Install nimble deps
if: steps.cache-nimbledeps.outputs.cache-hit != 'true' if: steps.cache-nimbledeps.outputs.cache-hit != 'true'
run: | run: |
nimble setup --localdeps -y # nim's source tree checksums differently across platforms, so its
# locked checksum is unreliable. --useSystemNim uses the CI nim and
# skips that check, while still verifying every other locked dep.
nimble setup --localdeps -y --useSystemNim
make rebuild-nat-libs-nimbledeps make rebuild-nat-libs-nimbledeps
make rebuild-bearssl-nimbledeps make rebuild-bearssl-nimbledeps
touch nimbledeps/.nimble-setup touch nimbledeps/.nimble-setup
@ -142,12 +145,15 @@ jobs:
path: | path: |
nimbledeps/ nimbledeps/
nimble.paths nimble.paths
key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} key: ${{ runner.os }}-nimbledeps-v2-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }}
- name: Install nimble deps - name: Install nimble deps
if: steps.cache-nimbledeps.outputs.cache-hit != 'true' if: steps.cache-nimbledeps.outputs.cache-hit != 'true'
run: | run: |
nimble setup --localdeps -y # nim's source tree checksums differently across platforms, so its
# locked checksum is unreliable. --useSystemNim uses the CI nim and
# skips that check, while still verifying every other locked dep.
nimble setup --localdeps -y --useSystemNim
make rebuild-nat-libs-nimbledeps make rebuild-nat-libs-nimbledeps
make rebuild-bearssl-nimbledeps make rebuild-bearssl-nimbledeps
touch nimbledeps/.nimble-setup touch nimbledeps/.nimble-setup
@ -207,12 +213,15 @@ jobs:
path: | path: |
nimbledeps/ nimbledeps/
nimble.paths nimble.paths
key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} key: ${{ runner.os }}-nimbledeps-v2-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }}
- name: Install nimble deps - name: Install nimble deps
if: steps.cache-nimbledeps.outputs.cache-hit != 'true' if: steps.cache-nimbledeps.outputs.cache-hit != 'true'
run: | run: |
nimble setup --localdeps -y # nim's source tree checksums differently across platforms, so its
# locked checksum is unreliable. --useSystemNim uses the CI nim and
# skips that check, while still verifying every other locked dep.
nimble setup --localdeps -y --useSystemNim
make rebuild-nat-libs-nimbledeps make rebuild-nat-libs-nimbledeps
make rebuild-bearssl-nimbledeps make rebuild-bearssl-nimbledeps
touch nimbledeps/.nimble-setup touch nimbledeps/.nimble-setup

View File

@ -15,6 +15,10 @@ env:
NPROC: 2 NPROC: 2
MAKEFLAGS: "-j${NPROC}" MAKEFLAGS: "-j${NPROC}"
NIMFLAGS: "--parallelBuild:${NPROC}" NIMFLAGS: "--parallelBuild:${NPROC}"
# waku.nimble reads compile flags from NIM_PARAMS, not NIMFLAGS. Without
# -d:disableMarchNative here, config.nims applies -march=native and
# secp256k1 fails to compile.
NIM_PARAMS: "-d:disableMarchNative"
NIM_VERSION: '2.2.4' NIM_VERSION: '2.2.4'
NIMBLE_VERSION: '0.22.3' NIMBLE_VERSION: '0.22.3'

View File

@ -24,6 +24,16 @@ jobs:
MSYSTEM: MINGW64 MSYSTEM: MINGW64
steps: steps:
- name: Configure Git to keep LF line endings
# Windows Git defaults to core.autocrlf=true. The LF→CRLF conversion
# changes the SHA1 of nimble's cloned deps, so they no longer match
# nimble.lock and nimble re-downloads them on every run (hanging the
# job). Disable autocrlf so clones match the Linux-computed checksums.
shell: pwsh
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -52,6 +62,14 @@ jobs:
mingw-w64-x86_64-clang mingw-w64-x86_64-clang
mingw-w64-x86_64-nasm mingw-w64-x86_64-nasm
- name: Configure Git in MSYS2 to keep LF line endings
# The step above only configures Git for Windows. nimble clones its deps
# from the MSYS2 shell, whose git reads a separate global config, so the
# same setting must be repeated here.
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Manually install nasm - name: Manually install nasm
run: | run: |
bash scripts/install_nasm_in_windows.sh bash scripts/install_nasm_in_windows.sh
@ -80,19 +98,16 @@ jobs:
cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y
echo "$HOME/.nimble/bin" >> $GITHUB_PATH echo "$HOME/.nimble/bin" >> $GITHUB_PATH
- name: Patch nimble.lock for Windows nim checksum
# nimble.exe uses Windows Git (core.autocrlf=true by default), which converts LF→CRLF
# on checkout. This changes the SHA1 of the nim package source tree relative to the
# Linux-computed checksum stored in nimble.lock. Patch the lock file with the
# Windows-computed checksum before nimble reads it.
run: |
sed -i 's/68bb85cbfb1832ce4db43943911b046c3af3caab/a092a045d3a427d127a5334a6e59c76faff54686/g' nimble.lock
- name: Install nimble deps - name: Install nimble deps
if: steps.cache-nimbledeps.outputs.cache-hit != 'true' if: steps.cache-nimbledeps.outputs.cache-hit != 'true'
run: | run: |
export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH" export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH"
nimble setup --localdeps -y # nim's source tree checks out differently per platform (its own
# .gitattributes forces line endings), so its locked checksum never
# matches on Windows — even with autocrlf disabled. --useSystemNim
# uses the CI-installed nim and skips that check, while still
# verifying every other locked dependency.
nimble setup --localdeps -y --useSystemNim
make rebuild-nat-libs-nimbledeps CC=gcc make rebuild-nat-libs-nimbledeps CC=gcc
make rebuild-bearssl-nimbledeps CC=gcc make rebuild-bearssl-nimbledeps CC=gcc
touch nimbledeps/.nimble-setup touch nimbledeps/.nimble-setup

View File

@ -24,7 +24,9 @@ export PATH := $(HOME)/.nimble/bin:$(PATH)
# NIM binary location # NIM binary location
NIM_BINARY := $(shell which nim 2>/dev/null) NIM_BINARY := $(shell which nim 2>/dev/null)
NPH := $(HOME)/.nimble/bin/nph NPH := $(HOME)/.nimble/bin/nph
NIMBLE := $(HOME)/.nimble/bin/nimble # Resolve nimble via PATH (Windows has no $(HOME)/.nimble/bin); --useSystemNim
# reuses the nim on PATH so nimble never re-clones the locked nim.
NIMBLE := nimble --useSystemNim
NIMBLEDEPS_STAMP := nimbledeps/.nimble-setup NIMBLEDEPS_STAMP := nimbledeps/.nimble-setup
# Compilation parameters # Compilation parameters
@ -204,7 +206,7 @@ clean: | clean-librln
testcommon: | build-deps build testcommon: | build-deps build
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble testcommon $(NIMBLE) testcommon
########## ##########
## Waku ## ## Waku ##
@ -213,47 +215,54 @@ testcommon: | build-deps build
testwaku: | build-deps build rln-deps librln testwaku: | build-deps build rln-deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble test $(NIMBLE) test
# Windows: build with nim directly — `nimble <task>` re-clones git deps every
# build and they intermittently hang on the MSYS2 runner. Flags mirror waku.nimble.
wakunode2: | build-deps build deps librln wakunode2: | build-deps build deps librln
ifeq ($(detected_OS),Windows)
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble wakunode2 nim c --out:build/wakunode2 --mm:refc --cpu:amd64 $(NIM_PARAMS) -d:chronicles_log_level=TRACE apps/wakunode2/wakunode2.nim
else
echo -e $(BUILD_MSG) "build/$@" && \
$(NIMBLE) wakunode2
endif
benchmarks: | build-deps build deps librln benchmarks: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble benchmarks $(NIMBLE) benchmarks
testwakunode2: | build-deps build deps librln testwakunode2: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble testwakunode2 $(NIMBLE) testwakunode2
example2: | build-deps build deps librln example2: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble example2 $(NIMBLE) example2
chat2: | build-deps build deps librln chat2: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble chat2 $(NIMBLE) chat2
chat2mix: | build-deps build deps librln chat2mix: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble chat2mix $(NIMBLE) chat2mix
rln-db-inspector: | build-deps build deps librln rln-db-inspector: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble rln_db_inspector $(NIMBLE) rln_db_inspector
chat2bridge: | build-deps build deps librln chat2bridge: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble chat2bridge $(NIMBLE) chat2bridge
liteprotocoltester: | build-deps build deps librln liteprotocoltester: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble liteprotocoltester $(NIMBLE) liteprotocoltester
lightpushwithmix: | build-deps build deps librln lightpushwithmix: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble lightpushwithmix $(NIMBLE) lightpushwithmix
api_example: | build-deps build deps librln api_example: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
@ -261,12 +270,12 @@ api_example: | build-deps build deps librln
build/%: | build-deps build deps librln build/%: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$*" && \ echo -e $(BUILD_MSG) "build/$*" && \
nimble buildone $* $(NIMBLE) buildone $*
compile-test: | build-deps build deps librln compile-test: | build-deps build deps librln
echo -e $(BUILD_MSG) "$(TEST_FILE)" "\"$(TEST_NAME)\"" && \ echo -e $(BUILD_MSG) "$(TEST_FILE)" "\"$(TEST_NAME)\"" && \
nimble buildTest $(TEST_FILE) && \ $(NIMBLE) buildTest $(TEST_FILE) && \
nimble execTest $(TEST_FILE) "\"$(TEST_NAME)\"" $(NIMBLE) execTest $(TEST_FILE) "\"$(TEST_NAME)\""
################ ################
## Waku tools ## ## Waku tools ##
@ -277,11 +286,11 @@ tools: networkmonitor wakucanary
wakucanary: | build-deps build deps librln wakucanary: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble wakucanary $(NIMBLE) wakucanary
networkmonitor: | build-deps build deps librln networkmonitor: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble networkmonitor $(NIMBLE) networkmonitor
############ ############
## Format ## ## Format ##
@ -327,7 +336,7 @@ clean:
docs: | build deps docs: | build deps
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
nimble 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: coverage:
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
@ -423,11 +432,16 @@ else ifeq ($(detected_OS),Linux)
BUILD_COMMAND := $(BUILD_COMMAND)Linux BUILD_COMMAND := $(BUILD_COMMAND)Linux
endif endif
# Windows: build with nim directly (see wakunode2). Flags mirror waku.nimble.
libwaku: | build-deps librln libwaku: | build-deps librln
nimble --verbose libwaku$(BUILD_COMMAND) waku.nimble ifeq ($(detected_OS),Windows)
nim c --out:build/libwaku.dll --threads:on --app:lib --opt:speed --noMain --mm:refc --header -d:metrics --nimMainPrefix:libwaku --skipParentCfg:off -d:discv5_protocol_id=d5waku --cpu:amd64 $(NIM_PARAMS) library/libwaku.nim
else
$(NIMBLE) --verbose libwaku$(BUILD_COMMAND) waku.nimble
endif
liblogosdelivery: | build-deps librln liblogosdelivery: | build-deps librln
nimble --verbose liblogosdelivery$(BUILD_COMMAND) waku.nimble $(NIMBLE) --verbose liblogosdelivery$(BUILD_COMMAND) waku.nimble
logosdelivery_example: | build liblogosdelivery logosdelivery_example: | build liblogosdelivery
@echo -e $(BUILD_MSG) "build/$@" @echo -e $(BUILD_MSG) "build/$@"
@ -502,7 +516,7 @@ endif
build-libwaku-for-android-arch: build-libwaku-for-android-arch:
ifneq ($(findstring /nix/store,$(LIBRLN_FILE)),) ifneq ($(findstring /nix/store,$(LIBRLN_FILE)),)
mkdir -p $(CURDIR)/build/android/$(ABIDIR)/ mkdir -p $(CURDIR)/build/android/$(ABIDIR)/
CPU=$(CPU) ABIDIR=$(ABIDIR) ANDROID_ARCH=$(ANDROID_ARCH) ANDROID_COMPILER=$(ANDROID_COMPILER) ANDROID_TOOLCHAIN_DIR=$(ANDROID_TOOLCHAIN_DIR) nimble libWakuAndroid CPU=$(CPU) ABIDIR=$(ABIDIR) ANDROID_ARCH=$(ANDROID_ARCH) ANDROID_COMPILER=$(ANDROID_COMPILER) ANDROID_TOOLCHAIN_DIR=$(ANDROID_TOOLCHAIN_DIR) $(NIMBLE) libWakuAndroid
else else
./scripts/build_rln_android.sh $(CURDIR)/build $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(CROSS_TARGET) $(ABIDIR) ./scripts/build_rln_android.sh $(CURDIR)/build $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(CROSS_TARGET) $(ABIDIR)
endif endif
@ -559,7 +573,7 @@ else
endif endif
build-libwaku-for-ios-arch: build-libwaku-for-ios-arch:
IOS_SDK=$(IOS_SDK) IOS_ARCH=$(IOS_ARCH) IOS_SDK_PATH=$(IOS_SDK_PATH) nimble libWakuIOS IOS_SDK=$(IOS_SDK) IOS_ARCH=$(IOS_ARCH) IOS_SDK_PATH=$(IOS_SDK_PATH) $(NIMBLE) libWakuIOS
libwaku-ios-device: IOS_ARCH=arm64 libwaku-ios-device: IOS_ARCH=arm64
libwaku-ios-device: IOS_SDK=iphoneos libwaku-ios-device: IOS_SDK=iphoneos

View File

@ -123,7 +123,7 @@ when isMainModule:
error "Waku initialization failed", error = error error "Waku initialization failed", error = error
quit(QuitFailure) quit(QuitFailure)
(waitFor startWaku(addr waku)).isOkOr: (waitFor waku.start()).isOkOr:
error "Starting waku failed", error = error error "Starting waku failed", error = error
quit(QuitFailure) quit(QuitFailure)

View File

@ -13,6 +13,7 @@ import
import import
./certsgenerator, ./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/net/net_config,
waku/waku_metadata/protocol, waku/waku_metadata/protocol,
waku/common/callbacks waku/common/callbacks

View File

@ -55,7 +55,7 @@ when isMainModule:
error "Waku initialization failed", error = error error "Waku initialization failed", error = error
quit(QuitFailure) quit(QuitFailure)
(waitFor startWaku(addr waku)).isOkOr: (waitFor waku.start()).isOkOr:
error "Starting waku failed", error = error error "Starting waku failed", error = error
quit(QuitFailure) quit(QuitFailure)

View File

@ -1,7 +1,7 @@
## Reliable Channel event types emitted to API consumers. ## Reliable Channel event types emitted to API consumers.
## ##
## Lifecycle events for individual segments (sent / propagated / errored) ## Lifecycle events for individual segments (sent / propagated / errored)
## are the same as the network-level ones the DeliveryService already ## are the same as the network-level ones the MessagingClient already
## emits — `requestId` is shared across layers — so we just re-export ## emits — `requestId` is shared across layers — so we just re-export
## `waku/events/message_events` and avoid declaring duplicates. ## `waku/events/message_events` and avoid declaring duplicates.
## ##

View File

@ -20,8 +20,7 @@ import bearssl/rand
import stew/byteutils import stew/byteutils
import libp2p/crypto/crypto as libp2p_crypto import libp2p/crypto/crypto as libp2p_crypto
import waku/api/api import waku/api/types
import waku/factory/waku as waku_factory
import waku/node/delivery_service/send_service import waku/node/delivery_service/send_service
import waku/waku_core/topics import waku/waku_core/topics
@ -32,7 +31,7 @@ import ./rate_limit_manager/rate_limit_manager
import ./encryption/encryption import ./encryption/encryption
export export
api, waku_factory, events, segmentation, scalable_data_sync, rate_limit_manager, types, send_service, events, segmentation, scalable_data_sync, rate_limit_manager,
encryption encryption
const LipWireReliableChannelVersion* = "RELIABLE-CHANNEL-API/1" const LipWireReliableChannelVersion* = "RELIABLE-CHANNEL-API/1"
@ -47,9 +46,10 @@ type
SendHandler* = proc(envelope: MessageEnvelope): Future[Result[RequestId, string]] {. SendHandler* = proc(envelope: MessageEnvelope): Future[Result[RequestId, string]] {.
async: (raises: [CatchableError]), gcsafe async: (raises: [CatchableError]), gcsafe
.} .}
## Egress dispatch boundary. Defaults to `waku.send`; tests inject a ## Egress dispatch boundary. Typically wraps `MessagingClient.send`;
## fake that records calls and returns canned `RequestId`s so the ## tests inject a fake that records calls and returns canned
## send state machine can be exercised end-to-end without a network. ## `RequestId`s so the send state machine can be exercised end-to-end
## without a network.
MessagePersistence {.pure.} = enum MessagePersistence {.pure.} = enum
Persistent Persistent
@ -264,20 +264,20 @@ proc onReadyToSend(
meta: LipWireReliableChannelVersion.toBytes(), meta: LipWireReliableChannelVersion.toBytes(),
) )
## `waku.send` is not annotated `(raises: [])`, but this listener is. ## `sendHandler` is not annotated `(raises: [])`, but this listener is.
## Convert any raise to a Result error so the state machine handles ## Convert any raise to a Result error so the state machine handles
## both failure modes (Result.err and exception) through one path. ## both failure modes (Result.err and exception) through one path.
let sendRes = let sendRes =
try: try:
await self.sendHandler(envelope) await self.sendHandler(envelope)
except CatchableError as e: except CatchableError as e:
Result[RequestId, string].err("waku send raised: " & e.msg) Result[RequestId, string].err("messaging send raised: " & e.msg)
let messagingReqId = sendRes.valueOr: let messagingReqId = sendRes.valueOr:
MessageErrorEvent.emit( MessageErrorEvent.emit(
self.brokerCtx, self.brokerCtx,
MessageErrorEvent( MessageErrorEvent(
requestId: channelReqId, messageHash: "", error: "waku send failed: " & error requestId: channelReqId, messageHash: "", error: "messaging send failed: " & error
), ),
) )
self.markSegmentFailed(channelReqId) self.markSegmentFailed(channelReqId)
@ -374,7 +374,7 @@ proc onMessageReceived(
proc new*( proc new*(
T: type ReliableChannel, T: type ReliableChannel,
waku: Waku, sendHandler: SendHandler,
channelId: ChannelId, channelId: ChannelId,
contentTopic: ContentTopic, contentTopic: ContentTopic,
senderId: SdsParticipantID, senderId: SdsParticipantID,
@ -382,7 +382,6 @@ proc new*(
sdsConfig: SdsConfig, sdsConfig: SdsConfig,
rateConfig: RateLimitConfig, rateConfig: RateLimitConfig,
brokerCtx: BrokerContext = globalBrokerContext(), brokerCtx: BrokerContext = globalBrokerContext(),
sendHandler: SendHandler = nil,
): T = ): T =
## Pipeline handlers (segmentation/SDS/rate-limit) are constructed ## Pipeline handlers (segmentation/SDS/rate-limit) are constructed
## inside the channel rather than handed in by the caller — they are ## inside the channel rather than handed in by the caller — they are
@ -391,19 +390,11 @@ proc new*(
## `Decrypt` request brokers, so the channel keeps no per-instance ## `Decrypt` request brokers, so the channel keeps no per-instance
## encryption state either. ## encryption state either.
## ##
## `sendHandler` defaults to `waku.send`; tests pass a fake to drive ## `sendHandler` is the egress dispatch. The owning `ReliableChannelManager`
## the send state machine without touching the network. ## typically constructs it as a closure over `MessagingClient.send`. Tests
let resolvedSendHandler = ## pass a fake to drive the send state machine without touching the network.
if sendHandler.isNil():
proc(
envelope: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
return await waku.send(envelope)
else:
sendHandler
let chn = T( let chn = T(
sendHandler: resolvedSendHandler, sendHandler: sendHandler,
channelId: channelId, channelId: channelId,
contentTopic: contentTopic, contentTopic: contentTopic,
senderId: senderId, senderId: senderId,

View File

@ -10,11 +10,10 @@ import results
import chronos import chronos
import stew/byteutils import stew/byteutils
import waku/api/api import brokers/broker_context
import waku/api/api_conf
import waku/events/message_events as waku_message_events import waku/events/message_events as waku_message_events
import waku/factory/waku as waku_factory import waku/messaging_client
import waku/node/delivery_service/delivery_service
import waku/waku_core/topics import waku/waku_core/topics
import ./reliable_channel import ./reliable_channel
@ -24,40 +23,43 @@ export reliable_channel
type ReliableChannelManager* = ref object type ReliableChannelManager* = ref object
channels: Table[ChannelId, ReliableChannel] channels: Table[ChannelId, ReliableChannel]
waku: Waku messagingClient: MessagingClient
## Owned by the manager. The channel layer reaches the messaging ## Borrowed from the owning `Waku`.
## API through `waku.send(envelope)`; constructing DeliveryTasks sendHandler: SendHandler
## directly would breach the layer boundary. ## Default egress dispatch for channels created through this manager.
## Constructed at mount time as a closure over `MessagingClient.send`
## so the channel layer itself stays callable-only.
brokerCtx: BrokerContext brokerCtx: BrokerContext
proc new*( proc new*(
T: type ReliableChannelManager, T: type ReliableChannelManager,
conf: WakuNodeConf, messagingClient: MessagingClient,
sendHandler: SendHandler,
brokerCtx: BrokerContext = globalBrokerContext(), brokerCtx: BrokerContext = globalBrokerContext(),
): Future[Result[T, string]] {.async.} = ): Result[T, string] =
## TODO !! The proper ownership chain is: if messagingClient.isNil():
## ReliableChannelManager -> DeliveryService (MessagingClient) -> Waku (Kernel/Protocols) -> WakuNode, return err("messaging client is required")
## and this will be implemented in the future. For now, `createNode` if sendHandler.isNil():
## is called here to get a Waku instance, and the WakuNode is immediately discarded. return err("sendHandler is required")
## This is a temporary workaround to get the API return ok(
T(
let waku = ?(await createNode(conf)) channels: initTable[ChannelId, ReliableChannel](),
messagingClient: messagingClient,
let manager = T( sendHandler: sendHandler,
channels: initTable[ChannelId, ReliableChannel](), waku: waku, brokerCtx: brokerCtx brokerCtx: brokerCtx,
)
) )
return ok(manager)
proc start*(self: ReliableChannelManager): Result[void, string] = proc start*(self: ReliableChannelManager): Result[void, string] =
## Bring the owned DeliveryService up. Separated from `new` so callers ## Placeholder: per-channel listeners are installed in `ReliableChannel.new`,
## can register encryption providers / create channels before traffic ## so the manager has nothing to start at this layer. Kept for symmetry
## starts flowing. ## with the `Waku` mount/start lifecycle and as a hook for future state.
self.waku.deliveryService.startDeliveryService() discard
ok()
proc stop*(self: ReliableChannelManager) {.async.} = proc stop*(self: ReliableChannelManager) {.async.} =
if not self.waku.isNil(): ## Placeholder mirror of `start`.
await self.waku.deliveryService.stopDeliveryService() discard
proc createReliableChannel*( proc createReliableChannel*(
self: ReliableChannelManager, self: ReliableChannelManager,
@ -66,17 +68,17 @@ proc createReliableChannel*(
senderId: SdsParticipantID, senderId: SdsParticipantID,
sendHandler: SendHandler = nil, sendHandler: SendHandler = nil,
): Result[ChannelId, string] = ): Result[ChannelId, string] =
## Spec entry point. The `DeliveryService` and `rng` the channel needs ## Spec entry point. The `sendHandler` and `rng` the channel needs are
## are sourced from the owning `ReliableChannelManager` rather than ## sourced from the owning `ReliableChannelManager` rather than passed
## passed per call. Encryption is wired up through the `Encrypt`/ ## per call. Encryption is wired up through the `Encrypt`/`Decrypt`
## `Decrypt` request brokers — the application installs its own ## request brokers — the application installs its own providers
## providers (or `setNoopEncryption()`) before traffic flows. ## (or `setNoopEncryption()`) before traffic flows.
## ##
## Segmentation, SDS and rate-limit configs will eventually be read ## Segmentation, SDS and rate-limit configs will eventually be read
## from the node's `NodeConfig`. Defaults for now. ## from the node's `NodeConfig`. Defaults for now.
## ##
## `sendHandler` is left `nil` in production so the channel uses the ## `sendHandler` defaults to the manager's default (constructed at mount
## owned `waku.send`; tests pass a fake to bypass the network. ## from `MessagingClient.send`); tests pass a fake to bypass the network.
if self.channels.hasKey(channelId): if self.channels.hasKey(channelId):
return err("channel already exists: " & channelId) return err("channel already exists: " & channelId)
@ -95,8 +97,14 @@ proc createReliableChannel*(
epochPeriodSec: DefaultEpochPeriodSec, messagesPerEpoch: DefaultMessagesPerEpoch epochPeriodSec: DefaultEpochPeriodSec, messagesPerEpoch: DefaultMessagesPerEpoch
) )
let effectiveSendHandler =
if sendHandler.isNil():
self.sendHandler
else:
sendHandler
let chn = ReliableChannel.new( let chn = ReliableChannel.new(
waku = self.waku, sendHandler = effectiveSendHandler,
channelId = channelId, channelId = channelId,
contentTopic = contentTopic, contentTopic = contentTopic,
senderId = senderId, senderId = senderId,
@ -104,7 +112,6 @@ proc createReliableChannel*(
sdsConfig = sdsConfig, sdsConfig = sdsConfig,
rateConfig = rateConfig, rateConfig = rateConfig,
brokerCtx = self.brokerCtx, brokerCtx = self.brokerCtx,
sendHandler = sendHandler,
) )
self.channels[channelId] = chn self.channels[channelId] = chn
@ -137,5 +144,5 @@ proc send*(
## `ReliableChannel` installs its own `MessageReceivedEvent` listener ## `ReliableChannel` installs its own `MessageReceivedEvent` listener
## in `ReliableChannel.new`, filters by spec marker and `contentTopic`, ## in `ReliableChannel.new`, filters by spec marker and `contentTopic`,
## and routes to its private `onMessageReceived`. This keeps the lower ## and routes to its private `onMessageReceived`. This keeps the lower
## layer (MessagingAPI/Waku) unaware of the existence of ReliableChannel ## layer (MessagingClient/Waku) unaware of the existence of ReliableChannel
## and keeps the manager out of per-channel event dispatch. ## and keeps the manager out of per-channel event dispatch.

View File

@ -82,8 +82,12 @@ when isMainModule:
echo("Waku node created successfully!") echo("Waku node created successfully!")
node.mountMessagingClient().isOkOr:
echo "Failed to mount messaging: ", error
quit(QuitFailure)
# Start the node # Start the node
(waitFor startWaku(addr node)).isOkOr: (waitFor node.start()).isOkOr:
echo "Failed to start node: ", error echo "Failed to start node: ", error
quit(QuitFailure) quit(QuitFailure)

View File

@ -48,7 +48,7 @@ proc setup*(): Waku =
error "Waku initialization failed", error = error error "Waku initialization failed", error = error
quit(QuitFailure) quit(QuitFailure)
(waitFor startWaku(addr waku)).isOkOr: (waitFor waku.start()).isOkOr:
error "Starting waku failed", error = error error "Starting waku failed", error = error
quit(QuitFailure) quit(QuitFailure)

View File

@ -172,7 +172,17 @@ proc logosdelivery_start_node(
chronicles.error "ConnectionStatusChange.listen failed", err = $error chronicles.error "ConnectionStatusChange.listen failed", err = $error
return err("ConnectionStatusChange.listen failed: " & $error) return err("ConnectionStatusChange.listen failed: " & $error)
(await startWaku(addr ctx.myLib[])).isOkOr: ctx.myLib[].mountMessagingClient().isOkOr:
let errMsg = $error
chronicles.error "mountMessagingClient failed", error = errMsg
return err("failed to mount messaging: " & errMsg)
ctx.myLib[].mountReliableChannelManager().isOkOr:
let errMsg = $error
chronicles.error "mountReliableChannelManager failed", err = errMsg
return err("failed to mount reliable channel manager: " & errMsg)
(await ctx.myLib[].start()).isOkOr:
let errMsg = $error let errMsg = $error
chronicles.error "START_NODE failed", err = errMsg chronicles.error "START_NODE failed", err = errMsg
return err("failed to start: " & errMsg) return err("failed to start: " & errMsg)

View File

@ -5,8 +5,7 @@ import
waku/discovery/waku_dnsdisc, waku/discovery/waku_dnsdisc,
waku/discovery/waku_discv5, waku/discovery/waku_discv5,
waku/waku_core/peers, waku/waku_core/peers,
waku/node/waku_node, waku/waku_node,
waku/node/kernel_api,
library/declare_lib library/declare_lib
proc retrieveBootstrapNodes( proc retrieveBootstrapNodes(

View File

@ -71,7 +71,7 @@ registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]):
proc waku_start( proc waku_start(
ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer
) {.ffi.} = ) {.ffi.} =
(await startWaku(ctx[].myLib)).isOkOr: (await ctx.myLib[].start()).isOkOr:
error "START_NODE failed", error = error error "START_NODE failed", error = error
return err("failed to start: " & $error) return err("failed to start: " & $error)
return ok("") return ok("")

View File

@ -8,8 +8,7 @@ import
waku/waku_filter_v2/common, waku/waku_filter_v2/common,
waku/waku_core/subscription/push_handler, waku/waku_core/subscription/push_handler,
waku/node/peer_manager/peer_manager, waku/node/peer_manager/peer_manager,
waku/node/waku_node, waku/waku_node,
waku/node/kernel_api,
waku/waku_core/topics/pubsub_topic, waku/waku_core/topics/pubsub_topic,
waku/waku_core/topics/content_topic, waku/waku_core/topics/content_topic,
library/events/json_message_event, library/events/json_message_event,

View File

@ -7,7 +7,7 @@ import
waku/waku_core/message, waku/waku_core/message,
waku/waku_core/topics/pubsub_topic, waku/waku_core/topics/pubsub_topic,
waku/waku_core/topics, waku/waku_core/topics,
waku/node/kernel_api/relay, waku/node/waku_node/relay,
waku/waku_relay/protocol, waku/waku_relay/protocol,
waku/node/peer_manager, waku/node/peer_manager,
library/events/json_message_event, library/events/json_message_event,

View File

@ -328,8 +328,8 @@
} }
}, },
"brokers": { "brokers": {
"version": "#v2.0.1", "version": "#v3.1.1",
"vcsRevision": "2093ca4d50e581adda73fee7fd16231f990f4cbe", "vcsRevision": "a7316a35f1b62e3497ae8ee0fc1aace74df0beb2",
"url": "https://github.com/NagyZoltanPeter/nim-brokers.git", "url": "https://github.com/NagyZoltanPeter/nim-brokers.git",
"downloadMethod": "git", "downloadMethod": "git",
"dependencies": [ "dependencies": [
@ -341,7 +341,7 @@
"cbor_serialization" "cbor_serialization"
], ],
"checksums": { "checksums": {
"sha1": "cc74c987af94537e9d44d1b0143aa417299040c5" "sha1": "4447d7c1f9da14ae439afb23aee45116ce2ecb40"
} }
}, },
"stint": { "stint": {
@ -620,8 +620,8 @@
} }
}, },
"sds": { "sds": {
"version": "#2e9a7683f0e180bf112135fae3a3803eed8490d4", "version": "#abdd40cc645f1b024c3ee99cced7e287c4e4c441",
"vcsRevision": "2e9a7683f0e180bf112135fae3a3803eed8490d4", "vcsRevision": "abdd40cc645f1b024c3ee99cced7e287c4e4c441",
"url": "https://github.com/logos-messaging/nim-sds.git", "url": "https://github.com/logos-messaging/nim-sds.git",
"downloadMethod": "git", "downloadMethod": "git",
"dependencies": [ "dependencies": [
@ -636,7 +636,7 @@
"taskpools" "taskpools"
], ],
"checksums": { "checksums": {
"sha1": "d13f1bf8d1b90b27e9edfc063b043831242cda19" "sha1": "61c4ae13c6896bfa70e662520e8660a78c7f438c"
} }
}, },
"ffi": { "ffi": {

View File

@ -30,7 +30,7 @@ let
# while others use the repo root. Pass both so the compiler finds either layout. # while others use the repo root. Pass both so the compiler finds either layout.
pathArgs = pathArgs =
builtins.concatStringsSep " " builtins.concatStringsSep " "
(builtins.concatMap (p: [ "--path:${p}" "--path:${p}/src" ]) (builtins.concatMap (p: [ "--path:${p}" "--path:${p}/src" "--path:${p}/sds" ])
(builtins.attrValues otherDeps)); (builtins.attrValues otherDeps));
libExt = libExt =

View File

@ -103,7 +103,9 @@ suite "LM API health checking":
client = (await createNode(conf)).valueOr: client = (await createNode(conf)).valueOr:
raiseAssert error raiseAssert error
(await startWaku(addr client)).isOkOr: client.mountMessagingClient().isOkOr:
raiseAssert error
(await client.start()).isOkOr:
raiseAssert error raiseAssert error
asyncTeardown: asyncTeardown:
@ -281,7 +283,9 @@ suite "LM API health checking":
edgeWaku = (await createNode(edgeConf)).valueOr: edgeWaku = (await createNode(edgeConf)).valueOr:
raiseAssert "Failed to create edge node: " & error raiseAssert "Failed to create edge node: " & error
(await startWaku(addr edgeWaku)).isOkOr: edgeWaku.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount edge messaging: " & error
(await edgeWaku.start()).isOkOr:
raiseAssert "Failed to start edge waku: " & error raiseAssert "Failed to start edge waku: " & error
let relayReq = await RequestProtocolHealth.request( let relayReq = await RequestProtocolHealth.request(

View File

@ -6,6 +6,7 @@ import libp2p/[peerid, peerinfo, crypto/crypto]
import brokers/broker_context import brokers/broker_context
import ../testlib/[common, wakucore, wakunode, testasync] import ../testlib/[common, wakucore, wakunode, testasync]
import ../waku_archive/archive_utils import ../waku_archive/archive_utils
import waku/messaging_client
import import
waku, waku,
@ -16,7 +17,6 @@ import
waku_relay/protocol, waku_relay/protocol,
waku_archive, waku_archive,
waku_archive/common as archive_common, waku_archive/common as archive_common,
node/delivery_service/delivery_service,
node/delivery_service/recv_service, node/delivery_service/recv_service,
] ]
import waku/factory/waku_conf import waku/factory/waku_conf
@ -147,7 +147,8 @@ suite "Messaging API, Receive Service (store recovery)":
subscriber = (await createNode(createApiNodeConf(numShards))).expect( subscriber = (await createNode(createApiNodeConf(numShards))).expect(
"Failed to create subscriber" "Failed to create subscriber"
) )
(await startWaku(addr subscriber)).expect("Failed to start subscriber") subscriber.mountMessagingClient().expect("Failed to mount messaging")
(await subscriber.start()).expect("Failed to start subscriber")
# publish after the subscriber exists but before it connects to the # publish after the subscriber exists but before it connects to the
# store; the message reaches the archive but the subscriber doesn't # store; the message reaches the archive but the subscriber doesn't
@ -185,7 +186,7 @@ suite "Messaging API, Receive Service (store recovery)":
await eventManager.teardown() await eventManager.teardown()
# trigger store check, should recover and deliver via MessageReceivedEvent # trigger store check, should recover and deliver via MessageReceivedEvent
await subscriber.deliveryService.recvService.checkStore() await subscriber.messagingClient.recvService.checkStore()
let received = await eventManager.waitForEvents(TestTimeout) let received = await eventManager.waitForEvents(TestTimeout)
check received check received

View File

@ -241,7 +241,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf())).valueOr: node = (await createNode(createApiNodeConf())).valueOr:
raiseAssert error raiseAssert error
(await startWaku(addr node)).isOkOr: node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error raiseAssert "Failed to start Waku node: " & error
# node is not connected ! # node is not connected !
@ -263,7 +265,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf())).valueOr: node = (await createNode(createApiNodeConf())).valueOr:
raiseAssert error raiseAssert error
(await startWaku(addr node)).isOkOr: node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error raiseAssert "Failed to start Waku node: " & error
await node.node.connectToNodes( await node.node.connectToNodes(
@ -297,7 +301,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf())).valueOr: node = (await createNode(createApiNodeConf())).valueOr:
raiseAssert error raiseAssert error
(await startWaku(addr node)).isOkOr: node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error raiseAssert "Failed to start Waku node: " & error
await node.node.connectToNodes(@[relayNode1PeerInfo]) await node.node.connectToNodes(@[relayNode1PeerInfo])
@ -327,7 +333,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf())).valueOr: node = (await createNode(createApiNodeConf())).valueOr:
raiseAssert error raiseAssert error
(await startWaku(addr node)).isOkOr: node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error raiseAssert "Failed to start Waku node: " & error
await node.node.connectToNodes(@[lightpushNodePeerInfo]) await node.node.connectToNodes(@[lightpushNodePeerInfo])
@ -357,7 +365,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf())).valueOr: node = (await createNode(createApiNodeConf())).valueOr:
raiseAssert error raiseAssert error
(await startWaku(addr node)).isOkOr: node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error raiseAssert "Failed to start Waku node: " & error
await node.node.connectToNodes(@[lightpushNodePeerInfo, storeNodePeerInfo]) await node.node.connectToNodes(@[lightpushNodePeerInfo, storeNodePeerInfo])
@ -411,7 +421,9 @@ suite "Waku API - Send":
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
node = (await createNode(createApiNodeConf(cli_args.WakuMode.Edge))).valueOr: node = (await createNode(createApiNodeConf(cli_args.WakuMode.Edge))).valueOr:
raiseAssert error raiseAssert error
(await startWaku(addr node)).isOkOr: node.mountMessagingClient().isOkOr:
raiseAssert "Failed to mount messaging: " & error
(await node.start()).isOkOr:
raiseAssert "Failed to start Waku node: " & error raiseAssert "Failed to start Waku node: " & error
await node.node.connectToNodes(@[fakeLightpushNodePeerInfo]) await node.node.connectToNodes(@[fakeLightpushNodePeerInfo])

View File

@ -5,6 +5,7 @@ import chronos, testutils/unittests, stew/byteutils
import libp2p/[peerid, peerinfo, multiaddress, crypto/crypto] import libp2p/[peerid, peerinfo, multiaddress, crypto/crypto]
import brokers/broker_context import brokers/broker_context
import ../testlib/[common, wakucore, wakunode, testasync] import ../testlib/[common, wakucore, wakunode, testasync]
import waku/messaging_client
import import
waku, waku,
@ -13,14 +14,15 @@ import
waku_core, waku_core,
events/message_events, events/message_events,
waku_relay/protocol, waku_relay/protocol,
node/kernel_api/filter, node/waku_node/filter,
node/delivery_service/subscription_manager, node/subscription_manager,
] ]
import waku/factory/waku_conf import waku/factory/waku_conf
import tools/confutils/cli_args import tools/confutils/cli_args
const TestTimeout = chronos.seconds(10) const TestTimeout = chronos.seconds(10)
const NegativeTestTimeout = chronos.seconds(2) const NegativeTestTimeout = chronos.seconds(2)
const EdgeWaitTimeout = chronos.seconds(60)
type ReceiveEventListenerManager = ref object type ReceiveEventListenerManager = ref object
brokerCtx: BrokerContext brokerCtx: BrokerContext
@ -85,7 +87,8 @@ proc setupSubscriberNode(conf: WakuNodeConf): Future[Waku] {.async.} =
var node: Waku var node: Waku
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
node = (await createNode(conf)).expect("Failed to create subscriber node") node = (await createNode(conf)).expect("Failed to create subscriber node")
(await startWaku(addr node)).expect("Failed to start subscriber node") node.mountMessagingClient().expect("Failed to mount messaging")
(await node.start()).expect("Failed to start subscriber node")
return node return node
proc setupNetwork( proc setupNetwork(
@ -161,20 +164,39 @@ proc getRelayShard(node: WakuNode, contentTopic: ContentTopic): PubsubTopic =
return PubsubTopic($shardObj) return PubsubTopic($shardObj)
proc waitForMesh(node: WakuNode, shard: PubsubTopic) {.async.} = proc waitForMesh(node: WakuNode, shard: PubsubTopic) {.async.} =
for _ in 0 ..< 50: let deadline = Moment.now() + EdgeWaitTimeout
while Moment.now() < deadline:
if node.wakuRelay.getNumPeersInMesh(shard).valueOr(0) > 0: if node.wakuRelay.getNumPeersInMesh(shard).valueOr(0) > 0:
return return
await sleepAsync(100.milliseconds) await sleepAsync(100.milliseconds)
raise newException(ValueError, "GossipSub Mesh failed to stabilize on " & shard) raise newException(ValueError, "GossipSub Mesh failed to stabilize on " & shard)
proc waitForEdgeSubs(w: Waku, shard: PubsubTopic) {.async.} = proc waitForEdgeSubs(w: Waku, shard: PubsubTopic) {.async.} =
let sm = w.deliveryService.subscriptionManager let deadline = Moment.now() + EdgeWaitTimeout
for _ in 0 ..< 50: while Moment.now() < deadline:
if sm.edgeFilterPeerCount(shard) > 0: if w.node.subscriptionManager.edgeFilterPeerCount(shard) > 0:
return return
await sleepAsync(100.milliseconds) await sleepAsync(100.milliseconds)
raise newException(ValueError, "Edge filter subscription failed on " & shard) raise newException(ValueError, "Edge filter subscription failed on " & shard)
proc edgePeersReached(w: Waku, shard: PubsubTopic, n: int): Future[bool] {.async.} =
let deadline = Moment.now() + EdgeWaitTimeout
while Moment.now() < deadline:
if w.node.subscriptionManager.edgeFilterPeerCount(shard) >= n:
return true
await sleepAsync(100.milliseconds)
return false
proc edgePeersDroppedBelow(
w: Waku, shard: PubsubTopic, n: int
): Future[bool] {.async.} =
let deadline = Moment.now() + EdgeWaitTimeout
while Moment.now() < deadline:
if w.node.subscriptionManager.edgeFilterPeerCount(shard) < n:
return true
await sleepAsync(100.milliseconds)
return false
proc publishToMesh( proc publishToMesh(
net: TestNetwork, contentTopic: ContentTopic, payload: seq[byte] net: TestNetwork, contentTopic: ContentTopic, payload: seq[byte]
): Future[Result[int, string]] {.async.} = ): Future[Result[int, string]] {.async.} =
@ -621,7 +643,8 @@ suite "Messaging API, SubscriptionManager":
var subscriber: Waku var subscriber: Waku
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
subscriber = (await createNode(conf)).expect("Failed to create edge subscriber") subscriber = (await createNode(conf)).expect("Failed to create edge subscriber")
(await startWaku(addr subscriber)).expect("Failed to start edge subscriber") subscriber.mountMessagingClient().expect("Failed to mount messaging")
(await subscriber.start()).expect("Failed to start edge subscriber")
# Connect edge subscriber to both filter servers so selectPeers finds both # Connect edge subscriber to both filter servers so selectPeers finds both
await subscriber.node.connectToNodes(@[publisherPeerInfo, meshBuddyPeerInfo]) await subscriber.node.connectToNodes(@[publisherPeerInfo, meshBuddyPeerInfo])
@ -632,12 +655,7 @@ suite "Messaging API, SubscriptionManager":
(await subscriber.subscribe(testTopic)).expect("Failed to subscribe") (await subscriber.subscribe(testTopic)).expect("Failed to subscribe")
# Wait for dialing both filter servers (HealthyThreshold = 2) # Wait for dialing both filter servers (HealthyThreshold = 2)
for _ in 0 ..< 100: check await edgePeersReached(subscriber, shard, 2)
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 # Verify message delivery with both servers alive
await waitForMesh(publisher, shard) await waitForMesh(publisher, shard)
@ -659,12 +677,8 @@ suite "Messaging API, SubscriptionManager":
await subscriber.node.disconnectNode(meshBuddyPeerInfo) await subscriber.node.disconnectNode(meshBuddyPeerInfo)
# Wait for the dead peer to be pruned # Wait for the dead peer to be pruned
for _ in 0 ..< 50: check await edgePeersDroppedBelow(subscriber, shard, 2)
if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) < 2: check subscriber.node.subscriptionManager.edgeFilterPeerCount(shard) >= 1
break
await sleepAsync(100.milliseconds)
check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 1
# Verify messages still arrive through the surviving filter server (publisher) # Verify messages still arrive through the surviving filter server (publisher)
eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1)
@ -758,7 +772,8 @@ suite "Messaging API, SubscriptionManager":
var subscriber: Waku var subscriber: Waku
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
subscriber = (await createNode(conf)).expect("Failed to create edge subscriber") subscriber = (await createNode(conf)).expect("Failed to create edge subscriber")
(await startWaku(addr subscriber)).expect("Failed to start edge subscriber") subscriber.mountMessagingClient().expect("Failed to mount messaging")
(await subscriber.start()).expect("Failed to start edge subscriber")
await subscriber.node.connectToNodes( await subscriber.node.connectToNodes(
@[publisherPeerInfo, meshBuddyPeerInfo, sparePeerInfo] @[publisherPeerInfo, meshBuddyPeerInfo, sparePeerInfo]
@ -770,23 +785,13 @@ suite "Messaging API, SubscriptionManager":
(await subscriber.subscribe(testTopic)).expect("Failed to subscribe") (await subscriber.subscribe(testTopic)).expect("Failed to subscribe")
# Wait for 2 confirmed peers (HealthyThreshold). The 3rd is available but not dialed. # Wait for 2 confirmed peers (HealthyThreshold). The 3rd is available but not dialed.
for _ in 0 ..< 100: check await edgePeersReached(subscriber, shard, 2)
if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2: require subscriber.node.subscriptionManager.edgeFilterPeerCount(shard) == 2
break
await sleepAsync(100.milliseconds)
require subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) ==
2
await subscriber.node.disconnectNode(meshBuddyPeerInfo) await subscriber.node.disconnectNode(meshBuddyPeerInfo)
# Wait for the sub loop to detect the loss and dial a replacement # Wait for the sub loop to detect the loss and dial a replacement
for _ in 0 ..< 100: check await edgePeersReached(subscriber, shard, 2)
if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2:
break
await sleepAsync(100.milliseconds)
check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2
await waitForMesh(publisher, shard) await waitForMesh(publisher, shard)

View File

@ -35,7 +35,7 @@ suite "Reliable Channel - ingress":
## Unit test for the receive side of the API: instead of standing ## Unit test for the receive side of the API: instead of standing
## up two libp2p nodes and a relay mesh, we drive the manager ## up two libp2p nodes and a relay mesh, we drive the manager
## directly by emitting a `MessageReceivedEvent` (the exact event ## directly by emitting a `MessageReceivedEvent` (the exact event
## the DeliveryService emits when a `WakuMessage` arrives off the ## the MessagingClient emits when a `WakuMessage` arrives off the
## wire). The manager must: ## wire). The manager must:
## - drop traffic missing the Reliable Channel spec marker ## - drop traffic missing the Reliable Channel spec marker
## - dispatch the matching channel's `onMessageReceived` ## - dispatch the matching channel's `onMessageReceived`
@ -45,13 +45,15 @@ suite "Reliable Channel - ingress":
contentTopic = ContentTopic("/reliable-channel/test/proto") contentTopic = ContentTopic("/reliable-channel/test/proto")
let appPayload = "hello reliable channel".toBytes() let appPayload = "hello reliable channel".toBytes()
var waku: Waku
var manager: ReliableChannelManager var manager: ReliableChannelManager
var brokerCtx: BrokerContext var brokerCtx: BrokerContext
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext() brokerCtx = globalBrokerContext()
manager = (await ReliableChannelManager.new(createApiNodeConf())).expect( waku = (await createNode(createApiNodeConf())).expect("createNode")
"Failed to create manager" waku.mountMessagingClient().expect("mountMessagingClient")
) waku.mountReliableChannelManager().expect("mountReliableChannelManager")
manager = waku.reliableChannelManager
## Noop encryption providers so the Encrypt/Decrypt brokers have ## Noop encryption providers so the Encrypt/Decrypt brokers have
## something to dispatch to; without this the channel falls back to ## something to dispatch to; without this the channel falls back to
@ -95,7 +97,7 @@ suite "Reliable Channel - ingress":
if arrived: if arrived:
check received.read() == appPayload check received.read() == appPayload
await manager.stop() (await waku.stop()).expect("stop")
asyncTest "manager drops unmarked WakuMessage": asyncTest "manager drops unmarked WakuMessage":
## Mirror of the above: same content topic, but `meta` is empty ## Mirror of the above: same content topic, but `meta` is empty
@ -105,13 +107,15 @@ suite "Reliable Channel - ingress":
contentTopic = ContentTopic("/reliable-channel/test/proto") contentTopic = ContentTopic("/reliable-channel/test/proto")
let appPayload = "foreign payload".toBytes() let appPayload = "foreign payload".toBytes()
var waku: Waku
var manager: ReliableChannelManager var manager: ReliableChannelManager
var brokerCtx: BrokerContext var brokerCtx: BrokerContext
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext() brokerCtx = globalBrokerContext()
manager = (await ReliableChannelManager.new(createApiNodeConf())).expect( waku = (await createNode(createApiNodeConf())).expect("createNode")
"Failed to create manager" waku.mountMessagingClient().expect("mountMessagingClient")
) waku.mountReliableChannelManager().expect("mountReliableChannelManager")
manager = waku.reliableChannelManager
setNoopEncryption() setNoopEncryption()
@ -146,7 +150,7 @@ suite "Reliable Channel - ingress":
await sleepAsync(100.milliseconds) await sleepAsync(100.milliseconds)
check not fired check not fired
await manager.stop() (await waku.stop()).expect("stop")
suite "Reliable Channel - send state machine": suite "Reliable Channel - send state machine":
asyncTest "MessageSentEvent finalises the channelReqId as Sent": asyncTest "MessageSentEvent finalises the channelReqId as Sent":
@ -162,13 +166,15 @@ suite "Reliable Channel - send state machine":
contentTopic = ContentTopic("/reliable-channel/test/sm-success") contentTopic = ContentTopic("/reliable-channel/test/sm-success")
fakeMsgReqId = RequestId("fake-msg-req-1") fakeMsgReqId = RequestId("fake-msg-req-1")
var waku: Waku
var manager: ReliableChannelManager var manager: ReliableChannelManager
var brokerCtx: BrokerContext var brokerCtx: BrokerContext
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext() brokerCtx = globalBrokerContext()
manager = (await ReliableChannelManager.new(createApiNodeConf())).expect( waku = (await createNode(createApiNodeConf())).expect("createNode")
"Failed to create manager" waku.mountMessagingClient().expect("mountMessagingClient")
) waku.mountReliableChannelManager().expect("mountReliableChannelManager")
manager = waku.reliableChannelManager
setNoopEncryption() setNoopEncryption()
@ -213,7 +219,7 @@ suite "Reliable Channel - send state machine":
if finalised: if finalised:
check sentFut.read() == channelReqId check sentFut.read() == channelReqId
await manager.stop() (await waku.stop()).expect("stop")
asyncTest "two independent channelReqIds are finalised independently": asyncTest "two independent channelReqIds are finalised independently":
## Two `send()` calls -> two independent `channelReqId`s, each with ## Two `send()` calls -> two independent `channelReqId`s, each with
@ -227,13 +233,15 @@ suite "Reliable Channel - send state machine":
channelId = ChannelId("sm-multi-channel") channelId = ChannelId("sm-multi-channel")
contentTopic = ContentTopic("/reliable-channel/test/sm-multi") contentTopic = ContentTopic("/reliable-channel/test/sm-multi")
var waku: Waku
var manager: ReliableChannelManager var manager: ReliableChannelManager
var brokerCtx: BrokerContext var brokerCtx: BrokerContext
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext() brokerCtx = globalBrokerContext()
manager = (await ReliableChannelManager.new(createApiNodeConf())).expect( waku = (await createNode(createApiNodeConf())).expect("createNode")
"Failed to create manager" waku.mountMessagingClient().expect("mountMessagingClient")
) waku.mountReliableChannelManager().expect("mountReliableChannelManager")
manager = waku.reliableChannelManager
setNoopEncryption() setNoopEncryption()
@ -303,7 +311,7 @@ suite "Reliable Channel - send state machine":
if erroredArrived: if erroredArrived:
check erroredFut.read() == channelReqId2 check erroredFut.read() == channelReqId2
await manager.stop() (await waku.stop()).expect("stop")
asyncTest "TODO: channelReqId not pruned until ALL its segments are final": asyncTest "TODO: channelReqId not pruned until ALL its segments are final":
## Placeholder for the multi-sibling prune rule. Today's ## Placeholder for the multi-sibling prune rule. Today's
@ -326,13 +334,15 @@ suite "Reliable Channel - send state machine":
channelId = ChannelId("sm-race-channel") channelId = ChannelId("sm-race-channel")
contentTopic = ContentTopic("/reliable-channel/test/sm-race") contentTopic = ContentTopic("/reliable-channel/test/sm-race")
var waku: Waku
var manager: ReliableChannelManager var manager: ReliableChannelManager
var brokerCtx: BrokerContext var brokerCtx: BrokerContext
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext() brokerCtx = globalBrokerContext()
manager = (await ReliableChannelManager.new(createApiNodeConf())).expect( waku = (await createNode(createApiNodeConf())).expect("createNode")
"Failed to create manager" waku.mountMessagingClient().expect("mountMessagingClient")
) waku.mountReliableChannelManager().expect("mountReliableChannelManager")
manager = waku.reliableChannelManager
setNoopEncryption() setNoopEncryption()
@ -413,4 +423,4 @@ suite "Reliable Channel - send state machine":
check channelReqId1 in finalisedReqIds check channelReqId1 in finalisedReqIds
check channelReqId2 in finalisedReqIds check channelReqId2 in finalisedReqIds
await manager.stop() (await waku.stop()).expect("stop")

View File

@ -11,7 +11,14 @@ import
import import
tests/testlib/[wakunode, wakucore], tests/testlib/[wakunode, wakucore],
waku/[waku_node, waku_enr, net/auto_port, discovery/waku_discv5, node/waku_metrics], waku/[
waku_node,
net/net_config,
waku_enr,
net/auto_port,
discovery/waku_discv5,
node/waku_metrics,
],
waku/factory/[ waku/factory/[
node_factory, node_factory,
internal_config, internal_config,

View File

@ -11,8 +11,7 @@ import
waku/[ waku/[
waku_core, waku_core,
node/peer_manager, node/peer_manager,
node/waku_node, waku_node,
node/kernel_api,
waku_filter_v2, waku_filter_v2,
waku_filter_v2/client, waku_filter_v2/client,
waku_filter_v2/subscriptions, waku_filter_v2/subscriptions,

View File

@ -15,18 +15,18 @@ import
node/health_monitor/protocol_health, node/health_monitor/protocol_health,
node/health_monitor/topic_health, node/health_monitor/topic_health,
node/health_monitor/node_health_monitor, node/health_monitor/node_health_monitor,
node/delivery_service/delivery_service, messaging_client,
node/delivery_service/subscription_manager, node/waku_node/relay,
node/kernel_api/relay, node/waku_node/store,
node/kernel_api/store, node/waku_node/lightpush,
node/kernel_api/lightpush, node/waku_node/filter,
node/kernel_api/filter,
events/health_events, events/health_events,
events/peer_events, events/peer_events,
waku_archive, waku_archive,
] ]
import ../testlib/[wakunode, wakucore], ../waku_archive/archive_utils import ../testlib/[wakunode, wakucore], ../waku_archive/archive_utils
import waku/node/subscription_manager
const MockDLow = 4 # Mocked GossipSub DLow value const MockDLow = 4 # Mocked GossipSub DLow value
@ -229,8 +229,8 @@ suite "Health Monitor - events":
await nodeA.start() await nodeA.start()
let ds = let ds =
DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService") MessagingClient.new(false, nodeA).expect("Failed to create MessagingClient")
ds.startDeliveryService().expect("Failed to start DeliveryService") ds.start().expect("Failed to start MessagingClient")
let monitorA = NodeHealthMonitor.new(nodeA) let monitorA = NodeHealthMonitor.new(nodeA)
@ -317,7 +317,7 @@ suite "Health Monitor - events":
lastStatus == ConnectionStatus.Disconnected lastStatus == ConnectionStatus.Disconnected
await monitorA.stopHealthMonitor() await monitorA.stopHealthMonitor()
await ds.stopDeliveryService() await ds.stop()
await nodeA.stop() await nodeA.stop()
asyncTest "Edge health driven by confirmed filter subscriptions": asyncTest "Edge health driven by confirmed filter subscriptions":
@ -333,9 +333,9 @@ suite "Health Monitor - events":
await nodeA.start() await nodeA.start()
let ds = let ds =
DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService") MessagingClient.new(false, nodeA).expect("Failed to create MessagingClient")
ds.startDeliveryService().expect("Failed to start DeliveryService") ds.start().expect("Failed to start MessagingClient")
let subMgr = ds.subscriptionManager let subMgr = nodeA.subscriptionManager
var nodeB: WakuNode var nodeB: WakuNode
lockNewGlobalBrokerContext: lockNewGlobalBrokerContext:
@ -416,7 +416,7 @@ suite "Health Monitor - events":
await EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis) await EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis)
check shardHealthOk == true check shardHealthOk == true
check subMgr.edgeFilterSubStates.len > 0 check nodeA.subscriptionManager.edgeFilterSubStates.len > 0
healthSignal.clear() healthSignal.clear()
deadline = Moment.now() + TestConnectivityTimeLimit deadline = Moment.now() + TestConnectivityTimeLimit
@ -428,7 +428,7 @@ suite "Health Monitor - events":
check lastStatus == ConnectionStatus.PartiallyConnected check lastStatus == ConnectionStatus.PartiallyConnected
await ds.stopDeliveryService() await ds.stop()
await monitorA.stopHealthMonitor() await monitorA.stopHealthMonitor()
await nodeB.stop() await nodeB.stop()
await nodeA.stop() await nodeA.stop()

View File

@ -11,9 +11,7 @@ import
waku/[ waku/[
waku_core, waku_core,
node/peer_manager, node/peer_manager,
node/waku_node, waku_node,
node/kernel_api,
node/kernel_api/lightpush,
waku_lightpush_legacy, waku_lightpush_legacy,
waku_lightpush_legacy/common, waku_lightpush_legacy/common,
waku_lightpush_legacy/protocol_metrics, waku_lightpush_legacy/protocol_metrics,

View File

@ -8,15 +8,7 @@ import
libp2p/crypto/crypto libp2p/crypto/crypto
import import
waku/[ waku/[waku_core, node/peer_manager, waku_node, waku_lightpush, waku_rln_relay],
waku_core,
node/peer_manager,
node/waku_node,
node/kernel_api,
node/kernel_api/lightpush,
waku_lightpush,
waku_rln_relay,
],
../testlib/[wakucore, wakunode, testasync, futures], ../testlib/[wakucore, wakunode, testasync, futures],
../resources/payloads, ../resources/payloads,
../waku_rln_relay/[rln/waku_rln_relay_utils, utils_onchain] ../waku_rln_relay/[rln/waku_rln_relay_utils, utils_onchain]

View File

@ -9,17 +9,12 @@ import
libp2p/peerId, libp2p/peerId,
libp2p/crypto/crypto, libp2p/crypto/crypto,
eth/keys, eth/keys,
eth/p2p/discoveryv5/enr eth/p2p/discoveryv5/enr,
brokers/broker_context
import import
waku/[ waku/
waku_node, [waku_node, discovery/waku_discv5, waku_peer_exchange, node/peer_manager, waku_core],
node/kernel_api,
discovery/waku_discv5,
waku_peer_exchange,
node/peer_manager,
waku_core,
],
../waku_peer_exchange/utils, ../waku_peer_exchange/utils,
../testlib/[wakucore, wakunode, testasync] ../testlib/[wakucore, wakunode, testasync]
@ -184,114 +179,115 @@ suite "Waku Peer Exchange":
suite "Waku Peer Exchange with discv5": suite "Waku Peer Exchange with discv5":
asyncTest "Node successfully exchanges px peers with real discv5": asyncTest "Node successfully exchanges px peers with real discv5":
## Given (copied from test_waku_discv5.nim) lockNewGlobalBrokerContext:
let ## Given (copied from test_waku_discv5.nim)
# todo: px flag let
flags = CapabilitiesBitfield.init( # todo: px flag
lightpush = false, filter = false, store = false, relay = true flags = CapabilitiesBitfield.init(
) lightpush = false, filter = false, store = false, relay = true
bindIp = parseIpAddress("0.0.0.0") )
extIp = parseIpAddress("127.0.0.1") bindIp = parseIpAddress("0.0.0.0")
extIp = parseIpAddress("127.0.0.1")
nodeKey1 = generateSecp256k1Key() nodeKey1 = generateSecp256k1Key()
nodeTcpPort1 = Port(64010) nodeTcpPort1 = Port(64010)
nodeUdpPort1 = Port(9000) nodeUdpPort1 = Port(9000)
node1 = newTestWakuNode( node1 = newTestWakuNode(
nodeKey1, nodeKey1,
bindIp, bindIp,
nodeTcpPort1, nodeTcpPort1,
some(extIp), some(extIp),
wakuFlags = some(flags), wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort1), discv5UdpPort = some(nodeUdpPort1),
)
nodeKey2 = generateSecp256k1Key()
nodeTcpPort2 = Port(64012)
nodeUdpPort2 = Port(9002)
node2 = newTestWakuNode(
nodeKey2,
bindIp,
nodeTcpPort2,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort2),
)
nodeKey3 = generateSecp256k1Key()
nodeTcpPort3 = Port(64014)
nodeUdpPort3 = Port(9004)
node3 = newTestWakuNode(
nodeKey3,
bindIp,
nodeTcpPort3,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort3),
)
# discv5
let conf1 = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort1,
privateKey: keys.PrivateKey(nodeKey1.skkey),
bootstrapRecords: @[],
autoupdateRecord: true,
) )
nodeKey2 = generateSecp256k1Key() let disc1 =
nodeTcpPort2 = Port(64012) WakuDiscoveryV5.new(node1.rng, conf1, some(node1.enr), some(node1.peerManager))
nodeUdpPort2 = Port(9002)
node2 = newTestWakuNode( let conf2 = WakuDiscoveryV5Config(
nodeKey2, discv5Config: none(DiscoveryConfig),
bindIp, address: bindIp,
nodeTcpPort2, port: nodeUdpPort2,
some(extIp), privateKey: keys.PrivateKey(nodeKey2.skkey),
wakuFlags = some(flags), bootstrapRecords: @[disc1.protocol.getRecord()],
discv5UdpPort = some(nodeUdpPort2), autoupdateRecord: true,
) )
nodeKey3 = generateSecp256k1Key() let disc2 =
nodeTcpPort3 = Port(64014) WakuDiscoveryV5.new(node2.rng, conf2, some(node2.enr), some(node2.peerManager))
nodeUdpPort3 = Port(9004)
node3 = newTestWakuNode( await allFutures(node1.start(), node2.start(), node3.start())
nodeKey3, let resultDisc1StartRes = await disc1.start()
bindIp, assert resultDisc1StartRes.isOk(), resultDisc1StartRes.error
nodeTcpPort3, let resultDisc2StartRes = await disc2.start()
some(extIp), assert resultDisc2StartRes.isOk(), resultDisc2StartRes.error
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort3), ## When
var attempts = 10
while (disc1.protocol.nodesDiscovered < 1 or disc2.protocol.nodesDiscovered < 1) and
attempts > 0:
await sleepAsync(1.seconds)
attempts -= 1
# node2 can be connected, so will be returned by peer exchange
require (
await node1.peerManager.connectPeer(node2.switch.peerInfo.toRemotePeerInfo())
) )
# discv5 # Mount peer exchange
let conf1 = WakuDiscoveryV5Config( await node1.mountPeerExchange()
discv5Config: none(DiscoveryConfig), await node3.mountPeerExchange()
address: bindIp, await node3.mountPeerExchangeClient()
port: nodeUdpPort1,
privateKey: keys.PrivateKey(nodeKey1.skkey),
bootstrapRecords: @[],
autoupdateRecord: true,
)
let disc1 = let dialResponse =
WakuDiscoveryV5.new(node1.rng, conf1, some(node1.enr), some(node1.peerManager)) await node3.dialForPeerExchange(node1.switch.peerInfo.toRemotePeerInfo())
let conf2 = WakuDiscoveryV5Config( check dialResponse.isOk
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort2,
privateKey: keys.PrivateKey(nodeKey2.skkey),
bootstrapRecords: @[disc1.protocol.getRecord()],
autoupdateRecord: true,
)
let disc2 = let
WakuDiscoveryV5.new(node2.rng, conf2, some(node2.enr), some(node2.peerManager)) requestPeers = 1
currentPeers = node3.peerManager.switch.peerStore.peers.len
let res = await node3.fetchPeerExchangePeers(1)
check res.tryGet() == 1
await allFutures(node1.start(), node2.start(), node3.start()) # Then node3 has received 1 peer from node1
let resultDisc1StartRes = await disc1.start() check:
assert resultDisc1StartRes.isOk(), resultDisc1StartRes.error node3.peerManager.switch.peerStore.peers.len == currentPeers + requestPeers
let resultDisc2StartRes = await disc2.start()
assert resultDisc2StartRes.isOk(), resultDisc2StartRes.error
## When await allFutures(
var attempts = 10 [node1.stop(), node2.stop(), node3.stop(), disc1.stop(), disc2.stop()]
while (disc1.protocol.nodesDiscovered < 1 or disc2.protocol.nodesDiscovered < 1) and )
attempts > 0:
await sleepAsync(1.seconds)
attempts -= 1
# node2 can be connected, so will be returned by peer exchange
require (
await node1.peerManager.connectPeer(node2.switch.peerInfo.toRemotePeerInfo())
)
# Mount peer exchange
await node1.mountPeerExchange()
await node3.mountPeerExchange()
await node3.mountPeerExchangeClient()
let dialResponse =
await node3.dialForPeerExchange(node1.switch.peerInfo.toRemotePeerInfo())
check dialResponse.isOk
let
requestPeers = 1
currentPeers = node3.peerManager.switch.peerStore.peers.len
let res = await node3.fetchPeerExchangePeers(1)
check res.tryGet() == 1
# Then node3 has received 1 peer from node1
check:
node3.peerManager.switch.peerStore.peers.len == currentPeers + requestPeers
await allFutures(
[node1.stop(), node2.stop(), node3.stop(), disc1.stop(), disc2.stop()]
)

View File

@ -16,8 +16,7 @@ import
waku/[ waku/[
waku_core, waku_core,
node/peer_manager, node/peer_manager,
node/waku_node, waku_node,
node/kernel_api,
discovery/waku_discv5, discovery/waku_discv5,
waku_filter_v2/common, waku_filter_v2/common,
waku_relay/protocol, waku_relay/protocol,

View File

@ -13,11 +13,9 @@ from std/times import epochTime
import import
../../../waku/[ ../../../waku/[
node/waku_node,
node/peer_manager, node/peer_manager,
waku_core, waku_core,
waku_node, waku_node,
node/kernel_api,
common/error_handling, common/error_handling,
waku_rln_relay, waku_rln_relay,
waku_rln_relay/rln, waku_rln_relay/rln,

View File

@ -14,8 +14,7 @@ import
waku/[ waku/[
waku_core/topics/pubsub_topic, waku_core/topics/pubsub_topic,
waku_core/topics/sharding, waku_core/topics/sharding,
node/waku_node, waku_node,
node/kernel_api,
common/paging, common/paging,
waku_core, waku_core,
waku_store/common, waku_store/common,

View File

@ -5,8 +5,7 @@ import std/[options, sequtils, sets], testutils/unittests, chronos, libp2p/crypt
import import
waku/[ waku/[
common/paging, common/paging,
node/waku_node, waku_node,
node/kernel_api,
node/peer_manager, node/peer_manager,
waku_core, waku_core,
waku_core/message/digest, waku_core/message/digest,

View File

@ -5,5 +5,6 @@ import ./test_backend
import ./test_lifecycle import ./test_lifecycle
import ./test_facade import ./test_facade
import ./test_encoding import ./test_encoding
import ./test_sds_persistency
import ./test_string_lookup import ./test_string_lookup
import ./test_singleton import ./test_singleton

View File

@ -0,0 +1,155 @@
{.used.}
## Behavioural tests for the SDS Persistence adapter (nim-sds 0.3.0 snapshot
## model). Importing `sds_persistency` also compile-checks the real adapter.
##
## Writes go through the fire-and-forget Job path (the Future resolves when
## the op is queued, not applied — Persistency v1), so every read-back polls
## until the row appears/disappears.
import std/[options, os, times]
import chronos, results
import testutils/unittests
import waku/persistency/persistency
import waku/persistency/keys
import waku/persistency/sds_persistency
proc tmpRoot(label: string): string =
let p = getTempDir() / ("sds_persistency_test_" & label & "_" & $epochTime().int)
removeDir(p)
p
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 t.exists(category, k)
if r.isOk and r.get():
return true
await sleepAsync(chronos.milliseconds(2))
return false
proc pollGone(
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 not r.get():
return true
await sleepAsync(chronos.milliseconds(2))
return false
proc mkMsg(channelId: SdsChannelID, msgId: SdsMessageID, lamport: int64): SdsMessage =
SdsMessage.init(
messageId = msgId,
lamportTimestamp = lamport,
causalHistory = @[],
channelId = channelId,
content = @[byte(1), byte(2)],
bloomFilter = @[],
)
suite "SDS persistency adapter (0.3.0 snapshot model)":
asyncTest "saveChannelMeta + updateHistory round-trip via loadChannel":
let root = tmpRoot("roundtrip")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let job = p.openJob("sds").get()
let persistence = newSdsPersistence(job)
let channelId = "chan-1".SdsChannelID
var meta = ChannelMeta.init()
meta.lamportTimestamp = 42
check (await persistence.saveChannelMeta(channelId, meta)).isOk
check (await job.pollExists(CatMeta, toKey(channelId)))
# append out of (lamport) order on purpose; loadChannel must sort.
var upd = HistoryUpdate.init()
upd.append = @[mkMsg(channelId, "m2", 2), mkMsg(channelId, "m1", 1)]
check (await persistence.updateHistory(channelId, upd)).isOk
check (await job.pollExists(CatLog, key(channelId, "m2")))
let data = (await persistence.loadChannel(channelId)).valueOr:
check false
return
check data.meta.lamportTimestamp == 42
check data.messageHistory.len == 2
check data.messageHistory[0].messageId == "m1"
check data.messageHistory[1].messageId == "m2"
asyncTest "loadChannel on a fresh channel returns empty ChannelData":
let root = tmpRoot("empty")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let job = p.openJob("sds").get()
let persistence = newSdsPersistence(job)
let data = (await persistence.loadChannel("nope".SdsChannelID)).valueOr:
check false
return
check data.meta.lamportTimestamp == 0
check data.messageHistory.len == 0
asyncTest "updateHistory evict removes a log row":
let root = tmpRoot("evict")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let job = p.openJob("sds").get()
let persistence = newSdsPersistence(job)
let channelId = "c".SdsChannelID
var upd = HistoryUpdate.init()
upd.append = @[mkMsg(channelId, "a", 1), mkMsg(channelId, "b", 2)]
check (await persistence.updateHistory(channelId, upd)).isOk
check (await job.pollExists(CatLog, key(channelId, "b")))
var ev = HistoryUpdate.init()
ev.evict = @["a".SdsMessageID]
check (await persistence.updateHistory(channelId, ev)).isOk
check (await job.pollGone(CatLog, key(channelId, "a")))
let data = (await persistence.loadChannel(channelId)).valueOr:
check false
return
check data.messageHistory.len == 1
check data.messageHistory[0].messageId == "b"
asyncTest "dropChannel wipes meta and log":
let root = tmpRoot("drop")
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let job = p.openJob("sds").get()
let persistence = newSdsPersistence(job)
let channelId = "d".SdsChannelID
var meta = ChannelMeta.init()
meta.lamportTimestamp = 7
check (await persistence.saveChannelMeta(channelId, meta)).isOk
var upd = HistoryUpdate.init()
upd.append = @[mkMsg(channelId, "x", 1)]
check (await persistence.updateHistory(channelId, upd)).isOk
check (await job.pollExists(CatMeta, toKey(channelId)))
check (await job.pollExists(CatLog, key(channelId, "x")))
check (await persistence.dropChannel(channelId)).isOk
check (await job.pollGone(CatMeta, toKey(channelId)))
let data = (await persistence.loadChannel(channelId)).valueOr:
check false
return
check data.meta.lamportTimestamp == 0
check data.messageHistory.len == 0

View File

@ -9,7 +9,12 @@ import
libp2p/stream/bufferstream, libp2p/stream/bufferstream,
libp2p/stream/connection, libp2p/stream/connection,
libp2p/crypto/crypto libp2p/crypto/crypto
import waku/waku_core, waku/waku_node, ./testlib/wakucore, ./testlib/wakunode import
waku/waku_core,
waku/waku_node,
waku/node/health_monitor,
./testlib/wakucore,
./testlib/wakunode
suite "Waku Keepalive": suite "Waku Keepalive":
asyncTest "handle ping keepalives": asyncTest "handle ping keepalives":

View File

@ -10,6 +10,7 @@ import
import import
waku/[ waku/[
waku_node, waku_node,
net/net_config,
waku_core/topics, waku_core/topics,
node/peer_manager, node/peer_manager,
waku_enr, waku_enr,

View File

@ -21,8 +21,7 @@ import
waku_enr/capabilities, waku_enr/capabilities,
factory/conf_builder/conf_builder, factory/conf_builder/conf_builder,
factory/waku, factory/waku,
node/waku_node, waku_node,
node/kernel_api,
node/peer_manager, node/peer_manager,
], ],
../testlib/[wakucore, testasync, assertions, futures, wakunode, testutils], ../testlib/[wakucore, testasync, assertions, futures, wakunode, testutils],
@ -431,7 +430,7 @@ suite "Waku Discovery v5":
let waku0 = (await Waku.new(conf)).valueOr: let waku0 = (await Waku.new(conf)).valueOr:
raiseAssert error raiseAssert error
(waitFor startWaku(addr waku0)).isOkOr: (waitFor waku0.start()).isOkOr:
raiseAssert error raiseAssert error
confBuilder.withNodeKey(crypto.PrivateKey.random(Secp256k1, myRng[])[]) confBuilder.withNodeKey(crypto.PrivateKey.random(Secp256k1, myRng[])[])
@ -445,7 +444,7 @@ suite "Waku Discovery v5":
let waku1 = (await Waku.new(conf1)).valueOr: let waku1 = (await Waku.new(conf1)).valueOr:
raiseAssert error raiseAssert error
(waitFor startWaku(addr waku1)).isOkOr: (waitFor waku1.start()).isOkOr:
raiseAssert error raiseAssert error
await waku1.node.mountPeerExchange() await waku1.node.mountPeerExchange()
@ -461,7 +460,7 @@ suite "Waku Discovery v5":
let waku2 = (await Waku.new(conf2)).valueOr: let waku2 = (await Waku.new(conf2)).valueOr:
raiseAssert error raiseAssert error
(waitFor startWaku(addr waku2)).isOkOr: (waitFor waku2.start()).isOkOr:
raiseAssert error raiseAssert error
# leave some time for discv5 to act # leave some time for discv5 to act

View File

@ -3,7 +3,8 @@
import std/[options, sequtils, json], testutils/unittests, results, chronos import std/[options, sequtils, json], testutils/unittests, results, chronos
import import
waku/node/[peer_manager, waku_node, kernel_api], waku/node/peer_manager,
waku/waku_node,
waku/waku_core, waku/waku_core,
waku/waku_filter_v2/[common, client, subscriptions, protocol, rpc_codec], waku/waku_filter_v2/[common, client, subscriptions, protocol, rpc_codec],
../testlib/[wakucore, testasync, testutils, futures, sequtils, wakunode], ../testlib/[wakucore, testasync, testutils, futures, sequtils, wakunode],

View File

@ -5,7 +5,8 @@ import
testutils/unittests, testutils/unittests,
chronos, chronos,
libp2p/[switch, peerId, crypto/crypto], libp2p/[switch, peerId, crypto/crypto],
eth/[keys, p2p/discoveryv5/enr] eth/[keys, p2p/discoveryv5/enr],
brokers/broker_context
import import
waku/[ waku/[
@ -31,110 +32,113 @@ suite "Waku Peer Exchange":
suite "request": suite "request":
asyncTest "Retrieve and provide peer exchange peers from discv5": asyncTest "Retrieve and provide peer exchange peers from discv5":
## Given (copied from test_waku_discv5.nim) lockNewGlobalBrokerContext:
let ## Given (copied from test_waku_discv5.nim)
# todo: px flag let
flags = CapabilitiesBitfield.init( # todo: px flag
lightpush = false, filter = false, store = false, relay = true flags = CapabilitiesBitfield.init(
) lightpush = false, filter = false, store = false, relay = true
bindIp = parseIpAddress("0.0.0.0") )
extIp = parseIpAddress("127.0.0.1") bindIp = parseIpAddress("0.0.0.0")
extIp = parseIpAddress("127.0.0.1")
nodeKey1 = generateSecp256k1Key() nodeKey1 = generateSecp256k1Key()
nodeTcpPort1 = Port(64010) nodeTcpPort1 = Port(64010)
nodeUdpPort1 = Port(9000) nodeUdpPort1 = Port(9000)
node1 = newTestWakuNode( node1 = newTestWakuNode(
nodeKey1, nodeKey1,
bindIp, bindIp,
nodeTcpPort1, nodeTcpPort1,
some(extIp), some(extIp),
wakuFlags = some(flags), wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort1), discv5UdpPort = some(nodeUdpPort1),
)
nodeKey2 = generateSecp256k1Key()
nodeTcpPort2 = Port(64012)
nodeUdpPort2 = Port(9002)
node2 = newTestWakuNode(
nodeKey2,
bindIp,
nodeTcpPort2,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort2),
)
nodeKey3 = generateSecp256k1Key()
nodeTcpPort3 = Port(64014)
nodeUdpPort3 = Port(9004)
node3 = newTestWakuNode(
nodeKey3,
bindIp,
nodeTcpPort3,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort3),
)
# discv5
let conf1 = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort1,
privateKey: keys.PrivateKey(nodeKey1.skkey),
bootstrapRecords: @[],
autoupdateRecord: true,
) )
nodeKey2 = generateSecp256k1Key() let disc1 = WakuDiscoveryV5.new(
nodeTcpPort2 = Port(64012) node1.rng, conf1, some(node1.enr), some(node1.peerManager)
nodeUdpPort2 = Port(9002)
node2 = newTestWakuNode(
nodeKey2,
bindIp,
nodeTcpPort2,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort2),
) )
nodeKey3 = generateSecp256k1Key() let conf2 = WakuDiscoveryV5Config(
nodeTcpPort3 = Port(64014) discv5Config: none(DiscoveryConfig),
nodeUdpPort3 = Port(9004) address: bindIp,
node3 = newTestWakuNode( port: nodeUdpPort2,
nodeKey3, privateKey: keys.PrivateKey(nodeKey2.skkey),
bindIp, bootstrapRecords: @[disc1.protocol.getRecord()],
nodeTcpPort3, autoupdateRecord: true,
some(extIp),
wakuFlags = some(flags),
discv5UdpPort = some(nodeUdpPort3),
) )
# discv5 let disc2 = WakuDiscoveryV5.new(
let conf1 = WakuDiscoveryV5Config( node2.rng, conf2, some(node2.enr), some(node2.peerManager)
discv5Config: none(DiscoveryConfig), )
address: bindIp,
port: nodeUdpPort1,
privateKey: keys.PrivateKey(nodeKey1.skkey),
bootstrapRecords: @[],
autoupdateRecord: true,
)
let disc1 = await allFutures(node1.start(), node2.start(), node3.start())
WakuDiscoveryV5.new(node1.rng, conf1, some(node1.enr), some(node1.peerManager)) let resultDisc1StartRes = await disc1.start()
assert resultDisc1StartRes.isOk(), resultDisc1StartRes.error
let resultDisc2StartRes = await disc2.start()
assert resultDisc2StartRes.isOk(), resultDisc2StartRes.error
let conf2 = WakuDiscoveryV5Config( ## When
discv5Config: none(DiscoveryConfig), var attempts = 10
address: bindIp, while (disc1.protocol.nodesDiscovered < 1 or disc2.protocol.nodesDiscovered < 1) and
port: nodeUdpPort2, attempts > 0:
privateKey: keys.PrivateKey(nodeKey2.skkey), await sleepAsync(1.seconds)
bootstrapRecords: @[disc1.protocol.getRecord()], attempts -= 1
autoupdateRecord: true,
)
let disc2 = # node2 can be connected, so will be returned by peer exchange
WakuDiscoveryV5.new(node2.rng, conf2, some(node2.enr), some(node2.peerManager)) require (
await node1.peerManager.connectPeer(node2.switch.peerInfo.toRemotePeerInfo())
)
await allFutures(node1.start(), node2.start(), node3.start()) # Mount peer exchange
let resultDisc1StartRes = await disc1.start() await node1.mountPeerExchange()
assert resultDisc1StartRes.isOk(), resultDisc1StartRes.error await node3.mountPeerExchange()
let resultDisc2StartRes = await disc2.start()
assert resultDisc2StartRes.isOk(), resultDisc2StartRes.error
## When let dialResponse =
var attempts = 10 await node3.dialForPeerExchange(node1.switch.peerInfo.toRemotePeerInfo())
while (disc1.protocol.nodesDiscovered < 1 or disc2.protocol.nodesDiscovered < 1) and let response = dialResponse.get()
attempts > 0:
await sleepAsync(1.seconds)
attempts -= 1
# node2 can be connected, so will be returned by peer exchange ## Then
require ( check:
await node1.peerManager.connectPeer(node2.switch.peerInfo.toRemotePeerInfo()) response.get().peerInfos.len == 1
) response.get().peerInfos[0].enr == disc2.protocol.localNode.record.raw
# Mount peer exchange await allFutures(
await node1.mountPeerExchange() [node1.stop(), node2.stop(), node3.stop(), disc1.stop(), disc2.stop()]
await node3.mountPeerExchange() )
let dialResponse =
await node3.dialForPeerExchange(node1.switch.peerInfo.toRemotePeerInfo())
let response = dialResponse.get()
## Then
check:
response.get().peerInfos.len == 1
response.get().peerInfos[0].enr == disc2.protocol.localNode.record.raw
await allFutures(
[node1.stop(), node2.stop(), node3.stop(), disc1.stop(), disc2.stop()]
)
asyncTest "Request returns some discovered peers": asyncTest "Request returns some discovered peers":
let let

View File

@ -46,7 +46,7 @@ suite "Wakunode2 - Waku initialization":
var waku = (waitFor Waku.new(conf)).valueOr: var waku = (waitFor Waku.new(conf)).valueOr:
raiseAssert error raiseAssert error
(waitFor startWaku(addr waku)).isOkOr: (waitFor waku.start()).isOkOr:
raiseAssert error raiseAssert error
## Then ## Then
@ -71,7 +71,7 @@ suite "Wakunode2 - Waku initialization":
var waku = (waitFor Waku.new(conf)).valueOr: var waku = (waitFor Waku.new(conf)).valueOr:
raiseAssert error raiseAssert error
(waitFor startWaku(addr waku)).isOkOr: (waitFor waku.start()).isOkOr:
raiseAssert error raiseAssert error
## Then ## Then
@ -128,7 +128,7 @@ suite "Wakunode2 - Waku initialization":
(waitFor waku.stop()).isOkOr: (waitFor waku.stop()).isOkOr:
raiseAssert error raiseAssert error
(waitFor startWaku(addr waku)).isOkOr: (waitFor waku.start()).isOkOr:
raiseAssert error raiseAssert error
let portsJson = waku.stateInfo.getNodeInfoItem(NodeInfoId.MyBoundPorts) let portsJson = waku.stateInfo.getNodeInfoItem(NodeInfoId.MyBoundPorts)

View File

@ -59,19 +59,12 @@ requires "nim >= 2.2.4",
"unittest2" "unittest2"
# Packages not on nimble (use git URLs) # Packages not on nimble (use git URLs)
requires "https://github.com/logos-messaging/nim-ffi#v0.1.3" requires "https://github.com/logos-messaging/nim-ffi#v0.1.3"
requires "https://github.com/logos-messaging/nim-sds.git#2e9a7683f0e180bf112135fae3a3803eed8490d4" requires "https://github.com/logos-messaging/nim-sds.git#abdd40cc645f1b024c3ee99cced7e287c4e4c441"
# brokers: pinned by URL+commit rather than the bare `brokers >= 2.0.1` requires "https://github.com/NagyZoltanPeter/nim-brokers.git#v3.1.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-lsquic"
requires "https://github.com/vacp2p/nim-jwt.git#057ec95eb5af0eea9c49bfe9025b3312c95dc5f2" requires "https://github.com/vacp2p/nim-jwt.git#057ec95eb5af0eea9c49bfe9025b3312c95dc5f2"

View File

@ -1,9 +1,10 @@
import chronicles, chronos, results import chronicles, chronos, results
import waku/factory/waku import waku/factory/waku
import waku/messaging_client
import waku/[requests/health_requests, 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/send_service
import waku/node/delivery_service/subscription_manager import waku/node/subscription_manager
import libp2p/peerid import libp2p/peerid
import ../../tools/confutils/cli_args import ../../tools/confutils/cli_args
import ./[api_conf, types] import ./[api_conf, types]
@ -38,39 +39,15 @@ proc subscribe*(
): Future[Result[void, string]] {.async.} = ): Future[Result[void, string]] {.async.} =
?checkApiAvailability(w) ?checkApiAvailability(w)
return w.deliveryService.subscriptionManager.subscribe(contentTopic) return w.node.subscriptionManager.subscribe(contentTopic)
proc unsubscribe*(w: Waku, contentTopic: ContentTopic): Result[void, string] = proc unsubscribe*(w: Waku, contentTopic: ContentTopic): Result[void, string] =
?checkApiAvailability(w) ?checkApiAvailability(w)
return w.deliveryService.subscriptionManager.unsubscribe(contentTopic) return w.node.subscriptionManager.unsubscribe(contentTopic)
proc send*( proc send*(
w: Waku, envelope: MessageEnvelope w: Waku, envelope: MessageEnvelope
): Future[Result[RequestId, string]] {.async.} = ): Future[Result[RequestId, string]] {.async.} =
?checkApiAvailability(w) ?checkApiAvailability(w)
return await w.messagingClient.send(envelope)
let isSubbed = w.deliveryService.subscriptionManager
.isSubscribed(envelope.contentTopic)
.valueOr(false)
if not isSubbed:
info "Auto-subscribing to topic on send", contentTopic = envelope.contentTopic
w.deliveryService.subscriptionManager.subscribe(envelope.contentTopic).isOkOr:
warn "Failed to auto-subscribe", error = error
return err("Failed to auto-subscribe before sending: " & error)
let requestId = RequestId.new(w.rng)
let deliveryTask = DeliveryTask.new(requestId, envelope, w.brokerCtx).valueOr:
return err("API send: Failed to create delivery task: " & error)
info "API send: scheduling delivery task",
requestId = $requestId,
pubsubTopic = deliveryTask.pubsubTopic,
contentTopic = deliveryTask.msg.contentTopic,
msgHash = deliveryTask.msgHash.to0xHex(),
myPeerId = w.node.peerId()
asyncSpawn w.deliveryService.sendService.send(deliveryTask)
return ok(requestId)

View File

@ -15,6 +15,7 @@ import
../waku_enr, ../waku_enr,
../discovery/waku_discv5, ../discovery/waku_discv5,
../waku_node, ../waku_node,
../net/net_config,
../node/peer_manager, ../node/peer_manager,
../common/rate_limit/setting, ../common/rate_limit/setting,
../common/utils/parse_size_units ../common/utils/parse_size_units

View File

@ -17,6 +17,7 @@ import
./validator_signed, ./validator_signed,
../waku_enr/sharding, ../waku_enr/sharding,
../waku_node, ../waku_node,
../net/net_config,
../waku_core, ../waku_core,
../waku_core/codecs, ../waku_core/codecs,
../waku_rln_relay, ../waku_rln_relay,

View File

@ -30,12 +30,13 @@ import
waku_enr/sharding, waku_enr/sharding,
waku_enr/multiaddr, waku_enr/multiaddr,
api/types, api/types,
messaging_client,
common/logging, common/logging,
node/peer_manager, node/peer_manager,
node/health_monitor, node/health_monitor,
net/net_config,
node/waku_metrics, node/waku_metrics,
node/delivery_service/delivery_service, node/subscription_manager,
node/delivery_service/subscription_manager,
rest_api/message_cache, rest_api/message_cache,
rest_api/endpoint/server, rest_api/endpoint/server,
rest_api/endpoint/builder as rest_server_builder, rest_api/endpoint/builder as rest_server_builder,
@ -48,6 +49,7 @@ import
factory/app_callbacks, factory/app_callbacks,
persistency/persistency, persistency/persistency,
], ],
channels/reliable_channel_manager,
./waku_conf, ./waku_conf,
./waku_state_info ./waku_state_info
@ -73,7 +75,9 @@ type Waku* = ref object
healthMonitor*: NodeHealthMonitor healthMonitor*: NodeHealthMonitor
deliveryService*: DeliveryService messagingClient*: MessagingClient
reliableChannelManager*: ReliableChannelManager
restServer*: WakuRestServerRef restServer*: WakuRestServerRef
metricsServer*: MetricsHttpServerRef metricsServer*: MetricsHttpServerRef
@ -215,10 +219,6 @@ proc new*(
error "Failed setting up app callbacks", error = error error "Failed setting up app callbacks", error = error
return err("Failed setting up app callbacks: " & $error) return err("Failed setting up app callbacks: " & $error)
## Delivery Monitor
let deliveryService = DeliveryService.new(wakuConf.p2pReliability, node).valueOr:
return err("could not create delivery service: " & $error)
var waku = Waku( var waku = Waku(
stateInfo: WakuStateInfo.init(node), stateInfo: WakuStateInfo.init(node),
conf: wakuConf, conf: wakuConf,
@ -226,7 +226,6 @@ proc new*(
key: wakuConf.nodeKey, key: wakuConf.nodeKey,
node: node, node: node,
healthMonitor: healthMonitor, healthMonitor: healthMonitor,
deliveryService: deliveryService,
appCallbacks: appCallbacks, appCallbacks: appCallbacks,
restServer: restServer, restServer: restServer,
brokerCtx: brokerCtx, brokerCtx: brokerCtx,
@ -254,9 +253,9 @@ proc getPorts(
return ok((tcpPort: tcpPort, websocketPort: websocketPort)) return ok((tcpPort: tcpPort, websocketPort: websocketPort))
proc getRunningNetConfig(waku: ptr Waku): Future[Result[NetConfig, string]] {.async.} = proc getRunningNetConfig(waku: Waku): Future[Result[NetConfig, string]] {.async.} =
let conf = waku[].conf let conf = waku.conf
let (tcpPort, websocketPort) = getPorts(waku[].node.switch.peerInfo.listenAddrs).valueOr: let (tcpPort, websocketPort) = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr:
return err("Could not retrieve ports: " & error) return err("Could not retrieve ports: " & error)
if tcpPort.isSome(): if tcpPort.isSome():
@ -276,67 +275,67 @@ proc getRunningNetConfig(waku: ptr Waku): Future[Result[NetConfig, string]] {.as
return ok(netConf) return ok(netConf)
proc updateEnr(waku: ptr Waku): Future[Result[void, string]] {.async.} = proc updateEnr(waku: Waku): Future[Result[void, string]] {.async.} =
let netConf: NetConfig = (await getRunningNetConfig(waku)).valueOr: let netConf: NetConfig = (await getRunningNetConfig(waku)).valueOr:
return err("error calling updateNetConfig: " & $error) return err("error calling updateNetConfig: " & $error)
let record = enrConfiguration(waku[].conf, netConf).valueOr: let record = enrConfiguration(waku.conf, netConf).valueOr:
return err("ENR setup failed: " & error) return err("ENR setup failed: " & error)
if isClusterMismatched(record, waku[].conf.clusterId): if isClusterMismatched(record, waku.conf.clusterId):
return err("cluster-id mismatch configured shards") return err("cluster-id mismatch configured shards")
waku[].node.enr = record waku.node.enr = record
# If TCP/WS was configured with port 0, node.announcedAddresses was built # 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. # pre-bind with a port value of 0. In any case, the resync is harmless.
waku[].node.announcedAddresses = netConf.announcedAddresses waku.node.announcedAddresses = netConf.announcedAddresses
return ok() return ok()
proc updateAddressInENR(waku: ptr Waku): Result[void, string] = proc updateAddressInENR(waku: Waku): Result[void, string] =
let addresses: seq[MultiAddress] = waku[].node.announcedAddresses let addresses: seq[MultiAddress] = waku.node.announcedAddresses
let encodedAddrs = multiaddr.encodeMultiaddrs(addresses) let encodedAddrs = multiaddr.encodeMultiaddrs(addresses)
## First update the enr info contained in WakuNode ## First update the enr info contained in WakuNode
let keyBytes = waku[].key.getRawBytes().valueOr: let keyBytes = waku.key.getRawBytes().valueOr:
return err("failed to retrieve raw bytes from waku key: " & $error) return err("failed to retrieve raw bytes from waku key: " & $error)
let parsedPk = keys.PrivateKey.fromHex(keyBytes.toHex()).valueOr: let parsedPk = keys.PrivateKey.fromHex(keyBytes.toHex()).valueOr:
return err("failed to parse the private key: " & $error) return err("failed to parse the private key: " & $error)
let enrFields = @[toFieldPair(MultiaddrEnrField, encodedAddrs)] let enrFields = @[toFieldPair(MultiaddrEnrField, encodedAddrs)]
waku[].node.enr.update(parsedPk, extraFields = enrFields).isOkOr: waku.node.enr.update(parsedPk, extraFields = enrFields).isOkOr:
return err("failed to update multiaddress in ENR updateAddressInENR: " & $error) return err("failed to update multiaddress in ENR updateAddressInENR: " & $error)
info "Waku node ENR updated successfully with new multiaddress", info "Waku node ENR updated successfully with new multiaddress",
enr = waku[].node.enr.toUri(), record = $(waku[].node.enr) enr = waku.node.enr.toUri(), record = $(waku.node.enr)
## Now update the ENR infor in discv5 ## Now update the ENR infor in discv5
if not waku[].wakuDiscv5.isNil(): if not waku.wakuDiscv5.isNil():
waku[].wakuDiscv5.protocol.localNode.record = waku[].node.enr waku.wakuDiscv5.protocol.localNode.record = waku.node.enr
let enr = waku[].wakuDiscv5.protocol.localNode.record let enr = waku.wakuDiscv5.protocol.localNode.record
info "Waku discv5 ENR updated successfully with new multiaddress", info "Waku discv5 ENR updated successfully with new multiaddress",
enr = enr.toUri(), record = $(enr) enr = enr.toUri(), record = $(enr)
return ok() return ok()
proc updateWaku(waku: ptr Waku): Future[Result[void, string]] {.async.} = proc updateWaku(waku: Waku): Future[Result[void, string]] {.async.} =
(await updateEnr(waku)).isOkOr: (await updateEnr(waku)).isOkOr:
return err("error calling updateEnr: " & $error) return err("error calling updateEnr: " & $error)
?updateAnnouncedAddrWithPrimaryIpAddr(waku[].node) ?updateAnnouncedAddrWithPrimaryIpAddr(waku.node)
?updateAddressInENR(waku) ?updateAddressInENR(waku)
return ok() return ok()
proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} = proc startDnsDiscoveryRetryLoop(waku: Waku): Future[void] {.async.} =
while true: while true:
await sleepAsync(30.seconds) await sleepAsync(30.seconds)
if waku.conf.dnsDiscoveryConf.isSome(): if waku.conf.dnsDiscoveryConf.isSome():
let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get() let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get()
waku[].dynamicBootstrapNodes = ( waku.dynamicBootstrapNodes = (
await waku_dnsdisc.retrieveDynamicBootstrapNodes( await waku_dnsdisc.retrieveDynamicBootstrapNodes(
dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers
) )
@ -344,35 +343,61 @@ proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} =
error "Retrieving dynamic bootstrap nodes failed", error = error error "Retrieving dynamic bootstrap nodes failed", error = error
continue continue
if not waku[].wakuDiscv5.isNil(): if not waku.wakuDiscv5.isNil():
let dynamicBootstrapEnrs = waku[].dynamicBootstrapNodes let dynamicBootstrapEnrs =
.filterIt(it.hasUdpPort()) waku.dynamicBootstrapNodes.filterIt(it.hasUdpPort()).mapIt(it.enr.get().toUri())
.mapIt(it.enr.get().toUri())
var discv5BootstrapEnrs: seq[enr.Record] var discv5BootstrapEnrs: seq[enr.Record]
# parse enrURIs from the configuration and add the resulting ENRs to the discv5BootstrapEnrs seq # parse enrURIs from the configuration and add the resulting ENRs to the discv5BootstrapEnrs seq
for enrUri in dynamicBootstrapEnrs: for enrUri in dynamicBootstrapEnrs:
addBootstrapNode(enrUri, discv5BootstrapEnrs) addBootstrapNode(enrUri, discv5BootstrapEnrs)
waku[].wakuDiscv5.updateBootstrapRecords( waku.wakuDiscv5.updateBootstrapRecords(
waku[].wakuDiscv5.protocol.bootstrapRecords & discv5BootstrapEnrs waku.wakuDiscv5.protocol.bootstrapRecords & discv5BootstrapEnrs
) )
info "Connecting to dynamic bootstrap peers" info "Connecting to dynamic bootstrap peers"
try: try:
await connectToNodes( await connectToNodes(waku.node, waku.dynamicBootstrapNodes, "dynamic bootstrap")
waku[].node, waku[].dynamicBootstrapNodes, "dynamic bootstrap"
)
except CatchableError: except CatchableError:
error "failed to connect to dynamic bootstrap nodes: " & getCurrentExceptionMsg() error "failed to connect to dynamic bootstrap nodes: " & getCurrentExceptionMsg()
return return
proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: []).} = proc mountMessagingClient*(waku: Waku): Result[void, string] =
if waku[].node.started: if not waku.messagingClient.isNil():
warn "startWaku: waku node already started" return err("messaging client already mounted")
if waku.node.started:
return err("cannot mount messaging client on a started node")
waku.messagingClient = MessagingClient.new(waku.conf.p2pReliability, waku.node).valueOr:
return err("could not create messaging client: " & $error)
return ok()
proc mountReliableChannelManager*(waku: Waku): Result[void, string] =
if not waku.reliableChannelManager.isNil():
return err("reliable channel manager already mounted")
if waku.messagingClient.isNil():
return err("reliable channel manager requires a mounted messaging client")
if waku.node.started:
return err("cannot mount reliable channel manager on a started node")
let messagingClient = waku.messagingClient
let defaultSendHandler: SendHandler = proc(
envelope: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
return await messagingClient.send(envelope)
waku.reliableChannelManager = ReliableChannelManager.new(
messagingClient, defaultSendHandler, waku.brokerCtx
).valueOr:
return err("could not create reliable channel manager: " & $error)
return ok()
proc start*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} =
if waku.node.started:
warn "start: waku node already started"
return ok() return ok()
info "Retrieve dynamic bootstrap nodes" info "Retrieve dynamic bootstrap nodes"
let conf = waku[].conf let conf = waku.conf
if conf.dnsDiscoveryConf.isSome(): if conf.dnsDiscoveryConf.isSome():
let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get() let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get()
@ -390,9 +415,9 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
error "Retrieving dynamic bootstrap nodes failed", error "Retrieving dynamic bootstrap nodes failed",
error = dynamicBootstrapNodesRes.error error = dynamicBootstrapNodesRes.error
# Start Dns Discovery retry loop # Start Dns Discovery retry loop
waku[].dnsRetryLoopHandle = waku.startDnsDiscoveryRetryLoop() waku.dnsRetryLoopHandle = waku.startDnsDiscoveryRetryLoop()
else: else:
waku[].dynamicBootstrapNodes = dynamicBootstrapNodesRes.get() waku.dynamicBootstrapNodes = dynamicBootstrapNodesRes.get()
## Initialize persistency singleton instance - we don't need the instance itself here, ## Initialize persistency singleton instance - we don't need the instance itself here,
## but this ensures it's initialized before any store job starts. ## but this ensures it's initialized before any store job starts.
@ -405,12 +430,12 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
let bound = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr: let bound = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr:
return err("failed to read bound ports from switch: " & $error) return err("failed to read bound ports from switch: " & $error)
waku[].node.ports.tcp = bound.tcpPort.get(Port(0)).uint16 waku.node.ports.tcp = bound.tcpPort.get(Port(0)).uint16
waku[].node.ports.webSocket = bound.websocketPort.get(Port(0)).uint16 waku.node.ports.webSocket = bound.websocketPort.get(Port(0)).uint16
## Discv5 ## Discv5
if conf.discv5Conf.isSome(): if conf.discv5Conf.isSome():
waku[].wakuDiscV5 = ( waku.wakuDiscV5 = (
await waku_discv5.setupAndStartDiscv5( await waku_discv5.setupAndStartDiscv5(
waku.node.enr, waku.node.enr,
waku.node.peerManager, waku.node.peerManager,
@ -425,23 +450,21 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
).valueOr: ).valueOr:
return err("failed to start waku discovery v5: " & error) return err("failed to start waku discovery v5: " & error)
waku[].node.ports.discv5Udp = waku[].wakuDiscV5.udpPort.uint16 waku.node.ports.discv5Udp = waku.wakuDiscV5.udpPort.uint16
waku[].conf.discv5Conf.get().udpPort = waku[].wakuDiscV5.udpPort waku.conf.discv5Conf.get().udpPort = waku.wakuDiscV5.udpPort
## Update waku data that is set dynamically on node start ## Update waku data that is set dynamically on node start
try: try:
(await updateWaku(waku)).isOkOr: (await updateWaku(waku)).isOkOr:
return err("Error in startWaku: " & $error) return err("Error in start: " & $error)
except CatchableError: except CatchableError:
return err("Caught exception in startWaku: " & getCurrentExceptionMsg()) return err("Caught exception in start: " & getCurrentExceptionMsg())
## Reliability waku.node.subscriptionManager.subscribeAllAutoshards().isOkOr:
if not waku[].deliveryService.isNil(): return err("failed to auto-subscribe autosharding shards: " & $error)
waku[].deliveryService.startDeliveryService().isOkOr:
return err("failed to start delivery service: " & $error)
## Health Monitor ## Health Monitor
waku[].healthMonitor.startHealthMonitor().isOkOr: waku.healthMonitor.startHealthMonitor().isOkOr:
return err("failed to start health monitor: " & $error) return err("failed to start health monitor: " & $error)
## Setup RequestConnectionStatus provider ## Setup RequestConnectionStatus provider
@ -450,7 +473,7 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
globalBrokerContext(), globalBrokerContext(),
proc(): Result[RequestConnectionStatus, string] = proc(): Result[RequestConnectionStatus, string] =
try: try:
let healthReport = waku[].healthMonitor.getSyncNodeHealthReport() let healthReport = waku.healthMonitor.getSyncNodeHealthReport()
return return
ok(RequestConnectionStatus(connectionStatus: healthReport.connectionStatus)) ok(RequestConnectionStatus(connectionStatus: healthReport.connectionStatus))
except CatchableError: except CatchableError:
@ -467,7 +490,7 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
): Future[Result[RequestProtocolHealth, string]] {.async.} = ): Future[Result[RequestProtocolHealth, string]] {.async.} =
try: try:
let protocolHealthStatus = let protocolHealthStatus =
await waku[].healthMonitor.getProtocolHealthInfo(protocol) await waku.healthMonitor.getProtocolHealthInfo(protocol)
return ok(RequestProtocolHealth(healthStatus: protocolHealthStatus)) return ok(RequestProtocolHealth(healthStatus: protocolHealthStatus))
except CatchableError: except CatchableError:
return err("Failed to get protocol health: " & getCurrentExceptionMsg()), return err("Failed to get protocol health: " & getCurrentExceptionMsg()),
@ -480,7 +503,7 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
globalBrokerContext(), globalBrokerContext(),
proc(): Future[Result[RequestHealthReport, string]] {.async.} = proc(): Future[Result[RequestHealthReport, string]] {.async.} =
try: try:
let report = await waku[].healthMonitor.getNodeHealthReport() let report = await waku.healthMonitor.getNodeHealthReport()
return ok(RequestHealthReport(healthReport: report)) return ok(RequestHealthReport(healthReport: report))
except CatchableError: except CatchableError:
return err("Failed to get health report: " & getCurrentExceptionMsg()), return err("Failed to get health report: " & getCurrentExceptionMsg()),
@ -489,9 +512,9 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
if conf.restServerConf.isSome(): if conf.restServerConf.isSome():
rest_server_builder.startRestServerProtocolSupport( rest_server_builder.startRestServerProtocolSupport(
waku[].restServer, waku.restServer,
waku[].node, waku.node,
waku[].wakuDiscv5, waku.wakuDiscv5,
conf.restServerConf.get(), conf.restServerConf.get(),
conf.relay, conf.relay,
conf.lightPush, conf.lightPush,
@ -509,21 +532,27 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises:
) )
).valueOr: ).valueOr:
return err("Starting monitoring and external interfaces failed: " & error) return err("Starting monitoring and external interfaces failed: " & error)
waku[].metricsServer = server waku.metricsServer = server
waku[].node.ports.metrics = port.uint16 waku.node.ports.metrics = port.uint16
waku[].conf.metricsServerConf.get().httpPort = port waku.conf.metricsServerConf.get().httpPort = port
except CatchableError: except CatchableError:
return err( return err(
"Caught exception starting monitoring and external interfaces failed: " & "Caught exception starting monitoring and external interfaces failed: " &
getCurrentExceptionMsg() getCurrentExceptionMsg()
) )
waku[].healthMonitor.setOverallHealth(HealthStatus.READY) waku.healthMonitor.setOverallHealth(HealthStatus.READY)
if not waku.messagingClient.isNil():
waku.messagingClient.start().isOkOr:
return err("failed to start messaging client: " & $error)
if not waku.reliableChannelManager.isNil():
waku.reliableChannelManager.start().isOkOr:
return err("failed to start reliable channel manager: " & $error)
return ok() return ok()
proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} = proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} =
## Waku shutdown
if not waku.node.started: if not waku.node.started:
warn "stop: attempting to stop node that isn't running" warn "stop: attempting to stop node that isn't running"
@ -538,9 +567,11 @@ proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} =
if not waku.wakuDiscv5.isNil(): if not waku.wakuDiscv5.isNil():
await waku.wakuDiscv5.stop() await waku.wakuDiscv5.stop()
if not waku.deliveryService.isNil(): if not waku.reliableChannelManager.isNil():
await waku.deliveryService.stopDeliveryService() await waku.reliableChannelManager.stop()
waku.deliveryService = nil
if not waku.messagingClient.isNil():
await waku.messagingClient.stop()
if not waku.node.isNil(): if not waku.node.isNil():
await waku.node.stop() await waku.node.stop()

63
waku/messaging_client.nim Normal file
View File

@ -0,0 +1,63 @@
import results, chronos
import chronicles
import
./api/types,
./node/[
waku_node,
subscription_manager,
delivery_service/recv_service,
delivery_service/send_service,
delivery_service/send_service/delivery_task,
]
type MessagingClient* = ref object
node: WakuNode
sendService*: SendService
recvService*: RecvService
started: bool
proc new*(
T: type MessagingClient, useP2PReliability: bool, node: WakuNode
): Result[T, string] =
let sendService = ?SendService.new(useP2PReliability, node)
let recvService = RecvService.new(node)
ok(T(node: node, sendService: sendService, recvService: recvService))
proc start*(self: MessagingClient): Result[void, string] =
if self.started:
return ok()
self.recvService.startRecvService()
self.sendService.startSendService()
self.started = true
ok()
proc stop*(self: MessagingClient) {.async.} =
if not self.started:
return
await self.sendService.stopSendService()
await self.recvService.stopRecvService()
self.started = false
proc send*(
self: MessagingClient, envelope: MessageEnvelope
): Future[Result[RequestId, string]] {.async.} =
## High-level messaging API send. Auto-subscribes to the content topic
## (so the local node sees its own gossipsub broadcast), builds a
## `DeliveryTask`, and hands it to the send service. Returns the request
## id the caller can correlate with `MessageSentEvent` / `MessageErrorEvent`.
let isSubbed =
self.node.subscriptionManager.isSubscribed(envelope.contentTopic).valueOr(false)
if not isSubbed:
info "Auto-subscribing to topic on send", contentTopic = envelope.contentTopic
self.node.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(self.node.rng)
let deliveryTask = DeliveryTask.new(requestId, envelope, self.node.brokerCtx).valueOr:
return err("MessagingClient.send: Failed to create delivery task: " & error)
asyncSpawn self.sendService.send(deliveryTask)
return ok(requestId)

View File

@ -1,44 +0,0 @@
## This module helps to ensure the correct transmission and reception of messages
import results
import chronos, chronicles
import
./recv_service,
./send_service,
./subscription_manager,
waku/[
waku_core, waku_node, waku_store/client, waku_relay/protocol, waku_lightpush/client
]
type DeliveryService* = ref object
sendService*: SendService
recvService*: RecvService
subscriptionManager*: SubscriptionManager
proc new*(
T: type DeliveryService, useP2PReliability: bool, w: WakuNode
): Result[T, string] =
## storeClient is needed to give store visitility to DeliveryService
## wakuRelay and wakuLightpushClient are needed to give a mechanism to SendService to re-publish
let subscriptionManager = SubscriptionManager.new(w)
let sendService = ?SendService.new(useP2PReliability, w, subscriptionManager)
let recvService = RecvService.new(w, subscriptionManager)
return ok(
DeliveryService(
sendService: sendService,
recvService: recvService,
subscriptionManager: subscriptionManager,
)
)
proc startDeliveryService*(self: DeliveryService): Result[void, string] =
?self.subscriptionManager.startSubscriptionManager()
self.recvService.startRecvService()
self.sendService.startSendService()
return ok()
proc stopDeliveryService*(self: DeliveryService) {.async.} =
await self.sendService.stopSendService()
await self.recvService.stopRecvService()
await self.subscriptionManager.stopSubscriptionManager()

View File

@ -4,17 +4,17 @@
import std/[tables, sequtils, options, sets] import std/[tables, sequtils, options, sets]
import chronos, chronicles, libp2p/utility import chronos, chronicles, libp2p/utility
import ../[subscription_manager]
import brokers/broker_context import brokers/broker_context
import import
waku/[ waku/[
waku_core, waku_core,
waku_core/topics,
waku_store/client, waku_store/client,
waku_store/common, waku_store/common,
waku_filter_v2/client, waku_filter_v2/client,
waku_core/topics,
events/message_events, events/message_events,
waku_node, waku_node,
node/subscription_manager,
] ]
const StoreCheckPeriod = chronos.minutes(5) ## How often to perform store queries const StoreCheckPeriod = chronos.minutes(5) ## How often to perform store queries
@ -38,7 +38,6 @@ type RecvService* = ref object of RootObj
brokerCtx: BrokerContext brokerCtx: BrokerContext
node: WakuNode node: WakuNode
seenMsgListener: MessageSeenEventListener seenMsgListener: MessageSeenEventListener
subscriptionManager: SubscriptionManager
recentReceivedMsgs: seq[RecvMessage] recentReceivedMsgs: seq[RecvMessage]
@ -77,7 +76,9 @@ proc processIncomingMessage(
## or if the message is a duplicate (recently-seen). Otherwise, save it as ## or if the message is a duplicate (recently-seen). Otherwise, save it as
## recently-seen, emit a MessageReceivedEvent, and return true. ## recently-seen, emit a MessageReceivedEvent, and return true.
if not self.subscriptionManager.isSubscribed(pubsubTopic, message.contentTopic): if not self.node.subscriptionManager.isContentSubscribed(
pubsubTopic, message.contentTopic
):
trace "skipping message as I am not subscribed", trace "skipping message as I am not subscribed",
shard = pubsubTopic, contentTopic = message.contentTopic shard = pubsubTopic, contentTopic = message.contentTopic
return false return false
@ -101,7 +102,7 @@ proc checkStore*(self: RecvService) {.async.} =
self.endTimeToCheck = getNowInNanosecondTime() self.endTimeToCheck = getNowInNanosecondTime()
## query store and deliver new recovered messages per subscribed topic ## query store and deliver new recovered messages per subscribed topic
for pubsubTopic, contentTopics in self.subscriptionManager.subscribedTopics: for pubsubTopic, contentTopics in self.node.subscriptionManager.subscribedContentTopics:
let storeResp: StoreQueryResponse = ( let storeResp: StoreQueryResponse = (
await self.node.wakuStoreClient.queryToAny( await self.node.wakuStoreClient.queryToAny(
StoreQueryRequest( StoreQueryRequest(
@ -146,7 +147,7 @@ proc msgChecker(self: RecvService) {.async.} =
await sleepAsync(StoreCheckPeriod) await sleepAsync(StoreCheckPeriod)
await self.checkStore() await self.checkStore()
proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T = proc new*(T: typedesc[RecvService], node: WakuNode): T =
## The storeClient will help to acquire any possible missed messages ## The storeClient will help to acquire any possible missed messages
let now = getNowInNanosecondTime() let now = getNowInNanosecondTime()
@ -154,7 +155,6 @@ proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T =
node: node, node: node,
startTimeToCheck: now, startTimeToCheck: now,
brokerCtx: node.brokerCtx, brokerCtx: node.brokerCtx,
subscriptionManager: s,
recentReceivedMsgs: @[], recentReceivedMsgs: @[],
) )

View File

@ -6,10 +6,10 @@ import chronos, chronicles, libp2p/utility
import brokers/broker_context import brokers/broker_context
import import
./[send_processor, relay_processor, lightpush_processor, delivery_task], ./[send_processor, relay_processor, lightpush_processor, delivery_task],
../[subscription_manager],
waku/[ waku/[
waku_core, waku_core,
node/waku_node, node/waku_node,
node/subscription_manager,
node/peer_manager, node/peer_manager,
waku_store/client, waku_store/client,
waku_store/common, waku_store/common,
@ -58,7 +58,6 @@ type SendService* = ref object of RootObj
node: WakuNode node: WakuNode
checkStoreForMessages: bool checkStoreForMessages: bool
subscriptionManager: SubscriptionManager
proc setupSendProcessorChain( proc setupSendProcessorChain(
peerManager: PeerManager, peerManager: PeerManager,
@ -96,10 +95,7 @@ proc setupSendProcessorChain(
return ok(processors[0]) return ok(processors[0])
proc new*( proc new*(
T: typedesc[SendService], T: typedesc[SendService], preferP2PReliability: bool, w: WakuNode
preferP2PReliability: bool,
w: WakuNode,
s: SubscriptionManager,
): Result[T, string] = ): Result[T, string] =
if w.wakuRelay.isNil() and w.wakuLightpushClient.isNil(): if w.wakuRelay.isNil() and w.wakuLightpushClient.isNil():
return err( return err(
@ -120,7 +116,6 @@ proc new*(
sendProcessor: sendProcessorChain, sendProcessor: sendProcessorChain,
node: w, node: w,
checkStoreForMessages: checkStoreForMessages, checkStoreForMessages: checkStoreForMessages,
subscriptionManager: s,
) )
return ok(sendService) return ok(sendService)
@ -263,7 +258,7 @@ proc send*(self: SendService, task: DeliveryTask) {.async.} =
info "SendService.send: processing delivery task", info "SendService.send: processing delivery task",
requestId = task.requestId, msgHash = task.msgHash.to0xHex() requestId = task.requestId, msgHash = task.msgHash.to0xHex()
self.subscriptionManager.subscribe(task.msg.contentTopic).isOkOr: self.node.subscriptionManager.subscribe(task.msg.contentTopic).isOkOr:
error "SendService.send: failed to subscribe to content topic", error "SendService.send: failed to subscribe to content topic",
contentTopic = task.msg.contentTopic, error = error contentTopic = task.msg.contentTopic, error = error

View File

@ -1,596 +0,0 @@
import std/[sequtils, sets, tables, options, strutils], chronos, chronicles, results
import libp2p/[peerid, peerinfo]
import brokers/broker_context
import
waku/[
waku_core,
waku_core/topics,
waku_core/topics/sharding,
waku_node,
waku_relay,
waku_filter_v2/common as filter_common,
waku_filter_v2/client as filter_client,
waku_filter_v2/protocol as filter_protocol,
events/health_events,
events/peer_events,
requests/health_requests,
node/peer_manager,
node/health_monitor/topic_health,
node/health_monitor/connection_status,
]
# ---------------------------------------------------------------------------
# Logos Messaging API SubscriptionManager
#
# Maps all topic subscription intent and centralizes all consistency
# maintenance of the pubsub and content topic subscription model across
# the various network drivers that handle topics (Edge/Filter and Core/Relay).
# ---------------------------------------------------------------------------
type EdgeFilterSubState* = object
peers: seq[RemotePeerInfo]
## Filter service peers with confirmed subscriptions on this shard.
pending: seq[Future[void]] ## In-flight dial futures for peers not yet confirmed.
pendingPeers: HashSet[PeerId] ## PeerIds of peers currently being dialed.
currentHealth: TopicHealth
## Cached health derived from peers.len; updated on every peer set change.
func toTopicHealth*(peersCount: int): TopicHealth =
if peersCount >= HealthyThreshold:
TopicHealth.SUFFICIENTLY_HEALTHY
elif peersCount > 0:
TopicHealth.MINIMALLY_HEALTHY
else:
TopicHealth.UNHEALTHY
type SubscriptionManager* = ref object of RootObj
node: WakuNode
contentTopicSubs: Table[PubsubTopic, HashSet[ContentTopic]]
## Map of Shard to ContentTopic needed because e.g. WakuRelay is PubsubTopic only.
## A present key with an empty HashSet value means pubsubtopic already subscribed
## (via subscribePubsubTopics()) but there's no specific content topic interest yet.
edgeFilterSubStates*: Table[PubsubTopic, EdgeFilterSubState]
## Per-shard filter subscription state for edge mode.
edgeFilterWakeup: AsyncEvent
## Signalled when the edge filter sub loop should re-reconcile.
edgeFilterSubLoopFut: Future[void]
edgeFilterHealthLoopFut: Future[void]
peerEventListener: WakuPeerEventListener
## Listener for peer connect/disconnect events (edge filter wakeup).
iterator subscribedTopics*(
self: SubscriptionManager
): (PubsubTopic, HashSet[ContentTopic]) =
## Iterate over all subscribed content topics, batched per shard.
## This is guaranteed to return a non-empty `topics` (content topics) list on iteration.
for pubsub, topics in self.contentTopicSubs.pairs:
# We are iterating over subscribed content topics; if we are subscribed to
# a shard but have no subscription (interest) for any content topic in that
# shard, then avoid triggering an iteration that doesn't advance the intent
# to iterate over content topic subscriptions.
if topics.len == 0:
continue
yield (pubsub, topics)
proc edgeFilterPeerCount*(sm: SubscriptionManager, shard: PubsubTopic): int =
sm.edgeFilterSubStates.withValue(shard, state):
return state.peers.len
return 0
proc new*(T: typedesc[SubscriptionManager], node: WakuNode): T =
SubscriptionManager(
node: node, contentTopicSubs: initTable[PubsubTopic, HashSet[ContentTopic]]()
)
proc addContentTopicInterest(
self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic
): Result[void, string] =
var changed = false
if not self.contentTopicSubs.hasKey(shard):
self.contentTopicSubs[shard] = initHashSet[ContentTopic]()
changed = true
self.contentTopicSubs.withValue(shard, cTopics):
if not cTopics[].contains(topic):
cTopics[].incl(topic)
changed = true
if changed and not isNil(self.edgeFilterWakeup):
self.edgeFilterWakeup.fire()
return ok()
proc removeContentTopicInterest(
self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic
): Result[void, string] =
var changed = false
self.contentTopicSubs.withValue(shard, cTopics):
if cTopics[].contains(topic):
cTopics[].excl(topic)
changed = true
if cTopics[].len == 0 and isNil(self.node.wakuRelay):
self.contentTopicSubs.del(shard) # We're done with cTopics here
if changed and not isNil(self.edgeFilterWakeup):
self.edgeFilterWakeup.fire()
return ok()
proc subscribePubsubTopics(
self: SubscriptionManager, shards: seq[PubsubTopic]
): Result[void, string] =
if isNil(self.node.wakuRelay):
return err("subscribePubsubTopics requires a Relay")
var errors: seq[string]
for shard in shards:
if not self.contentTopicSubs.hasKey(shard):
self.node.subscribe((kind: PubsubSub, topic: shard), nil).isOkOr:
errors.add("shard " & shard & ": " & error)
continue
self.contentTopicSubs[shard] = initHashSet[ContentTopic]()
if errors.len > 0:
return err("subscribeShard errors: " & errors.join("; "))
return ok()
proc getShardForContentTopic(
self: SubscriptionManager, topic: ContentTopic
): Result[PubsubTopic, string] =
if self.node.wakuAutoSharding.isSome():
let shardObj = ?self.node.wakuAutoSharding.get().getShard(topic)
return ok($shardObj)
return err("SubscriptionManager requires AutoSharding")
proc isSubscribed*(
self: SubscriptionManager, topic: ContentTopic
): Result[bool, string] =
let shard = ?self.getShardForContentTopic(topic)
return ok(
self.contentTopicSubs.hasKey(shard) and self.contentTopicSubs[shard].contains(topic)
)
proc isSubscribed*(
self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic
): bool {.raises: [].} =
self.contentTopicSubs.withValue(shard, cTopics):
return cTopics[].contains(contentTopic)
return false
proc subscribe*(self: SubscriptionManager, topic: ContentTopic): Result[void, string] =
if isNil(self.node.wakuRelay) and isNil(self.node.wakuFilterClient):
return err("SubscriptionManager requires either Relay or Filter Client.")
let shard = ?self.getShardForContentTopic(topic)
if not isNil(self.node.wakuRelay) and not self.contentTopicSubs.hasKey(shard):
?self.subscribePubsubTopics(@[shard])
?self.addContentTopicInterest(shard, topic)
return ok()
proc unsubscribe*(
self: SubscriptionManager, topic: ContentTopic
): Result[void, string] =
if isNil(self.node.wakuRelay) and isNil(self.node.wakuFilterClient):
return err("SubscriptionManager requires either Relay or Filter Client.")
let shard = ?self.getShardForContentTopic(topic)
if self.isSubscribed(shard, topic):
?self.removeContentTopicInterest(shard, topic)
return ok()
# ---------------------------------------------------------------------------
# Edge Filter driver for the Logos Messaging API
#
# The SubscriptionManager absorbs natively the responsibility of using the
# Edge Filter protocol to effect subscriptions and message receipt for edge.
# ---------------------------------------------------------------------------
const EdgeFilterSubscribeTimeout = chronos.seconds(15)
## Timeout for a single filter subscribe/unsubscribe RPC to a service peer.
const EdgeFilterPingTimeout = chronos.seconds(5)
## Timeout for a filter ping health check.
const EdgeFilterLoopInterval = chronos.seconds(30)
## Interval for the edge filter health ping loop.
const EdgeFilterSubLoopDebounce = chronos.seconds(1)
## Debounce delay to coalesce rapid-fire wakeups into a single reconciliation pass.
type EdgeDialTask = object
peer: RemotePeerInfo
shard: PubsubTopic
topics: seq[ContentTopic]
proc updateShardHealth(
self: SubscriptionManager, shard: PubsubTopic, state: var EdgeFilterSubState
) =
## Recompute and emit health for a shard after its peer set changed.
let newHealth = toTopicHealth(state.peers.len)
if newHealth != state.currentHealth:
state.currentHealth = newHealth
EventShardTopicHealthChange.emit(self.node.brokerCtx, shard, newHealth)
proc removePeer(self: SubscriptionManager, shard: PubsubTopic, peerId: PeerId) =
## Remove a peer from edgeFilterSubStates for the given shard,
## update health, and wake the sub loop to dial a replacement.
## Best-effort unsubscribe so the service peer stops pushing to us.
self.edgeFilterSubStates.withValue(shard, state):
var peer: RemotePeerInfo
var found = false
for p in state.peers:
if p.peerId == peerId:
peer = p
found = true
break
if not found:
return
state.peers.keepItIf(it.peerId != peerId)
self.updateShardHealth(shard, state[])
self.edgeFilterWakeup.fire()
if not self.node.wakuFilterClient.isNil():
self.contentTopicSubs.withValue(shard, topics):
let ct = toSeq(topics[])
if ct.len > 0:
proc doUnsubscribe() {.async.} =
discard await self.node.wakuFilterClient.unsubscribe(peer, shard, ct)
asyncSpawn doUnsubscribe()
type SendChunkedFilterRpcKind = enum
FilterSubscribe
FilterUnsubscribe
proc sendChunkedFilterRpc(
self: SubscriptionManager,
peer: RemotePeerInfo,
shard: PubsubTopic,
topics: seq[ContentTopic],
kind: SendChunkedFilterRpcKind,
): Future[bool] {.async.} =
## Send a chunked filter subscribe or unsubscribe RPC. Returns true on
## success. On failure the peer is removed and false is returned.
try:
var i = 0
while i < topics.len:
let chunk =
topics[i ..< min(i + filter_protocol.MaxContentTopicsPerRequest, topics.len)]
let fut =
case kind
of FilterSubscribe:
self.node.wakuFilterClient.subscribe(peer, shard, chunk)
of FilterUnsubscribe:
self.node.wakuFilterClient.unsubscribe(peer, shard, chunk)
if not (await fut.withTimeout(EdgeFilterSubscribeTimeout)) or fut.read().isErr():
trace "sendChunkedFilterRpc: chunk failed",
op = kind, shard = shard, peer = peer.peerId
self.removePeer(shard, peer.peerId)
return false
i += filter_protocol.MaxContentTopicsPerRequest
except CatchableError as exc:
debug "sendChunkedFilterRpc: failed",
op = kind, shard = shard, peer = peer.peerId, err = exc.msg
self.removePeer(shard, peer.peerId)
return false
return true
proc syncFilterDeltas(
self: SubscriptionManager,
peer: RemotePeerInfo,
shard: PubsubTopic,
added: seq[ContentTopic],
removed: seq[ContentTopic],
) {.async.} =
## Push content topic changes (adds/removes) to an already-tracked peer.
if added.len > 0:
if not await self.sendChunkedFilterRpc(peer, shard, added, FilterSubscribe):
return
if removed.len > 0:
discard await self.sendChunkedFilterRpc(peer, shard, removed, FilterUnsubscribe)
proc dialFilterPeer(
self: SubscriptionManager,
peer: RemotePeerInfo,
shard: PubsubTopic,
contentTopics: seq[ContentTopic],
) {.async.} =
## Subscribe a new peer to all content topics on a shard and start tracking it.
self.edgeFilterSubStates.withValue(shard, state):
state.pendingPeers.incl(peer.peerId)
try:
if not await self.sendChunkedFilterRpc(peer, shard, contentTopics, FilterSubscribe):
return
self.edgeFilterSubStates.withValue(shard, state):
if state.peers.anyIt(it.peerId == peer.peerId):
trace "dialFilterPeer: peer already tracked, skipping duplicate",
shard = shard, peer = peer.peerId
return
state.peers.add(peer)
self.updateShardHealth(shard, state[])
trace "dialFilterPeer: successfully subscribed to all chunks",
shard = shard, peer = peer.peerId, totalPeers = state.peers.len
do:
trace "dialFilterPeer: shard removed while subscribing, discarding result",
shard = shard, peer = peer.peerId
finally:
self.edgeFilterSubStates.withValue(shard, state):
state.pendingPeers.excl(peer.peerId)
proc edgeFilterHealthLoop*(self: SubscriptionManager) {.async.} =
## Periodically pings all connected filter service peers to verify they are
## still alive at the application layer. Peers that fail the ping are removed.
while true:
await sleepAsync(EdgeFilterLoopInterval)
if self.node.wakuFilterClient.isNil():
warn "filter client is nil within edge filter health loop"
continue
var connected = initTable[PeerId, RemotePeerInfo]()
for state in self.edgeFilterSubStates.values:
for peer in state.peers:
if self.node.peerManager.switch.peerStore.isConnected(peer.peerId):
connected[peer.peerId] = peer
var alive = initHashSet[PeerId]()
if connected.len > 0:
var pingTasks: seq[(PeerId, Future[FilterSubscribeResult])]
for peer in connected.values:
pingTasks.add(
(peer.peerId, self.node.wakuFilterClient.ping(peer, EdgeFilterPingTimeout))
)
# extract future tasks from (PeerId, Future) tuples and await them
await allFutures(pingTasks.mapIt(it[1]))
for (peerId, task) in pingTasks:
if task.read().isOk():
alive.incl(peerId)
var changed = false
for shard, state in self.edgeFilterSubStates.mpairs:
let oldLen = state.peers.len
state.peers.keepItIf(it.peerId notin connected or alive.contains(it.peerId))
if state.peers.len < oldLen:
changed = true
self.updateShardHealth(shard, state)
trace "Edge Filter health degraded by Ping failure",
shard = shard, new = state.currentHealth
if changed:
self.edgeFilterWakeup.fire()
proc selectFilterCandidates(
self: SubscriptionManager, shard: PubsubTopic, exclude: HashSet[PeerId], needed: int
): seq[RemotePeerInfo] =
## Select filter service peer candidates for a shard.
# Start with every filter server peer that can serve the shard
var allCandidates = self.node.peerManager.selectPeers(
filter_common.WakuFilterSubscribeCodec, some(shard)
)
# Remove all already used in this shard or being dialed for it
allCandidates.keepItIf(it.peerId notin exclude)
# Collect peer IDs already tracked on other shards
var trackedOnOther = initHashSet[PeerId]()
for otherShard, otherState in self.edgeFilterSubStates.pairs:
if otherShard != shard:
for peer in otherState.peers:
trackedOnOther.incl(peer.peerId)
# Prefer peers we already have a connection to first, preserving shuffle
var candidates =
allCandidates.filterIt(it.peerId in trackedOnOther) &
allCandidates.filterIt(it.peerId notin trackedOnOther)
# We need to return 'needed' peers only
if candidates.len > needed:
candidates.setLen(needed)
return candidates
proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} =
## Reconciles filter subscriptions with the desired state from SubscriptionManager.
var lastSynced = initTable[PubsubTopic, HashSet[ContentTopic]]()
while true:
await self.edgeFilterWakeup.wait()
await sleepAsync(EdgeFilterSubLoopDebounce)
self.edgeFilterWakeup.clear()
trace "edgeFilterSubLoop: woke up"
if isNil(self.node.wakuFilterClient):
trace "edgeFilterSubLoop: wakuFilterClient is nil, skipping"
continue
let desired = self.contentTopicSubs
trace "edgeFilterSubLoop: desired state", numShards = desired.len
let allShards = toHashSet(toSeq(desired.keys)) + toHashSet(toSeq(lastSynced.keys))
# Step 1: read state across all shards at once and
# create a list of peer dial tasks and shard tracking to delete.
var dialTasks: seq[EdgeDialTask]
var shardsToDelete: seq[PubsubTopic]
for shard in allShards:
let currTopics = desired.getOrDefault(shard)
let prevTopics = lastSynced.getOrDefault(shard)
if shard notin self.edgeFilterSubStates:
self.edgeFilterSubStates[shard] =
EdgeFilterSubState(currentHealth: TopicHealth.UNHEALTHY)
let addedTopics = toSeq(currTopics - prevTopics)
let removedTopics = toSeq(prevTopics - currTopics)
self.edgeFilterSubStates.withValue(shard, state):
state.peers.keepItIf(
self.node.peerManager.switch.peerStore.isConnected(it.peerId)
)
state.pending.keepItIf(not it.finished)
if addedTopics.len > 0 or removedTopics.len > 0:
for peer in state.peers:
asyncSpawn self.syncFilterDeltas(peer, shard, addedTopics, removedTopics)
if currTopics.len == 0:
shardsToDelete.add(shard)
else:
self.updateShardHealth(shard, state[])
let needed = max(0, HealthyThreshold - state.peers.len - state.pending.len)
if needed > 0:
let tracked = state.peers.mapIt(it.peerId).toHashSet() + state.pendingPeers
let candidates = self.selectFilterCandidates(shard, tracked, needed)
let toDial = min(needed, candidates.len)
trace "edgeFilterSubLoop: shard reconciliation",
shard = shard,
num_peers = state.peers.len,
num_pending = state.pending.len,
num_needed = needed,
num_available = candidates.len,
toDial = toDial
for i in 0 ..< toDial:
dialTasks.add(
EdgeDialTask(
peer: candidates[i], shard: shard, topics: toSeq(currTopics)
)
)
# Step 2: execute deferred shard tracking deletion and dial tasks.
for shard in shardsToDelete:
self.edgeFilterSubStates.withValue(shard, state):
for fut in state.pending:
if not fut.finished:
await fut.cancelAndWait()
self.edgeFilterSubStates.del(shard)
for task in dialTasks:
let fut = self.dialFilterPeer(task.peer, task.shard, task.topics)
self.edgeFilterSubStates.withValue(task.shard, state):
state.pending.add(fut)
lastSynced = desired
proc startEdgeFilterLoops(self: SubscriptionManager): Result[void, string] =
## Start the edge filter orchestration loops.
## Caller must ensure this is only called in edge mode (relay nil, filter client present).
self.edgeFilterWakeup = newAsyncEvent()
self.peerEventListener = WakuPeerEvent.listen(
self.node.brokerCtx,
proc(evt: WakuPeerEvent) {.async: (raises: []), gcsafe.} =
if evt.kind == WakuPeerEventKind.EventDisconnected or
evt.kind == WakuPeerEventKind.EventMetadataUpdated:
self.edgeFilterWakeup.fire()
,
).valueOr:
return err("Failed to listen to peer events for edge filter: " & error)
self.edgeFilterSubLoopFut = self.edgeFilterSubLoop()
self.edgeFilterHealthLoopFut = self.edgeFilterHealthLoop()
return ok()
proc stopEdgeFilterLoops(self: SubscriptionManager) {.async: (raises: []).} =
## Stop the edge filter orchestration loops and clean up pending futures.
if not isNil(self.edgeFilterSubLoopFut):
await self.edgeFilterSubLoopFut.cancelAndWait()
self.edgeFilterSubLoopFut = nil
if not isNil(self.edgeFilterHealthLoopFut):
await self.edgeFilterHealthLoopFut.cancelAndWait()
self.edgeFilterHealthLoopFut = nil
for shard, state in self.edgeFilterSubStates:
for fut in state.pending:
if not fut.finished:
await fut.cancelAndWait()
await WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener)
# ---------------------------------------------------------------------------
# SubscriptionManager Lifecycle (calls Edge behavior above)
#
# startSubscriptionManager and stopSubscriptionManager orchestrate both the
# core (relay) and edge (filter) paths, and register/clear broker providers.
# ---------------------------------------------------------------------------
proc startSubscriptionManager*(self: SubscriptionManager): Result[void, string] =
# Register edge filter broker providers. The shard/content health providers
# in WakuNode query these via the broker as a fallback when relay health is
# not available. If edge mode is not active, these providers simply return
# NOT_SUBSCRIBED / strength 0, which is harmless.
RequestEdgeShardHealth.setProvider(
self.node.brokerCtx,
proc(shard: PubsubTopic): Result[RequestEdgeShardHealth, string] =
self.edgeFilterSubStates.withValue(shard, state):
return ok(RequestEdgeShardHealth(health: state.currentHealth))
return ok(RequestEdgeShardHealth(health: TopicHealth.NOT_SUBSCRIBED)),
).isOkOr:
error "Can't set provider for RequestEdgeShardHealth", error = error
RequestEdgeFilterPeerCount.setProvider(
self.node.brokerCtx,
proc(): Result[RequestEdgeFilterPeerCount, string] =
var minPeers = high(int)
for state in self.edgeFilterSubStates.values:
minPeers = min(minPeers, state.peers.len)
if minPeers == high(int):
minPeers = 0
return ok(RequestEdgeFilterPeerCount(peerCount: minPeers)),
).isOkOr:
error "Can't set provider for RequestEdgeFilterPeerCount", error = error
if self.node.wakuRelay.isNil():
return self.startEdgeFilterLoops()
# Core mode: auto-subscribe relay to all shards in autosharding.
if self.node.wakuAutoSharding.isSome():
let autoSharding = self.node.wakuAutoSharding.get()
let clusterId = autoSharding.clusterId
let numShards = autoSharding.shardCountGenZero
if numShards > 0:
var clusterPubsubTopics = newSeqOfCap[PubsubTopic](numShards)
for i in 0 ..< numShards:
let shardObj = RelayShard(clusterId: clusterId, shardId: uint16(i))
clusterPubsubTopics.add(PubsubTopic($shardObj))
self.subscribePubsubTopics(clusterPubsubTopics).isOkOr:
error "Failed to auto-subscribe Relay to cluster shards: ", error = error
else:
info "SubscriptionManager has no AutoSharding configured; skipping auto-subscribe."
return ok()
proc stopSubscriptionManager*(self: SubscriptionManager) {.async: (raises: []).} =
if self.node.wakuRelay.isNil():
await self.stopEdgeFilterLoops()
RequestEdgeShardHealth.clearProvider(self.node.brokerCtx)
RequestEdgeFilterPeerCount.clearProvider(self.node.brokerCtx)

View File

@ -0,0 +1,13 @@
{.push raises: [].}
import std/sets
import chronos, libp2p/peerid
import ../waku_core, ./health_monitor/topic_health
type EdgeFilterSubState* = object
peers*: seq[RemotePeerInfo]
pending*: seq[Future[void]]
pendingPeers*: HashSet[PeerId]
currentHealth*: TopicHealth
{.pop.}

View File

@ -14,8 +14,9 @@ import
events/health_events, events/health_events,
events/peer_events, events/peer_events,
node/waku_node, node/waku_node,
node/node_telemetry,
node/peer_manager, node/peer_manager,
node/kernel_api, node/waku_node/ping,
node/health_monitor/online_monitor, node/health_monitor/online_monitor,
node/health_monitor/health_status, node/health_monitor/health_status,
node/health_monitor/health_report, node/health_monitor/health_report,

View File

@ -1,9 +0,0 @@
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

View File

@ -0,0 +1,27 @@
{.push raises: [].}
import metrics
declarePublicGauge waku_version,
"Waku version info (in git describe format)", ["version"]
declarePublicCounter waku_node_errors, "number of wakunode errors", ["type"]
declarePublicGauge waku_lightpush_peers, "number of lightpush peers"
declarePublicGauge waku_filter_peers, "number of filter peers"
declarePublicGauge waku_store_peers, "number of store peers"
declarePublicGauge waku_px_peers,
"number of peers (in the node's peerManager) supporting the peer exchange protocol"
declarePublicCounter waku_node_messages, "number of messages received", ["type"]
declarePublicHistogram waku_histogram_message_size,
"message size histogram in kB",
buckets = [
0.0, 1.0, 3.0, 5.0, 15.0, 50.0, 75.0, 100.0, 125.0, 150.0, 500.0, 700.0, 1000.0, Inf
]
{.pop.}

View File

@ -0,0 +1,11 @@
{.push raises: [].}
import std/sets
import ../waku_core
type ShardSubscription* = object
contentTopics*: HashSet[ContentTopic]
directShardSub*: bool
## shard subscribed directly (PubsubSub), independent of content-topic interest
{.pop.}

View File

@ -0,0 +1,711 @@
import std/[sequtils, sets, tables, options], chronos, chronicles, metrics, results
import libp2p/[peerid, peerinfo]
import brokers/broker_context
import
waku/[
waku_core,
waku_core/topics/sharding,
node/waku_node,
node/node_telemetry,
waku_relay,
waku_archive,
waku_store_sync,
waku_filter_v2/common as filter_common,
waku_filter_v2/client as filter_client,
waku_filter_v2/protocol as filter_protocol,
events/health_events,
events/message_events,
events/peer_events,
requests/health_requests,
node/peer_manager,
node/health_monitor/topic_health,
node/health_monitor/connection_status,
]
{.push raises: [].}
proc registerRelayHandler(
node: WakuNode, shard: PubsubTopic, appHandler: WakuRelayHandler = nil
): bool =
## Returns true iff we did a new (and only) subscription for this shard in GossipSub.
let alreadySubscribed = node.wakuRelay.isSubscribed(shard)
if not appHandler.isNil():
if not alreadySubscribed or not node.legacyAppHandlers.hasKey(shard):
node.legacyAppHandlers[shard] = appHandler
else:
debug "Legacy appHandler already exists for active shard, ignoring new handler",
shard
if alreadySubscribed:
return false
proc traceHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
let msgSizeKB = msg.payload.len / 1000
waku_node_messages.inc(labelValues = ["relay"])
waku_histogram_message_size.observe(msgSizeKB)
proc filterHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuFilter.isNil():
return
await node.wakuFilter.handleMessage(topic, msg)
proc archiveHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuArchive.isNil():
return
await node.wakuArchive.handleMessage(topic, msg)
proc syncHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuStoreReconciliation.isNil():
return
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.} =
await traceHandler(topic, msg)
await filterHandler(topic, msg)
await archiveHandler(topic, msg)
await syncHandler(topic, msg)
await internalHandler(topic, msg)
if node.legacyAppHandlers.hasKey(topic) and not node.legacyAppHandlers[topic].isNil():
await node.legacyAppHandlers[topic](topic, msg)
node.wakuRelay.subscribe(shard, uniqueTopicHandler)
return true
proc unregisterRelayHandler(node: WakuNode, shard: PubsubTopic): bool =
## Returns true iff we had a subscription for this shard in GossipSub and it was removed.
if node.legacyAppHandlers.hasKey(shard):
node.legacyAppHandlers.del(shard)
if node.wakuRelay.isSubscribed(shard):
node.wakuRelay.unsubscribe(shard)
return true
return false
proc doRelaySubscribe(
node: WakuNode, shard: PubsubTopic, appHandler: WakuRelayHandler = nil
): bool =
## Subscribes the node to a shard.
## Returns true if we actually subscribed (transitioned from unsubscribed to subscribed).
## Emit the shard subscription event if we actually subscribed.
let installed = node.registerRelayHandler(shard, appHandler)
if installed:
node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: shard))
return installed
proc doRelayUnsubscribe(node: WakuNode, shard: PubsubTopic): bool =
## Unsubscribes the node from a shard.
## Returns true if we actually unsubscribed (transitioned from subscribed to unsubscribed).
## Emit the shard unsubscription event if we actually unsubscribed.
let unsubscribed = node.unregisterRelayHandler(shard)
if unsubscribed:
node.topicSubscriptionQueue.emit((kind: PubsubUnsub, topic: shard))
return unsubscribed
proc new*(T: type SubscriptionManager, node: WakuNode): T =
T(
node: node,
shards: initTable[PubsubTopic, ShardSubscription](),
edgeFilterSubStates: initTable[PubsubTopic, EdgeFilterSubState](),
edgeFilterWakeup: newAsyncEvent(),
)
func wanted(entry: ShardSubscription): bool =
## True if the shard has content-topic interest or a direct subscription.
return entry.contentTopics.len > 0 or entry.directShardSub
proc isContentSubscribed*(
self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic
): bool =
self.shards.withValue(shard, sub):
return contentTopic in sub.contentTopics
return false
iterator subscribedContentTopics*(
self: SubscriptionManager
): (PubsubTopic, HashSet[ContentTopic]) =
## Yields each shard with its non-empty content-topic set.
for shard, sub in self.shards.pairs:
if sub.contentTopics.len > 0:
yield (shard, sub.contentTopics)
func toTopicHealth*(peersCount: int): TopicHealth =
if peersCount >= HealthyThreshold:
return TopicHealth.SUFFICIENTLY_HEALTHY
elif peersCount > 0:
return TopicHealth.MINIMALLY_HEALTHY
else:
return TopicHealth.UNHEALTHY
proc edgeFilterPeerCount*(self: SubscriptionManager, shard: PubsubTopic): int =
self.edgeFilterSubStates.withValue(shard, state):
return state.peers.len
return 0
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("autosharding is not configured; pass an explicit shard")
proc subscribeShard*(
self: SubscriptionManager, shard: PubsubTopic, handler: WakuRelayHandler = nil
): Result[void, string] =
## Subscribes to the shard directly and joins the relay mesh.
var added = false
self.shards.withValue(shard, entry):
if not entry.directShardSub:
entry.directShardSub = true
added = true
do:
self.shards[shard] = ShardSubscription(
contentTopics: initHashSet[ContentTopic](), directShardSub: true
)
added = true
if added:
self.edgeFilterWakeup.fire()
if not isNil(self.node.wakuRelay):
discard self.node.doRelaySubscribe(shard, handler)
return ok()
proc unsubscribeShard*(
self: SubscriptionManager, shard: PubsubTopic
): Result[void, string] =
## Drops the direct shard subscription; unsubscribes the mesh if no content topic wants it.
var removed = false
var shardEmpty = false
self.shards.withValue(shard, entry):
if entry.directShardSub:
entry.directShardSub = false
removed = true
shardEmpty = not entry[].wanted()
if removed:
self.edgeFilterWakeup.fire()
if shardEmpty:
self.shards.del(shard)
if not isNil(self.node.wakuRelay):
discard self.node.doRelayUnsubscribe(shard)
return ok()
proc subscribe*(
self: SubscriptionManager,
shard: PubsubTopic,
contentTopic: ContentTopic,
handler: WakuRelayHandler = nil,
): Result[void, string] =
## Adds content-topic interest on the shard and joins the relay mesh.
var added = false
self.shards.withValue(shard, entry):
if contentTopic notin entry.contentTopics:
entry.contentTopics.incl(contentTopic)
added = true
do:
var entry = ShardSubscription(contentTopics: initHashSet[ContentTopic]())
entry.contentTopics.incl(contentTopic)
self.shards[shard] = entry
added = true
if added:
self.edgeFilterWakeup.fire()
if not isNil(self.node.wakuRelay):
discard self.node.doRelaySubscribe(shard, handler)
return ok()
proc unsubscribe*(
self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic
): Result[void, string] =
## Drops content-topic interest on the shard; unsubscribes the mesh if nothing else wants it.
var removed = false
var shardEmpty = false
self.shards.withValue(shard, entry):
if contentTopic in entry.contentTopics:
entry.contentTopics.excl(contentTopic)
removed = true
shardEmpty = not entry[].wanted()
if removed:
self.edgeFilterWakeup.fire()
if shardEmpty:
self.shards.del(shard)
if not isNil(self.node.wakuRelay):
discard self.node.doRelayUnsubscribe(shard)
return ok()
proc subscribe*(self: SubscriptionManager, topic: ContentTopic): Result[void, string] =
## Subscribes to a content topic, resolving its shard via autosharding.
let shard = ?self.getShardForContentTopic(topic)
return self.subscribe(shard, topic)
proc unsubscribe*(
self: SubscriptionManager, topic: ContentTopic
): Result[void, string] =
## Unsubscribes from a content topic, resolving its shard via autosharding.
let shard = ?self.getShardForContentTopic(topic)
return self.unsubscribe(shard, topic)
proc unsubscribeAll*(
self: SubscriptionManager, shard: PubsubTopic
): Result[void, string] =
## Drops every content topic on the shard, then the direct subscription.
var snapshot: seq[ContentTopic]
self.shards.withValue(shard, sub):
snapshot = toSeq(sub.contentTopics)
for contentTopic in snapshot:
?self.unsubscribe(shard, contentTopic)
return self.unsubscribeShard(shard)
proc isSubscribed*(
self: SubscriptionManager, topic: ContentTopic
): Result[bool, string] =
let shard = ?self.getShardForContentTopic(topic)
return ok(self.isContentSubscribed(shard, topic))
proc subscribeAllAutoshards*(self: SubscriptionManager): Result[void, string] =
## Subscribes the relay to every shard in the configured autosharding cluster.
if self.node.wakuRelay.isNil() or self.node.wakuAutoSharding.isNone():
return ok()
let autoSharding = self.node.wakuAutoSharding.get()
let numShards = autoSharding.shardCountGenZero
if numShards == 0:
return ok()
for i in 0'u32 ..< numShards:
let shardObj = RelayShard(clusterId: autoSharding.clusterId, shardId: uint16(i))
self.subscribeShard(PubsubTopic($shardObj)).isOkOr:
error "failed to auto-subscribe relay to cluster shard",
shard = $shardObj, error = error
ok()
{.pop.}
const EdgeFilterSubscribeTimeout = chronos.seconds(15)
## Timeout for a single filter subscribe/unsubscribe RPC to a service peer.
const EdgeFilterPingTimeout = chronos.seconds(5)
## Timeout for a filter ping health check.
const EdgeFilterLoopInterval = chronos.seconds(30)
## Interval for the edge filter health ping loop.
const EdgeFilterSubLoopDebounce = chronos.seconds(1)
## Debounce delay to coalesce rapid-fire wakeups into a single reconciliation pass.
type EdgeFilterSubscribeTask = object
peer: RemotePeerInfo
shard: PubsubTopic
topics: seq[ContentTopic]
proc updateShardHealth(
self: SubscriptionManager, shard: PubsubTopic, state: var EdgeFilterSubState
) =
## Recompute and emit health for a shard after its peer set changed.
let newHealth = toTopicHealth(state.peers.len)
if newHealth != state.currentHealth:
state.currentHealth = newHealth
EventShardTopicHealthChange.emit(self.node.brokerCtx, shard, newHealth)
proc removePeer(self: SubscriptionManager, shard: PubsubTopic, peerId: PeerId) =
## Remove a peer from edgeFilterSubStates for the given shard,
## update health, and wake the sub loop to filter-subscribe a replacement.
## Best-effort unsubscribe so the service peer stops pushing to us.
self.edgeFilterSubStates.withValue(shard, state):
var idx = -1
for i, p in state.peers:
if p.peerId == peerId:
idx = i
break
if idx < 0:
return
let peer = state.peers[idx]
state.peers.del(idx)
self.updateShardHealth(shard, state[])
self.edgeFilterWakeup.fire()
if not self.node.wakuFilterClient.isNil():
self.shards.withValue(shard, sub):
let ct = toSeq(sub.contentTopics)
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 subscribeFilterPeer(
self: SubscriptionManager,
peer: RemotePeerInfo,
shard: PubsubTopic,
contentTopics: seq[ContentTopic],
) {.async.} =
## Filter-subscribe to a service peer for all content topics on a shard and
## start tracking it (note that the filter client dials the peer if not connected).
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 "subscribeFilterPeer: peer already tracked, skipping duplicate",
shard = shard, peer = peer.peerId
return
state.peers.add(peer)
self.updateShardHealth(shard, state[])
trace "subscribeFilterPeer: successfully subscribed to all chunks",
shard = shard, peer = peer.peerId, totalPeers = state.peers.len
do:
trace "subscribeFilterPeer: shard removed while subscribing, discarding result",
shard = shard, peer = peer.peerId
finally:
self.edgeFilterSubStates.withValue(shard, state):
state.pendingPeers.excl(peer.peerId)
proc edgeFilterConnectionLoop(self: SubscriptionManager) {.async.} =
## Periodically pings all tracked 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 connection 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))
)
await allFutures(pingTasks.mapIt(it[1]))
for (peerId, task) in pingTasks:
if task.read().isOk():
alive.incl(peerId)
var changed = false
for shard, state in self.edgeFilterSubStates.mpairs:
let oldLen = state.peers.len
state.peers.keepItIf(it.peerId notin connected or alive.contains(it.peerId))
if state.peers.len < oldLen:
changed = true
self.updateShardHealth(shard, state)
trace "Edge Filter health degraded by Ping failure",
shard = shard, new = state.currentHealth
if changed:
self.edgeFilterWakeup.fire()
proc selectFilterCandidates(
self: SubscriptionManager, shard: PubsubTopic, exclude: HashSet[PeerId], needed: int
): seq[RemotePeerInfo] =
## Select filter service peer candidates for a shard.
# Start with every filter server peer that can serve the shard
var allCandidates = self.node.peerManager.selectPeers(
filter_common.WakuFilterSubscribeCodec, some(shard)
)
# Remove all already used in this shard or being filter-subscribed for it
allCandidates.keepItIf(it.peerId notin exclude)
# Collect peer IDs already tracked on other shards
var trackedOnOther = initHashSet[PeerId]()
for otherShard, otherState in self.edgeFilterSubStates.pairs:
if otherShard != shard:
for peer in otherState.peers:
trackedOnOther.incl(peer.peerId)
# Prefer peers we already have a connection to first, preserving shuffle
var candidates =
allCandidates.filterIt(it.peerId in trackedOnOther) &
allCandidates.filterIt(it.peerId notin trackedOnOther)
# We need to return 'needed' peers only
if candidates.len > needed:
candidates.setLen(needed)
return candidates
proc edgeFilterSubLoop(self: SubscriptionManager) {.async.} =
## Reconciles filter subscriptions with the desired state from SubscriptionManager.
var lastSynced = initTable[PubsubTopic, HashSet[ContentTopic]]()
while true:
await self.edgeFilterWakeup.wait()
await sleepAsync(EdgeFilterSubLoopDebounce)
self.edgeFilterWakeup.clear()
trace "edgeFilterSubLoop: woke up"
if isNil(self.node.wakuFilterClient):
trace "edgeFilterSubLoop: wakuFilterClient is nil, skipping"
continue
var newSynced = initTable[PubsubTopic, HashSet[ContentTopic]]()
var allShards: HashSet[PubsubTopic]
for shard, sub in self.shards.pairs:
if sub.contentTopics.len > 0:
newSynced[shard] = sub.contentTopics
allShards.incl(shard)
for shard in lastSynced.keys:
allShards.incl(shard)
trace "edgeFilterSubLoop: desired state", numShards = newSynced.len
# Step 1: read state across all shards at once and
# create a list of peer filter-subscribe tasks and shard tracking to delete.
var subscribeTasks: seq[EdgeFilterSubscribeTask]
var shardsToDelete: seq[PubsubTopic]
for shard in allShards:
# Compute added/removed deltas via direct iteration; no HashSet copies.
var addedTopics: seq[ContentTopic]
var removedTopics: seq[ContentTopic]
newSynced.withValue(shard, curr):
lastSynced.withValue(shard, prev):
for t in curr[]:
if t notin prev[]:
addedTopics.add(t)
for t in prev[]:
if t notin curr[]:
removedTopics.add(t)
do:
for t in curr[]:
addedTopics.add(t)
do:
lastSynced.withValue(shard, prev):
for t in prev[]:
removedTopics.add(t)
discard self.edgeFilterSubStates.mgetOrPut(
shard, EdgeFilterSubState(currentHealth: TopicHealth.UNHEALTHY)
)
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 shard notin newSynced:
shardsToDelete.add(shard)
else:
self.updateShardHealth(shard, state[])
let needed = max(0, HealthyThreshold - state.peers.len - state.pending.len)
if needed > 0:
var tracked: HashSet[PeerId]
for p in state.peers:
tracked.incl(p.peerId)
for p in state.pendingPeers:
tracked.incl(p)
let candidates = self.selectFilterCandidates(shard, tracked, needed)
let toSubscribe = 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,
toSubscribe = toSubscribe
var subscribeTopics: seq[ContentTopic]
newSynced.withValue(shard, curr):
subscribeTopics = toSeq(curr[])
for i in 0 ..< toSubscribe:
subscribeTasks.add(
EdgeFilterSubscribeTask(
peer: candidates[i], shard: shard, topics: subscribeTopics
)
)
# Step 2: execute deferred shard tracking deletion and filter-subscribe 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 subscribeTasks:
let fut = self.subscribeFilterPeer(task.peer, task.shard, task.topics)
self.edgeFilterSubStates.withValue(task.shard, state):
state.pending.add(fut)
lastSynced = newSynced
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.peerEventListener = WakuPeerEvent.listen(
self.node.brokerCtx,
proc(evt: WakuPeerEvent) {.async: (raises: []), gcsafe.} =
if evt.kind == WakuPeerEventKind.EventDisconnected:
# We know a peer is gone, so if it was a service filter peer for this
# edge node, remove it from the list of service filter peers for each
# shard it served and re-evaluate shard health for the affected shards.
for shard, state in self.edgeFilterSubStates.mpairs:
let oldLen = state.peers.len
state.peers.keepItIf(it.peerId != evt.peerId)
if state.peers.len < oldLen:
self.updateShardHealth(shard, state)
self.edgeFilterWakeup.fire()
elif 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.edgeFilterConnectionLoopFut = self.edgeFilterConnectionLoop()
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.edgeFilterConnectionLoopFut):
await self.edgeFilterConnectionLoopFut.cancelAndWait()
self.edgeFilterConnectionLoopFut = nil
for shard, state in self.edgeFilterSubStates:
for fut in state.pending:
if not fut.finished:
await fut.cancelAndWait()
await WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener)
proc start*(self: SubscriptionManager): Result[void, string] =
let edgeShardHealthRes = 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)),
)
self.ownsEdgeShardHealthProvider = edgeShardHealthRes.isOk()
if edgeShardHealthRes.isErr():
error "Can't set provider for RequestEdgeShardHealth",
error = edgeShardHealthRes.error
let edgeFilterPeerCountRes = 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)),
)
self.ownsEdgeFilterPeerCountProvider = edgeFilterPeerCountRes.isOk()
if edgeFilterPeerCountRes.isErr():
error "Can't set provider for RequestEdgeFilterPeerCount",
error = edgeFilterPeerCountRes.error
# Start Edge workers only when we are in Edge mode (relay not mounted)
# AND the filter client is mounted (otherwise the loops have nothing
# to talk to and just spam "filter client is nil" warnings).
if self.node.wakuRelay.isNil() and not self.node.wakuFilterClient.isNil():
return self.startEdgeFilterLoops()
return ok()
proc stop*(self: SubscriptionManager) {.async: (raises: []).} =
# Stop Edge workers if we started them in `start` (Edge mode + filter client).
if self.node.wakuRelay.isNil() and not self.node.wakuFilterClient.isNil():
await self.stopEdgeFilterLoops()
# Only clear providers we actually registered: another SubscriptionManager
# sharing this brokerCtx may have won the race, and clearing its provider
# would leave the broker silently provider-less.
if self.ownsEdgeShardHealthProvider:
RequestEdgeShardHealth.clearProvider(self.node.brokerCtx)
self.ownsEdgeShardHealthProvider = false
if self.ownsEdgeFilterPeerCountProvider:
RequestEdgeFilterPeerCount.clearProvider(self.node.brokerCtx)
self.ownsEdgeFilterPeerCountProvider = false

View File

@ -4,6 +4,7 @@ import chronicles, chronos, metrics, metrics/chronos_httpserver
import import
waku/[net/auto_port, 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, ./peer_manager,
./node_telemetry,
./waku_node ./waku_node
const LogInterval = 10.minutes const LogInterval = 10.minutes

View File

@ -60,23 +60,18 @@ import
requests/health_requests, requests/health_requests,
events/health_events, events/health_events,
events/message_events, events/message_events,
events/peer_events,
], ],
waku/discovery/waku_kademlia, waku/discovery/waku_kademlia,
waku/net/[bound_ports, net_config], waku/net/[bound_ports, net_config],
./peer_manager, ./peer_manager,
./health_monitor/health_status, ./health_monitor/health_status,
./health_monitor/topic_health ./health_monitor/topic_health,
./node_telemetry,
./shard_subscription,
./edge_filter_sub_state
declarePublicCounter waku_node_messages, "number of messages received", ["type"] export shard_subscription, edge_filter_sub_state
declarePublicGauge waku_version,
"Waku version info (in git describe format)", ["version"]
declarePublicCounter waku_node_errors, "number of wakunode errors", ["type"]
declarePublicGauge waku_lightpush_peers, "number of lightpush peers"
declarePublicGauge waku_filter_peers, "number of filter peers"
declarePublicGauge waku_store_peers, "number of store peers"
declarePublicGauge waku_px_peers,
"number of peers (in the node's peerManager) supporting the peer exchange protocol"
logScope: logScope:
topics = "waku node" topics = "waku node"
@ -94,7 +89,6 @@ const clientId* = "Nimbus Waku v2 node"
const WakuNodeVersionString* = "version / git commit hash: " & git_version const WakuNodeVersionString* = "version / git commit hash: " & git_version
# key and crypto modules different
type type
# TODO: Move to application instance (e.g., `WakuNode2`) # TODO: Move to application instance (e.g., `WakuNode2`)
WakuInfo* = object # NOTE One for simplicity, can extend later as needed WakuInfo* = object # NOTE One for simplicity, can extend later as needed
@ -137,11 +131,25 @@ type
rateLimitSettings*: ProtocolRateLimitSettings rateLimitSettings*: ProtocolRateLimitSettings
legacyAppHandlers*: Table[PubsubTopic, WakuRelayHandler] legacyAppHandlers*: Table[PubsubTopic, WakuRelayHandler]
## Kernel API Relay appHandlers (if any) ## Kernel API Relay appHandlers (if any)
subscriptionManager*: SubscriptionManager
wakuMix*: WakuMix wakuMix*: WakuMix
kademliaDiscoveryLoop*: Future[void] kademliaDiscoveryLoop*: Future[void]
wakuKademlia*: WakuKademlia wakuKademlia*: WakuKademlia
ports*: BoundPorts ports*: BoundPorts
SubscriptionManager* = ref object of RootObj
node*: WakuNode
shards*: Table[PubsubTopic, ShardSubscription]
edgeFilterSubStates*: Table[PubsubTopic, EdgeFilterSubState]
edgeFilterWakeup*: AsyncEvent
edgeFilterSubLoopFut*: Future[void]
edgeFilterConnectionLoopFut*: Future[void]
peerEventListener*: WakuPeerEventListener
ownsEdgeShardHealthProvider*: bool
ownsEdgeFilterPeerCountProvider*: bool
import ./subscription_manager
proc deduceRelayShard( proc deduceRelayShard(
node: WakuNode, node: WakuNode,
contentTopic: ContentTopic, contentTopic: ContentTopic,
@ -230,6 +238,8 @@ proc new*(
peerManager.setShardGetter(node.getShardsGetter(@[])) peerManager.setShardGetter(node.getShardsGetter(@[]))
node.subscriptionManager = SubscriptionManager.new(node)
return node return node
proc peerInfo*(node: WakuNode): PeerInfo = proc peerInfo*(node: WakuNode): PeerInfo =
@ -600,6 +610,9 @@ proc start*(node: WakuNode) {.async.} =
node.startProvidersAndListeners() node.startProvidersAndListeners()
node.subscriptionManager.start().isOkOr:
error "failed to start subscription manager", error = error
if not zeroPortPresent: if not zeroPortPresent:
updateAnnouncedAddrWithPrimaryIpAddr(node).isOkOr: updateAnnouncedAddrWithPrimaryIpAddr(node).isOkOr:
error "failed update announced addr", error = $error error "failed update announced addr", error = $error
@ -611,6 +624,8 @@ proc start*(node: WakuNode) {.async.} =
proc stop*(node: WakuNode) {.async.} = proc stop*(node: WakuNode) {.async.} =
## By stopping the switch we are stopping all the underlying mounted protocols ## By stopping the switch we are stopping all the underlying mounted protocols
await node.subscriptionManager.stop()
node.stopProvidersAndListeners() node.stopProvidersAndListeners()
## NOTE: This will dispatch gossipsub stop to the WakuRelay.stop method override ## NOTE: This will dispatch gossipsub stop to the WakuRelay.stop method override

View File

@ -21,6 +21,7 @@ import
import import
../waku_node, ../waku_node,
../node_telemetry,
../../waku_core, ../../waku_core,
../../waku_core/topics/sharding, ../../waku_core/topics/sharding,
../../waku_filter_v2, ../../waku_filter_v2,

View File

@ -19,6 +19,7 @@ import
import import
../waku_node, ../waku_node,
../node_telemetry,
../../waku_peer_exchange, ../../waku_peer_exchange,
../../waku_core, ../../waku_core,
../peer_manager, ../peer_manager,

View File

@ -29,90 +29,18 @@ import
waku_store_sync, waku_store_sync,
waku_rln_relay, waku_rln_relay,
node/waku_node, node/waku_node,
node/subscription_manager,
node/peer_manager, node/peer_manager,
events/message_events, events/message_events,
] ]
export waku_relay.WakuRelayHandler export waku_relay.WakuRelayHandler
declarePublicHistogram waku_histogram_message_size,
"message size histogram in kB",
buckets = [
0.0, 1.0, 3.0, 5.0, 15.0, 50.0, 75.0, 100.0, 125.0, 150.0, 500.0, 700.0, 1000.0, Inf
]
logScope: logScope:
topics = "waku node relay api" topics = "waku node relay api"
## Waku relay ## Waku relay
proc registerRelayHandler(
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.
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
waku_node_messages.inc(labelValues = ["relay"])
waku_histogram_message_size.observe(msgSizeKB)
proc filterHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuFilter.isNil():
return
await node.wakuFilter.handleMessage(topic, msg)
proc archiveHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuArchive.isNil():
return
await node.wakuArchive.handleMessage(topic, msg)
proc syncHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
if node.wakuStoreReconciliation.isNil():
return
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.} =
await traceHandler(topic, msg)
await filterHandler(topic, msg)
await archiveHandler(topic, msg)
await syncHandler(topic, msg)
await internalHandler(topic, msg)
# Call the legacy (kernel API) app handler if it exists.
# Normally, hasKey is false and the MessageSeenEvent bus (new API) is used instead.
# But we need to support legacy behavior (kernel API use), hence this.
# NOTE: We can delete `legacyAppHandlers` if instead we refactor WakuRelay to support multiple
# PubsubTopic handlers, since that's actually supported by libp2p PubSub (bigger refactor...)
if node.legacyAppHandlers.hasKey(topic) and not node.legacyAppHandlers[topic].isNil():
await node.legacyAppHandlers[topic](topic, msg)
node.wakuRelay.subscribe(topic, uniqueTopicHandler)
proc getTopicOfSubscriptionEvent( proc getTopicOfSubscriptionEvent(
node: WakuNode, subscription: SubscriptionEvent node: WakuNode, subscription: SubscriptionEvent
): Result[(PubsubTopic, Option[ContentTopic]), string] = ): Result[(PubsubTopic, Option[ContentTopic]), string] =
@ -143,21 +71,15 @@ proc subscribe*(
error "Invalid API call to `subscribe`. WakuRelay not mounted." error "Invalid API call to `subscribe`. WakuRelay not mounted."
return err("Invalid API call to `subscribe`. WakuRelay not mounted.") return err("Invalid API call to `subscribe`. WakuRelay not mounted.")
let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr: let (pubsubTopic, _) = getTopicOfSubscriptionEvent(node, subscription).valueOr:
error "Failed to decode subscription event", error = error error "Failed to decode subscription event", error = error
return err("Failed to decode subscription event: " & error) return err("Failed to decode subscription event: " & error)
if node.registerRelayHandler(pubsubTopic, handler): # strict version
info "subscribe", pubsubTopic, contentTopicOp #if contentTopicOp.isSome():
node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: pubsubTopic)) # return
else: # node.subscriptionManager.subscribe(pubsubTopic, contentTopicOp.get(), handler)
if isNil(handler): return node.subscriptionManager.subscribeShard(pubsubTopic, 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()
proc unsubscribe*( proc unsubscribe*(
node: WakuNode, subscription: SubscriptionEvent node: WakuNode, subscription: SubscriptionEvent
@ -170,26 +92,14 @@ proc unsubscribe*(
error "Invalid API call to `unsubscribe`. WakuRelay not mounted." error "Invalid API call to `unsubscribe`. WakuRelay not mounted."
return err("Invalid API call to `unsubscribe`. WakuRelay not mounted.") return err("Invalid API call to `unsubscribe`. WakuRelay not mounted.")
let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr: let (pubsubTopic, _) = getTopicOfSubscriptionEvent(node, subscription).valueOr:
error "Failed to decode unsubscribe event", error = error error "Failed to decode unsubscribe event", error = error
return err("Failed to decode unsubscribe event: " & error) return err("Failed to decode unsubscribe event: " & error)
let hadHandler = node.legacyAppHandlers.hasKey(pubsubTopic) # strict version
if hadHandler: #if contentTopicOp.isSome():
node.legacyAppHandlers.del(pubsubTopic) # return node.subscriptionManager.unsubscribe(pubsubTopic, contentTopicOp.get())
return node.subscriptionManager.unsubscribeAll(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()
proc isSubscribed*( proc isSubscribed*(
node: WakuNode, subscription: SubscriptionEvent node: WakuNode, subscription: SubscriptionEvent

View File

@ -68,7 +68,7 @@ proc mtMarshalValue*(
of txPut: of txPut:
if not mtMarshalValue(buf, cap, value.payload, pos): if not mtMarshalValue(buf, cap, value.payload, pos):
return false return false
of txDelete: of txDelete, txDeletePrefix:
discard discard
return true return true
@ -93,6 +93,8 @@ proc mtUnmarshalValue*(
value = TxOp(category: category, key: key, kind: txPut, payload: payload) value = TxOp(category: category, key: key, kind: txPut, payload: payload)
of txDelete: of txDelete:
value = TxOp(category: category, key: key, kind: txDelete) value = TxOp(category: category, key: key, kind: txDelete)
of txDeletePrefix:
value = TxOp(category: category, key: key, kind: txDeletePrefix)
return true return true
EventBroker(mt): EventBroker(mt):

View File

@ -7,7 +7,7 @@
import std/options import std/options
import results, sqlite3_abi import results, sqlite3_abi
import ../common/databases/[common, db_sqlite] import ../common/databases/[common, db_sqlite]
import ./[types, schema] import ./[types, keys, schema]
type type
KvBackend* = ref object KvBackend* = ref object
@ -121,6 +121,37 @@ proc close*(b: KvBackend) =
b.db.close() b.db.close()
b.db = nil b.db = nil
proc deletePrefix(
b: KvBackend, category: string, prefix: Key
): Result[void, PersistencyError] =
let rng = prefixRange(prefix)
let openEnded = bytes(rng.stop).len == 0
let sql =
if openEnded:
"DELETE FROM kv WHERE category = ? AND key >= ?;"
else:
"DELETE FROM kv WHERE category = ? AND key >= ? AND key < ?;"
var s: ptr sqlite3_stmt
let rc = sqlite3_prepare_v2(b.db.env, sql.cstring, sql.len.cint, addr s, nil)
if rc != SQLITE_OK:
return err(toErr("deletePrefix prepare: " & $sqlite3_errstr(rc)))
defer:
discard sqlite3_finalize(s)
var bc = bindBlob(s, 1.cint, catBytes(category))
if bc != SQLITE_OK:
return err(toErr("deletePrefix bind cat: " & $sqlite3_errstr(bc)))
bc = bindBlob(s, 2.cint, keyBytes(rng.start))
if bc != SQLITE_OK:
return err(toErr("deletePrefix bind start: " & $sqlite3_errstr(bc)))
if not openEnded:
bc = bindBlob(s, 3.cint, keyBytes(rng.stop))
if bc != SQLITE_OK:
return err(toErr("deletePrefix bind stop: " & $sqlite3_errstr(bc)))
let v = sqlite3_step(s)
if v != SQLITE_DONE:
return err(toErr("deletePrefix step: " & $sqlite3_errstr(v)))
return ok()
proc applyOne(b: KvBackend, op: TxOp): Result[void, PersistencyError] = proc applyOne(b: KvBackend, op: TxOp): Result[void, PersistencyError] =
case op.kind case op.kind
of txPut: of txPut:
@ -131,6 +162,8 @@ proc applyOne(b: KvBackend, op: TxOp): Result[void, PersistencyError] =
let r = b.deleteStmt.exec((catBytes(op.category), keyBytes(op.key))) let r = b.deleteStmt.exec((catBytes(op.category), keyBytes(op.key)))
if r.isErr: if r.isErr:
return err(toErr("delete failed: " & r.error)) return err(toErr("delete failed: " & r.error))
of txDeletePrefix:
?b.deletePrefix(op.category, op.key)
return ok() return ok()
proc execSql(b: KvBackend, sql: string): Result[void, PersistencyError] = proc execSql(b: KvBackend, sql: string): Result[void, PersistencyError] =

View File

@ -284,6 +284,11 @@ proc persistPut*(
proc persistDelete*(t: Job, category: string, key: Key): Future[void] {.async.} = proc persistDelete*(t: Job, category: string, key: Key): Future[void] {.async.} =
await persist(t, TxOp(category: category, key: key, kind: txDelete)) await persist(t, TxOp(category: category, key: key, kind: txDelete))
proc persistDeletePrefix*(
t: Job, category: string, prefix: Key
): Future[void] {.async.} =
await persist(t, TxOp(category: category, key: prefix, kind: txDeletePrefix))
proc persistEncoded*[T]( proc persistEncoded*[T](
t: Job, category: string, key: Key, value: T t: Job, category: string, key: Key, value: T
): Future[void] {.async.} = ): Future[void] {.async.} =
@ -335,6 +340,13 @@ proc persistDelete*(
if not j.isNil(): if not j.isNil():
await j.persistDelete(category, key) await j.persistDelete(category, key)
proc persistDeletePrefix*(
p: Persistency, jobId: string, category: string, prefix: Key
): Future[void] {.async.} =
let j = p.jobOrWarn(jobId)
if not j.isNil():
await j.persistDeletePrefix(category, prefix)
proc persistEncoded*[T]( proc persistEncoded*[T](
p: Persistency, jobId: string, category: string, key: Key, value: T p: Persistency, jobId: string, category: string, key: Key, value: T
): Future[void] {.async.} = ): Future[void] {.async.} =

View File

@ -0,0 +1,176 @@
## Adapter that materialises the SDS `Persistence` contract (nim-sds 0.3.0,
## snapshot model) on top of a waku-persistency `Job`. One `Job` (== one
## SQLite file, one worker thread) services all channels for a given SDS
## context; rows are namespaced by category and the channelId is the first
## key component so per-channel prefix scans stay cheap.
##
## ## Snapshot contract (nim-sds 0.3.0)
##
## The fine-grained per-row callbacks of 0.2.4 are gone. SDS now persists via
## five procs, all `Future[Result[void, string]]` (load returns
## `Result[ChannelData, string]`), `{.async: (raises: []), gcsafe.}`:
##
## * **`saveChannelMeta`** — the complete fast-changing per-channel state
## (lamport clock, outgoing/incoming buffers, both SDS-R repair buffers)
## as ONE blob. Idempotent; a missed write self-heals on the next save.
## * **`updateHistory`** — append newly-delivered messages / evict the
## oldest past the cap, applied as one transactional batch.
## * **`loadChannel`** — bootstrap: returns the prior `ChannelData`
## (meta + ordered message history) or an empty one. Surfaces errors.
## * **`dropChannel`** — wipe all state for a channel. Surfaces errors.
##
## Failure policy mirrors the interface docs: save/update/hint are non-fatal
## (we log and still return the error string); load/drop are durability-intent
## and propagate their error to the caller.
##
## ## Codec
##
## The blob transform is owned by nim-sds: `ChannelMeta` round-trips through
## `sds/snapshot_codec` (protobuf, schema-versioned — refuses unknown
## versions), and each persisted `SdsMessage` log row through the SDS wire
## codec in `sds/protobuf`. We do not maintain a second codec for these
## shapes (the previous `payload_codec`/`BlobCodec` path is retired).
##
## ## Retrieval hints
##
## `setRetrievalHint` is intentionally a no-op: persisted hints are never read
## back — `loadChannel` returns `ChannelData` (meta + messageHistory) with no
## hint field, and `ChannelMeta` carries none. Hints are supplied live via the
## `onRetrievalHint` provider, so persisting them would be write-only dead
## data. The closure still exists because the field is required by the
## `Persistence` object (SDS calls it from `getRecentHistoryEntries`).
##
## ## Storage layout
##
## | Category | Key | Value |
## |---------------|--------------------------|----------------------------------------|
## | `sds.meta` | `key(channelId)` | `ChannelMeta` (snapshot_codec protobuf)|
## | `sds.log` | `key(channelId, msgId)` | `SdsMessage` (sds wire protobuf) |
##
## `messageHistory` is reconstructed in memory by sorting on
## `(lamportTimestamp, messageId)` — the same total order SDS uses for
## delivery (see sds/sds_utils.nim).
{.push raises: [].}
import std/[algorithm, options]
import chronos, chronicles, results
import libp2p/protobuf/minprotobuf
import ./persistency
import ./keys
import types/persistence
import snapshot_codec
import protobuf
export persistence, persistency
logScope:
topics = "sds-persistency"
const
CatMeta* = "sds.meta"
CatLog* = "sds.log"
# ── Public factory ──────────────────────────────────────────────────────
proc newSdsPersistence*(job: Job): Persistence {.gcsafe, raises: [].} =
## Build an SDS `Persistence` value backed by ``job``. One Job services
## all channels — channelId is part of every key.
##
## The closures capture ``job`` by ref. They must be invoked from a thread
## that owns a running chronos loop (the SDS context's worker thread
## satisfies this).
doAssert not job.isNil, "newSdsPersistence: job is nil"
# Built field-by-field via assignment rather than an object literal: every
# field is an async closure whose body uses `await`/`return` statements,
# which cannot be followed by the `,` field separator a `Persistence(..)`
# literal would require. Assignments have no separator, so bodies stay plain.
var persistence = Persistence()
persistence.saveChannelMeta = proc(
channelId: SdsChannelID, meta: ChannelMeta
): Future[Result[void, string]] {.async: (raises: []), gcsafe.} =
try:
await job.persistPut(CatMeta, toKey(channelId), encode(meta).buffer)
return ok()
except CatchableError as e:
warn "sds-persistency: saveChannelMeta failed", channelId, err = e.msg
return err(e.msg)
persistence.updateHistory = proc(
channelId: SdsChannelID, update: HistoryUpdate
): Future[Result[void, string]] {.async: (raises: []), gcsafe.} =
if update.isEmpty:
return ok()
# One transactional batch: append rows (txPut) and evictions (txDelete).
var ops = newSeq[TxOp]()
for m in update.append:
ops.add TxOp(
category: CatLog,
key: key(channelId, m.messageId),
kind: txPut,
payload: encode(m).buffer,
)
for id in update.evict:
ops.add TxOp(category: CatLog, key: key(channelId, id), kind: txDelete)
try:
await job.persist(ops)
return ok()
except CatchableError as e:
warn "sds-persistency: updateHistory failed",
channelId, appended = update.append.len, evicted = update.evict.len, err = e.msg
return err(e.msg)
persistence.loadChannel = proc(
channelId: SdsChannelID
): Future[Result[ChannelData, string]] {.async: (raises: []), gcsafe.} =
let chanKey = toKey(channelId)
var data = ChannelData.init()
try:
block meta:
let opt = (await job.get(CatMeta, chanKey)).valueOr:
return err("loadChannel: get meta: " & $error)
if opt.isSome:
# schema-versioned decode; refuses unknown versions loudly.
data.meta = ChannelMeta.decode(opt.get).valueOr:
return err("loadChannel: corrupt or unsupported ChannelMeta blob")
block history:
let rows = (await job.scanPrefix(CatLog, chanKey)).valueOr:
return err("loadChannel: scan log: " & $error)
var msgs = newSeq[SdsMessage]()
for row in rows:
let m = SdsMessage.decode(row.payload).valueOr:
warn "sds-persistency: skipping undecodable log row", channelId
continue
msgs.add(m)
msgs.sort do(a, b: SdsMessage) -> int:
result = cmp(a.lamportTimestamp, b.lamportTimestamp)
if result == 0:
result = cmp(a.messageId, b.messageId)
data.messageHistory = msgs
return ok(data)
except CatchableError as e:
return err("loadChannel: " & e.msg)
persistence.dropChannel = proc(
channelId: SdsChannelID
): Future[Result[void, string]] {.async: (raises: []), gcsafe.} =
let chanKey = toKey(channelId)
try:
await job.persist(
@[
TxOp(category: CatLog, key: chanKey, kind: txDeletePrefix),
TxOp(category: CatMeta, key: chanKey, kind: txDelete),
]
)
return ok()
except CatchableError as e:
error "sds-persistency: dropChannel failed", channelId, err = e.msg
return err(e.msg)
return persistence
{.pop.}

View File

@ -19,6 +19,7 @@ type
TxOpKind* = enum TxOpKind* = enum
txPut txPut
txDelete txDelete
txDeletePrefix
TxOp* = object TxOp* = object
category*: string category*: string
@ -28,6 +29,8 @@ type
payload*: seq[byte] payload*: seq[byte]
of txDelete: of txDelete:
discard discard
of txDeletePrefix:
discard
PersistencyErrorKind* = enum PersistencyErrorKind* = enum
peBackend peBackend

View File

@ -38,14 +38,14 @@ RequestBroker:
proc signature(protocol: WakuProtocol): Future[Result[RequestProtocolHealth, string]] proc signature(protocol: WakuProtocol): Future[Result[RequestProtocolHealth, string]]
# Get edge filter health for a single shard (set by DeliveryService when edge mode is active) # Get edge filter health for a single shard (set when edge mode is active)
RequestBroker(sync): RequestBroker(sync):
type RequestEdgeShardHealth* = object type RequestEdgeShardHealth* = object
health*: TopicHealth health*: TopicHealth
proc signature(shard: PubsubTopic): Result[RequestEdgeShardHealth, string] proc signature(shard: PubsubTopic): Result[RequestEdgeShardHealth, string]
# Get edge filter confirmed peer count (set by DeliveryService when edge mode is active) # Get edge filter confirmed peer count (set when edge mode is active)
RequestBroker(sync): RequestBroker(sync):
type RequestEdgeFilterPeerCount* = object type RequestEdgeFilterPeerCount* = object
peerCount*: int peerCount*: int

View File

@ -4,6 +4,7 @@ import net, tables
import presto import presto
import import
waku/waku_node, waku/waku_node,
waku/node/health_monitor,
waku/discovery/waku_discv5, waku/discovery/waku_discv5,
waku/rest_api/message_cache, waku/rest_api/message_cache,
waku/rest_api/handlers, waku/rest_api/handlers,

View File

@ -1,7 +1,8 @@
{.push raises: [].} {.push raises: [].}
import chronicles, json_serialization, presto/route import chronicles, json_serialization, presto/route
import ../../../waku_node, ../responses, ../serdes, ./types import
../../../waku_node, ../../../node/health_monitor, ../responses, ../serdes, ./types
logScope: logScope:
topics = "waku node rest health_api" topics = "waku node rest health_api"

View File

@ -3,7 +3,7 @@
import results import results
import chronicles, json_serialization, json_serialization/std/options import chronicles, json_serialization, json_serialization/std/options
import ../serdes import ../serdes
import waku/[waku_node, api/types] import waku/[waku_node, api/types, node/health_monitor]
#### Serialization and deserialization #### Serialization and deserialization

View File

@ -1,8 +1,13 @@
import import
./net/net_config,
./node/waku_switch as switch, ./node/waku_switch as switch,
./node/waku_node as node, ./node/waku_node as node,
./node/health_monitor as health_monitor, ./node/waku_node/filter as filter_api,
./node/kernel_api as kernel_api ./node/waku_node/lightpush as lightpush_api,
./node/waku_node/store as store_api,
./node/waku_node/relay as relay_api,
./node/waku_node/peer_exchange as peer_exchange_api,
./node/waku_node/ping as ping_api
export net_config, switch, node, health_monitor, kernel_api export
switch, node, filter_api, lightpush_api, store_api, relay_api, peer_exchange_api,
ping_api

View File

@ -0,0 +1,3 @@
import waku/persistency/persistency
export persistency