logos-delivery/tests/channels/test_reliable_channel_send_receive.nim
NagyZoltanPeter a7f893555d
Integrate api-shape phase2 (#3989) + api interfaces (#3975) (#3999)
* Reshape per-layer API into api/ folders and thin the FFI over them

Each layer now separates its constructible core from its public surface:

  - core module (waku.nim / messaging_client.nim /
    reliable_channel_manager.nim): the type plus new/start/stop and the
    private construction helpers.
  - api/ folder: one module per differentiated set of operations
    (waku: topics/relay/filter/lightpush/store/peer_manager/discovery/
    debug/health) plus an events surface.

The waku api is reshaped to be the complete operation surface the C
bindings need, so the library no longer reaches into node internals:
relayPublish returns the message hash, relaySubscribe takes an optional
handler, filter/lightpush auto-select the service peer, connectedPeersInfo
returns structured data, pingPeer honours the timeout, plus
relayNumPeersInMesh / relayNumConnectedPeers / isOnline. library/ is now a
thin C-ABI shim: each {.ffi.} proc only marshals cstring/JSON/callbacks and
delegates to ctx.myLib[].waku.<op> (or messagingClient.<op>).
app_callbacks re-exports the modules defining its handler types, which the
included FFI files previously relied on by leakage.

Events move next to the surface that owns them, with each dependency kept
pointing the right way:

  - waku/events/ relocated under waku/api/events/.
  - channel events live in channels/api/events.nim.
  - the four messaging-level message events move to messaging/api/events;
    MessageSeenEvent stays in waku because it is emitted by waku core, so
    moving it would make waku depend on the messaging layer.
  - delivery_events renamed to filter_subscribe_events to match the
    OnFilterSubscribe/Unsubscribe events it actually declares.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add reliable-channel FFI ops + events (nim-ffi v0.1.3)

Expose the reliable-channel layer through the v0.1.3 FFI:
- channel_create / channel_send / channel_close call the
  ReliableChannelManager api (createReliableChannel / send / closeChannel),
  marshalling channel id + base64 payload + ephemeral by hand
- channel message received / sent / errored are surfaced by listening to the
  channel-layer broker events in start_node and forwarding them through
  callEventCallback (received payload base64-encoded), dropped in stop_node

Stays on nim-ffi v0.1.3 (no typed/CBOR rewrite).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Expose reliable-channel ops in the stable C header (#3851)

The library already ships as a single .so with a tiered header surface
(liblogosdelivery.h = stable Messaging/Reliable-Channels, liblogosdelivery_kernel.h
= advanced Kernel). Per that tiering, the reliable-channel ops belong on the
stable surface, so declare channel_create / channel_send / channel_close in
liblogosdelivery.h and document the channel lifecycle events delivered through
the event callback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Graft PR#3975 interface layer onto decomposed foundation (events deduped)

Add IKernel/IMessagingClient/IReliableChannelManager/ILogosDelivery interface
classes under logos_delivery/api/. The EventBroker types PR#3975 hoisted into
these files already exist in PR#3989's decomposed */api/events/ modules, so the
interface files re-export those modules instead of redefining the types
(avoids 8 duplicate EventBroker definitions). api/types.nim kept at the
foundation version (ChannelId stays in channels/types.nim, which the decomposed
modules import).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Wire impl classes to interfaces (inherit; relocate SendHandler)

- Waku : IKernel, MessagingClient : IMessagingClient,
  ReliableChannelManager : IReliableChannelManager.
- The operation procs already live in PR#3989's decomposed */api/ modules and
  stay as plain procs (nothing dispatches through the interface types, so no
  method-ization is needed).
- SendHandler now lives in reliable_channel_manager_api.nim (its PR#3975 home);
  removed the duplicate from reliable_channel.nim, which re-exports the
  interface module so channels/api/{channel_lifecycle,send} still see it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Wire LogosDelivery to ILogosDelivery orchestrator interface

LogosDelivery : ILogosDelivery; start/stop/isOnline become method overrides.
Peripheral PR#3975 edits (lightpush/store clients, self_req_handlers,
statistics) are import-reorg artifacts of deleting waku/utils/requests.nim,
which the decomposed structure keeps -- so they are intentionally not ported.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Dedup EventConnectionStatusChange (re-export from health_events)

9th duplicate EventBroker type: defined in both logos_delivery_api.nim and the
decomposed waku/api/events/health_events.nim. The interface file now re-exports
it. liblogosdelivery builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Move events back into interface-class source files (restore #3975 placement)

Reverses the earlier dedup-by-re-export: event TYPE definitions now live in the
interface classes, and the emptied decomposed event files are removed.

- MessageSeenEvent            -> logos_delivery/api/kernel_api.nim
- Message{Sent,Error,Propagated,Received}Event -> api/messaging_client_api.nim
- ChannelMessage{Received,Sent,Error}Event     -> api/reliable_channel_manager_api.nim
- EventConnectionStatusChange -> api/logos_delivery_api.nim

Deleted (became empty after the move):
- logos_delivery/waku/api/events/message_events.nim
- logos_delivery/messaging/api/events.nim
- logos_delivery/channels/api/events.nim
health_events.nim keeps its two remaining events (content/shard topic health).

Rewiring: each layer re-exports its interface module (waku->kernel_api,
messaging_client->messaging_client_api, reliable_channel->reliable_channel_manager_api,
which also re-exports messaging_client_api). Deep emitters/listeners
(subscription_manager, waku_node, waku_node/relay, node_health_monitor,
recv_service, send_service) import the owning interface module directly.
kernel_api stays below node level (types/topics/message/store-common) so the
node->kernel_api imports are acyclic. liblogosdelivery builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* nph formatting

---------

Co-authored-by: Ivan FB <ivansete@status.im>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:51:22 +02:00

1172 lines
42 KiB
Nim

{.used.}
import std/[net, options, os]
from std/times import epochTime
import chronos, testutils/unittests, stew/byteutils
import brokers/broker_context
import ../testlib/[common, wakucore, wakunode, testasync]
import logos_delivery
import logos_delivery/waku/[waku_node, waku_core]
import logos_delivery/waku/factory/waku_conf
import logos_delivery/api/messaging_client_api as waku_message_events
import tools/confutils/cli_args
import logos_delivery/channels/reliable_channel_manager
import logos_delivery/channels/encryption/noop_encryption
import logos_delivery/waku/persistency/keys
import logos_delivery/waku/persistency/sds_persistency
## Full nim-sds API: ingress tests act as the remote peer producing real
## SDS envelopes; protocol-semantics tests decode wires and meta snapshots.
import sds
import snapshot_codec
const TestTimeout = chronos.seconds(15)
proc createApiNodeConf(): WakuNodeConf =
var conf = defaultWakuNodeConf().valueOr:
raiseAssert error
conf.mode = cli_args.WakuMode.Core
conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0)
conf.clusterId = some(3'u16)
conf.numShardsInNetwork = 1
conf.reliabilityEnabled = some(true)
conf.rest = false
return conf
suite "Reliable Channel - ingress":
asyncTest "manager dispatches marked WakuMessage to the right channel":
## Unit test for the receive side of the API: instead of standing
## up two libp2p nodes and a relay mesh, we drive the manager
## directly by emitting a `MessageReceivedEvent` (the exact event
## the MessagingClient emits when a `WakuMessage` arrives off the
## wire). The manager must:
## - drop traffic missing the Reliable Channel spec marker
## - dispatch the matching channel's `onMessageReceived`
## - emit `ChannelMessageReceivedEvent` with the payload
const
channelId = ChannelId("test-channel")
contentTopic = ContentTopic("/reliable-channel/test/proto")
let appPayload = "hello reliable channel".toBytes()
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
## Noop encryption providers so the Encrypt/Decrypt brokers have
## something to dispatch to; without this the channel falls back to
## plaintext anyway, but installing them is the documented setup.
setNoopEncryption()
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("createReliableChannel")
let received = newFuture[seq[byte]]("channel-message-received")
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if not received.finished() and evt.channelId == channelId:
received.complete(evt.payload)
,
)
.expect("listen ChannelMessageReceivedEvent")
## Build a `WakuMessage` as it would arrive off the wire: spec marker
## on `meta`, right content topic, payload wrapped in a real SDS
## envelope by a stand-in remote peer.
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
let sdsWire = (
await remotePeer.wrapOutgoingMessage(
appPayload, "ingress-test-msg-1", SdsChannelID(channelId)
)
).expect("wrapOutgoingMessage")
let inboundMsg = WakuMessage(
payload: sdsWire,
contentTopic: contentTopic,
version: 0,
meta: LipWireReliableChannelVersion.toBytes(),
)
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(messageHash: "", message: inboundMsg),
)
let arrived = await received.withTimeout(TestTimeout)
check arrived
if arrived:
check received.read() == appPayload
(await waku.stop()).expect("stop")
asyncTest "manager drops unmarked WakuMessage":
## Mirror of the above: same content topic, but `meta` is empty
## (i.e. foreign traffic). The channel-level event must NOT fire.
const
channelId = ChannelId("test-channel-2")
contentTopic = ContentTopic("/reliable-channel/test/proto")
let appPayload = "foreign payload".toBytes()
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("createReliableChannel")
var fired = false
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
fired = true
,
)
.expect("listen ChannelMessageReceivedEvent")
let inboundMsg = WakuMessage(
payload: appPayload,
contentTopic: contentTopic,
version: 0,
meta: @[], ## no Reliable Channel spec marker
)
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(messageHash: "", message: inboundMsg),
)
## Give the event broker a chance to fan out.
await sleepAsync(100.milliseconds)
check not fired
(await waku.stop()).expect("stop")
suite "Reliable Channel - send state machine":
asyncTest "MessageSentEvent finalises the channelReqId as Sent":
## Drives the real send pipeline (`send` -> segmentation -> SDS ->
## rate_limit -> encrypt -> dispatch) via a fake `SendHandler` that
## returns a canned `RequestId` instead of hitting the network.
## Emitting the delivery-layer `MessageSentEvent` must drive the
## channel-level state machine through `Confirmed` and produce a
## `ChannelMessageSentEvent` (channel-level terminal event) for the
## `channelReqId` returned by `send()`.
const
channelId = ChannelId("sm-success-channel")
contentTopic = ContentTopic("/reliable-channel/test/sm-success")
fakeMsgReqId = RequestId("fake-msg-req-1")
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
var sendCalls = 0
let fakeSend: SendHandler = proc(
env: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
sendCalls.inc
return ok(fakeMsgReqId)
discard manager
.createReliableChannel(
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
)
.expect("createReliableChannel")
let sentFut = newFuture[RequestId]("channel-sent")
discard ChannelMessageSentEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageSentEvent) {.async: (raises: []).} =
if not sentFut.finished() and evt.channelId == channelId:
sentFut.complete(evt.requestId)
,
)
.expect("listen ChannelMessageSentEvent")
let channelReqId = (await manager.send(channelId, "hello".toBytes())).expect("send")
let dispatchDeadline = Moment.now() + 1.seconds
while Moment.now() < dispatchDeadline and sendCalls == 0:
await sleepAsync(5.milliseconds)
check sendCalls == 1
waku_message_events.MessageSentEvent.emit(
brokerCtx,
waku_message_events.MessageSentEvent(requestId: fakeMsgReqId, messageHash: ""),
)
let finalised = await sentFut.withTimeout(1.seconds)
check finalised
if finalised:
check sentFut.read() == channelReqId
(await waku.stop()).expect("stop")
asyncTest "two independent channelReqIds are finalised independently":
## Two `send()` calls -> two independent `channelReqId`s, each with
## one segment under the current segmentation skeleton
## (`performSegmentation` always emits exactly one segment). The
## fake `SendHandler` returns distinct `messagingReqId`s; finalising
## the first emits `ChannelMessageSentEvent` for its `channelReqId`,
## finalising the second as a failure emits `ChannelMessageErrorEvent`
## for the other.
const
channelId = ChannelId("sm-multi-channel")
contentTopic = ContentTopic("/reliable-channel/test/sm-multi")
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
var msgReqIds: seq[RequestId]
let fakeSend: SendHandler = proc(
env: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
let id = RequestId("fake-msg-req-" & $(msgReqIds.len + 1))
msgReqIds.add(id)
return ok(id)
discard manager
.createReliableChannel(
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
)
.expect("createReliableChannel")
let sentFut = newFuture[RequestId]("channel-sent")
let erroredFut = newFuture[RequestId]("channel-errored")
discard ChannelMessageSentEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageSentEvent) {.async: (raises: []).} =
if not sentFut.finished() and evt.channelId == channelId:
sentFut.complete(evt.requestId)
,
)
.expect("listen ChannelMessageSentEvent")
discard ChannelMessageErrorEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageErrorEvent) {.async: (raises: []).} =
if not erroredFut.finished() and evt.channelId == channelId:
erroredFut.complete(evt.requestId)
,
)
.expect("listen ChannelMessageErrorEvent")
let channelReqId1 =
(await manager.send(channelId, "first".toBytes())).expect("send 1")
let channelReqId2 =
(await manager.send(channelId, "second".toBytes())).expect("send 2")
let dispatchDeadline = Moment.now() + 1.seconds
while Moment.now() < dispatchDeadline and msgReqIds.len < 2:
await sleepAsync(5.milliseconds)
check msgReqIds.len == 2
waku_message_events.MessageSentEvent.emit(
brokerCtx,
waku_message_events.MessageSentEvent(requestId: msgReqIds[0], messageHash: ""),
)
let sentArrived = await sentFut.withTimeout(1.seconds)
check sentArrived
if sentArrived:
check sentFut.read() == channelReqId1
## The second `channelReqId` must NOT have finalised yet — its
## segment is still `InFlight`.
check not erroredFut.finished()
waku_message_events.MessageErrorEvent.emit(
brokerCtx,
waku_message_events.MessageErrorEvent(
requestId: msgReqIds[1], messageHash: "", error: "synthetic"
),
)
let erroredArrived = await erroredFut.withTimeout(1.seconds)
check erroredArrived
if erroredArrived:
check erroredFut.read() == channelReqId2
(await waku.stop()).expect("stop")
asyncTest "TODO: channelReqId not pruned until ALL its segments are final":
## Placeholder for the multi-sibling prune rule. Today's
## `performSegmentation` (segmentation skeleton) always emits
## exactly one segment per `send()`, so multiple siblings under one
## `channelReqId` cannot be produced through the real pipeline.
## Implement once segmentation does real chunking: send a payload
## larger than `DefaultSegmentSizeBytes`, capture the N
## `messagingReqId`s from a fake `SendHandler`, finalise some, and
## assert prune only fires once every sibling is final.
skip()
asyncTest "sibling MessageSentEvent during sendHandler await does not corrupt state":
## Regression test for the prune-during-await race
## (PR #3914 review comment r3324891059). Locks in that a sibling
## `MessageSentEvent` firing while `onReadyToSend` is paused at an
## `await` does not lose the second `channelReqId`'s terminal
## event.
const
channelId = ChannelId("sm-race-channel")
contentTopic = ContentTopic("/reliable-channel/test/sm-race")
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
var msgReqIds: seq[RequestId]
var sendsReturned = 0
let fakeSend: SendHandler = proc(
env: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
## Call 2 fires the first segment's terminal event and then
## yields, so the listener task runs while the second segment
## is still mid-`await` in `onReadyToSend` — the exact race
## window the regression test targets.
let id = RequestId("race-msg-req-" & $(msgReqIds.len + 1))
msgReqIds.add(id)
if msgReqIds.len == 2:
waku_message_events.MessageSentEvent.emit(
brokerCtx,
waku_message_events.MessageSentEvent(requestId: msgReqIds[0], messageHash: ""),
)
await sleepAsync(50.milliseconds)
sendsReturned.inc()
return ok(id)
discard manager
.createReliableChannel(
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
)
.expect("createReliableChannel")
var finalisedReqIds: seq[RequestId]
let bothFinalised = newFuture[void]("both-finalised")
discard ChannelMessageSentEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageSentEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
finalisedReqIds.add(evt.requestId)
if finalisedReqIds.len == 2 and not bothFinalised.finished():
bothFinalised.complete()
,
)
.expect("listen ChannelMessageSentEvent")
let channelReqId1 =
(await manager.send(channelId, "first".toBytes())).expect("send 1")
## Drain the first segment fully before queueing the second, so
## the rate-limit FIFO between sibling sends isn't itself under
## test here.
let firstDispatched = Moment.now() + 1.seconds
while Moment.now() < firstDispatched and msgReqIds.len < 1:
await sleepAsync(5.milliseconds)
check msgReqIds.len == 1
let channelReqId2 =
(await manager.send(channelId, "second".toBytes())).expect("send 2")
## Wait until `fakeSend(m2)` has fully returned and yield once
## more so `onReadyToSend`'s post-await continuation gets a chance
## to register `id2` in `inflightMessagingIds` before we emit its
## terminal event.
let dispatchDeadline = Moment.now() + 1.seconds
while Moment.now() < dispatchDeadline and sendsReturned < 2:
await sleepAsync(5.milliseconds)
check sendsReturned == 2
await sleepAsync(50.milliseconds)
## Finalise the second segment from the outside. If the race
## corrupted state, `channelReqId2`'s entry would never reach
## `inflightMessagingIds` and this event would silently miss.
waku_message_events.MessageSentEvent.emit(
brokerCtx,
waku_message_events.MessageSentEvent(requestId: msgReqIds[1], messageHash: ""),
)
let arrived = await bothFinalised.withTimeout(2.seconds)
check arrived
if arrived:
check finalisedReqIds.len == 2
check channelReqId1 in finalisedReqIds
check channelReqId2 in finalisedReqIds
(await waku.stop()).expect("stop")
suite "Reliable Channel - SDS persistence":
asyncTest "send persists SDS channel state through the persistency job":
## A send must flush `sds.meta` + `sds.log` through the shared "sds"
## job. Writes resolve on enqueue, so reads poll.
const
channelId = ChannelId("sds-persist-channel")
contentTopic = ContentTopic("/reliable-channel/test/persist")
Persistency.reset()
let root = getTempDir() / ("reliable_channel_sds_" & $epochTime().int)
removeDir(root)
let persistency = Persistency.instance(root).expect("persistency init")
defer:
Persistency.reset()
removeDir(root)
var waku: LogosDelivery
var manager: ReliableChannelManager
lockNewGlobalBrokerContext:
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
let fakeSend: SendHandler = proc(
env: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
return ok(RequestId("persist-msg-req-1"))
discard manager
.createReliableChannel(
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
)
.expect("createReliableChannel")
discard (await manager.send(channelId, "persist me".toBytes())).expect("send")
## Same handle the channel layer writes through (`openJob` is idempotent).
let job = persistency.openJob("sds").expect("openJob sds")
let chanKey = toKey(SdsChannelID(channelId))
proc pollMetaExists(): Future[bool] {.async.} =
let deadline = Moment.now() + 2.seconds
while Moment.now() < deadline:
let r = await job.exists(CatMeta, chanKey)
if r.isOk() and r.get():
return true
await sleepAsync(5.milliseconds)
return false
proc pollLogRow(): Future[bool] {.async.} =
## `sds.log` keys rows by (channelId, messageId) — scan the prefix.
let deadline = Moment.now() + 2.seconds
while Moment.now() < deadline:
let r = await job.scanPrefix(CatLog, chanKey)
if r.isOk() and r.get().len > 0:
return true
await sleepAsync(5.milliseconds)
return false
check await pollMetaExists()
check await pollLogRow()
(await waku.stop()).expect("stop")
## A marked WakuMessage carrying an SDS envelope, as it arrives off the wire.
proc sdsWakuMessage(contentTopic: ContentTopic, sdsWire: seq[byte]): WakuMessage =
WakuMessage(
payload: sdsWire,
contentTopic: contentTopic,
version: 0,
meta: LipWireReliableChannelVersion.toBytes(),
)
suite "Reliable Channel - SDS lifecycle":
asyncTest "out-of-order segments are parked and delivered in causal order":
## m2 depends on m1: m2 alone delivers nothing; m1 then delivers both,
## in causal order.
const
channelId = ChannelId("sds-causal-channel")
contentTopic = ContentTopic("/reliable-channel/test/causal")
let payload1 = "first message".toBytes()
let payload2 = "second message".toBytes()
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("createReliableChannel")
var deliveries: seq[seq[byte]]
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
deliveries.add(evt.payload)
,
)
.expect("listen ChannelMessageReceivedEvent")
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
let wire1 = (
await remotePeer.wrapOutgoingMessage(
payload1, "causal-m1", SdsChannelID(channelId)
)
).expect("wrap m1")
let wire2 = (
await remotePeer.wrapOutgoingMessage(
payload2, "causal-m2", SdsChannelID(channelId)
)
).expect("wrap m2")
## m2 first: missing dependency m1 -> parked, nothing delivered.
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "hash-m2", message: sdsWakuMessage(contentTopic, wire2)
),
)
await sleepAsync(100.milliseconds)
check deliveries.len == 0
## m1 arrives: m1 delivered, then the parked m2 released after it.
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "hash-m1", message: sdsWakuMessage(contentTopic, wire1)
),
)
let deadline = Moment.now() + 2.seconds
while Moment.now() < deadline and deliveries.len < 2:
await sleepAsync(5.milliseconds)
check deliveries.len == 2
if deliveries.len == 2:
check deliveries[0] == payload1
check deliveries[1] == payload2
(await waku.stop()).expect("stop")
asyncTest "duplicate SDS envelope is delivered to the app only once":
const
channelId = ChannelId("sds-dup-channel")
contentTopic = ContentTopic("/reliable-channel/test/dup")
let appPayload = "deliver once".toBytes()
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("createReliableChannel")
var deliveryCount = 0
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
deliveryCount.inc()
,
)
.expect("listen ChannelMessageReceivedEvent")
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
let wire = (
await remotePeer.wrapOutgoingMessage(
appPayload, "dup-m1", SdsChannelID(channelId)
)
).expect("wrap")
## Same envelope twice (different hashes) — the second must be suppressed.
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "dup-hash-1", message: sdsWakuMessage(contentTopic, wire)
),
)
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "dup-hash-2", message: sdsWakuMessage(contentTopic, wire)
),
)
await sleepAsync(200.milliseconds)
check deliveryCount == 1
(await waku.stop()).expect("stop")
asyncTest "SDS envelope for a foreign channel is dropped":
## Same content topic, different SDS channel id — dropped before unwrap.
const
channelId = ChannelId("sds-foreign-channel")
contentTopic = ContentTopic("/reliable-channel/test/foreign")
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("createReliableChannel")
var fired = false
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
fired = true
,
)
.expect("listen ChannelMessageReceivedEvent")
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
let wire = (
await remotePeer.wrapOutgoingMessage(
"not for you".toBytes(), "foreign-m1", SdsChannelID("some-other-channel")
)
).expect("wrap")
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "foreign-hash", message: sdsWakuMessage(contentTopic, wire)
),
)
await sleepAsync(200.milliseconds)
check not fired
(await waku.stop()).expect("stop")
asyncTest "received history survives channel close and re-create":
## Receive m1, close, re-create, replay m1: the duplicate is only
## suppressed if the history was actually restored from SQLite.
const
channelId = ChannelId("sds-restore-channel")
contentTopic = ContentTopic("/reliable-channel/test/restore")
let appPayload = "survive restart".toBytes()
Persistency.reset()
let root = getTempDir() / ("reliable_channel_sds_restore_" & $epochTime().int)
removeDir(root)
let persistency = Persistency.instance(root).expect("persistency init")
defer:
Persistency.reset()
removeDir(root)
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("createReliableChannel")
var deliveryCount = 0
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
deliveryCount.inc()
,
)
.expect("listen ChannelMessageReceivedEvent")
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
let wire = (
await remotePeer.wrapOutgoingMessage(
appPayload, "restore-m1", SdsChannelID(channelId)
)
).expect("wrap")
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "restore-hash-1", message: sdsWakuMessage(contentTopic, wire)
),
)
var deadline = Moment.now() + 2.seconds
while Moment.now() < deadline and deliveryCount < 1:
await sleepAsync(5.milliseconds)
check deliveryCount == 1
## Writes resolve on enqueue — wait until the row is applied before closing.
let job = persistency.openJob("sds").expect("openJob sds")
let chanKey = toKey(SdsChannelID(channelId))
deadline = Moment.now() + 2.seconds
var logVisible = false
while Moment.now() < deadline and not logVisible:
let r = await job.scanPrefix(CatLog, chanKey)
logVisible = r.isOk() and r.get().len > 0
if not logVisible:
await sleepAsync(5.milliseconds)
check logVisible
(await manager.closeChannel(channelId)).expect("closeChannel")
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("re-createReliableChannel")
## Replay the same envelope. Only a restored history suppresses it.
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "restore-hash-2", message: sdsWakuMessage(contentTopic, wire)
),
)
await sleepAsync(300.milliseconds)
check deliveryCount == 1
(await waku.stop()).expect("stop")
suite "Reliable Channel - SDS protocol semantics":
asyncTest "a reply references the received message and advances the lamport clock":
## After receiving m1, our outgoing wire must reference m1 in its causal
## history (that reference IS the ack) with a higher lamport.
const
channelId = ChannelId("sds-semantics-channel")
contentTopic = ContentTopic("/reliable-channel/test/semantics")
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
var capturedWires: seq[seq[byte]]
let fakeSend: SendHandler = proc(
env: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
## Noop encryption is identity, so the envelope payload IS the SDS wire.
capturedWires.add(env.payload)
return ok(RequestId("semantics-req-" & $capturedWires.len))
discard manager
.createReliableChannel(
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
)
.expect("createReliableChannel")
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
let wire1 = (
await remotePeer.wrapOutgoingMessage(
"from remote".toBytes(), "semantics-m1", SdsChannelID(channelId)
)
).expect("wrap m1")
let m1 = deserializeMessage(wire1).expect("deserialize m1")
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "semantics-hash-1", message: sdsWakuMessage(contentTopic, wire1)
),
)
await sleepAsync(100.milliseconds)
discard (await manager.send(channelId, "reply".toBytes())).expect("send")
var deadline = Moment.now() + 2.seconds
while Moment.now() < deadline and capturedWires.len < 1:
await sleepAsync(5.milliseconds)
check capturedWires.len == 1
let reply = deserializeMessage(capturedWires[0]).expect("deserialize reply")
check SdsMessageID("semantics-m1") in reply.causalHistory.getMessageIds()
check reply.lamportTimestamp > m1.lamportTimestamp
(await waku.stop()).expect("stop")
asyncTest "an unacknowledged send is acked by a later remote message":
## Our send sits in the outgoing buffer (visible in the persisted meta)
## until any later remote message references it — then the buffer drains.
const
channelId = ChannelId("sds-ack-channel")
contentTopic = ContentTopic("/reliable-channel/test/ack")
Persistency.reset()
let root = getTempDir() / ("reliable_channel_sds_ack_" & $epochTime().int)
removeDir(root)
let persistency = Persistency.instance(root).expect("persistency init")
defer:
Persistency.reset()
removeDir(root)
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
var capturedWires: seq[seq[byte]]
let fakeSend: SendHandler = proc(
env: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
capturedWires.add(env.payload)
return ok(RequestId("ack-req-" & $capturedWires.len))
discard manager
.createReliableChannel(
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
)
.expect("createReliableChannel")
discard (await manager.send(channelId, "needs ack".toBytes())).expect("send")
var deadline = Moment.now() + 2.seconds
while Moment.now() < deadline and capturedWires.len < 1:
await sleepAsync(5.milliseconds)
check capturedWires.len == 1
let job = persistency.openJob("sds").expect("openJob sds")
let chanKey = toKey(SdsChannelID(channelId))
proc outgoingBufferLen(): Future[int] {.async.} =
## Decode the persisted meta snapshot; -1 while not yet readable.
let r = await job.get(CatMeta, chanKey)
if r.isErr() or r.get().isNone():
return -1
let meta = ChannelMeta.decode(r.get().get()).valueOr:
return -1
return meta.outgoingBuffer.len
## After the send the message must sit unacknowledged in the buffer.
deadline = Moment.now() + 2.seconds
var bufLen = -1
while Moment.now() < deadline and bufLen != 1:
bufLen = await outgoingBufferLen()
if bufLen != 1:
await sleepAsync(5.milliseconds)
check bufLen == 1
## The remote received our wire; its next message references it.
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
discard
(await remotePeer.unwrapReceivedMessage(capturedWires[0])).expect("remote unwrap")
let ackCarrier = (
await remotePeer.wrapOutgoingMessage(
"any later message".toBytes(), "ack-carrier-1", SdsChannelID(channelId)
)
).expect("wrap ack carrier")
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "ack-hash-1", message: sdsWakuMessage(contentTopic, ackCarrier)
),
)
## Receiving it must drain the outgoing buffer (op-end meta flush).
deadline = Moment.now() + 2.seconds
bufLen = -1
while Moment.now() < deadline and bufLen != 0:
bufLen = await outgoingBufferLen()
if bufLen != 0:
await sleepAsync(5.milliseconds)
check bufLen == 0
(await waku.stop()).expect("stop")
asyncTest "three-deep dependency chain is released in causal order":
## m1 <- m2 <- m3 arriving as m3, m2, m1: all held until m1 lands,
## then released as m1, m2, m3.
const
channelId = ChannelId("sds-chain-channel")
contentTopic = ContentTopic("/reliable-channel/test/chain")
let payloads =
@["chain first".toBytes(), "chain second".toBytes(), "chain third".toBytes()]
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("createReliableChannel")
var deliveries: seq[seq[byte]]
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
deliveries.add(evt.payload)
,
)
.expect("listen ChannelMessageReceivedEvent")
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
var wires: seq[seq[byte]]
for i in 0 .. 2:
wires.add(
(
await remotePeer.wrapOutgoingMessage(
payloads[i], "chain-m" & $(i + 1), SdsChannelID(channelId)
)
).expect("wrap chain-m" & $(i + 1))
)
## Deepest first: m3, then m2 — both must be parked.
for i in [2, 1]:
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "chain-hash-" & $(i + 1),
message: sdsWakuMessage(contentTopic, wires[i]),
),
)
await sleepAsync(150.milliseconds)
check deliveries.len == 0
## The root arrives: everything drains in causal order.
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "chain-hash-1", message: sdsWakuMessage(contentTopic, wires[0])
),
)
let deadline = Moment.now() + 2.seconds
while Moment.now() < deadline and deliveries.len < 3:
await sleepAsync(5.milliseconds)
check deliveries.len == 3
if deliveries.len == 3:
check deliveries[0] == payloads[0]
check deliveries[1] == payloads[1]
check deliveries[2] == payloads[2]
(await waku.stop()).expect("stop")
asyncTest "sync envelope without app payload is consumed silently":
## Sync traffic has no app payload: no event, and normal traffic
## keeps flowing afterwards.
const
channelId = ChannelId("sds-sync-channel")
contentTopic = ContentTopic("/reliable-channel/test/sync")
let appPayload = "real message".toBytes()
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("createReliableChannel")
var deliveryCount = 0
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
deliveryCount.inc()
,
)
.expect("listen ChannelMessageReceivedEvent")
## Hand-built sync envelope: valid SDS message, empty content.
let syncMsg = SdsMessage.init(
messageId = SdsMessageID("sync-1"),
lamportTimestamp = 42,
causalHistory = @[],
channelId = SdsChannelID(channelId),
content = @[],
bloomFilter = @[],
)
let syncWire = serializeMessage(syncMsg).expect("serialize sync")
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "sync-hash-1", message: sdsWakuMessage(contentTopic, syncWire)
),
)
await sleepAsync(150.milliseconds)
check deliveryCount == 0
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
let wire = (
await remotePeer.wrapOutgoingMessage(
appPayload, "sync-m1", SdsChannelID(channelId)
)
).expect("wrap")
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "sync-hash-2", message: sdsWakuMessage(contentTopic, wire)
),
)
let deadline = Moment.now() + 2.seconds
while Moment.now() < deadline and deliveryCount < 1:
await sleepAsync(5.milliseconds)
check deliveryCount == 1
(await waku.stop()).expect("stop")
asyncTest "identical payloads get distinct message ids and both deliver":
## Identical content sent twice must get distinct message ids and
## reach the app twice — not collapse via the SDS duplicate check.
const
channelId = ChannelId("sds-unique-id-channel")
contentTopic = ContentTopic("/reliable-channel/test/unique-id")
let appPayload = "ok".toBytes()
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
var capturedWires: seq[seq[byte]]
let fakeSend: SendHandler = proc(
env: MessageEnvelope
): Future[Result[RequestId, string]] {.async: (raises: [CatchableError]), gcsafe.} =
capturedWires.add(env.payload)
return ok(RequestId("unique-req-" & $capturedWires.len))
discard manager
.createReliableChannel(
channelId, contentTopic, SdsParticipantID("local"), sendHandler = fakeSend
)
.expect("createReliableChannel")
var deliveries: seq[seq[byte]]
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
deliveries.add(evt.payload)
,
)
.expect("listen ChannelMessageReceivedEvent")
## Send side: the same payload twice must produce two distinct ids.
discard (await manager.send(channelId, appPayload)).expect("send 1")
discard (await manager.send(channelId, appPayload)).expect("send 2")
var deadline = Moment.now() + 2.seconds
while Moment.now() < deadline and capturedWires.len < 2:
await sleepAsync(5.milliseconds)
check capturedWires.len == 2
let id1 = deserializeMessage(capturedWires[0]).expect("wire 1").messageId
let id2 = deserializeMessage(capturedWires[1]).expect("wire 2").messageId
check id1 != id2
## Receive side: identical content under distinct ids delivers twice.
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
for i in 1 .. 2:
let wire = (
await remotePeer.wrapOutgoingMessage(
appPayload, "unique-m" & $i, SdsChannelID(channelId)
)
).expect("wrap " & $i)
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(
messageHash: "unique-hash-" & $i, message: sdsWakuMessage(contentTopic, wire)
),
)
deadline = Moment.now() + 2.seconds
while Moment.now() < deadline and deliveries.len < 2:
await sleepAsync(5.milliseconds)
check deliveries.len == 2
(await waku.stop()).expect("stop")
asyncTest "manager rejects operations on unknown channels":
var waku: LogosDelivery
var manager: ReliableChannelManager
lockNewGlobalBrokerContext:
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
check (await manager.send(ChannelId("no-such-channel"), "x".toBytes())).isErr()
check (await manager.closeChannel(ChannelId("no-such-channel"))).isErr()
(await waku.stop()).expect("stop")