Merge remote-tracking branch 'origin/master' into e2e-migration-docker-subset

# Conflicts:
#	.github/workflows/ci.yml
#	.github/workflows/e2e-api-tests.yml
This commit is contained in:
Egor Rachkovskii 2026-07-24 16:03:06 +01:00
commit 80550e8429
No known key found for this signature in database
359 changed files with 5162 additions and 3200 deletions

View File

@ -0,0 +1,551 @@
---
name: nim-brokers
description: Reference for writing code against the nim-brokers (`brokers`) nimble package — EventBroker, RequestBroker, MultiRequestBroker, SignalBroker, body sugars (listenIt, provideIt, reprovideIt, onSignalIt), bind* sugar, BrokerContext scoping, (mt) multi-thread variants, the (API) FFI shared-library surface, and BrokerInterface/BrokerImplement. Use when declaring brokers, registering listeners/providers/handlers, emitting or requesting, or exposing brokers to C/C++/Python/Rust/Go.
---
# Working with `nim-brokers`
> Agent skill for any project that depends on the `brokers` nimble package.
> For auto-discovery, copy this file into the consuming project as
> `.claude/skills/nim-brokers/SKILL.md` (or reference it from CLAUDE.md/AGENTS.md).
> Type-safe, decoupled messaging on top of **chronos** + **results**.
> All public APIs are exception-free: errors ride `Result[T, string]`, never raises.
## Mental model
Four macros, each declares a **broker type** and generates its full API. The
type *is* the channel — you call class-method-style on the typedesc: `T.emit`,
`T.request`, `T.listen`, `T.setProvider`. No instances, no singletons to wire.
| Macro | Pattern | Producer side | Consumer side |
|-------|---------|---------------|---------------|
| `EventBroker` | pub/sub, many→many, fire-and-forget | `T.emit(...)` | `T.listen(handler)` |
| `RequestBroker` | request/response, **single** provider | `T.setProvider(handler)` | `T.request(...)` |
| `MultiRequestBroker` | request/response, **many** providers, fan-out | `T.setProvider(handler)` (N×) | `T.request(...)` |
| `SignalBroker` | one-way notification, **single** handler, no reply | `T.signal(...)` | `T.onSignal(handler)` |
`(mt)` suffix → multi-thread variant (cross-thread dispatch). `(sync)` on
RequestBroker → blocking, non-async. `(API)` → FFI shared-library surface.
Any provider/handler that is a **class method** (`self.foo`) can be installed
with the `bind*` / `rebind*` sugar instead of a hand-written forwarding closure —
see "Binding class-method providers" below. For inline bodies, each registration
verb also has a **body-sugar form** (`listenIt`, `provideIt` / `reprovideIt`,
`onSignalIt`) that takes a block instead of a lambda — see the per-broker sections.
Import only what you use:
```nim
import brokers/event_broker
import brokers/request_broker
import brokers/multi_request_broker
import brokers/signal_broker
import brokers/broker_context # only if you need explicit contexts
```
---
## EventBroker — pub/sub
```nim
import chronos, brokers/event_broker
EventBroker:
type UserLoggedIn = object
userId*: int
name*: string
# listen returns Result[ListenerHandle, string]; keep the handle to drop later
let h = UserLoggedIn.listen(
proc(evt: UserLoggedIn): Future[void] {.async: (raises: []).} =
info "login", id = evt.userId
)
UserLoggedIn.emit(UserLoggedIn(userId: 7, name: "zoli")) # by value
UserLoggedIn.emit(userId = 7, name = "zoli") # by fields (inline-object only)
await UserLoggedIn.dropListener(h.get()) # drop one — cancels its in-flight work
await UserLoggedIn.dropAllListeners() # drop all for this context
```
- `emit` is **sync `void`** in *every* lane (single-thread, `(mt)`, `(API)`):
snapshots listeners, `asyncSpawn`s each. It does not await delivery —
`await sleepAsync(0)` or yield to flush in tests. Never `await`/`waitFor` an emit.
- Handlers MUST be `{.async: (raises: []).}`. Swallow your own exceptions.
- `dropListener`/`dropAllListeners` are **`async` (`Future[void]`) in every lane**
`await` them (or `discard`/`waitFor` in sync/`{.thread.}` contexts). Single-thread
cancels in-flight handlers before returning; MT/API bodies are suspension-free.
### `listenIt` — listener body sugar (all lanes)
```nim
# The block IS the listener body; the event value is injected as `it`.
# Returns listen's Result[<T>Listener, string] — keep it to drop later.
let h2 = UserLoggedIn.listenIt:
echo "login: ", it.name, " (#", it.userId, ")"
let h3 = UserLoggedIn.listenIt(myCtx): # ctx-scoped form; body may await
await sleepAsync(chronos.milliseconds(1))
echo "scoped: ", it.name
```
- `raises: []` is enforced as for a hand-written listener; `void` event types
inject nothing (the block is just the body).
### Payload variants
```nim
EventBroker:
type Tick = void # payload-less signal: Tick.emit() / listen(proc(): Future[void]...)
EventBroker:
type Score = int # native/alias/external types auto-wrapped in distinct
EventBroker:
type Blob = ref object # ref payloads fine
data*: seq[byte]
```
---
## RequestBroker — single provider request/response
Two declaration styles. **Coupled** (named `type` + `proc`) and **proc-sugar**
(payload decoupled, broker named after the Capitalized verb).
```nim
import chronos, brokers/request_broker
# Coupled: broker name == type name == request() return payload
RequestBroker:
type FetchUser = object
name*: string
proc signature*(id: int): Future[Result[FetchUser, string]] {.async.}
FetchUser.setProvider(
proc(id: int): Future[Result[FetchUser, string]] {.async.} =
ok(FetchUser(name: "u" & $id))
).isOk()
let r = await FetchUser.request(42) # Result[FetchUser, string]
FetchUser.clearProvider()
```
```nim
# Proc-sugar: broker = Capitalized verb, request() returns the RAW payload
RequestBroker:
proc getVersion(): Future[Result[string, string]] {.async.} # -> broker `GetVersion`
GetVersion.setProvider(
proc(): Future[Result[string, string]] {.async.} = ok("1.2.3")).get()
let v = await GetVersion.request() # r.value is plain string, no unwrap
```
Rules & behaviors:
- **One provider per signature.** A second `setProvider` returns `err(...)` (no
silent override). `clearProvider()` first to swap.
- Two signature slots coexist: zero-arg and arg-based (overload by arity).
- Provider exceptions are caught → `err(<msg>)`. Unset provider → `err(...)`.
- `isProvided()` checks registration. `T.request` is `async` here.
### `provideIt` / `reprovideIt` — provider body sugar (all lanes)
```nim
# The block is the provider's REAL proc body — the declared signature arg
# names (`id` here) are injected; return / result= / trailing expression work.
discard FetchUser.provideIt: # -> setProvider (keeps "already set" guard)
if id < 0:
return err("bad id: " & $id)
return ok(FetchUser(name: "u" & $id))
discard FetchUser.reprovideIt: # -> replaceProvider (swap, no guard)
ok(FetchUser(name: "v2-u" & $id)) # trailing expression works too
```
- A body that could fall through without producing a value is a **compile
error** (it would silently answer `err("")`) — e.g. a bare `echo` body, or an
`if` without `else` where only one branch returns.
- Dual-slot brokers name the slots explicitly: `provideIt` / `reprovideIt` for
the args slot, `provideItNoArgs` / `reprovideItNoArgs` for the zero-arg slot.
- Sync mode: same sugar, body cannot `await`. Ctx-form: `T.provideIt(ctx): body`.
### Sync mode — no event loop needed
```nim
RequestBroker(sync):
proc getId(): Result[int, string] # note: no Future, no {.async.}
GetId.setProvider(proc(): Result[int, string] = ok(42)).isOk()
let id = GetId.request() # blocking, returns Result directly
```
### void payload (action with no return value)
```nim
RequestBroker:
proc doReset(force: bool): Future[Result[void, string]] {.async.}
DoReset.setProvider(proc(force: bool): Future[Result[void, string]] {.async.} =
if force: ok() else: err("need force")).isOk()
```
---
## MultiRequestBroker — fan-out to many providers
Async only. `request()` calls **all** providers via `allFinished`, returns
`Result[seq[Payload], string]`. Any provider failing fails the whole request.
```nim
import chronos, brokers/multi_request_broker
MultiRequestBroker:
type Quote = object
price*: int
proc signature*(sym: string): Future[Result[Quote, string]] {.async.}
discard Quote.setProvider(proc(sym: string): Future[Result[Quote, string]] {.async.} =
ok(Quote(price: 100)))
discard Quote.setProvider(proc(sym: string): Future[Result[Quote, string]] {.async.} =
ok(Quote(price: 101)))
let all = await Quote.request("BTC") # all.get() is seq[Quote], len == 2
Quote.removeProvider(handle.get()) # remove one (handle from setProvider)
Quote.clearProviders() # remove all
```
- No providers registered → `ok(@[])` (empty, not error).
- Identical handler refs deduplicated on registration.
- `setProvider` returns `Result[ProviderHandle, string]`; capture it for `removeProvider`.
### `provideIt` — provider body sugar (adds, not replaces)
```nim
# Same body sugar as RequestBroker, but every provideIt ADDS a provider —
# there is NO reprovideIt (no replace verb), and the generated closure is a
# fresh reference, so it never dedups. Returns setProvider's handle.
let h1 = Quote.provideIt:
ok(Quote(price: 100))
let h2 = Quote.provideIt: # a second provider — NOT a replacement
if sym.len == 0:
return err("empty symbol")
return ok(Quote(price: 101))
Quote.removeProvider(h1.get()) # drop just one by its handle
```
- Dual-slot brokers: `provideIt` = args slot, `provideItNoArgs` = zero-arg slot.
- Same fall-through compile check as RequestBroker's `provideIt`.
---
## SignalBroker — one-way notification, single handler
Fire-and-forget into a module: an **inverted EventBroker** (single handler, no
reply path). `signal()` is a **plain (non-async) proc** returning
`Result[void, string]`; it does not tell you whether the handler *succeeded*
only whether it was **accepted**. Handler exceptions are swallowed (chronicles
`warn`). For delivery confirmation, use `RequestBroker` with a `void` response.
```nim
import chronos, brokers/signal_broker
SignalBroker:
type IngestSample = object
deviceId*: string
value*: float64
# ONE handler (a second onSignal returns err). Handler is async, raises: [].
discard IngestSample.onSignal(
proc(s: IngestSample) {.async: (raises: []).} =
info "sample", dev = s.deviceId, v = s.value)
let r = IngestSample.signal(IngestSample(deviceId: "d1", value: 0.5)) # by value
discard IngestSample.signal(deviceId = "d2", value = 1.25) # by fields
# r: Result[void, string]
# ok() = ACCEPTED (a handler exists + queue had room) — NOT "handled"
# err() = "no signal handler installed" | "queue full"
await IngestSample.dropSignalHandler() # async Future[void] — await it
```
### `onSignalIt` — handler body sugar (all lanes)
```nim
# Same as listenIt, for the single signal handler: the block is the handler
# body, the signal value is injected as `it`. onSignal's duplicate guard and
# Result return are unchanged.
discard IngestSample.onSignalIt:
echo it.deviceId, " = ", it.value
discard Wakeup.onSignalIt: echo "tick" # void payload: nothing injected
```
- `signal()` is **sync**, never `await` it. `dropSignalHandler()` is **async**.
- `type Foo = void` → payload-less pulse: `Foo.signal()` / `onSignal(proc() ...)`.
- Mock/replace trio (owning-thread only on `(mt)`): `replaceSignalHandler`,
`getCurrentSignalHandler`, `withMockSignalHandler(ctx, mock): body`.
- `(mt)` and `(API)` variants mirror the other brokers (one handler per context).
---
## Binding class-method providers — `bind*` / `rebind*` (v3.1)
Nim has no bound-method values (`self.send` is not a closure), so installing a
class method as a provider/handler normally needs a hand-written trampoline. The
`bind*` sugar synthesises it — **identical codegen, identical `self` capture**.
Passing a plain closure works too, so it is a strict superset of the typed verbs
(`setProvider` / `listen` / `onSignal` stay untouched).
```nim
# before — hand-written forwarding closure
MessagingSend.setProvider(self.brokerCtx,
proc(e: MessageEnvelope): Future[Result[RequestId, string]] {.async.} =
await self.send(e))
# after — the sugar generates exactly that trampoline
MessagingSend.bindProvider(self.brokerCtx, self.send)
```
| Broker | install sugar | replace sugar |
|--------|---------------|---------------|
| `RequestBroker` / `(mt)` | `bindProvider` | `rebindProvider` |
| `MultiRequestBroker` | `bindProvider` (additive) | — |
| `EventBroker` / `(mt)` | `bindListener` (returns the listen handle) | — |
| `SignalBroker` / `(mt)` | `bindSignalHandler` | `rebindSignalHandler` |
- Each verb has a **ctx-form** (`bindProvider(ctx, m)`) and a **no-ctx-form**
(`bindProvider(m)` → thread-global context).
- Dual-slot RequestBrokers (zero-arg **and** arg signatures) disambiguate by
arity automatically.
- Works on the `(API)` lane too — usable inside `setupProviders(ctx)`.
---
## BrokerContext — scoping / multi-instance
Every API takes an **optional first `BrokerContext` arg**. Omit it → the
thread-global context (`DefaultBrokerContext`). Use contexts to run independent
broker instances (per component, per test, per thread).
```nim
import brokers/broker_context
let ctx = NewBrokerContext() # globally-unique id (atomic)
discard MyEvent.listen(ctx, handler)
MyEvent.emit(ctx, payload)
FetchUser.setProvider(ctx, provider)
let r = await FetchUser.request(ctx, 42)
await MyEvent.dropAllListeners(ctx)
```
Thread setup helpers (callable before the event loop starts):
| Call | Use |
|------|-----|
| `setThreadBrokerContext(ctx)` | adopt a context created elsewhere as this thread's global |
| `initThreadBrokerContext(): BrokerContext` | create + set as thread-global in one call |
| `threadGlobalBrokerContext()` | read current thread global (lock-free) |
Async scoped swap (needs chronos loop): `lockGlobalBrokerContext` /
`lockNewGlobalBrokerContext` templates.
---
## Multi-thread variants `(mt)`
Add `(mt)`. **Identical call surface**`emit` stays sync `void` and `drop*`
stay async (`Future[void]`), so the same source compiles with or without the
tag. Cross-thread dispatch is handled under the hood. Build with `--threads:on`.
```nim
EventBroker(mt):
type Job = object
id*: int
# from any thread:
proc worker() {.thread.} =
Job.emit(Job(id: 1)) # emit is sync void — same as single-thread
```
- Same-thread calls take a direct fast path; cross-thread go through a per-bucket
channel drained by one dispatch coroutine. fd cost is **O(threads)**, not per-broker.
- A thread that listens must keep its event loop alive (the broker dispatches on it).
- MT brokers accept capacity kwargs: `EventBroker(mt, queueDepth = ..., slabCapacity = ...,
maxPayloadBytes = ..., preset = "...")`. Omit for defaults.
---
## Decision guide
| You want… | Use |
|-----------|-----|
| Notify N listeners, don't care about replies | `EventBroker` |
| Ask one authority for an answer | `RequestBroker` |
| Blocking call, no async context | `RequestBroker(sync)` |
| Ask everyone, aggregate replies | `MultiRequestBroker` |
| One-way notify a single handler, no reply | `SignalBroker` |
| Same pattern across OS threads | add `(mt)`, `--threads:on` (call surface unchanged) |
| Multiple isolated instances | pass a `BrokerContext` first arg |
| Install a class method (`self.foo`) as provider/handler | `bind*` / `rebind*` sugar |
| Register an inline body without lambda boilerplate | `listenIt` / `provideIt` / `reprovideIt` / `onSignalIt` |
| Expose to C/C++/Python/Rust/Go | `(API)` + `registerBrokerLibrary` (see AGENTS.md) |
## Gotchas
- Handlers/providers are `raises: []` — never let an exception escape; return `err()`.
- `setProvider` on a RequestBroker that already has one **fails** — clear first.
- Single-thread `emit` returns immediately; await a yield before asserting in tests.
- A non-`object`/`ref object` broker type is auto-wrapped in `distinct`; construct
with `T(value)` and read with the base-type conversion.
- Keep all interaction with one context on one thread (single-thread brokers are
thread-local); cross-thread requires the `(mt)` variant.
---
## FFI API `(API)` — expose brokers as a C/C++/Python/Rust/Go shared library
Add `(API)` to `RequestBroker` / `EventBroker` / `SignalBroker`. Same declaration
syntax — it additionally generates a fixed C ABI and typed foreign wrappers. Wire
format is CBOR; wrappers carry the typed surface. Build with `-d:BrokerFfiApi
--threads:on --app:lib`. (`SignalBroker(API)` rides `_call` one-way — enqueue
only, no response slot; `_callAsync` rejects it with `ApiStatusOneWay`.)
```nim
{.push raises: [].}
import brokers/[event_broker, request_broker, broker_context, api_library]
# Plain Nim object types used in signatures are AUTO-registered — no annotation.
type DeviceInfo* = object
deviceId*: int64
name*: string
online*: bool
RequestBroker(API):
type GetDevice = object # broker name == type name == response payload
deviceId*: int64
name*: string
proc signature*(deviceId: int64): Future[Result[GetDevice, string]] {.async.}
EventBroker(API):
type DeviceStatusChanged = object
deviceId*: int64
online*: bool
timestampMs*: int64
```
Providers + event emission live in one proc named **`setupProviders`** (the
generated runtime calls it on the processing thread during `createContext`):
```nim
proc setupProviders(ctx: BrokerContext): Result[void, string] =
let r = GetDevice.setProvider(ctx, # always pass the ctx the runtime gives you
proc(deviceId: int64): Future[Result[GetDevice, string]] {.closure, async.} =
DeviceStatusChanged.emit(ctx, # emit is sync void (all lanes)
DeviceStatusChanged(deviceId: deviceId, online: true, timestampMs: 0))
ok(GetDevice(deviceId: deviceId, name: "u")))
if r.isErr(): return err("register GetDevice: " & r.error())
ok()
# MUST be the last declaration in the module:
registerBrokerLibrary:
name: "mylib" # MUST match --nimMainPrefix and the .so basename
version: "1.0.0" # baked into <lib>_version() static string
initializeRequest: InitializeRequest # post-create config broker (optional)
shutdownRequest: ShutdownRequest # orderly teardown broker (optional)
{.pop.}
```
Build (name / `--nimMainPrefix` / `registerBrokerLibrary name` must all match):
```
nim c -d:BrokerFfiApi --threads:on --app:lib --path:. \
--outdir:build --nimMainPrefix:mylib mylib.nim
```
What you get — a fixed **12-function C ABI** per library: `_version`,
`_initialize` (once per process), `_createContext` (per instance), `_shutdown(ctx)`,
`_allocBuffer`, `_freeBuffer`, `_call` (sync round-trip), `_callAsync`
(non-blocking, callback-completed), `_subscribe`, `_unsubscribe`, `_listApis`,
`_getSchema`. `<lib>.h` (C) and `<lib>.hpp` (C++) are always emitted. Wire format
is CBOR (the historical native C-ABI codegen was retired in 3.0.0).
| Flag | Emits | Notes |
|------|-------|-------|
| *(default)* | `<lib>.h`, `<lib>.hpp` | C + C++ always |
| `-d:BrokerFfiApiGenPy` | `<lib>.py` (cbor2) | next to the `.so` |
| `-d:BrokerFfiApiGenRust` | `<lib>_rs/` Cargo crate | ciborium + serde |
| `-d:BrokerFfiApiGenGo` | `<lib>_go/` Go module | fxamacker/cbor |
FFI rules:
- `registerBrokerLibrary` is a **no-op without `-d:BrokerFfiApi`** — no `when defined`
guard needed; the normal in-process broker API still works.
- `(API)` brokers ride the MT lane, so they accept the same capacity kwargs as
`(mt)`: `RequestBroker(API, queueDepth = .., slabCapacity = .., maxPayloadBytes = ..,
preset = "..")`.
- `_createContext()` is readiness-synchronous: returns only after providers +
listeners are installed and the event courier is live.
- Inspect generated Nim with `-d:brokerDebug``build/broker_debug/*.gen.nim`.
---
## BrokerInterface / BrokerImplement — hierarchical / OOP layer
An object-oriented facade over the brokers: an **interface** groups several
brokers behind one abstract type; an **implementation** provides per-instance
methods. Each instance gets its own `BrokerContext`, so two instances of the same
impl are fully isolated. Direct `instance.method()` calls **tunnel through broker
dispatch** (so provider mocks are honored — not a plain vtable call).
```nim
import brokers/broker_interface
import brokers/broker_implement
BrokerInterface(IGreeter):
EventBroker:
type Greeted = object
who: string
RequestBroker:
proc greet(name: string): Future[Result[string, string]] {.async.}
RequestBroker:
proc version(): Future[Result[string, string]] {.async.}
type GreeterImpl = ref object of IGreeter # MUST be `ref object of <Interface>`
prefix: string
BrokerImplement GreeterImpl of IGreeter:
proc new(T: typedesc[GreeterImpl], prefix: string): GreeterImpl =
GreeterImpl(prefix: prefix) # optional ctor; create() calls it
method greet(self: GreeterImpl, name: string): Future[Result[string, string]] {.async.} =
ok(self.prefix & name)
method version(self: GreeterImpl): Future[Result[string, string]] {.async.} =
ok("v2")
```
Use it:
```nim
let g = GreeterImpl.create(prefix = "hi ") # new() + wires providers under g.brokerCtx
echo (waitFor g.greet("sue")).value # "hi sue" — tunnels through Greet broker
let base: IGreeter = g # virtual dispatch via the interface type
echo (waitFor base.greet("x")).value # resolves to the override
# Each instance is isolated by its own context:
let a = GreeterImpl.create(prefix = "a:")
let b = GreeterImpl.create(prefix = "b:")
# a.brokerCtx != b.brokerCtx
g.close() # clears THIS instance's providers + listeners; idempotent
```
Event facade (instance-scoped listen/emit — context is injected for you):
```nim
discard g.listen(Greeted,
proc(ev: Greeted): Future[void] {.async: (raises: []), gcsafe.} = …)
g.emit(Greeted, Greeted(who: "bob"))
```
Factory / dependency injection (resolve an impl behind the interface):
```nim
IGreeter.provideFactory(
proc(cfg: string): Result[IGreeter, string] =
ok(GreeterImpl.create(prefix = cfg)))
let d = IGreeter.create("cfg:") # Result[IGreeter, string]; last factory wins
```
Key points:
- The broker for `proc greet` is named **`Greet`** (Capitalized verb). Address it
directly with the instance context: `Greet.request(g.brokerCtx, "bob")`,
`Greet.clearProvider(g.brokerCtx)` (e.g. to install a mock).
- `Impl.create(args…)` = fresh context + `new` + provider wiring.
`Impl.createUnderContext(ctx, args…)` wires under an externally-supplied context
(the path the FFI runtime drives).
- `BrokerInterface(API, IName)` lowers the sub-brokers onto the MT/FFI lane so the
whole interface can be exposed as a shared library; `BrokerImplement` is unchanged.
- Sub-instances returned from a method (factory pattern) share the parent's
`classCtx` (routing) but get a distinct `instanceCtx` — see `classCtx()` /
`instanceCtx()` accessors.

View File

@ -86,20 +86,34 @@ jobs:
- name: Build binaries
id: build
if: ${{ steps.secrets.outcome == 'success' }}
# Sequential make invocations: wakunode2 and logosdeliverynode each run
# via `nimble <task>`. Under a single `make -j` the two nimble
# invocations re-resolve git deps concurrently and clobber each other
# in the shared ~/.nimble/pkgcache (e.g. "destination already exists",
# "fetch-pack: invalid index-pack output"), failing the build.
# Serializing the targets removes the race; Nim's own --parallelBuild
# still parallelizes compilation. Mirrors the -j1 fix in ci.yml.
run: |
make -j${NPROC} V=1 POSTGRES=1 NIMFLAGS="-d:disableMarchNative -d:chronicles_colors:none" wakunode2
make -j${NPROC} V=1 POSTGRES=1 NIMFLAGS="-d:disableMarchNative -d:chronicles_colors:none" logosdeliverynode
SHORT_REF=$(git rev-parse --short HEAD)
TAG=$([ "${PR_NUMBER}" == "" ] && echo "${SHORT_REF}" || echo "${PR_NUMBER}")
IMAGE=quay.io/wakuorg/nwaku-pr:${TAG}
LD_IMAGE=quay.io/wakuorg/nwaku-pr:${TAG}-logosdeliverynode
echo "image=${IMAGE}" >> $GITHUB_OUTPUT
echo "ld_image=${LD_IMAGE}" >> $GITHUB_OUTPUT
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
docker login -u ${QUAY_USER} -p ${QUAY_PASSWORD} quay.io
docker build -t ${IMAGE} -f docker/binaries/Dockerfile.bn.amd64 --label quay.expires-after=30d .
docker push ${IMAGE}
# logosdeliverynode image (same generic Dockerfile, selected via MAKE_TARGET)
docker build -t ${LD_IMAGE} --build-arg MAKE_TARGET=logosdeliverynode -f docker/binaries/Dockerfile.bn.amd64 --label quay.expires-after=30d .
docker push ${LD_IMAGE}
env:
QUAY_PASSWORD: ${{ secrets.QUAY_PASSWORD }}
QUAY_USER: ${{ secrets.QUAY_USER }}
@ -110,10 +124,11 @@ jobs:
if: ${{ github.event_name == 'pull_request' && steps.secrets.outcome == 'success' }}
with:
message: |
You can find the image built from this PR at
You can find the images built from this PR at
```
${{steps.build.outputs.image}}
${{steps.build.outputs.ld_image}}
```
Built from ${{ steps.build.outputs.commit_hash }}

View File

@ -54,7 +54,7 @@ endif
.PHONY: all test clean examples deps nimble install-nim install-nimble
# default target
all: | wakunode2 liblogosdelivery
all: | wakunode2 logosdeliverynode liblogosdelivery
examples: | example2 chat2 chat2bridge
@ -219,7 +219,7 @@ testcommon: | build-deps build
##########
## Waku ##
##########
.PHONY: testwaku wakunode2 testwakunode2 example2 chat2 chat2bridge liteprotocoltester
.PHONY: testwaku wakunode2 logosdeliverynode testwakunode2 example2 chat2 chat2bridge liteprotocoltester
testwaku: | build-deps build rln-deps librln
echo -e $(BUILD_MSG) "build/$@" && \
@ -236,6 +236,17 @@ else
$(NIMBLE) wakunode2
endif
# Windows: build with nim directly — `nimble <task>` re-clones git deps every
# build and they intermittently hang on the MSYS2 runner. Flags mirror logos_delivery.nimble.
logosdeliverynode: | build-deps build deps librln
ifeq ($(detected_OS),Windows)
echo -e $(BUILD_MSG) "build/$@" && \
nim c --out:build/logosdeliverynode --mm:refc --cpu:amd64 $(NIM_PARAMS) -d:chronicles_log_level=TRACE apps/logos_delivery_node/logosdeliverynode.nim
else
echo -e $(BUILD_MSG) "build/$@" && \
$(NIMBLE) logosdeliverynode
endif
benchmarks: | build-deps build deps librln
echo -e $(BUILD_MSG) "build/$@" && \
$(NIMBLE) benchmarks

View File

@ -1,5 +1,4 @@
import
std/[strutils, times, sequtils, osproc], math, results, options, testutils/unittests
import std/[strutils, times, sequtils, osproc], math, results, testutils/unittests
import
logos_delivery/waku/[

View File

@ -6,7 +6,7 @@ when not (compileOption("threads")):
{.push raises: [].}
import std/[strformat, strutils, times, options, random, sequtils]
import std/[strformat, strutils, times, random, sequtils]
import
confutils,
chronicles,
@ -48,7 +48,7 @@ import
./config_chat2
import libp2p/protocols/pubsub/rpc/messages, libp2p/protocols/pubsub/pubsub
import ../../logos_delivery/waku/rln
import logos_delivery/waku/rln
const Help = """
Commands: /[?|help|connect|nick|exit]
@ -215,10 +215,10 @@ proc publish(c: Chat, line: string) =
try:
if not c.node.wakuLegacyLightPush.isNil():
# Attempt lightpush
(waitFor c.node.legacyLightpushPublish(some(DefaultPubsubTopic), message)).isOkOr:
(waitFor c.node.legacyLightpushPublish(Opt.some(DefaultPubsubTopic), message)).isOkOr:
error "failed to publish lightpush message", error = error
else:
(waitFor c.node.publish(some(DefaultPubsubTopic), message)).isOkOr:
(waitFor c.node.publish(Opt.some(DefaultPubsubTopic), message)).isOkOr:
error "failed to publish message", error = error
except CatchableError:
error "caught error publishing message: ", error = getCurrentExceptionMsg()
@ -383,25 +383,25 @@ proc processInput(rfd: AsyncFD, rng: crypto.Rng) {.async.} =
echo "Connecting to static peers..."
await connectToNodes(chat, conf.staticnodes)
var dnsDiscoveryUrl = none(string)
var dnsDiscoveryUrl = Opt.none(string)
if conf.fleet != Fleet.none:
# Use DNS discovery to connect to selected fleet
echo "Connecting to " & $conf.fleet & " fleet using DNS discovery..."
if conf.fleet == Fleet.test:
dnsDiscoveryUrl = some(
dnsDiscoveryUrl = Opt.some(
"enrtree://AOGYWMBYOUIMOENHXCHILPKY3ZRFEULMFI4DOM442QSZ73TT2A7VI@test.waku.nodes.status.im"
)
else:
# Connect to sandbox by default
dnsDiscoveryUrl = some(
dnsDiscoveryUrl = Opt.some(
"enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im"
)
elif conf.dnsDiscoveryUrl != "":
# No pre-selected fleet. Discover nodes via DNS using user config
info "Discovering nodes using Waku DNS discovery", url = conf.dnsDiscoveryUrl
dnsDiscoveryUrl = some(conf.dnsDiscoveryUrl)
dnsDiscoveryUrl = Opt.some(conf.dnsDiscoveryUrl)
var discoveredNodes: seq[RemotePeerInfo]
@ -437,17 +437,17 @@ proc processInput(rfd: AsyncFD, rng: crypto.Rng) {.async.} =
if (conf.storenode != "") or (conf.store == true):
await node.mountStore()
var storenode: Option[RemotePeerInfo]
var storenode: Opt[RemotePeerInfo]
if conf.storenode != "":
let peerInfo = parsePeerInfo(conf.storenode)
if peerInfo.isOk():
storenode = some(peerInfo.value)
storenode = Opt.some(peerInfo.value)
else:
error "Incorrect conf.storenode", error = peerInfo.error
elif discoveredNodes.len > 0:
echo "Store enabled, but no store nodes configured. Choosing one at random from discovered peers"
storenode = some(discoveredNodes[rand(0 .. len(discoveredNodes) - 1)])
storenode = Opt.some(discoveredNodes[rand(0 .. len(discoveredNodes) - 1)])
if storenode.isSome():
# We have a viable storenode. Let's query it for historical messages.
@ -537,14 +537,14 @@ proc processInput(rfd: AsyncFD, rng: crypto.Rng) {.async.} =
credIndex: conf.rlnRelayCredIndex,
chainId: UInt256.fromBytesBE(conf.rlnRelayChainId.toBytesBE()),
ethClientUrls: conf.ethClientUrls.mapIt(string(it)),
creds: some(
creds: Opt.some(
RlnCreds(path: conf.rlnRelayCredPath, password: conf.rlnRelayCredPassword)
),
userMessageLimit: conf.rlnRelayUserMessageLimit,
epochSizeSec: conf.rlnEpochSizeSec,
)
waitFor node.setRlnValidator(rlnConf, spamHandler = some(spamHandler))
waitFor node.setRlnValidator(rlnConf, spamHandler = Opt.some(spamHandler))
let membershipIndex = node.rln.groupManager.membershipIndex.get()
let identityCredential = node.rln.groupManager.idCredentials.get()

View File

@ -1,4 +1,5 @@
import
results,
chronicles,
chronos,
confutils,
@ -26,7 +27,7 @@ type
.}: LogLevel
nodekey* {.desc: "P2P node private key as 64 char hex string.", name: "nodekey".}:
Option[crypto.PrivateKey]
Opt[crypto.PrivateKey]
listenAddress* {.
defaultValue: defaultListenAddress(config),
@ -234,7 +235,7 @@ type
rlnRelayCredIndex* {.
desc: "the index of the onchain commitment to use", name: "rln-relay-cred-index"
.}: Option[uint]
.}: Opt[uint]
rlnRelayDynamic* {.
desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false",
@ -317,9 +318,9 @@ proc parseCmdArg*(T: type Port, p: string): T =
proc completeCmdArg*(T: type Port, val: string): seq[string] =
return @[]
proc parseCmdArg*(T: type Option[uint], p: string): T =
proc parseCmdArg*(T: type Opt[uint], p: string): T =
try:
some(parseUint(p))
Opt.some(parseUint(p))
except CatchableError:
raise newException(ValueError, "Invalid unsigned integer")

View File

@ -1,7 +1,8 @@
{.push raises: [].}
import
std/[tables, times, strutils, hashes, sequtils, json, options],
results,
std/[tables, times, strutils, hashes, sequtils, json],
chronos,
confutils,
chronicles,
@ -102,7 +103,7 @@ proc toChat2(cmb: Chat2MatterBridge, jsonNode: JsonNode) {.async.} =
chat2_mb_transfers.inc(labelValues = ["mb_to_chat2"])
(await cmb.nodev2.publish(some(DefaultPubsubTopic), msg)).isOkOr:
(await cmb.nodev2.publish(Opt.some(DefaultPubsubTopic), msg)).isOkOr:
error "failed to publish message", error = error
proc toMatterbridge(
@ -156,8 +157,8 @@ proc new*(
nodev2Key: crypto.PrivateKey,
nodev2BindIp: IpAddress,
nodev2BindPort: Port,
nodev2ExtIp = none[IpAddress](),
nodev2ExtPort = none[Port](),
nodev2ExtIp = Opt.none(IpAddress),
nodev2ExtPort = Opt.none(Port),
contentTopic: string,
): T {.
raises: [Defect, ValueError, KeyError, TLSStreamProtocolError, IOError, LPError]
@ -264,7 +265,7 @@ when isMainModule:
## config, the external port is the same as the bind port.
let extPort =
if nodev2ExtIp.isSome() and nodev2ExtPort.isNone():
some(Port(uint16(conf.libp2pTcpPort) + conf.portsShift))
Opt.some(Port(uint16(conf.libp2pTcpPort) + conf.portsShift))
else:
nodev2ExtPort

View File

@ -1,5 +1,5 @@
import
std/options,
results,
confutils,
confutils/defs,
confutils/std/net,
@ -64,7 +64,7 @@ type Chat2MatterbridgeConf* = object
nodekey* {.
desc: "P2P node private key as hex", defaultValueDesc: "random", name: "nodekey"
.}: Option[crypto.PrivateKey]
.}: Opt[crypto.PrivateKey]
store* {.
desc: "Flag whether to start store protocol", defaultValue: true, name: "store"

View File

@ -6,7 +6,7 @@ when not (compileOption("threads")):
{.push raises: [].}
import std/[strformat, strutils, times, options, random, sequtils]
import std/[strformat, strutils, times, random, sequtils, sets]
import
confutils,
chronicles,
@ -31,9 +31,8 @@ import
protocols/kademlia/types,
protocols/service_discovery/types as sd_types,
nameresolving/dnsresolver,
protocols/mix/curve25519,
protocols/mix/mix_protocol,
] # define DNS resolution
import libp2p_mix/[curve25519, mix_protocol]
import
logos_delivery/waku/[
waku_core,
@ -216,9 +215,9 @@ proc publish(c: Chat, line: string) {.async.} =
(
waitFor c.node.lightpushPublish(
some(c.conf.getPubsubTopic(c.node, c.contentTopic)),
Opt.some(c.conf.getPubsubTopic(c.node, c.contentTopic)),
message,
none(RemotePeerInfo),
Opt.none(RemotePeerInfo),
true,
)
).isOkOr:
@ -312,7 +311,7 @@ proc readInput(wfd: AsyncFD) {.thread, raises: [Defect, CatchableError].} =
var alreadyUsedServicePeers {.threadvar.}: seq[RemotePeerInfo]
proc selectRandomServicePeer*(
pm: PeerManager, actualPeer: Option[RemotePeerInfo], codec: string
pm: PeerManager, actualPeer: Opt[RemotePeerInfo], codec: string
): Result[RemotePeerInfo, void] =
if actualPeer.isSome():
alreadyUsedServicePeers.add(actualPeer.get())
@ -355,7 +354,7 @@ proc maintainSubscription(
let subscribeErr = (
await wakuNode.filterSubscribe(
some(filterPubsubTopic), filterContentTopic, actualFilterPeer
Opt.some(filterPubsubTopic), filterContentTopic, actualFilterPeer
)
).errorOr:
await sleepAsync(SubscriptionMaintenance)
@ -377,7 +376,7 @@ proc maintainSubscription(
elif not preventPeerSwitch:
# try again with new peer without delay
let actualFilterPeer = selectRandomServicePeer(
wakuNode.peerManager, some(actualFilterPeer), WakuFilterSubscribeCodec
wakuNode.peerManager, Opt.some(actualFilterPeer), WakuFilterSubscribeCodec
).valueOr:
error "Failed to find new service peer. Exiting."
noFailedServiceNodeSwitches += 1
@ -463,10 +462,9 @@ proc processInput(rfd: AsyncFD, rng: crypto.Rng) {.async.} =
if conf.kadBootstrapNodes.len > 0:
var kadBootstrapPeers: seq[(PeerId, seq[MultiAddress])]
for nodeStr in conf.kadBootstrapNodes:
let (peerId, ma) = block:
parseFullAddress(nodeStr).isOkOr:
error "Failed to parse kademlia bootstrap node", node = nodeStr, error
continue
let (peerId, ma) = parseFullAddress(nodeStr).valueOr:
error "Failed to parse kademlia bootstrap node", node = nodeStr, error = error
continue
kadBootstrapPeers.add((peerId, @[ma]))
@ -474,7 +472,7 @@ proc processInput(rfd: AsyncFD, rng: crypto.Rng) {.async.} =
node.mountKademlia(
KademliaDiscoveryConf(
bootstrapNodes: kadBootstrapPeers,
servicesToDiscover: @[MixProtocolID],
servicesToDiscover: toHashSet([MixProtocolID]),
randomLookupInterval: chronos.seconds(60),
serviceLookupInterval: chronos.seconds(60),
kadDhtConfig: KadDHTConfig.new(),
@ -511,25 +509,25 @@ proc processInput(rfd: AsyncFD, rng: crypto.Rng) {.async.} =
conf: conf,
)
var dnsDiscoveryUrl = none(string)
var dnsDiscoveryUrl = Opt.none(string)
if conf.fleet != Fleet.none:
# Use DNS discovery to connect to selected fleet
echo "Connecting to " & $conf.fleet & " fleet using DNS discovery..."
if conf.fleet == Fleet.test:
dnsDiscoveryUrl = some(
dnsDiscoveryUrl = Opt.some(
"enrtree://AOGYWMBYOUIMOENHXCHILPKY3ZRFEULMFI4DOM442QSZ73TT2A7VI@test.waku.nodes.status.im"
)
else:
# Connect to sandbox by default
dnsDiscoveryUrl = some(
dnsDiscoveryUrl = Opt.some(
"enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im"
)
elif conf.dnsDiscoveryUrl != "":
# No pre-selected fleet. Discover nodes via DNS using user config
info "Discovering nodes using Waku DNS discovery", url = conf.dnsDiscoveryUrl
dnsDiscoveryUrl = some(conf.dnsDiscoveryUrl)
dnsDiscoveryUrl = Opt.some(conf.dnsDiscoveryUrl)
var discoveredNodes: seq[RemotePeerInfo]
@ -565,17 +563,17 @@ proc processInput(rfd: AsyncFD, rng: crypto.Rng) {.async.} =
if (conf.storenode != "") or (conf.store == true):
await node.mountStore()
var storenode: Option[RemotePeerInfo]
var storenode: Opt[RemotePeerInfo]
if conf.storenode != "":
let peerInfo = parsePeerInfo(conf.storenode)
if peerInfo.isOk():
storenode = some(peerInfo.value)
storenode = Opt.some(peerInfo.value)
else:
error "Incorrect conf.storenode", error = peerInfo.error
elif discoveredNodes.len > 0:
echo "Store enabled, but no store nodes configured. Choosing one at random from discovered peers"
storenode = some(discoveredNodes[rand(0 .. len(discoveredNodes) - 1)])
storenode = Opt.some(discoveredNodes[rand(0 .. len(discoveredNodes) - 1)])
if storenode.isSome():
# We have a viable storenode. Let's query it for historical messages.
@ -620,7 +618,7 @@ proc processInput(rfd: AsyncFD, rng: crypto.Rng) {.async.} =
if servicePeerInfo == nil or $servicePeerInfo.peerId == "":
# Assuming that service node supports all services
servicePeerInfo = selectRandomServicePeer(
node.peerManager, none(RemotePeerInfo), WakuLightpushCodec
node.peerManager, Opt.none(RemotePeerInfo), WakuLightpushCodec
).valueOr:
error "Couldn't find any service peer"
quit(QuitFailure)

View File

@ -1,4 +1,4 @@
import chronicles, chronos, std/strutils, regex
import results, chronicles, chronos, std/strutils, regex
import
eth/keys,
@ -32,7 +32,7 @@ type
.}: LogLevel
nodekey* {.desc: "P2P node private key as 64 char hex string.", name: "nodekey".}:
Option[crypto.PrivateKey]
Opt[crypto.PrivateKey]
listenAddress* {.
defaultValue: defaultListenAddress(config),
@ -287,9 +287,9 @@ proc parseCmdArg*(T: type Port, p: string): T =
proc completeCmdArg*(T: type Port, val: string): seq[string] =
return @[]
proc parseCmdArg*(T: type Option[uint], p: string): T =
proc parseCmdArg*(T: type Opt[uint], p: string): T =
try:
some(parseUint(p))
Opt.some(parseUint(p))
except CatchableError:
raise newException(ValueError, "Invalid unsigned integer")

View File

@ -4,7 +4,7 @@ else:
{.push raises: [].}
import
std/[options, net, strformat],
std/[net, strformat],
chronicles,
chronos,
metrics,

View File

@ -1,4 +1,4 @@
import chronos, results, options
import chronos, results
import logos_delivery/waku/[waku_node, waku_core]
import publisher_base
@ -18,7 +18,7 @@ method send*(
): Future[Result[void, string]] {.async.} =
# when error it must return original error desc due the text is used for distinction between error types in metrics.
discard (
await self.wakuNode.legacyLightpushPublish(some(topic), message, servicePeer)
await self.wakuNode.legacyLightpushPublish(Opt.some(topic), message, servicePeer)
).valueOr:
return err(error)
return ok()

View File

@ -1,7 +1,8 @@
{.push raises: [].}
import
std/[options, strutils, os, sequtils, net],
results,
std/[strutils, os, sequtils, net],
chronicles,
chronos,
metrics,
@ -95,7 +96,7 @@ when isMainModule:
wakuNodeConf.shards = @[conf.shard]
wakuNodeConf.contentTopics = conf.contentTopics
wakuNodeConf.clusterId = some(conf.clusterId)
wakuNodeConf.clusterId = Opt.some(conf.clusterId)
## TODO: Depending on the tester needs we might extend here with shards, clusterId, etc...
wakuNodeConf.metricsServer = true
@ -190,7 +191,7 @@ when isMainModule:
quit(QuitFailure)
serviceNodePeerInfo = selectRandomServicePeer(
waku.node.peerManager, none(RemotePeerInfo), codec
waku.node.peerManager, Opt.none(RemotePeerInfo), codec
).valueOr:
error "Service node selection failed"
quit(QuitFailure)

View File

@ -188,7 +188,7 @@ proc publishMessages(
if not preventPeerSwitch and noFailedPush > maxFailedPush:
info "Max push failure limit reached, Try switching peer."
actualServicePeer = selectRandomServicePeer(
wakuNode.peerManager, some(actualServicePeer), WakuLightPushCodec
wakuNode.peerManager, Opt.some(actualServicePeer), WakuLightPushCodec
).valueOr:
error "Failed to find new service peer. Exiting."
noFailedServiceNodeSwitches += 1

View File

@ -2,7 +2,6 @@
## subscribe to messages without relay
import
std/options,
system/ansi_c,
chronicles,
chronos,
@ -74,7 +73,7 @@ proc maintainSubscription(
let subscribeErr = (
await wakuNode.filterSubscribe(
some(filterPubsubTopic), filterContentTopic, actualFilterPeer
Opt.some(filterPubsubTopic), filterContentTopic, actualFilterPeer
)
).errorOr:
await sleepAsync(SubscriptionMaintenanceMs)
@ -99,7 +98,7 @@ proc maintainSubscription(
elif not preventPeerSwitch:
# try again with new peer without delay
actualFilterPeer = selectRandomServicePeer(
wakuNode.peerManager, some(actualFilterPeer), WakuFilterSubscribeCodec
wakuNode.peerManager, Opt.some(actualFilterPeer), WakuFilterSubscribeCodec
).valueOr:
error "Failed to find new service peer. Exiting."
noFailedServiceNodeSwitches += 1

View File

@ -1,7 +1,8 @@
{.push raises: [].}
import
std/[options, net, sysrand, random, strformat, strutils, sequtils],
results,
std/[net, sysrand, random, strformat, strutils, sequtils],
chronicles,
chronos,
metrics,
@ -53,7 +54,7 @@ proc translateToRemotePeerInfo*(peerAddress: string): Result[RemotePeerInfo, voi
## Note: This is kept for future use.
proc selectRandomCapablePeer*(
pm: PeerManager, codec: string, pubsubTopic: PubsubTopic
): Future[Option[RemotePeerInfo]] {.async.} =
): Future[Opt[RemotePeerInfo]] {.async.} =
var cap = Capabilities.Filter
if codec.contains("lightpush"):
cap = Capabilities.Lightpush
@ -65,9 +66,9 @@ proc selectRandomCapablePeer*(
trace "Found supportive peers count", count = supportivePeers.len()
trace "Found supportive peers", supportivePeers = $supportivePeers
if supportivePeers.len == 0:
return none(RemotePeerInfo)
return Opt.none(RemotePeerInfo)
var found = none(RemotePeerInfo)
var found = Opt.none(RemotePeerInfo)
while found.isNone() and supportivePeers.len > 0:
let rndPeerIndex = rand(0 .. supportivePeers.len - 1)
let randomPeer = supportivePeers[rndPeerIndex]
@ -80,7 +81,7 @@ proc selectRandomCapablePeer*(
let connOpt = pm.dialPeer(randomPeer, codec)
if (await connOpt.withTimeout(10.seconds)):
if connOpt.value().isSome():
found = some(randomPeer)
found = Opt.some(randomPeer)
info "Dialing successful",
peer = constructMultiaddrStr(randomPeer), codec = codec
else:
@ -94,7 +95,7 @@ proc selectRandomCapablePeer*(
# Debugging PX gathered peers connectivity
proc tryCallAllPxPeers*(
pm: PeerManager, codec: string, pubsubTopic: PubsubTopic
): Future[Option[seq[RemotePeerInfo]]] {.async.} =
): Future[Opt[seq[RemotePeerInfo]]] {.async.} =
var capability = Capabilities.Filter
if codec.contains("lightpush"):
capability = Capabilities.Lightpush
@ -107,7 +108,7 @@ proc tryCallAllPxPeers*(
info "Found supportive peers count", count = supportivePeers.len()
info "Found supportive peers", supportivePeers = $supportivePeers
if supportivePeers.len == 0:
return none(seq[RemotePeerInfo])
return Opt.none(seq[RemotePeerInfo])
var okPeers: seq[RemotePeerInfo] = @[]
@ -152,7 +153,7 @@ proc tryCallAllPxPeers*(
echo "PX returned peers found callable for " & codec & " / " & $capability & ":\n"
echo okPeersStr
return some(okPeers)
return Opt.some(okPeers)
proc pxLookupServiceNode*(
node: WakuNode, conf: LiteProtocolTesterConf
@ -168,7 +169,7 @@ proc pxLookupServiceNode*(
node.peerManager.addServicePeer(peerExchangeNode, WakuPeerExchangeCodec)
try:
await node.mountPeerExchange(some(conf.clusterId))
await node.mountPeerExchange(Opt.some(conf.clusterId))
except CatchableError:
error "failed to mount waku peer-exchange protocol",
error = getCurrentExceptionMsg()
@ -207,7 +208,7 @@ var alreadyUsedServicePeers {.threadvar.}: seq[RemotePeerInfo]
## Select service peers by codec from peer store randomly.
proc selectRandomServicePeer*(
pm: PeerManager, actualPeer: Option[RemotePeerInfo], codec: string
pm: PeerManager, actualPeer: Opt[RemotePeerInfo], codec: string
): Result[RemotePeerInfo, void] =
if actualPeer.isSome():
alreadyUsedServicePeers.add(actualPeer.get())

View File

@ -1,7 +1,7 @@
{.push raises: [].}
import
std/[sets, tables, sequtils, options, strformat],
std/[sets, tables, sequtils, strformat],
chronos/timer as chtimer,
chronicles,
chronos,
@ -123,10 +123,10 @@ proc addMessage*(
lpt_receiver_sender_peer_count.set(value = self.len)
proc lastMessageArrivedAt*(self: Statistics): Option[Moment] =
proc lastMessageArrivedAt*(self: Statistics): Opt[Moment] =
if self.receivedMessages > 0:
return some(self.helper.prevArrivedAt)
return none(Moment)
return Opt.some(self.helper.prevArrivedAt)
return Opt.none(Moment)
proc lossCount*(self: Statistics): uint32 =
self.helper.maxIndex - self.receivedMessages
@ -271,7 +271,7 @@ proc jsonStats*(self: PerPeerStatistics): string =
"{\"result:\": \"Error while generating json stats: " & getCurrentExceptionMsg() &
"\"}"
proc lastMessageArrivedAt*(self: PerPeerStatistics): Option[Moment] =
proc lastMessageArrivedAt*(self: PerPeerStatistics): Opt[Moment] =
var lastArrivedAt = Moment.init(0, Millisecond)
for stat in self.values:
let lastMsgFromPeerAt = stat.lastMessageArrivedAt().valueOr:
@ -281,9 +281,9 @@ proc lastMessageArrivedAt*(self: PerPeerStatistics): Option[Moment] =
lastArrivedAt = lastMsgFromPeerAt
if lastArrivedAt == Moment.init(0, Millisecond):
return none(Moment)
return Opt.none(Moment)
return some(lastArrivedAt)
return Opt.some(lastArrivedAt)
proc checkIfAllMessagesReceived*(
self: PerPeerStatistics, maxWaitForLastMessage: Duration

View File

@ -37,7 +37,7 @@ type LiteProtocolTesterConf* = object
desc:
"Loads configuration from a TOML file (cmd-line parameters take precedence) for the light waku node",
name: "config-file"
.}: Option[InputFile]
.}: Opt[InputFile]
## Log configuration
logLevel* {.

View File

@ -1,9 +1,10 @@
{.push raises: [].}
import
results,
chronicles,
json_serialization,
json_serialization/std/options,
json_serialization/pkg/results,
json_serialization/lexer
import logos_delivery/waku/rest_api/endpoint/serdes
@ -34,13 +35,13 @@ proc readValue*(
reader: var JsonReader[RestJson], value: var ProtocolTesterMessage
) {.gcsafe, raises: [SerializationError, IOError].} =
var
sender: Option[string]
index: Option[uint32]
count: Option[uint32]
startedAt: Option[int64]
sinceStart: Option[int64]
sincePrev: Option[int64]
size: Option[uint64]
sender: Opt[string]
index: Opt[uint32]
count: Opt[uint32]
startedAt: Opt[int64]
sinceStart: Opt[int64]
sincePrev: Opt[int64]
size: Opt[uint64]
for fieldName in readObjectFields(reader):
case fieldName
@ -49,43 +50,43 @@ proc readValue*(
reader.raiseUnexpectedField(
"Multiple `sender` fields found", "ProtocolTesterMessage"
)
sender = some(reader.readValue(string))
sender = Opt.some(reader.readValue(string))
of "index":
if index.isSome():
reader.raiseUnexpectedField(
"Multiple `index` fields found", "ProtocolTesterMessage"
)
index = some(reader.readValue(uint32))
index = Opt.some(reader.readValue(uint32))
of "count":
if count.isSome():
reader.raiseUnexpectedField(
"Multiple `count` fields found", "ProtocolTesterMessage"
)
count = some(reader.readValue(uint32))
count = Opt.some(reader.readValue(uint32))
of "startedAt":
if startedAt.isSome():
reader.raiseUnexpectedField(
"Multiple `startedAt` fields found", "ProtocolTesterMessage"
)
startedAt = some(reader.readValue(int64))
startedAt = Opt.some(reader.readValue(int64))
of "sinceStart":
if sinceStart.isSome():
reader.raiseUnexpectedField(
"Multiple `sinceStart` fields found", "ProtocolTesterMessage"
)
sinceStart = some(reader.readValue(int64))
sinceStart = Opt.some(reader.readValue(int64))
of "sincePrev":
if sincePrev.isSome():
reader.raiseUnexpectedField(
"Multiple `sincePrev` fields found", "ProtocolTesterMessage"
)
sincePrev = some(reader.readValue(int64))
sincePrev = Opt.some(reader.readValue(int64))
of "size":
if size.isSome():
reader.raiseUnexpectedField(
"Multiple `size` fields found", "ProtocolTesterMessage"
)
size = some(reader.readValue(uint64))
size = Opt.some(reader.readValue(uint64))
else:
unrecognizedFieldWarning(value)

View File

@ -1,4 +1,4 @@
import results, options, chronos
import results, chronos
import logos_delivery/waku/[waku_node, waku_core, waku_lightpush, waku_lightpush/common]
import publisher_base
@ -18,10 +18,12 @@ method send*(
): Future[Result[void, string]] {.async.} =
# when error it must return original error desc due the text is used for distinction between error types in metrics.
discard (
await self.wakuNode.lightpushPublish(some(topic), message, some(servicePeer))
await self.wakuNode.lightpushPublish(
Opt.some(topic), message, Opt.some(servicePeer)
)
).valueOr:
if error.code == LightPushErrorCode.NO_PEERS_TO_RELAY and
error.desc != some("No peers for topic, skipping publish"):
error.desc != Opt.some("No peers for topic, skipping publish"):
# TODO: We need better separation of errors happening on the client side or the server side.-
return err("dial_failure")
else:

View File

@ -0,0 +1,98 @@
{.push raises: [].}
import
std/[options, strutils, sequtils, net],
chronicles,
chronos,
metrics,
system/ansi_c,
libp2p/crypto/crypto
import
../../tools/confutils/cli_args,
logos_delivery/logos_delivery,
logos_delivery/waku/common/logging
logScope:
topics = "logosdeliverynode main"
const git_version* {.strdefine.} = "n/a"
{.pop.}
# @TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError
when isMainModule:
## Node setup happens in 6 phases:
## 1. Set up storage
## 2. Initialize node
## 3. Mount and initialize configured protocols
## 4. Start node and mounted protocols
## 5. Start monitoring tools and external interfaces
## 6. Setup graceful shutdown hooks
const versionString = "version / git commit hash: " & git_version
var wakuNodeConf = WakuNodeConf.load(version = versionString).valueOr:
error "failure while loading the configuration", error = error
quit(QuitFailure)
## Also called within LogosDelivery.new. The call to startRestServerEssentials
## needs the following line
logging.setupLog(wakuNodeConf.logLevel, wakuNodeConf.logFormat)
case wakuNodeConf.cmd
of generateRlnKeystore:
error "generateRlnKeystore not supported by logos_delivery_node; use wakunode2"
quit(QuitFailure)
of noCommand:
# `LogosDelivery` derives the per-layer config from `WakuNodeConf` itself
# (it runs `toWakuConf` internally), then builds the full stack bottom-up:
# Waku <- MessagingClient <- ReliableChannelManager
var node = (waitFor LogosDelivery.new(wakuNodeConf)).valueOr:
error "LogosDelivery initialization failed", error = error
quit(QuitFailure)
(waitFor node.start()).isOkOr:
error "Starting LogosDelivery failed", error = error
quit(QuitFailure)
info "Setting up shutdown hooks"
proc asyncStopper(node: LogosDelivery) {.async: (raises: [Exception]).} =
(await node.stop()).isOkOr:
error "LogosDelivery shutdown failed", error = error
quit(QuitSuccess)
# Handle Ctrl-C SIGINT
proc handleCtrlC() {.noconv.} =
when defined(windows):
# workaround for https://github.com/nim-lang/Nim/issues/4057
setupForeignThreadGc()
notice "Shutting down after receiving SIGINT"
asyncSpawn asyncStopper(node)
setControlCHook(handleCtrlC)
# Handle SIGTERM
when defined(posix):
proc handleSigterm(signal: cint) {.noconv.} =
notice "Shutting down after receiving SIGTERM"
asyncSpawn asyncStopper(node)
c_signal(ansi_c.SIGTERM, handleSigterm)
# Handle SIGSEGV
when defined(posix):
proc handleSigsegv(signal: cint) {.noconv.} =
# Require --debugger:native
fatal "Shutting down after receiving SIGSEGV"
# Not available in -d:release mode
writeStackTrace()
(waitFor node.stop()).isOkOr:
error "LogosDelivery shutdown failed", error = error
quit(QuitFailure)
c_signal(ansi_c.SIGSEGV, handleSigsegv)
info "Node setup complete"
runForever()

View File

@ -0,0 +1,10 @@
-d:chronicles_line_numbers
-d:discv5_protocol_id="d5waku"
-d:chronicles_runtime_filtering=on
-d:chronicles_sinks="textlines,json"
-d:chronicles_default_output_device=dynamic
# Disabling the following topics from nim-eth and nim-dnsdisc since some types cannot be serialized
-d:chronicles_disabled_topics="eth,dnsdisc.client"
# Results in empty output for some reason
#-d:"chronicles_enabled_topics=GossipSub:TRACE,WakuRelay:TRACE"
path = "../.."

View File

@ -422,7 +422,7 @@ proc initAndStartApp(
let
# some hardcoded parameters
rng = keys.newRng()
rng = crypto.newRng()
key = crypto.PrivateKey.random(Secp256k1, rng)[]
nodeTcpPort = Port(60000)
nodeUdpPort = Port(9000)
@ -433,7 +433,9 @@ proc initAndStartApp(
var builder = EnrBuilder.init(key)
builder.withIpAddressAndPorts(
ipAddr = some(extIp), tcpPort = some(nodeTcpPort), udpPort = some(nodeUdpPort)
ipAddr = Opt.some(extIp),
tcpPort = Opt.some(nodeTcpPort),
udpPort = Opt.some(nodeUdpPort),
)
builder.withWakuCapabilities(flags)
@ -450,7 +452,7 @@ proc initAndStartApp(
nodeBuilder.withNodeKey(key)
nodeBuilder.withRecord(record)
nodeBuilder.withSwitchConfiguration(maxConnections = some(MaxConnectedPeers))
nodeBuilder.withSwitchConfiguration(maxConnections = Opt.some(MaxConnectedPeers))
nodeBuilder.withPeerManagerConfig(
maxConnections = MaxConnectedPeers,
@ -473,7 +475,7 @@ proc initAndStartApp(
# discv5
let discv5Conf = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
discv5Config: Opt.none(DiscoveryConfig),
address: bindIp,
port: nodeUdpPort,
privateKey: keys.PrivateKey(key.skkey),
@ -481,7 +483,7 @@ proc initAndStartApp(
autoupdateRecord: false,
)
let wakuDiscv5 = WakuDiscoveryV5.new(node.rng, discv5Conf, some(record))
let wakuDiscv5 = WakuDiscoveryV5.new(node.rng, discv5Conf, Opt.some(record))
try:
wakuDiscv5.protocol.open()
@ -610,11 +612,11 @@ when isMainModule:
if conf.rlnRelay and conf.rlnRelayEthContractAddress != "":
let rlnConf = WakuRlnConfig(
dynamic: conf.rlnRelayDynamic,
credIndex: some(uint(0)),
credIndex: Opt.some(uint(0)),
ethContractAddress: conf.rlnRelayEthContractAddress,
ethClientUrls: conf.ethClientUrls.mapIt(string(it)),
epochSizeSec: conf.rlnEpochSizeSec,
creds: none(RlnCreds),
creds: Opt.none(RlnCreds),
onFatalErrorAction: onFatalErrorAction,
)

View File

@ -1,4 +1,5 @@
import
results,
std/[strutils, sequtils, tables, strformat],
confutils,
chronos,
@ -215,7 +216,7 @@ proc main(rng: Rng): Future[int] {.async.} =
let netConfig = NetConfig.init(
bindIp = bindIp,
bindPort = nodeTcpPort,
wsBindPort = some(wsBindPort),
wsBindPort = Opt.some(wsBindPort),
wsEnabled = isWs,
wssEnabled = isWss,
)
@ -244,7 +245,9 @@ proc main(rng: Rng): Future[int] {.async.} =
builder.withRecord(record)
builder.withNetworkConfiguration(netConfig.tryGet())
builder.withSwitchConfiguration(
secureKey = some(keyPath), secureCert = some(certPath), nameResolver = resolver
secureKey = Opt.some(keyPath),
secureCert = Opt.some(certPath),
nameResolver = resolver,
)
let node = builder.build().tryGet()

View File

@ -1,7 +1,7 @@
{.push raises: [].}
import
std/[options, strutils, sequtils, net],
std/[strutils, sequtils, net],
chronicles,
chronos,
metrics,

View File

@ -1,4 +1,3 @@
import std/options
import chronos, results, confutils, confutils/defs
import logos_delivery
@ -67,11 +66,9 @@ when isMainModule:
if args.ethRpcEndpoint == "":
# Create a basic configuration for the Waku node
# No RLN as we don't have an ETH RPC Endpoint
conf.mode = Core
conf.preset = "logos.dev"
else:
# Connect to TWN, use ETH RPC Endpoint for RLN
conf.mode = Core
conf.preset = "twn"
conf.ethClientUrls = @[EthRpcUrl(args.ethRpcEndpoint)]

View File

@ -43,13 +43,13 @@ proc messagePushHandler(
contentTopic = message.contentTopic,
timestamp = message.timestamp
proc setupAndSubscribe(rng: ref HmacDrbgContext) {.async.} =
proc setupAndSubscribe(rng: crypto.Rng) {.async.} =
# use notice to filter all waku messaging
setupLog(logging.LogLevel.NOTICE, logging.LogFormat.TEXT)
notice "starting subscriber", wakuPort = wakuPort
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[]
nodeKey = crypto.PrivateKey.random(Secp256k1, rng)[]
ip = parseIpAddress("0.0.0.0")
flags = CapabilitiesBitfield.init(relay = true)

View File

@ -48,13 +48,13 @@ proc splitPeerIdAndAddr(maddr: string): (string, string) =
peerId = parts[1]
return (address, peerId)
proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} =
proc setupAndPublish(rng: crypto.Rng, conf: LightPushMixConf) {.async.} =
# use notice to filter all waku messaging
setupLog(logging.LogLevel.DEBUG, logging.LogFormat.TEXT)
notice "starting publisher", wakuPort = conf.port
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[]).get()
nodeKey = crypto.PrivateKey.random(Secp256k1, rng).get()
ip = parseIpAddress("0.0.0.0")
flags = CapabilitiesBitfield.init(relay = true)
@ -84,7 +84,7 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.}
)
node.mountLightPushClient()
try:
await node.mountPeerExchange(some(uint16(clusterId)))
await node.mountPeerExchange(Opt.some(uint16(clusterId)))
except CatchableError:
error "failed to mount waku peer-exchange protocol",
error = getCurrentExceptionMsg()
@ -164,8 +164,9 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.}
timestamp: getNowInNanosecondTime(),
) # current timestamp
let res =
await node.wakuLightpushClient.publish(some(LightpushPubsubTopic), message, conn)
let res = await node.wakuLightpushClient.publish(
Opt.some(LightpushPubsubTopic), message, conn
)
let startTime = getNowInNanosecondTime()

View File

@ -35,13 +35,13 @@ const
LightpushPubsubTopic = PubsubTopic("/waku/2/rs/1/0")
LightpushContentTopic = ContentTopic("/examples/1/light-pubsub-example/proto")
proc setupAndPublish(rng: ref HmacDrbgContext) {.async.} =
proc setupAndPublish(rng: crypto.Rng) {.async.} =
# use notice to filter all waku messaging
setupLog(logging.LogLevel.NOTICE, logging.LogFormat.TEXT)
notice "starting publisher", wakuPort = wakuPort
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[]).get()
nodeKey = crypto.PrivateKey.random(Secp256k1, rng).get()
ip = parseIpAddress("0.0.0.0")
flags = CapabilitiesBitfield.init(relay = true)
@ -87,7 +87,7 @@ proc setupAndPublish(rng: ref HmacDrbgContext) {.async.} =
quit(QuitFailure)
let res = await node.legacyLightpushPublish(
some(LightpushPubsubTopic), message, lightpushPeer
Opt.some(LightpushPubsubTopic), message, lightpushPeer
)
if res.isOk:

View File

@ -1,4 +1,5 @@
import
results,
std/[tables, times, sequtils],
stew/byteutils,
chronicles,
@ -37,13 +38,13 @@ const bootstrapNode =
const wakuPort = 60000
const discv5Port = 9000
proc setupAndPublish(rng: ref HmacDrbgContext) {.async.} =
proc setupAndPublish(rng: crypto.Rng) {.async.} =
# use notice to filter all waku messaging
setupLog(logging.LogLevel.NOTICE, logging.LogFormat.TEXT)
notice "starting publisher", wakuPort = wakuPort, discv5Port = discv5Port
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[]).get()
nodeKey = crypto.PrivateKey.random(Secp256k1, rng).get()
ip = parseIpAddress("0.0.0.0")
flags = CapabilitiesBitfield.init(relay = true)
@ -63,7 +64,7 @@ proc setupAndPublish(rng: ref HmacDrbgContext) {.async.} =
discard bootstrapNodeEnr.fromURI(bootstrapNode)
let discv5Conf = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
discv5Config: Opt.none(DiscoveryConfig),
address: ip,
port: Port(discv5Port),
privateKey: keys.PrivateKey(nodeKey.skkey),
@ -75,8 +76,8 @@ proc setupAndPublish(rng: ref HmacDrbgContext) {.async.} =
let wakuDiscv5 = WakuDiscoveryV5.new(
node.rng,
discv5Conf,
some(node.enr),
some(node.peerManager),
Opt.some(node.enr),
Opt.some(node.peerManager),
node.topicSubscriptionQueue,
)
@ -119,7 +120,7 @@ proc setupAndPublish(rng: ref HmacDrbgContext) {.async.} =
timestamp: now(),
) # current timestamp
let res = await node.publish(some(pubSubTopic), message)
let res = await node.publish(Opt.some(pubSubTopic), message)
if res.isOk:
notice "published message",

View File

@ -1,4 +1,5 @@
import
results,
std/[tables, sequtils],
stew/byteutils,
chronicles,
@ -35,13 +36,13 @@ const bootstrapNode =
const wakuPort = 50000
const discv5Port = 8000
proc setupAndSubscribe(rng: ref HmacDrbgContext) {.async.} =
proc setupAndSubscribe(rng: crypto.Rng) {.async.} =
# use notice to filter all waku messaging
setupLog(logging.LogLevel.NOTICE, logging.LogFormat.TEXT)
notice "starting subscriber", wakuPort = wakuPort, discv5Port = discv5Port
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[]
nodeKey = crypto.PrivateKey.random(Secp256k1, rng)[]
ip = parseIpAddress("0.0.0.0")
flags = CapabilitiesBitfield.init(relay = true)
@ -61,7 +62,7 @@ proc setupAndSubscribe(rng: ref HmacDrbgContext) {.async.} =
discard bootstrapNodeEnr.fromURI(bootstrapNode)
let discv5Conf = WakuDiscoveryV5Config(
discv5Config: none(DiscoveryConfig),
discv5Config: Opt.none(DiscoveryConfig),
address: ip,
port: Port(discv5Port),
privateKey: keys.PrivateKey(nodeKey.skkey),
@ -73,8 +74,8 @@ proc setupAndSubscribe(rng: ref HmacDrbgContext) {.async.} =
let wakuDiscv5 = WakuDiscoveryV5.new(
node.rng,
discv5Conf,
some(node.enr),
some(node.peerManager),
Opt.some(node.enr),
Opt.some(node.peerManager),
node.topicSubscriptionQueue,
)

View File

@ -3,15 +3,16 @@
import tools/confutils/cli_args
import logos_delivery/waku/[common/logging, waku, factory/networks_config]
import
std/[options, strutils, os, sequtils],
results,
std/[strutils, os, sequtils],
chronicles,
chronos,
metrics,
libp2p/crypto/crypto
export
networks_config, waku, logging, options, strutils, os, sequtils, stewNet, chronicles,
chronos, metrics, crypto
networks_config, waku, logging, strutils, os, sequtils, stewNet, chronicles, chronos,
metrics, crypto
proc setup*(): Waku =
const versionString = "version / git commit hash: " & waku.git_version
@ -29,18 +30,18 @@ proc setup*(): Waku =
# Override configuration
conf.maxMessageSize = twnNetworkConf.maxMessageSize
conf.clusterId = some(twnNetworkConf.clusterId)
conf.clusterId = Opt.some(twnNetworkConf.clusterId)
conf.rlnRelayEthContractAddress = twnNetworkConf.rlnRelayEthContractAddress
conf.rlnRelayDynamic = some(twnNetworkConf.rlnRelayDynamic)
conf.discv5Discovery = some(twnNetworkConf.discv5Discovery)
conf.rlnRelayDynamic = Opt.some(twnNetworkConf.rlnRelayDynamic)
conf.discv5Discovery = Opt.some(twnNetworkConf.discv5Discovery)
conf.discv5BootstrapNodes =
conf.discv5BootstrapNodes & twnNetworkConf.discv5BootstrapNodes
conf.rlnEpochSizeSec = some(twnNetworkConf.rlnEpochSizeSec)
conf.rlnRelayUserMessageLimit = some(twnNetworkConf.rlnRelayUserMessageLimit)
conf.rlnEpochSizeSec = Opt.some(twnNetworkConf.rlnEpochSizeSec)
conf.rlnRelayUserMessageLimit = Opt.some(twnNetworkConf.rlnRelayUserMessageLimit)
# Only set rlnRelay to true if relay is configured
if conf.relay:
conf.rlnRelay = some(twnNetworkConf.rlnRelay)
conf.rlnRelay = Opt.some(twnNetworkConf.rlnRelay)
info "Starting node"
var waku = (waitFor Waku.new(conf)).valueOr:

View File

@ -51,7 +51,7 @@ proc sendThruWaku*(
).valueOr:
return err("could not append rate limit proof to the message: " & error)
(await self.waku.node.publish(some(DefaultPubsubTopic), message)).isOkOr:
(await self.waku.node.publish(Opt.some(DefaultPubsubTopic), message)).isOkOr:
return err("failed to publish message: " & $error)
info "rate limit proof is appended to the message"
@ -182,7 +182,7 @@ proc new*(
except CatchableError:
error "could not handle SCP message: ", err = getCurrentExceptionMsg()
waku.node.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), some(handler)).isOkOr:
waku.node.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), Opt.some(handler)).isOkOr:
error "could not subscribe to pubsub topic: ", err = $error
return err("could not subscribe to pubsub topic: " & $error)
return ok(SCP)

View File

@ -1,22 +1,20 @@
import std/[times, options]
import std/times
import confutils, chronicles, chronos, results
import logos_delivery/waku/[waku_core, common/protobuf]
import libp2p/protobuf/minprotobuf
export
times, options, confutils, chronicles, chronos, results, waku_core, protobuf,
minprotobuf
export times, confutils, chronicles, chronos, results, waku_core, protobuf, minprotobuf
type SerializedKey* = seq[byte]
type WakuStealthCommitmentMsg* = object
request*: bool
spendingPubKey*: Option[SerializedKey]
viewingPubKey*: Option[SerializedKey]
ephemeralPubKey*: Option[SerializedKey]
stealthCommitment*: Option[SerializedKey]
viewTag*: Option[uint64]
spendingPubKey*: Opt[SerializedKey]
viewingPubKey*: Opt[SerializedKey]
ephemeralPubKey*: Opt[SerializedKey]
stealthCommitment*: Opt[SerializedKey]
viewTag*: Opt[uint64]
proc decode*(T: type WakuStealthCommitmentMsg, buffer: seq[byte]): ProtoResult[T] =
var msg = WakuStealthCommitmentMsg()
@ -29,20 +27,20 @@ proc decode*(T: type WakuStealthCommitmentMsg, buffer: seq[byte]): ProtoResult[T
discard ?pb.getField(2, spendingPubKey)
msg.spendingPubKey =
if spendingPubKey.len > 0:
some(spendingPubKey)
Opt.some(spendingPubKey)
else:
none(SerializedKey)
Opt.none(SerializedKey)
var viewingPubKey = newSeq[byte]()
discard ?pb.getField(3, viewingPubKey)
msg.viewingPubKey =
if viewingPubKey.len > 0:
some(viewingPubKey)
Opt.some(viewingPubKey)
else:
none(SerializedKey)
Opt.none(SerializedKey)
if msg.spendingPubKey.isSome() and msg.viewingPubKey.isSome():
msg.stealthCommitment = none(SerializedKey)
msg.viewTag = none(uint64)
msg.stealthCommitment = Opt.none(SerializedKey)
msg.viewTag = Opt.none(uint64)
return ok(msg)
if msg.spendingPubKey.isSome() and msg.viewingPubKey.isNone():
return err(ProtoError.RequiredFieldMissing)
@ -55,25 +53,25 @@ proc decode*(T: type WakuStealthCommitmentMsg, buffer: seq[byte]): ProtoResult[T
discard ?pb.getField(4, stealthCommitment)
msg.stealthCommitment =
if stealthCommitment.len > 0:
some(stealthCommitment)
Opt.some(stealthCommitment)
else:
none(SerializedKey)
Opt.none(SerializedKey)
var ephemeralPubKey = newSeq[byte]()
discard ?pb.getField(5, ephemeralPubKey)
msg.ephemeralPubKey =
if ephemeralPubKey.len > 0:
some(ephemeralPubKey)
Opt.some(ephemeralPubKey)
else:
none(SerializedKey)
Opt.none(SerializedKey)
var viewTag: uint64
discard ?pb.getField(6, viewTag)
msg.viewTag =
if viewTag != 0:
some(viewTag)
Opt.some(viewTag)
else:
none(uint64)
Opt.none(uint64)
if msg.stealthCommitment.isNone() and msg.viewTag.isNone() and
msg.ephemeralPubKey.isNone():
@ -86,8 +84,8 @@ proc decode*(T: type WakuStealthCommitmentMsg, buffer: seq[byte]): ProtoResult[T
return err(ProtoError.RequiredFieldMissing)
if msg.stealthCommitment.isSome() and msg.viewTag.isSome():
msg.spendingPubKey = none(SerializedKey)
msg.viewingPubKey = none(SerializedKey)
msg.spendingPubKey = Opt.none(SerializedKey)
msg.viewingPubKey = Opt.none(SerializedKey)
ok(msg)
@ -118,8 +116,8 @@ proc constructRequest*(
): WakuStealthCommitmentMsg =
WakuStealthCommitmentMsg(
request: true,
spendingPubKey: some(spendingPubKey),
viewingPubKey: some(viewingPubKey),
spendingPubKey: Opt.some(spendingPubKey),
viewingPubKey: Opt.some(viewingPubKey),
)
proc constructResponse*(
@ -127,7 +125,7 @@ proc constructResponse*(
): WakuStealthCommitmentMsg =
WakuStealthCommitmentMsg(
request: false,
stealthCommitment: some(stealthCommitment),
ephemeralPubKey: some(ephemeralPubKey),
viewTag: some(viewTag),
stealthCommitment: Opt.some(stealthCommitment),
ephemeralPubKey: Opt.some(ephemeralPubKey),
viewTag: Opt.some(viewTag),
)

View File

@ -30,6 +30,21 @@ proc logosdelivery_channel_create(
return ok(string(id))
proc logosdelivery_channel_exists(
ctx: ptr FFIContext[LogosDelivery],
callback: FFICallBack,
userData: pointer,
channelIdStr: cstring,
) {.ffi.} =
## Returns `"true"` or `"false"`; a missing channel is not an error.
requireInitializedNode(ctx, "ChannelExists"):
return err(errMsg)
requireChannels(ctx, "ChannelExists"):
return err(errMsg)
return ok($ctx.myLib[].reliableChannelManager.channelExists(ChannelId($channelIdStr)))
proc logosdelivery_channel_send(
ctx: ptr FFIContext[LogosDelivery],
callback: FFICallBack,

View File

@ -1,4 +1,4 @@
import std/[json, sugar, options]
import std/[json, sugar]
import chronos, chronicles, results, ffi
import
logos_delivery,
@ -24,17 +24,17 @@ func fromJsonNode(jsonContent: JsonNode): Result[StoreQueryRequest, string] =
let pubsubTopic =
if jsonContent.contains("pubsubTopic"):
some(jsonContent["pubsubTopic"].getStr())
Opt.some(jsonContent["pubsubTopic"].getStr())
else:
none(string)
Opt.none(string)
let paginationCursor =
if jsonContent.contains("paginationCursor"):
let hash = jsonContent["paginationCursor"].getStr().hexToHash().valueOr:
return err("Failed converting paginationCursor hex string to bytes: " & error)
some(hash)
Opt.some(hash)
else:
none(WakuMessageHash)
Opt.none(WakuMessageHash)
let paginationForwardBool = jsonContent["paginationForward"].getBool()
let paginationForward =
@ -42,9 +42,9 @@ func fromJsonNode(jsonContent: JsonNode): Result[StoreQueryRequest, string] =
let paginationLimit =
if jsonContent.contains("paginationLimit"):
some(uint64(jsonContent["paginationLimit"].getInt()))
Opt.some(uint64(jsonContent["paginationLimit"].getInt()))
else:
none(uint64)
Opt.none(uint64)
let startTime = ?jsonContent.getProtoInt64("timeStart")
let endTime = ?jsonContent.getProtoInt64("timeEnd")

View File

@ -86,6 +86,13 @@ extern "C"
const char *contentTopic,
const char *senderId);
// Check whether a reliable channel is currently open. Returns "true" or
// "false"; an unknown channel id is not an error.
int logosdelivery_channel_exists(void *ctx,
FFICallBack callback,
void *userData,
const char *channelId);
// Send a message on a reliable channel.
// messageJson: { "payload": "base64-encoded-payload", "ephemeral": false }
// Returns a request ID that can be used to track delivery.

View File

@ -1,5 +1,4 @@
import logos_delivery/waku/compat/option_valueor
import std/[atomics, options, macros]
import std/[atomics, macros]
import chronicles, chronos, chronos/threadsync, ffi
import
logos_delivery/waku/waku_core/message/message,

View File

@ -1,7 +1,7 @@
import std/[json, options, strutils]
import std/[json, strutils]
import results
proc getProtoInt64*(node: JsonNode, key: string): Result[Option[int64], string] =
proc getProtoInt64*(node: JsonNode, key: string): Result[Opt[int64], string] =
try:
let (value, ok) =
if node.hasKey(key):
@ -13,8 +13,8 @@ proc getProtoInt64*(node: JsonNode, key: string): Result[Option[int64], string]
(0, false)
if ok:
return ok(some(value))
return ok(Opt.some(value))
return ok(none(int64))
return ok(Opt.none(int64))
except CatchableError:
return err("Invalid int64 value in `" & key & "`")

View File

@ -65,7 +65,7 @@ requires "https://github.com/logos-messaging/nim-ffi#v0.1.3"
requires "https://github.com/logos-messaging/nim-sds.git#b12f5ee07c5b764303b51fb948b32a4ade1de3b5"
requires "https://github.com/NagyZoltanPeter/nim-brokers.git#v3.1.4"
requires "https://github.com/NagyZoltanPeter/nim-brokers.git#v3.3.0"
requires "https://github.com/vacp2p/nim-lsquic.git#v0.5.1"
requires "https://github.com/vacp2p/nim-jwt.git#057ec95eb5af0eea9c49bfe9025b3312c95dc5f2"
@ -388,6 +388,10 @@ task wakunode2, "Build Waku v2 cli node":
let name = "wakunode2"
buildBinary name, "apps/wakunode2/", " -d:chronicles_log_level=TRACE "
task logosdeliverynode, "Build Logos Delivery cli node":
let name = "logosdeliverynode"
buildBinary name, "apps/logos_delivery_node/", " -d:chronicles_log_level=TRACE "
task benchmarks, "Some benchmarks":
let name = "benchmarks"
buildBinary name, "apps/benchmarks/", "-p:../.."

View File

@ -1,15 +1,15 @@
import std/options
import results
type ReliableChannelManagerConf* = object
## All-`Option` partial; unset fields fall back to `createReliableChannel` defaults.
segmentationEnableReedSolomon*: Option[bool]
## All-`Opt` partial; unset fields fall back to `createReliableChannel` defaults.
segmentationEnableReedSolomon*: Opt[bool]
## Add Reed-Solomon parity segments for recovery of lost segments.
segmentationSegmentSizeBytes*: Option[int] ## Maximum segment size in bytes.
sdsAcknowledgementTimeoutMs*: Option[int]
segmentationSegmentSizeBytes*: Opt[int] ## Maximum segment size in bytes.
sdsAcknowledgementTimeoutMs*: Opt[int]
## Time to wait before retransmitting an unacknowledged message.
sdsMaxRetransmissions*: Option[int]
sdsMaxRetransmissions*: Opt[int]
## Maximum retransmission attempts before delivery fails.
sdsCausalHistorySize*: Option[int] ## Number of message ids kept in causal history.
rateLimitEnabled*: Option[bool] ## Enable rate limiting.
rateLimitEpochPeriodSec*: Option[int] ## Rate-limit epoch length in seconds.
rateLimitMessagesPerEpoch*: Option[int] ## Messages allowed per rate-limit epoch.
sdsCausalHistorySize*: Opt[int] ## Number of message ids kept in causal history.
rateLimitEnabled*: Opt[bool] ## Enable rate limiting.
rateLimitEpochPeriodSec*: Opt[int] ## Rate-limit epoch length in seconds.
rateLimitMessagesPerEpoch*: Opt[int] ## Messages allowed per rate-limit epoch.

View File

@ -1,38 +1,45 @@
{.push raises: [].}
import std/options
import results
import logos_delivery/api/conf/modes
import logos_delivery/api/conf/messaging_conf
import logos_delivery/api/conf/channels_conf
export options, messaging_conf, channels_conf
export modes, messaging_conf, channels_conf
type LogosDeliveryConf* = object
## Aggregates the per-layer config objects. A layer is mounted iff its config
## is present.
kernelConf*: KernelConf
messagingConf*: Option[MessagingClientConf]
channelsConf*: Option[ReliableChannelManagerConf]
messagingConf*: Opt[MessagingClientConf]
channelsConf*: Opt[ReliableChannelManagerConf]
proc init*(T: type LogosDeliveryConf, kernelConf: KernelConf): LogosDeliveryConf =
return LogosDeliveryConf(kernelConf: kernelConf)
proc init*(
T: type LogosDeliveryConf,
entryLayer: EntryLayer = EntryLayer.channels,
mode: LogosDeliveryMode,
preset: string,
messagingOverrides: MessagingClientConf,
channelsOverrides: ReliableChannelManagerConf,
): ConfResult[LogosDeliveryConf] =
## Structured (preset + overrides) entry. Only `messaging` / `channels` layers
## reach here; the `kernel` layer uses `init(kernelConf)` (raw, mode ignored).
let merged = merge(?resolvePreset(preset), messagingOverrides)
var kernelConf = ?toWakuNodeConf(merged, mode)
kernelConf.preset = preset
return ok(
LogosDeliveryConf(
kernelConf: KernelConf(kernelConf),
messagingConf: some(merged),
channelsConf: some(channelsOverrides),
messagingConf: Opt.some(merged),
channelsConf:
if entryLayer == EntryLayer.channels:
Opt.some(channelsOverrides)
else:
Opt.none(ReliableChannelManagerConf),
)
)

View File

@ -1,6 +1,6 @@
{.push raises: [].}
import std/[json, options, strutils, tables]
import std/[json, strutils, tables]
import results
import tools/confutils/conf_from_json
@ -8,6 +8,7 @@ import logos_delivery/api/conf/logos_delivery_conf
const
# Lowercased, since `collectJsonFields` keys the object case-insensitively.
KeyEntryLayer = "entrylayer"
KeyMode = "mode"
KeyPreset = "preset"
KeyKernelConf = "kernelconf"
@ -24,10 +25,21 @@ proc parseMode(s: string): Result[LogosDeliveryMode, string] =
return ok(LogosDeliveryMode.Core)
of "edge":
return ok(LogosDeliveryMode.Edge)
of "fleet":
return ok(LogosDeliveryMode.Fleet)
else:
return err("invalid mode: '" & s & "' (expected 'Core', 'Edge' or 'Fleet')")
return err("invalid mode: '" & s & "' (expected 'Core' or 'Edge')")
proc parseEntryLayer(s: string): Result[EntryLayer, string] =
case s.strip().toLowerAscii()
of "kernel":
return ok(EntryLayer.kernel)
of "messaging":
return ok(EntryLayer.messaging)
of "channels":
return ok(EntryLayer.channels)
else:
return err(
"invalid entryLayer: '" & s & "' (expected 'kernel', 'messaging' or 'channels')"
)
proc parseOverrides[T](defaults: T, node: JsonNode, label: string): Result[T, string] =
## Parse the JSON object `node` as overrides on top of `defaults`.
@ -82,8 +94,8 @@ proc parseFlatConf(
return ok(
LogosDeliveryConf(
kernelConf: KernelConf(kernel),
messagingConf: some(messaging),
channelsConf: some(ReliableChannelManagerConf()),
messagingConf: Opt.some(messaging),
channelsConf: Opt.some(ReliableChannelManagerConf()),
)
)
@ -106,16 +118,25 @@ proc parseLogosDeliveryConf*(jsonStr: string): ConfResult[LogosDeliveryConf] =
mode = ?parseMode(v.getStr())
top.del(KeyMode)
if mode == LogosDeliveryMode.Fleet:
# Kernel-only: a raw kernelConf and no upper layers.
var entryLayer = EntryLayer.channels
if top.hasKey(KeyEntryLayer):
let (_, v) = top.getOrDefault(KeyEntryLayer)
if v.kind != JString:
return err("entryLayer must be a string")
entryLayer = ?parseEntryLayer(v.getStr())
top.del(KeyEntryLayer)
if entryLayer == EntryLayer.kernel:
# Kernel-only: a raw kernelConf and no upper layers; mode is ignored.
if not top.hasKey(KeyKernelConf):
return err("fleet mode requires a 'kernelConf' object")
return err("kernel entry layer requires a 'kernelConf' object")
let (_, v) = top.getOrDefault(KeyKernelConf)
let kernel = ?parseOverrides(?defaultWakuNodeConf(), v, "kernelConf")
top.del(KeyKernelConf)
if top.len > 0:
return
err(unknownKeysError(top, "fleet mode takes only 'kernelConf'; unexpected"))
return err(
unknownKeysError(top, "kernel entry layer takes only 'kernelConf'; unexpected")
)
return ok(LogosDeliveryConf.init(KernelConf(kernel)))
# [Legacy flat JSON config] A wrapper key marks our structured shape. Otherwise any
@ -158,6 +179,12 @@ proc parseLogosDeliveryConf*(jsonStr: string): ConfResult[LogosDeliveryConf] =
if top.len > 0:
return err(unknownKeysError(top, "Unrecognized configuration option(s) found"))
return LogosDeliveryConf.init(mode, preset, messagingOverrides, channelsOverrides)
return LogosDeliveryConf.init(
entryLayer = entryLayer,
mode = mode,
preset = preset,
messagingOverrides = messagingOverrides,
channelsOverrides = channelsOverrides,
)
{.pop.}

View File

@ -1,59 +1,60 @@
import std/options
import std/net
import results
import libp2p/crypto/crypto
import results, libp2p/crypto/crypto
import logos_delivery/api/conf/kernel_conf
import logos_delivery/waku/common/logging
import logos_delivery/waku/factory/networks_config
import logos_delivery/messaging/rate_limit_manager/rate_limit_manager
export kernel_conf
export kernel_conf, rate_limit_manager
type LogosDeliveryMode* {.pure.} = enum
Edge # client-only node
Core # full service node
Fleet # kernel-only node from a raw kernel config
# `LogosDeliveryMode` and `EntryLayer` are defined at the leaf (`cli_args`) so
# they can appear on `WakuNodeConf`; re-exported here via `kernel_conf`.
type MessagingClientConf* = object
clusterId* {.name: "cluster-id".}: Option[uint16] ## Network cluster id.
numShardsInCluster* {.name: "num-shards-in-network".}: Option[uint16]
clusterId* {.name: "cluster-id".}: Opt[uint16] ## Network cluster id.
numShardsInCluster* {.name: "num-shards-in-network".}: Opt[uint16]
## Number of shards in the cluster.
p2pTcpPort* {.name: "tcp-port".}: Option[Port] ## TCP listening port.
discv5UdpPort* {.name: "discv5-udp-port".}: Option[Port] ## discv5 UDP port.
websocketSupport* {.name: "websocket-support".}: Option[bool]
p2pTcpPort* {.name: "tcp-port".}: Opt[Port] ## TCP listening port.
discv5UdpPort* {.name: "discv5-udp-port".}: Opt[Port] ## discv5 UDP port.
websocketSupport* {.name: "websocket-support".}: Opt[bool]
## Enable the websocket transport.
websocketPort* {.name: "websocket-port".}: Option[Port] ## Websocket listening port.
quicSupport* {.name: "quic-support".}: Option[bool] ## Enable the QUIC transport.
quicPort* {.name: "quic-port".}: Option[Port] ## QUIC (UDP) listening port.
listenIpv4* {.name: "listen-address".}: Option[IpAddress] ## Inbound bind address.
maxMessageSize* {.name: "max-msg-size".}: Option[string]
websocketPort* {.name: "websocket-port".}: Opt[Port] ## Websocket listening port.
quicSupport* {.name: "quic-support".}: Opt[bool] ## Enable the QUIC transport.
quicPort* {.name: "quic-port".}: Opt[Port] ## QUIC (UDP) listening port.
listenIpv4* {.name: "listen-address".}: Opt[IpAddress] ## Inbound bind address.
maxMessageSize* {.name: "max-msg-size".}: Opt[string]
## Maximum accepted message size (e.g. "150 KiB").
entryNodes* {.name: "entry-node".}: Option[seq[string]]
entryNodes* {.name: "entry-node".}: Opt[seq[string]]
## Bootstrap / connectivity nodes (enrtree or multiaddr).
ethRpcEndpoints* {.name: "rln-relay-eth-client-address".}: Option[seq[EthRpcUrl]]
ethRpcEndpoints* {.name: "rln-relay-eth-client-address".}: Opt[seq[EthRpcUrl]]
## Ethereum RPC endpoints (required for RLN validation); multiple for fail-over.
rlnContractAddress* {.name: "rln-relay-eth-contract-address".}: Option[string]
rlnContractAddress* {.name: "rln-relay-eth-contract-address".}: Opt[string]
## RLN contract address; when set, RLN validation is enabled.
rlnChainId* {.name: "rln-relay-chain-id".}: Option[uint]
rlnChainId* {.name: "rln-relay-chain-id".}: Opt[uint]
## Chain id the RLN contract is deployed on.
rlnEpochSizeSec* {.name: "rln-relay-epoch-sec".}: Option[uint]
rlnEpochSizeSec* {.name: "rln-relay-epoch-sec".}: Opt[uint]
## RLN epoch size, in seconds.
reliabilityEnabled* {.name: "reliability".}: Option[bool]
reliabilityEnabled* {.name: "reliability".}: Opt[bool]
## Enable store-based send reliability.
store*: Option[bool] ## Enable the store protocol.
storenode* {.name: "storenode".}: Option[string]
storeMessageDbUrl* {.name: "store-message-db-url".}: Option[string]
store*: Opt[bool] ## Enable the store protocol.
storenode* {.name: "storenode".}: Opt[string]
storeMessageDbUrl* {.name: "store-message-db-url".}: Opt[string]
## Database connection URL for the store service's persistent storage.
storeMessageRetentionPolicy* {.name: "store-message-retention-policy".}:
Option[string] ## Store retention policy (e.g. "time:3600;size:1GB").
storeMaxNumDbConnections* {.name: "store-max-num-db-connections".}: Option[int]
storeMessageRetentionPolicy* {.name: "store-message-retention-policy".}: Opt[string]
## Store retention policy (e.g. "time:3600;size:1GB").
storeMaxNumDbConnections* {.name: "store-max-num-db-connections".}: Opt[int]
## Maximum number of simultaneous store database connections.
logLevel* {.name: "log-level".}: Option[logging.LogLevel]
logLevel* {.name: "log-level".}: Opt[logging.LogLevel]
## Process log level (TRACE..FATAL); applied by the kernel on node creation.
logFormat* {.name: "log-format".}: Option[logging.LogFormat]
logFormat* {.name: "log-format".}: Opt[logging.LogFormat]
## Process log format (TEXT or JSON); applied by the kernel on node creation.
nodeKey* {.name: "nodekey".}: Option[crypto.PrivateKey]
nodeKey* {.name: "nodekey".}: Opt[crypto.PrivateKey]
## P2P node private key (64-char hex): stable identity / peerId across restarts.
rateLimit*: Opt[RateLimitConfig]
## Per-epoch message rate limit enforced by the send service. `Opt` like
## every other field so `merge` propagates a caller's override; unset falls
## back to `DefaultRateLimitConfig` (rate limiting disabled).
proc applyMode*(conf: var WakuNodeConf, mode: LogosDeliveryMode): ConfResult[void] =
## Sets the protocol flags implied by the mode.
@ -62,7 +63,7 @@ proc applyMode*(conf: var WakuNodeConf, mode: LogosDeliveryMode): ConfResult[voi
conf.relay = true
conf.filter = true
conf.lightpush = true
conf.discv5Discovery = some(true)
conf.discv5Discovery = Opt.some(true)
conf.peerExchange = true
conf.rendezvous = true
of LogosDeliveryMode.Edge:
@ -71,9 +72,6 @@ proc applyMode*(conf: var WakuNodeConf, mode: LogosDeliveryMode): ConfResult[voi
conf.filter = false
conf.lightpush = false
conf.store = false
of LogosDeliveryMode.Fleet:
return
err("fleet mode takes a raw kernel config; use LogosDelivery.new(kernelConf)")
return ok()
proc toWakuNodeConf*(
@ -82,6 +80,10 @@ proc toWakuNodeConf*(
## Mode sets the protocol flags; set fields map to their kernel counterpart.
var conf = ?defaultWakuNodeConf()
?applyMode(conf, mode)
# Keep the `mode` field consistent with the applied flags so a later
# `LogosDelivery.new(WakuNodeConf)` re-application is idempotent instead of
# clobbering these flags with the field's default (`Core`).
conf.mode = mode
if self.store.isSome():
conf.store = self.store.get()
@ -108,11 +110,11 @@ proc toWakuNodeConf*(
conf.ethClientUrls = self.ethRpcEndpoints.get()
if self.rlnContractAddress.isSome():
conf.rlnRelayEthContractAddress = self.rlnContractAddress.get()
conf.rlnRelay = some(true)
conf.rlnRelay = Opt.some(true)
if self.rlnChainId.isSome():
conf.rlnRelayChainId = self.rlnChainId.get()
if self.rlnEpochSizeSec.isSome():
conf.rlnEpochSizeSec = some(self.rlnEpochSizeSec.get().uint64)
conf.rlnEpochSizeSec = Opt.some(self.rlnEpochSizeSec.get().uint64)
if self.logLevel.isSome():
conf.logLevel = self.logLevel.get()
if self.logFormat.isSome():
@ -132,7 +134,7 @@ proc toWakuNodeConf*(
proc merge*(base, overrides: MessagingClientConf): MessagingClientConf =
var m = base
for _, mField, oField in fieldPairs(m, overrides):
when oField is Option:
when oField is Opt:
if oField.isSome():
mField = oField
return m
@ -140,8 +142,8 @@ proc merge*(base, overrides: MessagingClientConf): MessagingClientConf =
proc resolvePreset*(preset: string): ConfResult[MessagingClientConf] =
## Preset to messaging-only fields. Kernel-mirrored fields stay unset; the
## kernel resolves those from `conf.preset`.
let npcOpt = ?toNetworkPresetConf(preset, none(uint16))
let npcOpt = ?toNetworkPresetConf(preset, Opt.none(uint16))
if npcOpt.isNone():
return ok(MessagingClientConf())
let npc = npcOpt.get()
return ok(MessagingClientConf(reliabilityEnabled: some(npc.p2pReliability)))
return ok(MessagingClientConf(reliabilityEnabled: Opt.some(npc.p2pReliability)))

View File

@ -0,0 +1,18 @@
## Leaf module for the app-level mode / entry-layer enums.
##
## These appear on `WakuNodeConf` (in the leaf `tools/confutils/cli_args`), so
## they must live in a module that `cli_args` can import without a cycle — i.e.
## a module that imports nothing from the config/api layers. `logos_delivery_conf`
## re-exports them so consumers still get them from there.
type LogosDeliveryMode* {.pure.} = enum
## Drives the kernel-internal protocol mountings. Applied only for the
## `messaging` / `channels` entry layers; ignored when `entryLayer == kernel`.
Edge # client-only node
Core # full service node
type EntryLayer* {.pure.} = enum
## Selects which API layer `LogosDelivery` instantiates.
kernel # transport kernel only; ignores `mode` and uses the config as-is
messaging # kernel + messaging client
channels # kernel + messaging + reliable channels

View File

@ -10,6 +10,7 @@ type ReliableChannelApi* = concept c
createReliableChannel(
c, channelId = ChannelId, contentTopic = ContentTopic, senderId = SdsParticipantID
) is Result[ChannelId, string]
channelExists(c, channelId = ChannelId) is bool
closeChannel(c, channelId = ChannelId) is Future[Result[void, string]]
send(c, channelId = ChannelId, appPayload = seq[byte]) is
Future[Result[RequestId, string]]

View File

@ -1,8 +1,6 @@
import logos_delivery/waku/compat/option_valueor
import libp2p/crypto/crypto
{.push raises: [].}
import bearssl/rand, std/times, chronos
import libp2p/crypto/crypto, bearssl/rand, std/times, chronos
import stew/byteutils
import logos_delivery/waku/utils/requests as request_utils
import logos_delivery/waku/waku_core/[topics/content_topic, message/message, time]

View File

@ -1,6 +1,6 @@
## Reliable Channel layer API — channel lifecycle
## (createReliableChannel / closeChannel).
import std/[options, tables]
import std/tables
import results, chronos, chronicles
import logos_delivery/api/types
@ -15,17 +15,17 @@ const SdsJobId = "sds"
## One persistency job shared by every channel's SDS state; rows are
## keyed by channelId.
proc sdsPersistence(): Option[Persistence] =
proc sdsPersistence(): Opt[Persistence] =
## SDS backend from the Persistency singleton; memory-only fallback when
## it is unavailable (e.g. unit tests).
let p = Persistency.instance().valueOr:
info "SDS persistence disabled, running memory-only", reason = $error
return none(Persistence)
return Opt.none(Persistence)
let job = p.openJob(SdsJobId).valueOr:
warn "SDS persistence disabled, could not open persistency job",
jobId = SdsJobId, reason = $error
return none(Persistence)
return some(newSdsPersistence(job))
return Opt.none(Persistence)
return Opt.some(newSdsPersistence(job))
proc createReliableChannel*(
self: ReliableChannelManager,
@ -51,15 +51,6 @@ proc createReliableChannel*(
causalHistorySize: cc.sdsCausalHistorySize.get(DefaultCausalHistorySize),
persistence: sdsPersistence(),
)
let rateConfig = RateLimitConfig(
# Setting a rate-limit parameter implies enabling; an explicit
# rateLimitEnabled still wins.
enabled: cc.rateLimitEnabled.get(
cc.rateLimitEpochPeriodSec.isSome() or cc.rateLimitMessagesPerEpoch.isSome()
),
epochPeriodSec: cc.rateLimitEpochPeriodSec.get(DefaultEpochPeriodSec),
messagesPerEpoch: cc.rateLimitMessagesPerEpoch.get(DefaultMessagesPerEpoch),
)
let chn = ReliableChannel.new(
channelId = channelId,
@ -67,13 +58,18 @@ proc createReliableChannel*(
senderId = senderId,
segConfig = segConfig,
sdsConfig = sdsConfig,
rateConfig = rateConfig,
brokerCtx = self.brokerCtx,
)
self.channels[channelId] = chn
return ok(channelId)
proc channelExists*(self: ReliableChannelManager, channelId: ChannelId): bool =
## True while the channel is held by the manager, i.e. between a successful
## `createReliableChannel` and `closeChannel`. Persisted SDS state for a
## closed channel does not count as existing.
return self.channels.hasKey(channelId)
proc closeChannel*(
self: ReliableChannelManager, channelId: ChannelId
): Future[Result[void, string]] {.async: (raises: []).} =

View File

@ -14,7 +14,7 @@ proc send*(
): Future[Result[RequestId, string]] {.async: (raises: []).} =
## Spec-level entry point. Looks the channel up by id and delegates
## to `ReliableChannel.send`, which exposes the visible pipeline
## segmentation -> sds -> rate_limit_manager -> encryption.
## segmentation -> sds -> encryption -> dispatch.
let chn = self.channels.getOrDefault(channelId)
if chn.isNil():
return err("unknown channel: " & channelId)

View File

@ -1,80 +0,0 @@
## Rate Limit Manager for the Reliable Channel API.
##
## Tracks messages sent per RLN epoch and delays dispatch when the
## limit is approached, ensuring RLN compliance on enforcing relays.
##
## For the skeleton this is a pass-through: messages are immediately
## released as ready-to-send. Real epoch budgeting will be added later.
##
## See: https://lip.logos.co/messaging/raw/reliable-channel-api.html
import std/times
import message
import brokers/event_broker
import brokers/broker_context
export event_broker, broker_context
export message.SdsChannelID
const
DefaultEpochPeriodSec* = 600
DefaultMessagesPerEpoch* = 1
EventBroker:
## Emitted by `enqueueToSend` carrying the batch of opaque message
## blobs that may now leave the rate limiter and continue down the
## outgoing pipeline (encryption -> dispatch). Bytes only: the rate
## limiter is intentionally agnostic of SDS, so anything serialisable
## can flow through it.
##
## `channelId` lets listeners filter to their own channel, since all
## reliable channels share the underlying Waku node's broker context.
type ReadyToSendEvent* = ref object
channelId*: SdsChannelID
msgs*: seq[seq[byte]]
type
RateLimitConfig* = object
enabled*: bool ## spec: rate limiting opt-in; SHOULD be true when RLN active
epochPeriodSec*: int
messagesPerEpoch*: int
RateLimitManager* = ref object
config*: RateLimitConfig
queue*: seq[seq[byte]]
currentEpochStart*: Time
sentInCurrentEpoch*: int
channelId*: SdsChannelID ## tag for the emitted `ReadyToSendEvent`
brokerCtx: BrokerContext
proc new*(
T: type RateLimitManager,
config: RateLimitConfig,
channelId: SdsChannelID,
brokerCtx: BrokerContext = globalBrokerContext(),
): T =
return T(
config: config,
queue: @[],
currentEpochStart: getTime(),
sentInCurrentEpoch: 0,
channelId: channelId,
brokerCtx: brokerCtx,
)
proc enqueueToSend*(self: RateLimitManager, msg: seq[byte]) =
## Skeleton behaviour: enqueue and immediately release as a single
## ready batch. Real per-epoch budgeting will park messages on
## `self.queue` and emit only when the budget allows.
ReadyToSendEvent.emit(
self.brokerCtx, ReadyToSendEvent(channelId: self.channelId, msgs: @[msg])
)
proc dequeueReady*(self: RateLimitManager): seq[seq[byte]] =
## Returns the set of queued messages that may be dispatched now
## without exceeding the configured rate limit.
discard
proc resetEpoch*(self: RateLimitManager) =
self.currentEpochStart = getTime()
self.sentInCurrentEpoch = 0

View File

@ -1,11 +1,10 @@
import logos_delivery/waku/compat/option_valueor
## Reliable Channel type.
##
## A `ReliableChannel` orchestrates segmentation, SDS (end-to-end
## reliability), optional encryption, and rate-limited dispatch on top
## of the Messaging API for a single channel.
## reliability), optional encryption, and dispatch on top of the
## Messaging API for a single channel.
##
## Outgoing pipeline: Segment -> SDS -> Rate Limit -> Encrypt -> Dispatch
## Outgoing pipeline: Segment -> SDS -> Encrypt -> Dispatch
## Incoming pipeline: Decrypt -> SDS -> Reassemble -> Emit event
##
## Channels are owned by a `ReliableChannelManager`. Lifecycle and send
@ -14,9 +13,8 @@ import logos_delivery/waku/compat/option_valueor
##
## See: https://lip.logos.co/messaging/raw/reliable-channel-api.html
import std/[options, tables]
import results
import chronos
import std/tables
import results, chronos
import bearssl/rand
import stew/byteutils
import libp2p/crypto/crypto as libp2p_crypto
@ -31,12 +29,9 @@ import logos_delivery/waku/waku_core/topics
import ./segmentation/segmentation
import ./scalable_data_sync/scalable_data_sync
import ./rate_limit_manager/rate_limit_manager
import ./encryption/encryption
export
types, reliable_channel_manager_api, segmentation, scalable_data_sync,
rate_limit_manager, encryption
export types, reliable_channel_manager_api, segmentation, scalable_data_sync, encryption
const LipWireReliableChannelVersion* = "RELIABLE-CHANNEL-API/1"
## Wire-format spec marker for the Reliable Channel layer, as defined
@ -53,33 +48,22 @@ type
ChannelReqState = object
## Per channel-level request, tracks how many of its segments are
## still queued, in flight, or have terminated. The channel-level
## final event fires when `confirmedCount + failedCount` reaches
## `totalExpectedSegments` AND no segments are still awaiting dispatch
## or in flight.
## still in flight or have terminated. The channel-level final event
## fires when `confirmedCount + failedCount` reaches
## `totalExpectedSegments` AND no segments are still in flight.
persistenceReqType: MessagePersistence
totalExpectedSegments: int
## Total segments produced by `segmentation.performSegmentation`
## for this `channelReqId`. Set once in `send`, never mutated.
awaitingDispatch: int
## Segments enqueued in `rate_limit_manager` but not yet claimed
## by `onReadyToSend`. Decremented when `onReadyToSend` picks a
## message and assigns it to this `channelReqId`.
inflightMessagingIds: seq[RequestId]
## Messaging-layer ids minted by the send handler that have not
## yet produced a final event. Removed on `MessageSentEvent` / `MessageErrorEvent`.
confirmedCount: int
failedCount: int
ChannelReqs = OrderedTable[RequestId, ChannelReqState]
ChannelReqs = Table[RequestId, ChannelReqState]
## Key: channelReqId (the parent id returned by channel `send`). Value:
## per-request state, see `ChannelReqState`.
##
## `OrderedTable` preserves insertion order, which matches the FIFO
## order `rate_limit_manager` re-emits messages in: `onReadyToSend`
## routes each segment to the first entry with `awaitingDispatch > 0`,
## and that scan is correct precisely because the outer iteration
## order matches the order `send` pushed entries.
ReliableChannel* = ref object
## Spec-defined public type. Fields are private so callers cannot
@ -91,7 +75,6 @@ type
rng: libp2p_crypto.Rng
segmentation: SegmentationHandler
sdsHandler: SdsHandler
rateLimit: RateLimitManager
channelReqs: ChannelReqs
brokerCtx: BrokerContext
@ -104,7 +87,6 @@ func init(
return ChannelReqState(
persistenceReqType: persistenceReqType,
totalExpectedSegments: totalExpectedSegments,
awaitingDispatch: totalExpectedSegments,
inflightMessagingIds: @[],
confirmedCount: 0,
failedCount: 0,
@ -125,8 +107,8 @@ proc stop*(self: ReliableChannel) {.async: (raises: []).} =
proc tryFinalizeChannelReq(self: ReliableChannel, channelReqId: RequestId) =
## Tries to finalize the channel-level request identified by `channelReqId` if
## certain conditions are met, i.e., no segments are still awaiting dispatch or in flight,
## and the total number of confirmed + failed segments equals the total expected segments.
## certain conditions are met, i.e., no segments are still in flight and the
## total number of confirmed + failed segments equals the total expected segments.
## Therefore, the channel-level request is removed from `self.channelReqs`
## and the appropriate final event is emitted.
##
@ -134,7 +116,7 @@ proc tryFinalizeChannelReq(self: ReliableChannel, channelReqId: RequestId) =
if state.totalExpectedSegments == 0:
## Either already finalized (and removed) or never inserted.
return
if state.awaitingDispatch != 0 or state.inflightMessagingIds.len != 0:
if state.inflightMessagingIds.len != 0:
return
if state.confirmedCount + state.failedCount < state.totalExpectedSegments:
return
@ -156,22 +138,6 @@ proc tryFinalizeChannelReq(self: ReliableChannel, channelReqId: RequestId) =
ChannelMessageSentEvent(channelId: self.channelId, requestId: channelReqId),
)
type ClaimedSegment = object
channelReqId: RequestId
isEphemeral: bool
proc claimAwaitingChannelReq(self: ReliableChannel): Option[ClaimedSegment] =
for channelReqId, state in self.channelReqs.mpairs:
if state.awaitingDispatch > 0:
state.awaitingDispatch.dec()
return some(
ClaimedSegment(
channelReqId: channelReqId,
isEphemeral: state.persistenceReqType == MessagePersistence.Ephemeral,
)
)
return none(ClaimedSegment)
type MessagingOutcome {.pure.} = enum
Sent
Failed
@ -210,62 +176,61 @@ proc markSegmentInflight(
error "unreachable: channelReqId not found in markSegmentInflight",
channelReqId = $channelReqId, error = e.msg
proc onReadyToSend(
self: ReliableChannel, readyToSendEvent: ReadyToSendEvent
) {.async: (raises: []).} =
## Tail of the outgoing pipeline. Invoked from the `ReadyToSendEvent`
## listener once `rate_limit_manager` releases a batch of opaque
## blobs (already-encoded SDS messages):
proc send*(
self: ReliableChannel, payload: seq[byte], ephemeral: bool = false
): Future[Result[RequestId, string]] {.async: (raises: []).} =
## Single application-level send:
##
## ... -> rate_limit_manager -> [encryption] -> dispatch
## segmentation -> sds -> encryption -> dispatch
##
## For each `m`, the next channelReqId still queued in rate-limit
## claims the slot (FIFO across sibling sends). The channelReqId is
## captured up front and used as a stable key for every later state
## update — no positional index is ever held across an `await`, so
## sibling events mutating other entries (or even this one's
## `inflightMessagingIds`) cannot corrupt this fiber's view.
for m in readyToSendEvent.msgs:
let claimed = self.claimAwaitingChannelReq().valueOr:
## rate_limit_manager emitted more messages than we have pending —
## should not happen given `send` increments `awaitingDispatch`
## once per enqueued SDS payload. Drop silently rather than
## corrupt state.
break
let channelReqId = claimed.channelReqId
let isEphemeral = claimed.isEphemeral
## The returned `RequestId` is the channel-level parent of one-or-more
## messaging-layer `RequestId`s; the mapping is held in
## `self.channelReqs` until every segment is final.
if payload.len == 0:
return err("empty payload")
let channelReqId = RequestId.new(self.rng)
let persistenceReqType =
if ephemeral: MessagePersistence.Ephemeral else: MessagePersistence.Persistent
var sdsSegments: seq[seq[byte]]
for segmentBytes in self.segmentation.performSegmentation(payload):
## Segments arrive already encoded; the segmentation module owns
## the wire format so SDS only ever sees opaque bytes.
let sdsBytes = (await self.sdsHandler.wrapOutgoing(segmentBytes)).valueOr:
return err("SDS wrap failed: " & error)
sdsSegments.add(sdsBytes)
self.channelReqs[channelReqId] =
ChannelReqState.init(persistenceReqType, sdsSegments.len)
for sdsBytes in sdsSegments:
## TODO: revisit which fields of the SDS message must be encrypted.
## Encrypting the whole encoded blob forces every receiver to attempt
## decryption before it can route, which breaks selective dispatch.
## Leave routing metadata (channelId, causal-history references) in
## clear and encrypt only the application payload.
let encRes = await Encrypt.request(m)
let encrypted = encRes.valueOr:
let encrypted = (await Encrypt.request(sdsBytes)).valueOr:
MessageErrorEvent.emit(
self.brokerCtx,
MessageErrorEvent(
requestId: channelReqId, messageHash: "", error: "encryption failed: " & error
),
)
## Encryption failed *before* we could hand the segment to the
self.markSegmentFailed(channelReqId)
continue
let wireBytes = seq[byte](encrypted)
## The `meta` field carries the Reliable Channel wire-format spec
## marker so the ingress side of any peer can route this WakuMessage
## to its Reliable Channel layer.
let envelope = MessageEnvelope(
contentTopic: self.contentTopic,
payload: wireBytes,
ephemeral: isEphemeral,
payload: seq[byte](encrypted),
ephemeral: ephemeral,
meta: LipWireReliableChannelVersion.toBytes(),
)
let sendRes = await MessagingSend.request(self.brokerCtx, envelope)
let messagingReqId = sendRes.valueOr:
let messagingReqId = (await MessagingSend.request(self.brokerCtx, envelope)).valueOr:
MessageErrorEvent.emit(
self.brokerCtx,
MessageErrorEvent(
@ -279,45 +244,6 @@ proc onReadyToSend(
self.markSegmentInflight(channelReqId, messagingReqId)
proc send*(
self: ReliableChannel, payload: seq[byte], ephemeral: bool = false
): Future[Result[RequestId, string]] {.async: (raises: []).} =
## Single application-level send. The first three stages of the
## outgoing pipeline are chained explicitly so the flow is visible
## at a glance:
##
## segmentation -> sds -> rate_limit_manager
##
## `rate_limit_manager.enqueueToSend` emits a `ReadyToSendEvent` with
## the SDS messages cleared for transmission; the channel's listener
## then runs the final stage (encryption -> dispatch).
##
## The returned `RequestId` is the channel-level parent of one-or-more
## messaging-layer `RequestId`s; the mapping is held in
## `self.channelReqs` until every segment is final.
if payload.len == 0:
return err("empty payload")
let channelReqId = RequestId.new(self.rng)
let persistenceReqType =
if ephemeral: MessagePersistence.Ephemeral else: MessagePersistence.Persistent
var segmentCount = 0
var enqueued: seq[seq[byte]]
for segmentBytes in self.segmentation.performSegmentation(payload):
## Segments arrive already encoded; the segmentation module owns
## the wire format so SDS only ever sees opaque bytes.
let sdsBytes = (await self.sdsHandler.wrapOutgoing(segmentBytes)).valueOr:
return err("SDS wrap failed: " & error)
enqueued.add(sdsBytes)
segmentCount.inc()
self.channelReqs[channelReqId] =
ChannelReqState.init(persistenceReqType, segmentCount)
for sdsBytes in enqueued:
self.rateLimit.enqueueToSend(sdsBytes)
return ok(channelReqId)
proc reportReceived(self: ReliableChannel, content: seq[byte]) =
@ -337,8 +263,7 @@ proc reportReceived(self: ReliableChannel, content: seq[byte]) =
)
proc dispatchRepair(self: ReliableChannel, wire: seq[byte]) {.async: (raises: []).} =
## Repair rebroadcasts skip the rate-limit queue — its emissions are
## claimed FIFO by pending sends. Pacing is done by SDS itself.
## SDS-driven repair rebroadcast. Pacing is done by SDS itself.
let encRes = await Encrypt.request(wire)
let encrypted = encRes.valueOr:
debug "SDS repair rebroadcast dropped: encryption failed",
@ -408,15 +333,13 @@ proc new*(
senderId: SdsParticipantID,
segConfig: SegmentationConfig,
sdsConfig: SdsConfig,
rateConfig: RateLimitConfig,
brokerCtx: BrokerContext = globalBrokerContext(),
): T =
## Pipeline handlers (segmentation/SDS/rate-limit) are constructed
## inside the channel rather than handed in by the caller — they are
## implementation details of the channel, not knobs the API consumer
## should be wiring up. Encryption is delegated to the `Encrypt`/
## `Decrypt` request brokers, so the channel keeps no per-instance
## encryption state either.
## Pipeline handlers (segmentation/SDS) are constructed inside the
## channel rather than handed in by the caller — they are implementation
## details of the channel, not knobs the API consumer should be wiring
## up. Encryption is delegated to the `Encrypt`/`Decrypt` request
## brokers, so the channel keeps no per-instance encryption state either.
let chn = T(
channelId: channelId,
contentTopic: contentTopic,
@ -424,8 +347,7 @@ proc new*(
rng: libp2p_crypto.newRng(),
segmentation: SegmentationHandler.new(segConfig),
sdsHandler: SdsHandler.new(sdsConfig, channelId, senderId),
rateLimit: RateLimitManager.new(rateConfig, channelId, brokerCtx),
channelReqs: initOrderedTable[RequestId, ChannelReqState](),
channelReqs: initTable[RequestId, ChannelReqState](),
brokerCtx: brokerCtx,
)
@ -434,20 +356,11 @@ proc new*(
asyncSpawn chn.dispatchRepair(wire)
chn.sdsHandler.start()
## Each channel owns its own egress + ingress + send-completion
## listeners on `chn.brokerCtx`, filtered to traffic addressed to
## this channel. Keeping the listeners (and the handler procs they
## call) inside the channel lets `onReadyToSend` /
## `onMessageReceived` / `onMessageFinal` stay private — the
## manager doesn't need to know about them.
discard ReadyToSendEvent.listen(
chn.brokerCtx,
proc(evt: ReadyToSendEvent): Future[void] {.async: (raises: []).} =
if evt.channelId == chn.channelId:
await chn.onReadyToSend(evt)
,
)
## Each channel owns its own ingress + send-completion listeners on
## `chn.brokerCtx`, filtered to traffic addressed to this channel.
## Keeping the listeners (and the handler procs they call) inside the
## channel lets `onMessageReceived` / `onMessageFinal` stay private —
## the manager doesn't need to know about them.
discard MessageReceivedEvent.listen(
chn.brokerCtx,
proc(evt: MessageReceivedEvent): Future[void] {.async: (raises: []).} =

View File

@ -18,6 +18,7 @@ import logos_delivery/api/reliable_channel_manager_api
import logos_delivery/api/conf/channels_conf
import ./reliable_channel
import ./encryption/noop_encryption
export reliable_channel, channels_conf
@ -31,6 +32,11 @@ proc new*(
conf: ReliableChannelManagerConf,
brokerCtx: BrokerContext = globalBrokerContext(),
): Result[T, string] =
if conf.rateLimitEnabled.isSome() or conf.rateLimitEpochPeriodSec.isSome() or
conf.rateLimitMessagesPerEpoch.isSome():
warn "channel-level rate-limit config is deprecated and ignored; " &
"rate limiting moved to the messaging client (MessagingClientConf.rateLimit)"
return ok(
T(
channels: initTable[ChannelId, ReliableChannel](),
@ -40,10 +46,15 @@ proc new*(
)
proc start*(self: ReliableChannelManager): Result[void, string] =
## Placeholder: per-channel listeners are installed in `ReliableChannel.new`,
## so the manager has nothing to start at this layer. Kept for symmetry
## with the `Waku` mount/start lifecycle and as a hook for future state.
discard
## Per-channel listeners are installed in `ReliableChannel.new`, so the only
## thing to wire up here is the encryption brokers. Channels encrypt on egress
## and decrypt on ingress via the `Encrypt`/`Decrypt` request brokers; with no
## provider registered every send and receive would fail, so `channel_send`
## would never reach the wire and `ChannelMessageReceivedEvent` would never
## fire. Install the pass-through noop so channels default to unencrypted
## payloads. `setProvider` refuses to overwrite, so an application that
## installed its own encryption before start keeps it.
setNoopEncryption()
ok()
proc stop*(self: ReliableChannelManager) {.async.} =

View File

@ -8,7 +8,7 @@
{.push raises: [].}
import std/[options, tables]
import std/tables
from std/times import initDuration, getTime, toUnix, nanosecond
import results, chronos, chronicles
import nimcrypto/keccak
@ -37,7 +37,7 @@ type
acknowledgementTimeoutMs*: int
maxRetransmissions*: int
causalHistorySize*: int
persistence*: Option[Persistence]
persistence*: Opt[Persistence]
## Durability backend. `none` runs memory-only: reliability still
## works, state does not survive a restart.

View File

@ -9,8 +9,7 @@
##
## See: https://lip.logos.co/messaging/raw/reliable-channel-api.html
import std/options
import ./segment_message_proto
import results, ./segment_message_proto
import ./segmentation_persistence
export segment_message_proto, segmentation_persistence
@ -54,12 +53,12 @@ proc performSegmentation*(
proc handleIncomingSegment*(
self: SegmentationHandler, segmentBytes: seq[byte]
): Option[ReassemblyResult] =
): Opt[ReassemblyResult] =
## Skeleton behaviour: every segment is already a complete message
## (since `performSegmentation` always emits one), so just hand the
## payload straight back.
let segment = SegmentMessageProto.decode(segmentBytes)
return some(
return Opt.some(
ReassemblyResult(
payload: segment.payload, entireMessageHash: segment.entireMessageHash
)

View File

@ -9,7 +9,6 @@
{.push raises: [].}
import std/options
import results, chronos, chronicles
# Each layer has a core module (type + new/start/stop) and an api/ folder whose
@ -40,6 +39,8 @@ import logos_delivery/messaging/[messaging_client, messaging_client_lifecycle]
export messaging_client
import logos_delivery/messaging/api/[subscription, send]
export subscription, send
import logos_delivery/messaging/rest_api/handlers as messaging_rest_api
export messaging_rest_api
import logos_delivery/api/events/messaging_client_events
export messaging_client_events
import logos_delivery/api/conf/messaging_conf
@ -107,21 +108,38 @@ proc new*(
proc new*(
T: type LogosDelivery, conf: WakuNodeConf, appCallbacks: AppCallbacks = nil
): Future[Result[LogosDelivery, string]] {.async.} =
## Builds the full stack from a kernel `WakuNodeConf`.
return await LogosDelivery.new(
LogosDeliveryConf(
kernelConf: KernelConf(conf),
messagingConf: some(MessagingClientConf()),
channelsConf: some(ReliableChannelManagerConf()),
),
appCallbacks,
## Builds the stack from a kernel `WakuNodeConf`, selecting which API layers to
## instantiate by `conf.entryLayer`:
## kernel -> transport only; `conf.mode` is ignored and the config is used as-is
## messaging -> kernel + messaging client
## channels -> kernel + messaging + reliable channels
## For `messaging`/`channels`, `conf.mode` (Edge/Core) sets the kernel protocol
## flags first (messaging-level concern); for `kernel` it is skipped.
var kernelConf = conf
if conf.entryLayer != EntryLayer.kernel:
applyMode(kernelConf, conf.mode).isOkOr:
return err("failed to apply mode: " & error)
let ldConf = LogosDeliveryConf(
kernelConf: KernelConf(kernelConf),
messagingConf:
if conf.entryLayer == EntryLayer.kernel:
Opt.none(MessagingClientConf)
else:
Opt.some(MessagingClientConf()),
channelsConf:
if conf.entryLayer == EntryLayer.channels:
Opt.some(ReliableChannelManagerConf())
else:
Opt.none(ReliableChannelManagerConf),
)
return await LogosDelivery.new(ldConf, appCallbacks)
proc new*(
T: type LogosDelivery, kernelConf: KernelConf, appCallbacks: AppCallbacks = nil
): Future[Result[LogosDelivery, string]] {.async.} =
## Fleet mode: mounts the kernel only from a raw `KernelConf`; no messaging client,
## no channel manager.
## Kernel entry layer: mounts the kernel only from a raw `KernelConf`; no
## messaging client, no channel manager.
return await LogosDelivery.new(LogosDeliveryConf.init(kernelConf), appCallbacks)
proc new*(
@ -136,22 +154,31 @@ proc new*(
return await LogosDelivery.new(
LogosDeliveryConf(
kernelConf: kernelConf,
messagingConf: some(messagingOverrides),
channelsConf: some(channelsOverrides),
messagingConf: Opt.some(messagingOverrides),
channelsConf: Opt.some(channelsOverrides),
),
appCallbacks,
)
proc new*(
T: type LogosDelivery,
entryLayer: EntryLayer = EntryLayer.channels,
mode: LogosDeliveryMode = LogosDeliveryMode.Core,
preset: string = "",
messagingOverrides: MessagingClientConf = MessagingClientConf(),
channelsOverrides: ReliableChannelManagerConf = ReliableChannelManagerConf(),
appCallbacks: AppCallbacks = nil,
): Future[Result[LogosDelivery, string]] {.async.} =
## Messaging entry point (app dev). Builds the full stack from preset, mode and overrides.
let conf = LogosDeliveryConf.init(mode, preset, messagingOverrides, channelsOverrides).valueOr:
## Messaging entry point (app dev). Builds the stack from preset, mode and
## overrides; `entryLayer` selects messaging vs channels (use `new(kernelConf)`
## for a kernel-only node).
let conf = LogosDeliveryConf.init(
entryLayer = entryLayer,
mode = mode,
preset = preset,
messagingOverrides = messagingOverrides,
channelsOverrides = channelsOverrides,
).valueOr:
return err("failed to synthesize configuration: " & error)
return await LogosDelivery.new(conf, appCallbacks)
@ -166,6 +193,10 @@ proc start*(self: LogosDelivery): Future[Result[void, string]] {.async.} =
if not self.messagingClient.isNil():
self.messagingClient.start().isOkOr:
return err("failed to start MessagingClient: " & error)
# Mount the messaging REST endpoints onto the kernel's REST router (no-op if
# REST is disabled). Done here rather than in MessagingClient.start so the
# core messaging module need not depend on the REST layer above it.
self.messagingClient.mountRestApi()
if not self.reliableChannelManager.isNil():
self.reliableChannelManager.start().isOkOr:
@ -191,15 +222,15 @@ proc isOnline*(self: LogosDelivery): Future[Result[bool, string]] {.async.} =
return await self.waku.isOnline()
proc ensureMessaging*(self: LogosDelivery): Result[void, string] =
## Fails if the node has no messaging client (a kernel-only / fleet node).
## Fails if the node has no messaging client (a kernel-only node).
if self.isNil() or self.messagingClient.isNil():
return err("node has no messaging client (kernel-only/fleet node)")
return err("node has no messaging client (kernel-only node)")
ok()
proc ensureChannels*(self: LogosDelivery): Result[void, string] =
## Fails if the node has no reliable channel manager (a kernel-only / fleet node).
## Fails if the node has no reliable channel manager (a kernel-only node).
if self.isNil() or self.reliableChannelManager.isNil():
return err("node has no reliable channel manager (kernel-only/fleet node)")
return err("node has no reliable channel manager (kernel-only node)")
ok()
# Compile-time check that each concrete type satisfies its API concept.

View File

@ -1,9 +1,8 @@
import logos_delivery/waku/compat/option_valueor
## This module is in charge of taking care of the messages that this node is expecting to
## receive and is backed by store-v3 requests to get an additional degree of certainty
##
import std/[tables, sequtils, options, sets]
import results, std/[tables, sequtils, sets]
import chronos, chronicles, libp2p/utility
import brokers/broker_context
import
@ -112,10 +111,10 @@ proc checkStore*(self: RecvService) {.async.} =
await self.waku.storeQueryToAny(
StoreQueryRequest(
includeData: false,
pubsubTopic: some(pubsubTopic),
pubsubTopic: Opt.some(pubsubTopic),
contentTopics: toSeq(contentTopics),
startTime: some(self.startTimeToCheck - DelayExtra.nanos),
endTime: some(self.endTimeToCheck + DelayExtra.nanos),
startTime: Opt.some(self.startTimeToCheck - DelayExtra.nanos),
endTime: Opt.some(self.endTimeToCheck + DelayExtra.nanos),
)
)
).valueOr:

View File

@ -1,5 +1,4 @@
import logos_delivery/waku/compat/option_valueor
import std/[options, times], chronos
import results, std/times, chronos
import brokers/broker_context
import
logos_delivery/waku/waku_core,
@ -24,7 +23,7 @@ type DeliveryTask* = ref object
tryCount*: int
state*: DeliveryState
deliveryTime*: Moment
firstPropagatedTime*: Option[Moment]
firstPropagatedTime*: Opt[Moment]
## Set once on the first successful propagation; never reset on re-publish.
## Anchors the store-validation time cap (see propagationAge).
propagateEventEmitted*: bool
@ -39,7 +38,7 @@ proc new*(
let msg = envelop.toWakuMessage()
# TODO: use sync request for such as soon as available
let relayShardRes = (
RequestRelayShard.request(brokerCtx, none[PubsubTopic](), envelop.contentTopic)
RequestRelayShard.request(brokerCtx, Opt.none(PubsubTopic), envelop.contentTopic)
).valueOr:
error "RequestRelayShard.request failed", error = error
return err("Failed create DeliveryTask: " & $error)

View File

@ -1,7 +1,4 @@
import logos_delivery/waku/compat/option_valueor
import chronicles, chronos, results
import std/options
import brokers/broker_context
import chronicles, chronos, results, brokers/broker_context
import logos_delivery/waku/waku_core, logos_delivery/waku/waku
import logos_delivery/waku/api/publish
@ -54,7 +51,7 @@ method sendImpl*(
task.state = DeliveryState.SuccessfullyPropagated
task.deliveryTime = Moment.now()
if task.firstPropagatedTime.isNone():
task.firstPropagatedTime = some(Moment.now())
task.firstPropagatedTime = Opt.some(Moment.now())
# TODO: with a simple retry processor it might be more accurate to say `Sent`
else:
# Controversial state, publish says ok but no peer. It should not happen.

View File

@ -1,6 +1,4 @@
import logos_delivery/waku/compat/option_valueor
import std/options
import chronos, chronicles
import results, chronos, chronicles
import brokers/broker_context
import logos_delivery/waku/[waku_core], logos_delivery/waku/waku_lightpush/[common, rpc]
import logos_delivery/waku/requests/health_requests
@ -77,7 +75,7 @@ method sendImpl*(self: RelaySendProcessor, task: DeliveryTask) {.async.} =
task.state = DeliveryState.SuccessfullyPropagated
task.deliveryTime = Moment.now()
if task.firstPropagatedTime.isNone():
task.firstPropagatedTime = some(Moment.now())
task.firstPropagatedTime = Opt.some(Moment.now())
else:
# It shall not happen, but still covering it
task.state = self.fallbackStateToSet

View File

@ -1,15 +1,15 @@
import logos_delivery/waku/compat/option_valueor
## This module reinforces the publish operation with regular store-v3 requests.
##
import std/[sequtils, tables, options, typetraits]
import std/[sequtils, tables, typetraits]
import chronos, chronicles, libp2p/utility
import brokers/broker_context
import
./[send_processor, relay_processor, lightpush_processor, delivery_task],
logos_delivery/waku/[waku_core, waku_store/common],
logos_delivery/waku/waku,
logos_delivery/waku/api/[store, subscriptions, publish]
logos_delivery/waku/api/[store, subscriptions, publish],
logos_delivery/messaging/rate_limit_manager/rate_limit_manager
import logos_delivery/api/events/messaging_client_events
logScope:
@ -47,6 +47,9 @@ type SendService* = ref object of RootObj
serviceLoopHandle: Future[void] ## handle that allows to stop the async task
sendProcessor: BaseSendProcessor
rateLimitManager: RateLimitManager
## Meters first transmissions against the per-epoch budget; re-publishes
## of an already-propagated message resend the same bytes and are free.
waku: Waku
checkStoreForMessages: bool
@ -78,7 +81,10 @@ proc setupSendProcessorChain(
return ok(processors[0])
proc new*(
T: typedesc[SendService], preferP2PReliability: bool, waku: Waku
T: typedesc[SendService],
preferP2PReliability: bool,
waku: Waku,
rateLimitManager: RateLimitManager,
): Result[T, string] =
if not waku.hasRelay() and not waku.hasLightpush():
return err(
@ -95,6 +101,7 @@ proc new*(
taskCache: newSeq[DeliveryTask](),
serviceLoopHandle: nil,
sendProcessor: sendProcessorChain,
rateLimitManager: rateLimitManager,
waku: waku,
checkStoreForMessages: checkStoreForMessages,
lastStoreCheckTime: Moment.now(),
@ -246,6 +253,12 @@ proc trySendMessages(self: SendService) {.async.} =
for task in tasksToSend:
# Todo, check if it has any perf gain to run them concurrent...
if task.firstPropagatedTime.isNone():
## Never propagated: this transmission consumes a fresh epoch slot, so
## it must be admitted. Tasks that stay over budget remain in
## `NextRoundRetry` and are retried as the epoch rolls over.
(await self.rateLimitManager.admit(task.msg.payload)).isOkOr:
continue
await self.sendProcessor.process(task)
proc serviceLoop(self: SendService) {.async.} =
@ -275,6 +288,13 @@ proc send*(self: SendService, task: DeliveryTask) {.async.} =
error "SendService.send: failed to subscribe to content topic",
contentTopic = task.msg.contentTopic, error = error
(await self.rateLimitManager.admit(task.msg.payload)).isOkOr:
info "SendService.send: over rate-limit budget, parking task",
requestId = task.requestId, msgHash = task.msgHash.to0xHex()
task.state = DeliveryState.NextRoundRetry
self.addTask(task)
return
await self.sendProcessor.process(task)
reportTaskResult(self, task)
if task.state != DeliveryState.FailedToDeliver:

View File

@ -1,14 +1,14 @@
## Messaging layer core: the `MessagingClient` type plus its construction and
## lifecycle. The public operations (subscribe / unsubscribe / send) live in
## `messaging/api.nim`.
import std/options
import results, chronos, chronicles
import
logos_delivery/api/conf/messaging_conf,
logos_delivery/api/messaging_client_api,
logos_delivery/waku/waku,
logos_delivery/waku/factory/conf_builder/waku_conf_builder,
logos_delivery/messaging/delivery_service/[recv_service, send_service]
logos_delivery/messaging/delivery_service/[recv_service, send_service],
logos_delivery/messaging/rate_limit_manager/rate_limit_manager
export messaging_client_api, messaging_conf
@ -25,7 +25,9 @@ proc new*(
## The messaging layer chains onto Waku: it drives the underlying Waku kernel
## for transport while exposing its own send/recv API.
let reliability = conf.reliabilityEnabled.get(DefaultP2pReliability)
let sendService = ?SendService.new(reliability, waku)
let sendService = ?SendService.new(
reliability, waku, RateLimitManager.new(conf.rateLimit.get(DefaultRateLimitConfig))
)
let recvService = RecvService.new(waku)
return ok(
T(

View File

@ -0,0 +1,58 @@
## Rate Limit Manager for the Messaging API.
##
## Tracks messages sent per RLN epoch and rejects admission when the
## limit is approached, ensuring RLN compliance on enforcing relays.
##
## For the skeleton this is a pass-through: every call is admitted.
## Real per-epoch budgeting will use `queue`, `currentEpochStart`,
## `sentInCurrentEpoch`, and `resetEpoch` to park messages and admit
## them as the epoch rolls over.
##
## See: https://lip.logos.co/messaging/raw/reliable-channel-api.html
import std/times
import results, chronos
type
RateLimitError* {.pure.} = enum
OverBudget
RateLimitConfig* = object
enabled*: bool ## spec: rate limiting opt-in; SHOULD be true when RLN active
epochPeriodSec*: int
messagesPerEpoch*: int
RateLimitManager* = ref object
config*: RateLimitConfig
queue*: seq[seq[byte]]
currentEpochStart*: Time
sentInCurrentEpoch*: int
const
DefaultEpochPeriodSec* = 600
DefaultMessagesPerEpoch* = 1
DefaultRateLimitConfig* = RateLimitConfig(
epochPeriodSec: DefaultEpochPeriodSec, messagesPerEpoch: DefaultMessagesPerEpoch
) ## Used when no rate-limit config is supplied; `enabled` defaults false.
proc new*(T: type RateLimitManager, config: RateLimitConfig): T =
return
T(config: config, queue: @[], currentEpochStart: getTime(), sentInCurrentEpoch: 0)
proc admit*(
self: RateLimitManager, msg: seq[byte]
): Future[Result[void, RateLimitError]] {.async: (raises: []).} =
## Skeleton behaviour: admits immediately. Real per-epoch budgeting
## will consult `config`, `sentInCurrentEpoch`, and the elapsed
## `epochPeriodSec` window before admitting or parking `msg`.
return ok()
proc dequeueReady*(self: RateLimitManager): seq[seq[byte]] =
## Returns the set of queued messages that may be dispatched now
## without exceeding the configured rate limit.
discard
proc resetEpoch*(self: RateLimitManager) =
self.currentEpochStart = getTime()
self.sentInCurrentEpoch = 0

View File

@ -0,0 +1,63 @@
{.push raises: [].}
import chronicles, json_serialization, presto/[route, client, common]
import
logos_delivery/waku/rest_api/endpoint/serdes,
logos_delivery/waku/rest_api/endpoint/rest_serdes,
logos_delivery/api/types,
./types
export types
logScope:
topics = "messaging rest client"
proc encodeBytes*(
value: seq[ContentTopic], contentType: string
): RestResult[seq[byte]] =
return encodeBytesOf(value, contentType)
proc encodeBytes*(
value: MessagingPostMessageRequest, contentType: string
): RestResult[seq[byte]] =
return encodeBytesOf(value, contentType)
proc messagingPostSubscriptionsV1*(
body: seq[ContentTopic]
): RestResponse[string] {.
rest, endpoint: "/messaging/v1/subscriptions", meth: HttpMethod.MethodPost
.}
proc messagingDeleteSubscriptionsV1*(
body: seq[ContentTopic]
): RestResponse[string] {.
rest, endpoint: "/messaging/v1/subscriptions", meth: HttpMethod.MethodDelete
.}
proc messagingPostMessagesV1*(
body: MessagingPostMessageRequest
): RestResponse[MessagingSendResponse] {.
rest, endpoint: "/messaging/v1/messages", meth: HttpMethod.MethodPost
.}
proc messagingGetSendEventsV1*(): RestResponse[seq[SendStatus]] {.
rest, endpoint: "/messaging/v1/events/send", meth: HttpMethod.MethodGet
.}
proc messagingGetSendEventsByIdV1*(
requestId: string
): RestResponse[SendStatus] {.
rest, endpoint: "/messaging/v1/events/send/{requestId}", meth: HttpMethod.MethodGet
.}
# Raw variant: the typed client above cannot decode a non-2xx text error body,
# so status-code assertions (e.g. 404) use this string form.
proc messagingGetSendEventsByIdRawV1*(
requestId: string
): RestResponse[string] {.
rest, endpoint: "/messaging/v1/events/send/{requestId}", meth: HttpMethod.MethodGet
.}
proc messagingGetReceivedMessagesV1*(): RestResponse[seq[ReceivedMessageRecord]] {.
rest, endpoint: "/messaging/v1/events/received", meth: HttpMethod.MethodGet
.}

View File

@ -0,0 +1,115 @@
## In-memory cache backing the messaging event REST endpoints.
##
## REST is a poll-based client/server surface, so the interactive MessagingClient
## events must be buffered here for later observation:
## * send-related events (sent / propagated / error) grouped by request id
## * received messages
##
## Both surfaces are evict-after-poll: a GET returns the buffered data and clears
## it. A generous overflow cap bounds memory if nobody polls. All access happens
## on the single chronos event loop (broker listeners + REST handlers), so the
## synchronous (no-await) ops below need no locking.
{.push raises: [].}
import std/[tables, deques, options]
import results
import logos_delivery/waku/waku_core/time
import ./types
const
DefaultMaxReceived* = 50 ## Received messages kept between polls (spec default).
DefaultMaxSendRequests* = 10_000
## Overflow guard: distinct request ids retained between polls.
type MessagingEventCache* = ref object
# Send events grouped by request id, with FIFO insertion order for overflow
# eviction. Cleared on poll.
sendByReqId: Table[string, SendStatus]
sendOrder: Deque[string]
maxSendRequests: int
# Received messages, bounded ring. Cleared on poll.
received: Deque[ReceivedMessageRecord]
maxReceived: int
proc new*(
T: type MessagingEventCache,
maxReceived = DefaultMaxReceived,
maxSendRequests = DefaultMaxSendRequests,
): MessagingEventCache =
MessagingEventCache(
sendByReqId: initTable[string, SendStatus](),
sendOrder: initDeque[string](),
maxSendRequests: maxSendRequests,
received: initDeque[ReceivedMessageRecord](),
maxReceived: maxReceived,
)
proc recordSend*(
self: MessagingEventCache,
requestId: string,
messageHash: string,
kind: SendEventKind,
error = "",
) =
## Append a send event to its request id's timeline, creating the entry (and
## evicting the oldest request id past the overflow cap) as needed.
let record = SendEventRecord(
kind: kind,
messageHash: messageHash,
error: error,
timestamp: getNowInNanosecondTime(),
)
if not self.sendByReqId.hasKey(requestId):
self.sendByReqId[requestId] = SendStatus(requestId: requestId, events: @[])
self.sendOrder.addLast(requestId)
while self.sendOrder.len > self.maxSendRequests:
let evicted = self.sendOrder.popFirst()
self.sendByReqId.del(evicted)
self.sendByReqId.withValue(requestId, status):
status[].events.add(record)
proc recordReceived*(
self: MessagingEventCache, messageHash: string, message: RelayWakuMessage
) =
## Buffer a received message, dropping the oldest past the ring capacity.
self.received.addLast(
ReceivedMessageRecord(messageHash: messageHash, message: message)
)
while self.received.len > self.maxReceived:
discard self.received.popFirst()
proc pollAllSend*(self: MessagingEventCache): seq[SendStatus] =
## Return all buffered send statuses and clear the store (evict-after-poll).
for reqId in self.sendOrder:
self.sendByReqId.withValue(reqId, status):
result.add(status[])
self.sendByReqId.clear()
self.sendOrder.clear()
proc pollSend*(self: MessagingEventCache, requestId: string): Opt[SendStatus] =
## Return one request id's send status and remove it (evict-after-poll).
var status: SendStatus
if not self.sendByReqId.pop(requestId, status):
return Opt.none(SendStatus)
# Deque has no random removal; rebuild order without the polled id.
var rebuilt = initDeque[string]()
for reqId in self.sendOrder:
if reqId != requestId:
rebuilt.addLast(reqId)
self.sendOrder = rebuilt
return Opt.some(status)
proc pollReceived*(self: MessagingEventCache): seq[ReceivedMessageRecord] =
## Return buffered received messages (oldest first) and clear (evict-after-poll).
for record in self.received:
result.add(record)
self.received.clear()
{.pop.}

View File

@ -0,0 +1,178 @@
{.push raises: [].}
import chronos, chronicles, results, json_serialization, json_serialization/std/options
import presto/[route, common]
import
logos_delivery/waku/waku,
logos_delivery/waku/rest_api/endpoint/serdes,
logos_delivery/waku/rest_api/endpoint/responses,
logos_delivery/waku/rest_api/endpoint/rest_serdes,
logos_delivery/messaging/messaging_client,
logos_delivery/messaging/api/subscription,
logos_delivery/messaging/api/send,
logos_delivery/api/types,
logos_delivery/api/events/messaging_client_events,
./types,
./event_cache
export types
logScope:
topics = "messaging rest api"
#### Routes
const ROUTE_MESSAGING_SUBSCRIPTIONSV1* = "/messaging/v1/subscriptions"
const ROUTE_MESSAGING_MESSAGESV1* = "/messaging/v1/messages"
const ROUTE_MESSAGING_EVENTS_SENDV1* = "/messaging/v1/events/send"
const ROUTE_MESSAGING_EVENTS_SEND_BY_IDV1* = "/messaging/v1/events/send/{requestId}"
const ROUTE_MESSAGING_EVENTS_RECEIVEDV1* = "/messaging/v1/events/received"
proc installEventListeners(brokerCtx: BrokerContext, cache: MessagingEventCache) =
## Buffers the MessagingClient events into `cache` so the poll-based REST
## endpoints can observe them. Listeners live for the node's lifetime (the
## captured `cache` keeps them and their data alive); no teardown is wired.
discard MessageSentEvent.listen(
brokerCtx,
proc(evt: MessageSentEvent): Future[void] {.async: (raises: []).} =
cache.recordSend($evt.requestId, evt.messageHash, SendEventKind.Sent),
)
discard MessagePropagatedEvent.listen(
brokerCtx,
proc(evt: MessagePropagatedEvent): Future[void] {.async: (raises: []).} =
cache.recordSend($evt.requestId, evt.messageHash, SendEventKind.Propagated),
)
discard MessageErrorEvent.listen(
brokerCtx,
proc(evt: MessageErrorEvent): Future[void] {.async: (raises: []).} =
cache.recordSend($evt.requestId, evt.messageHash, SendEventKind.Error, evt.error),
)
discard MessageReceivedEvent.listen(
brokerCtx,
proc(evt: MessageReceivedEvent): Future[void] {.async: (raises: []).} =
cache.recordReceived(evt.messageHash, toRelayWakuMessage(evt.message)),
)
proc installMessagingApiHandlers*(router: var RestRouter, client: MessagingClient) =
## Mounts the MessagingClient subscribe / unsubscribe / send operations as
## REST endpoints onto the given (kernel-owned) router. Subscriptions are
## keyed by content topic, matching the messaging layer's content-topic API.
# Event observability: buffer send/received events for the poll-based GETs.
let eventCache = MessagingEventCache.new()
installEventListeners(client.waku.brokerCtx, eventCache)
router.api(MethodOptions, ROUTE_MESSAGING_SUBSCRIPTIONSV1) do() -> RestApiResponse:
return RestApiResponse.ok()
router.api(MethodPost, ROUTE_MESSAGING_SUBSCRIPTIONSV1) do(
contentBody: Option[ContentBody]
) -> RestApiResponse:
## Subscribes the messaging client to a list of content topics.
let req: seq[ContentTopic] = decodeRequestBody[seq[ContentTopic]](contentBody).valueOr:
return error
for contentTopic in req:
(await client.subscribe(contentTopic)).isOkOr:
let errorMsg = "Subscribe failed: " & error
error "messaging SUBSCRIBE failed", error = errorMsg
return RestApiResponse.internalServerError(errorMsg)
return RestApiResponse.ok()
router.api(MethodDelete, ROUTE_MESSAGING_SUBSCRIPTIONSV1) do(
contentBody: Option[ContentBody]
) -> RestApiResponse:
## Unsubscribes the messaging client from a list of content topics.
let req: seq[ContentTopic] = decodeRequestBody[seq[ContentTopic]](contentBody).valueOr:
return error
for contentTopic in req:
client.unsubscribe(contentTopic).isOkOr:
let errorMsg = "Unsubscribe failed: " & error
error "messaging UNSUBSCRIBE failed", error = errorMsg
return RestApiResponse.internalServerError(errorMsg)
return RestApiResponse.ok()
router.api(MethodOptions, ROUTE_MESSAGING_MESSAGESV1) do() -> RestApiResponse:
return RestApiResponse.ok()
router.api(MethodPost, ROUTE_MESSAGING_MESSAGESV1) do(
contentBody: Option[ContentBody]
) -> RestApiResponse:
## Sends a message through the messaging client, returning the request id.
let req: MessagingJsonEnvelope = decodeRequestBody[MessagingJsonEnvelope](
contentBody
).valueOr:
return error
let envelope = req.toMessageEnvelope().valueOr:
return RestApiResponse.badRequest("Invalid message: " & error)
let requestId = (await client.send(envelope)).valueOr:
error "messaging SEND failed", error = error
return RestApiResponse.internalServerError("Send failed: " & error)
let data = MessagingSendResponse(requestId: $requestId)
return RestApiResponse.jsonResponse(data, status = Http200).valueOr:
error "An error occurred while building the json response", error = error
return RestApiResponse.internalServerError($error)
#### Event observability endpoints (poll-based, evict-after-poll)
router.api(MethodOptions, ROUTE_MESSAGING_EVENTS_SENDV1) do() -> RestApiResponse:
return RestApiResponse.ok()
router.api(MethodGet, ROUTE_MESSAGING_EVENTS_SENDV1) do() -> RestApiResponse:
## Returns all buffered send events grouped by request id, then clears them.
let data = eventCache.pollAllSend()
return RestApiResponse.jsonResponse(data, status = Http200).valueOr:
error "An error occurred while building the json response", error = error
return RestApiResponse.internalServerError($error)
router.api(MethodOptions, ROUTE_MESSAGING_EVENTS_SEND_BY_IDV1) do(
requestId: string
) -> RestApiResponse:
return RestApiResponse.ok()
router.api(MethodGet, ROUTE_MESSAGING_EVENTS_SEND_BY_IDV1) do(
requestId: string
) -> RestApiResponse:
## Returns the buffered send events for one request id, then removes them.
let reqId = requestId.valueOr:
return RestApiResponse.badRequest("Invalid requestId")
let status = eventCache.pollSend(reqId).valueOr:
return RestApiResponse.notFound("No send events for requestId: " & reqId)
return RestApiResponse.jsonResponse(status, status = Http200).valueOr:
error "An error occurred while building the json response", error = error
return RestApiResponse.internalServerError($error)
router.api(MethodOptions, ROUTE_MESSAGING_EVENTS_RECEIVEDV1) do() -> RestApiResponse:
return RestApiResponse.ok()
router.api(MethodGet, ROUTE_MESSAGING_EVENTS_RECEIVEDV1) do() -> RestApiResponse:
## Returns buffered received messages (up to the cache capacity, oldest
## first), then clears them — optimized for polling.
let data = eventCache.pollReceived()
return RestApiResponse.jsonResponse(data, status = Http200).valueOr:
error "An error occurred while building the json response", error = error
return RestApiResponse.internalServerError($error)
proc mountRestApi*(client: MessagingClient) =
## Mounts the messaging REST endpoints onto the kernel-owned REST router, if
## the REST server is enabled. Called by the `LogosDelivery` concentrator
## after the messaging layer has started. Lives here (not in the core
## `messaging_client` module) so the core need not depend on the REST layer
## above it — that would form an import cycle.
if not client.waku.restServer.isNil():
# The BTree route table is ref-backed, so mutating the copied router persists
# (same pattern as the waku REST builder).
var router = client.waku.restServer.router
installMessagingApiHandlers(router, client)
info "Mounted messaging REST API endpoints"

View File

@ -0,0 +1,276 @@
{.push raises: [].}
import
std/[sets, strformat],
chronicles,
results,
json_serialization,
json_serialization/pkg/results,
presto/[route, client, common]
import
logos_delivery/waku/common/base64,
logos_delivery/waku/rest_api/endpoint/serdes,
logos_delivery/waku/rest_api/endpoint/relay/types as relay_types,
logos_delivery/api/types
export types, relay_types
#### Types
type MessagingJsonEnvelope* = object
## REST wire (JSON) representation of the messaging API's `MessageEnvelope`.
## `payload` / `meta` are base64. Fields mirror `MessageEnvelope` exactly.
payload*: Base64String
contentTopic*: ContentTopic
ephemeral*: Opt[bool]
meta*: Opt[Base64String]
type MessagingPostMessageRequest* = MessagingJsonEnvelope
type MessagingSendResponse* = object
## Returned by the send endpoint on success; correlates with
## `MessageSentEvent` / `MessageErrorEvent`.
requestId*: string
#### Type conversion
proc toMessageEnvelope*(msg: MessagingJsonEnvelope): Result[MessageEnvelope, string] =
let
payload = ?msg.payload.decode()
meta = ?msg.meta.get(Base64String("")).decode()
return ok(
MessageEnvelope(
contentTopic: msg.contentTopic,
payload: payload,
ephemeral: msg.ephemeral.get(false),
meta: meta,
)
)
#### Serialization and deserialization
proc writeValue*(
writer: var JsonWriter[RestJson], value: MessagingJsonEnvelope
) {.raises: [IOError].} =
writer.beginRecord()
writer.writeField("payload", value.payload)
writer.writeField("contentTopic", value.contentTopic)
if value.ephemeral.isSome():
writer.writeField("ephemeral", value.ephemeral.get())
if value.meta.isSome():
writer.writeField("meta", value.meta.get())
writer.endRecord()
proc readValue*(
reader: var JsonReader[RestJson], value: var MessagingJsonEnvelope
) {.raises: [SerializationError, IOError].} =
var
payload = Opt.none(Base64String)
contentTopic = Opt.none(ContentTopic)
ephemeral = Opt.none(bool)
meta = Opt.none(Base64String)
var keys = initHashSet[string]()
for fieldName in readObjectFields(reader):
# Check for repeated keys
if keys.containsOrIncl(fieldName):
let err =
try:
fmt"Multiple `{fieldName}` fields found"
except CatchableError:
"Multiple fields with the same name found"
reader.raiseUnexpectedField(err, "MessagingJsonEnvelope")
case fieldName
of "payload":
payload = Opt.some(reader.readValue(Base64String))
of "contentTopic":
contentTopic = Opt.some(reader.readValue(ContentTopic))
of "ephemeral":
ephemeral = Opt.some(reader.readValue(bool))
of "meta":
meta = Opt.some(reader.readValue(Base64String))
else:
unrecognizedFieldWarning(value)
if payload.isNone() or isEmptyOrWhitespace(string(payload.get())):
reader.raiseUnexpectedValue("Field `payload` is missing or empty")
if contentTopic.isNone() or contentTopic.get().isEmptyOrWhitespace():
reader.raiseUnexpectedValue("Field `contentTopic` is missing or empty")
value = MessagingJsonEnvelope(
payload: payload.get(),
contentTopic: contentTopic.get(),
ephemeral: ephemeral,
meta: meta,
)
proc writeValue*(
writer: var JsonWriter[RestJson], value: MessagingSendResponse
) {.raises: [IOError].} =
writer.beginRecord()
writer.writeField("requestId", value.requestId)
writer.endRecord()
proc readValue*(
reader: var JsonReader[RestJson], value: var MessagingSendResponse
) {.raises: [SerializationError, IOError].} =
var requestId = Opt.none(string)
var keys = initHashSet[string]()
for fieldName in readObjectFields(reader):
if keys.containsOrIncl(fieldName):
let err =
try:
fmt"Multiple `{fieldName}` fields found"
except CatchableError:
"Multiple fields with the same name found"
reader.raiseUnexpectedField(err, "MessagingSendResponse")
case fieldName
of "requestId":
requestId = Opt.some(reader.readValue(string))
else:
unrecognizedFieldWarning(value)
if requestId.isNone():
reader.raiseUnexpectedValue("Field `requestId` is missing")
value = MessagingSendResponse(requestId: requestId.get())
#### Event observability DTOs
##
## Send-related events (sent / propagated / error) are grouped per request id.
## Received messages carry the full `WakuMessage` (serialized as
## `RelayWakuMessage`), matching the nim `MessageReceivedEvent`. Both surfaces
## are populated by the broker listeners installed in the messaging handlers.
type
SendEventKind* {.pure.} = enum
Sent = "sent"
Propagated = "propagated"
Error = "error"
SendEventRecord* = object
kind*: SendEventKind
messageHash*: string
error*: string ## populated only for `Error`
timestamp*: int64 ## nanoseconds, stamped when cached
SendStatus* = object ## All send events observed so far for a single request id.
requestId*: string
events*: seq[SendEventRecord]
ReceivedMessageRecord* = object
messageHash*: string
message*: RelayWakuMessage ## the received WakuMessage, full fidelity
#### Event DTO serialization
proc writeValue*(
writer: var JsonWriter[RestJson], value: SendEventRecord
) {.raises: [IOError].} =
writer.beginRecord()
writer.writeField("kind", $value.kind)
writer.writeField("messageHash", value.messageHash)
if value.error.len > 0:
writer.writeField("error", value.error)
writer.writeField("timestamp", value.timestamp)
writer.endRecord()
proc writeValue*(
writer: var JsonWriter[RestJson], value: SendStatus
) {.raises: [IOError].} =
writer.beginRecord()
writer.writeField("requestId", value.requestId)
writer.writeField("events", value.events)
writer.endRecord()
proc writeValue*(
writer: var JsonWriter[RestJson], value: ReceivedMessageRecord
) {.raises: [IOError].} =
writer.beginRecord()
writer.writeField("messageHash", value.messageHash)
writer.writeField("message", value.message)
writer.endRecord()
proc readValue*(
reader: var JsonReader[RestJson], value: var SendEventKind
) {.raises: [SerializationError, IOError].} =
let s = reader.readValue(string)
case s
of "sent":
value = SendEventKind.Sent
of "propagated":
value = SendEventKind.Propagated
of "error":
value = SendEventKind.Error
else:
reader.raiseUnexpectedValue("Invalid send event kind: " & s)
proc readValue*(
reader: var JsonReader[RestJson], value: var SendEventRecord
) {.raises: [SerializationError, IOError].} =
var
kind = Opt.none(SendEventKind)
messageHash = ""
error = ""
timestamp = int64(0)
for fieldName in readObjectFields(reader):
case fieldName
of "kind":
kind = Opt.some(reader.readValue(SendEventKind))
of "messageHash":
messageHash = reader.readValue(string)
of "error":
error = reader.readValue(string)
of "timestamp":
timestamp = reader.readValue(int64)
else:
unrecognizedFieldWarning(value)
if kind.isNone():
reader.raiseUnexpectedValue("Field `kind` is missing")
value = SendEventRecord(
kind: kind.get(), messageHash: messageHash, error: error, timestamp: timestamp
)
proc readValue*(
reader: var JsonReader[RestJson], value: var SendStatus
) {.raises: [SerializationError, IOError].} =
var
requestId = ""
events: seq[SendEventRecord] = @[]
for fieldName in readObjectFields(reader):
case fieldName
of "requestId":
requestId = reader.readValue(string)
of "events":
events = reader.readValue(seq[SendEventRecord])
else:
unrecognizedFieldWarning(value)
value = SendStatus(requestId: requestId, events: events)
proc readValue*(
reader: var JsonReader[RestJson], value: var ReceivedMessageRecord
) {.raises: [SerializationError, IOError].} =
var
messageHash = ""
message = RelayWakuMessage()
for fieldName in readObjectFields(reader):
case fieldName
of "messageHash":
messageHash = reader.readValue(string)
of "message":
message = reader.readValue(RelayWakuMessage)
else:
unrecognizedFieldWarning(value)
value = ReceivedMessageRecord(messageHash: messageHash, message: message)

View File

@ -1,8 +1,6 @@
## Waku layer API — filter (light client) operations.
import logos_delivery/waku/compat/option_valueor
{.push raises: [].}
import std/options
import results, chronos, chronicles
import logos_delivery/waku/waku
@ -36,7 +34,7 @@ proc filterSubscribe*(
let peer = self.node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr:
return err("could not find peer with WakuFilterSubscribeCodec when subscribing")
let subFut = self.node.filterSubscribe(some(pubsubTopic), contentTopics, peer)
let subFut = self.node.filterSubscribe(Opt.some(pubsubTopic), contentTopics, peer)
if not await subFut.withTimeout(FilterOpTimeout):
return err("filter subscription timed out")
subFut.read().isOkOr:
@ -57,7 +55,8 @@ proc filterUnsubscribe*(
let peer = self.node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr:
return err("could not find peer with WakuFilterSubscribeCodec when unsubscribing")
let unsubFut = self.node.filterUnsubscribe(some(pubsubTopic), contentTopics, peer)
let unsubFut =
self.node.filterUnsubscribe(Opt.some(pubsubTopic), contentTopics, peer)
if not await unsubFut.withTimeout(FilterOpTimeout):
return err("filter un-subscription timed out")
unsubFut.read().isOkOr:

View File

@ -1,5 +1,4 @@
## Waku layer API — lightpush (light client publish) operations.
import logos_delivery/waku/compat/option_valueor
{.push raises: [].}
import results, chronos, chronicles

View File

@ -1,7 +1,7 @@
## Waku layer API — peer management operations.
{.push raises: [].}
import std/[options, sequtils, strutils]
import std/[sequtils, strutils]
import results, chronos, chronicles
import libp2p/[peerid, peerstore]

View File

@ -5,10 +5,8 @@
## `WakuLightPushResult` (status code + description) that the send processors
## branch on for their retry decisions, and expose relay/lightpush availability
## so the messaging layer never inspects `waku.node` directly.
import logos_delivery/waku/compat/option_valueor
{.push raises: [].}
import std/options
import results, chronos
import logos_delivery/waku/waku
@ -46,7 +44,7 @@ proc relayPushHandler*(self: Waku): PushMessageHandler =
proc lightpushPeerAvailable*(self: Waku, shard: PubsubTopic): bool =
## True if a lightpush service peer is available for `shard`.
return self.node.peerManager.selectPeer(WakuLightPushCodec, some(shard)).isSome()
return self.node.peerManager.selectPeer(WakuLightPushCodec, Opt.some(shard)).isSome()
proc lightpushPublishToAny*(
self: Waku, shard: PubsubTopic, message: WakuMessage
@ -55,9 +53,9 @@ proc lightpushPublishToAny*(
## through the node's lightpush flow, which attaches an RLN proof per
## attempt when RLN is mounted. Returns SERVICE_NOT_AVAILABLE when no peer
## is available.
let peer = self.node.peerManager.selectPeer(WakuLightPushCodec, some(shard)).valueOr:
let peer = self.node.peerManager.selectPeer(WakuLightPushCodec, Opt.some(shard)).valueOr:
return lightpushResultServiceUnavailable("no lightpush peer available for shard")
try:
return await self.node.lightpushPublish(some(shard), message, some(peer))
return await self.node.lightpushPublish(Opt.some(shard), message, Opt.some(peer))
except CatchableError as e:
return lightpushResultInternalError(e.msg)

View File

@ -1,7 +1,6 @@
## Waku layer API — store (historical query) operations.
{.push raises: [].}
import std/options
import results, chronos, chronicles
import logos_delivery/waku/waku

View File

@ -1,4 +1,3 @@
import logos_delivery/waku/compat/option_valueor
import
std/[times, strutils, os, sets, strformat, tables],
results,

View File

@ -1,4 +1,3 @@
import logos_delivery/waku/compat/option_valueor
# Simple async pool driver for postgress.
# Inspired by: https://github.com/treeform/pg/
{.push raises: [].}

View File

@ -1,4 +1,3 @@
import logos_delivery/waku/compat/option_valueor
{.push raises: [].}
# The code in this file is an adaptation of the Sqlite KV Store found in nim-eth.
# https://github.com/status-im/nim-eth/blob/master/eth/db/kvstore_sqlite3.nim
@ -172,11 +171,11 @@ proc exec*[P](s: SqliteStmt[P, void], params: P): DatabaseResult[void] =
res
template readResult(s: RawStmtPtr, column: cint, T: type): auto =
when T is Option:
when T is Opt:
if sqlite3_column_type(s, column) == SQLITE_NULL:
none(typeof(default(T).get()))
Opt.none(typeof(default(T).get()))
else:
some(readSimpleResult(s, column, typeof(default(T).get())))
Opt.some(readSimpleResult(s, column, typeof(default(T).get())))
else:
readSimpleResult(s, column, T)

View File

@ -1,7 +1,7 @@
{.push raises: [].}
import
std/[options, net],
std/net,
results,
eth/keys as eth_keys,
eth/p2p/discoveryv5/enr,
@ -62,22 +62,22 @@ proc build*(builder: EnrBuilder): EnrResult[enr.Record] =
## Builder extension: IP address and TCP/UDP ports
proc addAddressAndPorts(
builder: var EnrBuilder, ip: IpAddress, tcpPort, udpPort: Option[Port]
builder: var EnrBuilder, ip: IpAddress, tcpPort, udpPort: Opt[Port]
) =
builder.ipAddress = Opt.some(ip)
builder.tcpPort = tcpPort.toOpt()
builder.udpPort = udpPort.toOpt()
builder.tcpPort = tcpPort
builder.udpPort = udpPort
proc addPorts(builder: var EnrBuilder, tcp, udp: Option[Port]) =
proc addPorts(builder: var EnrBuilder, tcp, udp: Opt[Port]) =
# Based on: https://github.com/status-im/nim-eth/blob/4b22fcd/eth/p2p/discoveryv5/enr.nim#L166
builder.tcpPort = tcp.toOpt()
builder.udpPort = udp.toOpt()
builder.tcpPort = tcp
builder.udpPort = udp
proc withIpAddressAndPorts*(
builder: var EnrBuilder,
ipAddr = none(IpAddress),
tcpPort = none(Port),
udpPort = none(Port),
ipAddr = Opt.none(IpAddress),
tcpPort = Opt.none(Port),
udpPort = Opt.none(Port),
) =
if ipAddr.isSome():
addAddressAndPorts(builder, ipAddr.get(), tcpPort, udpPort)

View File

@ -1,23 +1,9 @@
import logos_delivery/waku/compat/option_valueor
{.push raises: [].}
import std/options, results, eth/keys as eth_keys, libp2p/crypto/crypto as libp2p_crypto
import results, eth/keys as eth_keys, libp2p/crypto/crypto as libp2p_crypto
import eth/p2p/discoveryv5/enr except TypedRecord, toTypedRecord
## Since enr changed to result.Opt[T] from Option[T] for intercompatibility introduce a conversion between
func toOpt*[T](o: Option[T]): Opt[T] =
if o.isSome():
return Opt.some(o.get())
else:
return Opt.none(T)
func toOption*[T](o: Opt[T]): Option[T] =
if o.isSome():
return some(o.get())
else:
return none(T)
## ENR typed record
# Record identity scheme
@ -44,8 +30,8 @@ type TypedRecord* = object
proc init(T: type TypedRecord, record: Record): T =
TypedRecord(raw: record)
proc tryGet*(record: TypedRecord, field: string, T: type): Option[T] =
return record.raw.tryGet(field, T).toOption()
proc tryGet*(record: TypedRecord, field: string, T: type): Opt[T] =
return record.raw.tryGet(field, T)
func toTyped*(record: Record): EnrResult[TypedRecord] =
let tr = TypedRecord.init(record)
@ -61,38 +47,38 @@ func toTyped*(record: Record): EnrResult[TypedRecord] =
# Typed record field accessors
func id*(record: TypedRecord): Option[RecordId] =
func id*(record: TypedRecord): Opt[RecordId] =
let fieldOpt = record.tryGet("id", string)
if fieldOpt.isNone():
return none(RecordId)
return Opt.none(RecordId)
let field = toRecordId(fieldOpt.get()).valueOr:
return none(RecordId)
return Opt.none(RecordId)
return some(field)
return Opt.some(field)
func secp256k1*(record: TypedRecord): Option[array[33, byte]] =
func secp256k1*(record: TypedRecord): Opt[array[33, byte]] =
record.tryGet("secp256k1", array[33, byte])
func ip*(record: TypedRecord): Option[array[4, byte]] =
func ip*(record: TypedRecord): Opt[array[4, byte]] =
record.tryGet("ip", array[4, byte])
func ip6*(record: TypedRecord): Option[array[16, byte]] =
func ip6*(record: TypedRecord): Opt[array[16, byte]] =
record.tryGet("ip6", array[16, byte])
func tcp*(record: TypedRecord): Option[uint16] =
func tcp*(record: TypedRecord): Opt[uint16] =
record.tryGet("tcp", uint16)
func tcp6*(record: TypedRecord): Option[uint16] =
func tcp6*(record: TypedRecord): Opt[uint16] =
let port = record.tryGet("tcp6", uint16)
if port.isNone():
return record.tcp()
return port
func udp*(record: TypedRecord): Option[uint16] =
func udp*(record: TypedRecord): Opt[uint16] =
record.tryGet("udp", uint16)
func udp6*(record: TypedRecord): Option[uint16] =
func udp6*(record: TypedRecord): Opt[uint16] =
let port = record.tryGet("udp6", uint16)
if port.isNone():
return record.udp()

View File

@ -1,4 +1,4 @@
import std/options
import results
type PagingDirection* {.pure.} = enum
## PagingDirection determines the direction of pagination
@ -11,7 +11,7 @@ proc default*(): PagingDirection {.inline.} =
proc into*(b: bool): PagingDirection =
PagingDirection(b)
proc into*(b: Option[bool]): PagingDirection =
proc into*(b: Opt[bool]): PagingDirection =
if b.isNone():
return default()
b.get().into()
@ -19,7 +19,7 @@ proc into*(b: Option[bool]): PagingDirection =
proc into*(d: PagingDirection): bool =
d == PagingDirection.FORWARD
proc into*(d: Option[PagingDirection]): bool =
proc into*(d: Opt[PagingDirection]): bool =
if d.isNone():
return false
d.get().into()

View File

@ -2,7 +2,7 @@
{.push raises: [].}
import std/options, libp2p/protobuf/minprotobuf, libp2p/varint
import results, libp2p/protobuf/minprotobuf, libp2p/varint
export minprotobuf, varint
@ -39,7 +39,7 @@ proc invalidLengthField*(T: type ProtobufError, field: string): T =
## Extension methods
proc write3*(proto: var ProtoBuffer, field: int, value: auto) =
when value is Option:
when value is Opt:
if value.isSome():
proto.write(field, value.get())
else:

View File

@ -6,19 +6,19 @@
{.push raises: [].}
import std/[options, tables], libp2p/stream/connection
import results, std/tables, libp2p/stream/connection
import ./[single_token_limiter, service_metrics], ../../utils/tableutils
export token_bucket, setting, service_metrics
type PerPeerRateLimiter* = ref object of RootObj
setting*: Option[RateLimitSetting]
peerBucket: Table[PeerId, Option[TokenBucket]]
setting*: Opt[RateLimitSetting]
peerBucket: Table[PeerId, Opt[TokenBucket]]
proc mgetOrPut(
perPeerRateLimiter: var PerPeerRateLimiter, peerId: PeerId
): var Option[TokenBucket] =
): var Opt[TokenBucket] =
return perPeerRateLimiter.peerBucket.mgetOrPut(
peerId, newTokenBucket(perPeerRateLimiter.setting, ReplenishMode.Continuous)
)

View File

@ -1,4 +1,3 @@
import logos_delivery/waku/compat/option_valueor
## RequestRateLimiter
##
## RequestRateLimiter is a general service protection mechanism.
@ -17,11 +16,7 @@ import logos_delivery/waku/compat/option_valueor
{.push raises: [].}
import
std/[options, math],
chronicles,
chronos/timer,
libp2p/stream/connection,
libp2p/utility
results, std/math, chronicles, chronos/timer, libp2p/stream/connection, libp2p/utility
import std/times except TimeInterval, Duration, seconds, minutes
@ -41,7 +36,7 @@ const MINUTES_RATIO = 2
type RequestRateLimiter* = ref object of RootObj
tokenBucket: TokenBucket
setting*: Option[RateLimitSetting]
setting*: Opt[RateLimitSetting]
mainBucketSetting: RateLimitSetting
ratio: int
peerBucketSetting*: RateLimitSetting
@ -80,7 +75,7 @@ proc mgetOrPut(
requestRateLimiter: var RequestRateLimiter, peerId: PeerId, now: Moment
): var TokenBucket =
let bucketForNew = newTokenBucket(
some(requestRateLimiter.peerBucketSetting), Discrete, now
Opt.some(requestRateLimiter.peerBucketSetting), Discrete, now
).valueOr:
raiseAssert "This branch is not allowed to be reached as it will not be called if the setting is None."
@ -137,7 +132,7 @@ template checkUsageLimit*(
bodyRejected
# TODO: review these ratio assumptions! Debatable!
func calcPeriodRatio(settingOpt: Option[RateLimitSetting]): int =
func calcPeriodRatio(settingOpt: Opt[RateLimitSetting]): int =
settingOpt.withValue(setting):
if setting.isUnlimited():
return UNLIMITED_RATIO
@ -155,7 +150,7 @@ func calcPeriodRatio(settingOpt: Option[RateLimitSetting]): int =
# calculates peer cache items timeout
# effectively if a peer does not issue any requests for this amount of time will be forgotten.
func calcCacheTimeout(settingOpt: Option[RateLimitSetting], ratio: int): Duration =
func calcCacheTimeout(settingOpt: Opt[RateLimitSetting], ratio: int): Duration =
settingOpt.withValue(setting):
if setting.isUnlimited():
return UNLIMITED_TIMEOUT
@ -167,7 +162,7 @@ func calcCacheTimeout(settingOpt: Option[RateLimitSetting], ratio: int): Duratio
return UNLIMITED_TIMEOUT
func calcPeerTokenSetting(
setting: Option[RateLimitSetting], ratio: int
setting: Opt[RateLimitSetting], ratio: int
): RateLimitSetting =
let s = setting.valueOr:
return (0, 0.minutes)
@ -178,7 +173,7 @@ func calcPeerTokenSetting(
return (peerVolume, peerPeriod)
proc newRequestRateLimiter*(setting: Option[RateLimitSetting]): RequestRateLimiter =
proc newRequestRateLimiter*(setting: Opt[RateLimitSetting]): RequestRateLimiter =
let ratio = calcPeriodRatio(setting)
let isLimited = setting.isSome() and not setting.get().isUnlimited()
let mainBucketSetting =

View File

@ -1,7 +1,6 @@
{.push raises: [].}
import std/options
import metrics, setting
import results, metrics, setting
export metrics
@ -14,7 +13,7 @@ declarePublicCounter waku_service_requests,
declarePublicCounter waku_service_network_bytes,
"total incoming traffic of specific waku services", labels = ["service", "direction"]
proc setServiceLimitMetric*(service: string, limit: Option[RateLimitSetting]) =
proc setServiceLimitMetric*(service: string, limit: Opt[RateLimitSetting]) =
if limit.isSome() and not limit.get().isUnlimited():
waku_service_requests_limit.set(
limit.get().calculateLimitPerSecond(), labelValues = [service]

View File

@ -1,6 +1,6 @@
{.push raises: [].}
import chronos/timer, std/[tables, strutils, options], regex, results
import chronos/timer, std/[tables, strutils], regex, results
# Setting for TokenBucket defined as volume over period of time
type RateLimitSetting* = tuple[volume: int, period: Duration]

View File

@ -2,7 +2,7 @@
{.push raises: [].}
import std/[options], chronos/timer, libp2p/stream/connection, libp2p/utility
import results, chronos/timer, libp2p/stream/connection, libp2p/utility
import std/times except TimeInterval, Duration
@ -12,17 +12,17 @@ import ./[setting, service_metrics]
export token_bucket, setting, service_metrics
proc newTokenBucket*(
setting: Option[RateLimitSetting],
setting: Opt[RateLimitSetting],
replenishMode: static[ReplenishMode] = ReplenishMode.Continuous,
startTime: Moment = Moment.now(),
): Option[TokenBucket] =
): Opt[TokenBucket] =
if setting.isNone():
return none[TokenBucket]()
return Opt.none(TokenBucket)
if setting.get().isUnlimited():
return none[TokenBucket]()
return Opt.none(TokenBucket)
return some(
return Opt.some(
TokenBucket.new(
capacity = setting.get().volume,
fillDuration = setting.get().period,
@ -40,7 +40,7 @@ proc checkUsage(
return true
proc checkUsage(
t: var Option[TokenBucket], proto: string, now = Moment.now()
t: var Opt[TokenBucket], proto: string, now = Moment.now()
): bool {.raises: [].} =
if t.isNone():
return true
@ -49,7 +49,7 @@ proc checkUsage(
return checkUsage(tokenBucket, proto, now)
template checkUsageLimit*(
t: var Option[TokenBucket] | var TokenBucket,
t: var Opt[TokenBucket] | var TokenBucket,
proto: string,
conn: Connection,
bodyWithinLimit, bodyRejected: untyped,

View File

@ -1,6 +1,6 @@
{.push raises: [].}
import std/[httpclient, json, uri, options], results
import std/[httpclient, json, uri], results
const
# Resource locators

View File

@ -1,6 +1,6 @@
{.push raises: [].}
import std/[options, strutils, net]
import std/[strutils, net]
import chronicles, eth/net/nat, results, nativesockets
logScope:
@ -18,9 +18,9 @@ var singletonNat: bool = false
# TODO: pass `NatStrategy`, not a string
proc setupNat*(
natConf, clientId: string, tcpPort, udpPort: Port
): Result[
tuple[ip: Option[IpAddress], tcpPort: Option[Port], udpPort: Option[Port]], string
] {.gcsafe.} =
): Result[tuple[ip: Opt[IpAddress], tcpPort: Opt[Port], udpPort: Opt[Port]], string] {.
gcsafe
.} =
let strategy =
case natConf.toLowerAscii()
of "any": NatAny
@ -29,8 +29,7 @@ proc setupNat*(
of "pmp": NatPmp
else: NatNone
var endpoint:
tuple[ip: Option[IpAddress], tcpPort: Option[Port], udpPort: Option[Port]]
var endpoint: tuple[ip: Opt[IpAddress], tcpPort: Opt[Port], udpPort: Opt[Port]]
if strategy != NatNone:
## Only initialize the NAT module once
@ -47,7 +46,7 @@ proc setupNat*(
warn "exception in setupNat", error = getCurrentExceptionMsg()
if extIP.isSome():
endpoint.ip = some(extIp.get())
endpoint.ip = Opt.some(extIp.get())
# RedirectPorts in considered a gcsafety violation
# because it obtains the address of a non-gcsafe proc?
var extPorts: Opt[(Port, Port)]
@ -65,15 +64,15 @@ proc setupNat*(
if extPorts.isSome():
let (extTcpPort, extUdpPort) = extPorts.get()
endpoint.tcpPort = some(extTcpPort)
endpoint.udpPort = some(extUdpPort)
endpoint.tcpPort = Opt.some(extTcpPort)
endpoint.udpPort = Opt.some(extUdpPort)
else: # NatNone
if not natConf.startsWith("extip:"):
return err("not a valid NAT mechanism: " & $natConf)
try:
# any required port redirection is assumed to be done by hand
endpoint.ip = some(parseIpAddress(natConf[6 ..^ 1]))
endpoint.ip = Opt.some(parseIpAddress(natConf[6 ..^ 1]))
except ValueError:
return err("not a valid IP address: " & $natConf[6 ..^ 1])

View File

@ -1,4 +1,3 @@
import logos_delivery/waku/compat/option_valueor
import std/[strutils, math], results, regex
proc parseMsgSize*(input: string): Result[uint64, string] =

View File

@ -1,32 +0,0 @@
## Polyfill: `valueOr` / `withValue` templates for `std/options.Option[T]`.
##
## Previously provided transitively by `libp2p/utility`, removed in
## nim-libp2p PR #2162 (commit 8a9943145). logos-delivery uses these
## templates pervasively on `Option[T]`.
{.push raises: [].}
import std/[macros, options]
template valueOr*[T](self: Option[T], body: untyped): untyped =
let temp = (self)
if temp.isSome:
temp.get()
else:
body
template withValue*[T](self: Option[T], value, body: untyped): untyped =
let temp = (self)
if temp.isSome:
let `value` {.inject.} = temp.get()
body
macro withValue*[T](self: Option[T], value, body, elseStmt: untyped): untyped =
let elseBody = elseStmt[0]
quote:
let temp = (`self`)
if temp.isSome:
let `value` {.inject.} = temp.get()
`body`
else:
`elseBody`

View File

@ -1,4 +1,4 @@
import libp2p/crypto/crypto
import results, libp2p/crypto/crypto
import
chronos,
chronicles,

View File

@ -1,10 +1,9 @@
import libp2p/crypto/crypto
import libp2p/crypto/rng
import logos_delivery/waku/compat/option_valueor
{.push raises: [].}
import
std/[sequtils, strutils, options, sets, net, json],
std/[sequtils, strutils, sets, net, json],
results,
chronos,
chronicles,
@ -40,7 +39,7 @@ type Discv5Conf* {.requiresInit.} = object
enrAutoUpdate*: bool
type WakuDiscoveryV5Config* = object
discv5Config*: Option[DiscoveryConfig]
discv5Config*: Opt[DiscoveryConfig]
address*: IpAddress
port*: Port
privateKey*: eth_keys.PrivateKey
@ -56,21 +55,21 @@ type WakuDiscoveryV5* = ref object
conf: WakuDiscoveryV5Config
protocol*: protocol.Protocol
listening*: bool
predicate: Option[WakuDiscv5Predicate]
peerManager: Option[PeerManager]
predicate: Opt[WakuDiscv5Predicate]
peerManager: Opt[PeerManager]
topicSubscriptionQueue: AsyncEventQueue[SubscriptionEvent]
proc shardingPredicate*(
record: Record, bootnodes: seq[Record] = @[]
): Option[WakuDiscv5Predicate] =
): Opt[WakuDiscv5Predicate] =
## Filter peers based on relay sharding information
let typedRecord = record.toTyped().valueOr:
info "peer filtering failed", reason = error
return none(WakuDiscv5Predicate)
return Opt.none(WakuDiscv5Predicate)
let nodeShard = typedRecord.relaySharding().valueOr:
info "no relay sharding information, peer filtering disabled"
return none(WakuDiscv5Predicate)
return Opt.none(WakuDiscv5Predicate)
info "peer filtering updated"
@ -81,14 +80,14 @@ proc shardingPredicate*(
nodeShard.shardIds.anyIt(record.containsShard(nodeShard.clusterId, it))
) #RFC 64 guideline
return some(predicate)
return Opt.some(predicate)
proc new*(
T: type WakuDiscoveryV5,
rng: crypto.Rng,
conf: WakuDiscoveryV5Config,
record: Option[waku_enr.Record],
peerManager: Option[PeerManager] = none(PeerManager),
record: Opt[waku_enr.Record],
peerManager: Opt[PeerManager] = Opt.none(PeerManager),
queue: AsyncEventQueue[SubscriptionEvent] =
newAsyncEventQueue[SubscriptionEvent](30),
): T =
@ -100,7 +99,7 @@ proc new*(
privKey = conf.privateKey,
bootstrapRecords = conf.bootstrapRecords,
enrAutoUpdate = conf.autoupdateRecord,
previousRecord = record.toOpt(),
previousRecord = record,
enrIp = Opt.none(IpAddress),
enrTcpPort = Opt.none(Port),
enrUdpPort = Opt.none(Port),
@ -110,7 +109,7 @@ proc new*(
if record.isSome():
shardingPredicate(record.get(), conf.bootstrapRecords)
else:
none(WakuDiscv5Predicate)
Opt.none(WakuDiscv5Predicate)
WakuDiscoveryV5(
conf: conf,
@ -221,7 +220,7 @@ proc logDiscv5FoundPeers(discoveredRecords: seq[waku_enr.Record]) =
addrs = addrs, enr = recordUri, capabilities = capabilities, shards = shardsStr
proc findRandomPeers*(
wd: WakuDiscoveryV5, overridePred = none(WakuDiscv5Predicate)
wd: WakuDiscoveryV5, overridePred = Opt.none(WakuDiscv5Predicate)
): Future[seq[waku_enr.Record]] {.async.} =
## Find random peers to connect to using Discovery v5
let discoveredNodes = await wd.protocol.queryRandom()
@ -445,7 +444,7 @@ proc setupDiscoveryV5*(
let discv5UdpPort = conf.udpPort
let discv5Conf = WakuDiscoveryV5Config(
discv5Config: some(discv5Config),
discv5Config: Opt.some(discv5Config),
address: p2pListenAddress,
port: discv5UdpPort,
privateKey: eth_keys.PrivateKey(key.skkey),
@ -455,7 +454,11 @@ proc setupDiscoveryV5*(
return ok(
WakuDiscoveryV5.new(
rng, discv5Conf, some(myENR), some(nodePeerManager), nodeTopicSubscriptionQueue
rng,
discv5Conf,
Opt.some(myENR),
Opt.some(nodePeerManager),
nodeTopicSubscriptionQueue,
)
)

View File

@ -1,4 +1,3 @@
import logos_delivery/waku/compat/option_valueor
{.push raises: [].}
## A set of utilities to integrate EIP-1459 DNS-based discovery
@ -7,7 +6,7 @@ import logos_delivery/waku/compat/option_valueor
## EIP-1459 is defined in https://eips.ethereum.org/EIPS/eip-1459
import
std/[options, net, sequtils, sugar],
std/[net, sequtils, sugar],
chronicles,
chronos,
metrics,

View File

@ -1,7 +1,6 @@
import logos_delivery/waku/compat/option_valueor
{.push raises: [].}
import std/[options, sequtils, sets]
import std/[sequtils, sets]
import
chronos,
chronicles,
@ -51,45 +50,45 @@ type KademliaDiscoveryConf* = object
clientMode*: bool
xprPublishing*: bool
proc extractMixPubKey*(service: ServiceInfo): Option[Curve25519Key] =
proc extractMixPubKey*(service: ServiceInfo): Opt[Curve25519Key] =
if service.id != MixProtocolID:
return none(Curve25519Key)
return Opt.none(Curve25519Key)
if service.data.len != Curve25519KeySize:
trace "invalid mix pub key length",
expected = Curve25519KeySize,
actual = service.data.len,
dataHex = byteutils.toHex(service.data)
return none(Curve25519Key)
return Opt.none(Curve25519Key)
let key = intoCurve25519Key(service.data)
return some(key)
return Opt.some(key)
proc remotePeerInfoFrom*(record: ExtendedPeerRecord): Option[RemotePeerInfo] =
proc remotePeerInfoFrom*(record: ExtendedPeerRecord): Opt[RemotePeerInfo] =
if record.addresses.len == 0:
trace "missing addresses", peerId = record.peerId
return none(RemotePeerInfo)
return Opt.none(RemotePeerInfo)
let addrs = record.addresses.mapIt(it.address)
if addrs.len == 0:
trace "no dialable addresses", peerId = record.peerId
return none(RemotePeerInfo)
return Opt.none(RemotePeerInfo)
let protocols = record.services.mapIt(it.id)
var mixPubKey: Option[Curve25519Key] = none(Curve25519Key)
var mixPubKey: Opt[Curve25519Key] = Opt.none(Curve25519Key)
for service in record.services:
let key = extractMixPubKey(service).valueOr:
continue
mixPubKey = some(key)
mixPubKey = Opt.some(key)
trace "successfully extracted mix pub key",
peerId = record.peerId, keyHex = byteutils.toHex(mixPubKey.get())
break
return some(
return Opt.some(
RemotePeerInfo.init(
record.peerId,
addrs = addrs,

View File

@ -1,7 +1,7 @@
{.push raises: [].}
import
std/[options, net, math],
std/[net, math],
results,
chronicles,
libp2p/crypto/crypto,
@ -23,14 +23,14 @@ import
type
WakuNodeBuilder* = object # General
nodeRng: Option[crypto.Rng]
nodeKey: Option[crypto.PrivateKey]
netConfig: Option[NetConfig]
record: Option[enr.Record]
nodeRng: Opt[crypto.Rng]
nodeKey: Opt[crypto.PrivateKey]
netConfig: Opt[NetConfig]
record: Opt[enr.Record]
# Peer storage and peer manager
peerStorage: Option[PeerStorage]
peerStorageCapacity: Option[int]
peerStorage: Opt[PeerStorage]
peerStorageCapacity: Opt[int]
# Peer manager config
maxRelayPeers: int
@ -39,16 +39,16 @@ type
shardAware: bool
# Libp2p switch
switchMaxConnections: Option[int]
switchNameResolver: Option[NameResolver]
switchAgentString: Option[string]
switchSslSecureKey: Option[string]
switchSslSecureCert: Option[string]
switchSendSignedPeerRecord: Option[bool]
switchMaxConnections: Opt[int]
switchNameResolver: Opt[NameResolver]
switchAgentString: Opt[string]
switchSslSecureKey: Opt[string]
switchSslSecureCert: Opt[string]
switchSendSignedPeerRecord: Opt[bool]
circuitRelay: Relay
# Rate limit configs for non-relay req-resp protocols
rateLimitSettings: Option[ProtocolRateLimitSettings]
rateLimitSettings: Opt[ProtocolRateLimitSettings]
WakuNodeBuilderResult* = Result[void, string]
@ -60,29 +60,29 @@ proc init*(T: type WakuNodeBuilder): WakuNodeBuilder =
## General
proc withRng*(builder: var WakuNodeBuilder, rng: crypto.Rng) =
builder.nodeRng = some(rng)
builder.nodeRng = Opt.some(rng)
proc withNodeKey*(builder: var WakuNodeBuilder, nodeKey: crypto.PrivateKey) =
builder.nodeKey = some(nodeKey)
builder.nodeKey = Opt.some(nodeKey)
proc withRecord*(builder: var WakuNodeBuilder, record: enr.Record) =
builder.record = some(record)
builder.record = Opt.some(record)
proc withNetworkConfiguration*(builder: var WakuNodeBuilder, config: NetConfig) =
builder.netConfig = some(config)
builder.netConfig = Opt.some(config)
proc withNetworkConfigurationDetails*(
builder: var WakuNodeBuilder,
bindIp: IpAddress,
bindPort: Port,
extIp = none(IpAddress),
extPort = none(Port),
extIp = Opt.none(IpAddress),
extPort = Opt.none(Port),
extMultiAddrs = newSeq[MultiAddress](),
wsBindPort: Port = Port(8000),
wsEnabled: bool = false,
wssEnabled: bool = false,
wakuFlags = none(CapabilitiesBitfield),
dns4DomainName = none(string),
wakuFlags = Opt.none(CapabilitiesBitfield),
dns4DomainName = Opt.none(string),
dnsNameServers = @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")],
): WakuNodeBuilderResult {.
deprecated: "use 'builder.withNetworkConfiguration()' instead"
@ -93,7 +93,7 @@ proc withNetworkConfigurationDetails*(
extIp = extIp,
extPort = extPort,
extMultiAddrs = extMultiAddrs,
wsBindPort = some(wsBindPort),
wsBindPort = Opt.some(wsBindPort),
wsEnabled = wsEnabled,
wssEnabled = wssEnabled,
wakuFlags = wakuFlags,
@ -106,10 +106,10 @@ proc withNetworkConfigurationDetails*(
## Peer storage and peer manager
proc withPeerStorage*(
builder: var WakuNodeBuilder, peerStorage: PeerStorage, capacity = none(int)
builder: var WakuNodeBuilder, peerStorage: PeerStorage, capacity = Opt.none(int)
) =
if not peerStorage.isNil():
builder.peerStorage = some(peerStorage)
builder.peerStorage = Opt.some(peerStorage)
builder.peerStorageCapacity = capacity
@ -131,7 +131,7 @@ proc withColocationLimit*(builder: var WakuNodeBuilder, colocationLimit: int) =
builder.colocationLimit = colocationLimit
proc withRateLimit*(builder: var WakuNodeBuilder, limits: ProtocolRateLimitSettings) =
builder.rateLimitSettings = some(limits)
builder.rateLimitSettings = Opt.some(limits)
proc withCircuitRelay*(builder: var WakuNodeBuilder, circuitRelay: Relay) =
builder.circuitRelay = circuitRelay
@ -140,21 +140,21 @@ proc withCircuitRelay*(builder: var WakuNodeBuilder, circuitRelay: Relay) =
proc withSwitchConfiguration*(
builder: var WakuNodeBuilder,
maxConnections = none(int),
maxConnections = Opt.none(int),
nameResolver: NameResolver = nil,
sendSignedPeerRecord = false,
secureKey = none(string),
secureCert = none(string),
agentString = none(string),
secureKey = Opt.none(string),
secureCert = Opt.none(string),
agentString = Opt.none(string),
) =
builder.switchMaxConnections = maxConnections
builder.switchSendSignedPeerRecord = some(sendSignedPeerRecord)
builder.switchSendSignedPeerRecord = Opt.some(sendSignedPeerRecord)
builder.switchSslSecureKey = secureKey
builder.switchSslSecureCert = secureCert
builder.switchAgentString = agentString
if not nameResolver.isNil():
builder.switchNameResolver = some(nameResolver)
builder.switchNameResolver = Opt.some(nameResolver)
## Build
@ -209,8 +209,8 @@ proc build*(builder: WakuNodeBuilder): Result[WakuNode, string] =
let peerManager = PeerManager.new(
switch = switch,
storage = builder.peerStorage.get(nil),
maxRelayPeers = some(builder.maxRelayPeers),
maxServicePeers = some(builder.maxServicePeers),
maxRelayPeers = Opt.some(builder.maxRelayPeers),
maxServicePeers = Opt.some(builder.maxServicePeers),
colocationLimit = builder.colocationLimit,
shardedPeerManagement = builder.shardAware,
maxConnections = builder.switchMaxConnections.get(MaxConnections),

View File

@ -1,4 +1,4 @@
import chronicles, std/[net, options, sequtils], results
import chronicles, std/[net, sequtils], results
import ../waku_conf
logScope:
@ -16,49 +16,49 @@ const
## Discv5 Config Builder ##
###########################
type Discv5ConfBuilder* = object
enabled*: Option[bool]
enabled*: Opt[bool]
bootstrapNodes*: seq[string]
bitsPerHop*: Option[int]
bucketIpLimit*: Option[uint]
enrAutoUpdate*: Option[bool]
tableIpLimit*: Option[uint]
udpPort*: Option[Port]
bitsPerHop*: Opt[int]
bucketIpLimit*: Opt[uint]
enrAutoUpdate*: Opt[bool]
tableIpLimit*: Opt[uint]
udpPort*: Opt[Port]
proc init*(T: type Discv5ConfBuilder): Discv5ConfBuilder =
Discv5ConfBuilder()
proc withEnabled*(b: var Discv5ConfBuilder, enabled: bool) =
b.enabled = some(enabled)
b.enabled = Opt.some(enabled)
proc withBitsPerHop*(b: var Discv5ConfBuilder, bitsPerHop: int) =
b.bitsPerHop = some(bitsPerHop)
b.bitsPerHop = Opt.some(bitsPerHop)
proc withBucketIpLimit*(b: var Discv5ConfBuilder, bucketIpLimit: uint) =
b.bucketIpLimit = some(bucketIpLimit)
b.bucketIpLimit = Opt.some(bucketIpLimit)
proc withEnrAutoUpdate*(b: var Discv5ConfBuilder, enrAutoUpdate: bool) =
b.enrAutoUpdate = some(enrAutoUpdate)
b.enrAutoUpdate = Opt.some(enrAutoUpdate)
proc withTableIpLimit*(b: var Discv5ConfBuilder, tableIpLimit: uint) =
b.tableIpLimit = some(tableIpLimit)
b.tableIpLimit = Opt.some(tableIpLimit)
proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: Port) =
b.udpPort = some(udpPort)
b.udpPort = Opt.some(udpPort)
proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: uint16) =
b.udpPort = some(Port(udpPort))
b.udpPort = Opt.some(Port(udpPort))
proc withBootstrapNodes*(b: var Discv5ConfBuilder, bootstrapNodes: seq[string]) =
# TODO: validate ENRs?
b.bootstrapNodes = concat(b.bootstrapNodes, bootstrapNodes)
proc build*(b: Discv5ConfBuilder): Result[Option[Discv5Conf], string] =
proc build*(b: Discv5ConfBuilder): Result[Opt[Discv5Conf], string] =
if not b.enabled.get(DefaultDiscv5Enabled):
return ok(none(Discv5Conf))
return ok(Opt.none(Discv5Conf))
return ok(
some(
Opt.some(
Discv5Conf(
bootstrapNodes: b.bootstrapNodes,
bitsPerHop: b.bitsPerHop.get(DefaultDiscv5BitsPerHop),

View File

@ -1,4 +1,4 @@
import chronicles, std/[net, options, strutils], results
import chronicles, std/[net, strutils], results
import ../waku_conf
logScope:
@ -8,21 +8,21 @@ logScope:
## DNS Discovery Config Builder ##
##################################
type DnsDiscoveryConfBuilder* = object
enrTreeUrl*: Option[string]
enrTreeUrl*: Opt[string]
nameServers*: seq[IpAddress]
proc init*(T: type DnsDiscoveryConfBuilder): DnsDiscoveryConfBuilder =
DnsDiscoveryConfBuilder()
proc withEnrTreeUrl*(b: var DnsDiscoveryConfBuilder, enrTreeUrl: string) =
b.enrTreeUrl = some(enrTreeUrl)
b.enrTreeUrl = Opt.some(enrTreeUrl)
proc withNameServers*(b: var DnsDiscoveryConfBuilder, nameServers: seq[IpAddress]) =
b.nameServers = nameServers
proc build*(b: DnsDiscoveryConfBuilder): Result[Option[DnsDiscoveryConf], string] =
proc build*(b: DnsDiscoveryConfBuilder): Result[Opt[DnsDiscoveryConf], string] =
if b.enrTreeUrl.isNone():
return ok(none(DnsDiscoveryConf))
return ok(Opt.none(DnsDiscoveryConf))
if isEmptyOrWhiteSpace(b.enrTreeUrl.get()):
return err("dnsDiscovery.enrTreeUrl cannot be an empty string")
@ -30,5 +30,7 @@ proc build*(b: DnsDiscoveryConfBuilder): Result[Option[DnsDiscoveryConf], string
return err("dnsDiscovery.nameServers is not specified")
return ok(
some(DnsDiscoveryConf(nameServers: b.nameServers, enrTreeUrl: b.enrTreeUrl.get()))
Opt.some(
DnsDiscoveryConf(nameServers: b.nameServers, enrTreeUrl: b.enrTreeUrl.get())
)
)

View File

@ -1,4 +1,4 @@
import chronicles, std/options, results
import chronicles, results
import ../waku_conf
logScope:
@ -14,40 +14,40 @@ const
## Filter Service Config Builder ##
###################################
type FilterServiceConfBuilder* = object
enabled*: Option[bool]
maxPeersToServe*: Option[uint32]
subscriptionTimeout*: Option[uint16]
maxCriteria*: Option[uint32]
enabled*: Opt[bool]
maxPeersToServe*: Opt[uint32]
subscriptionTimeout*: Opt[uint16]
maxCriteria*: Opt[uint32]
proc init*(T: type FilterServiceConfBuilder): FilterServiceConfBuilder =
FilterServiceConfBuilder()
proc withEnabled*(b: var FilterServiceConfBuilder, enabled: bool) =
b.enabled = some(enabled)
b.enabled = Opt.some(enabled)
proc withMaxPeersToServe*(b: var FilterServiceConfBuilder, maxPeersToServe: uint32) =
b.maxPeersToServe = some(maxPeersToServe)
b.maxPeersToServe = Opt.some(maxPeersToServe)
proc withMaxPeersToServeIfNotAssigned*(
b: var FilterServiceConfBuilder, maxPeersToServe: uint32
) =
if b.maxPeersToServe.isNone():
b.maxPeersToServe = some(maxPeersToServe)
b.maxPeersToServe = Opt.some(maxPeersToServe)
proc withSubscriptionTimeout*(
b: var FilterServiceConfBuilder, subscriptionTimeout: uint16
) =
b.subscriptionTimeout = some(subscriptionTimeout)
b.subscriptionTimeout = Opt.some(subscriptionTimeout)
proc withMaxCriteria*(b: var FilterServiceConfBuilder, maxCriteria: uint32) =
b.maxCriteria = some(maxCriteria)
b.maxCriteria = Opt.some(maxCriteria)
proc build*(b: FilterServiceConfBuilder): Result[Option[FilterServiceConf], string] =
proc build*(b: FilterServiceConfBuilder): Result[Opt[FilterServiceConf], string] =
if not b.enabled.get(DefaultFilterEnabled):
return ok(none(FilterServiceConf))
return ok(Opt.none(FilterServiceConf))
return ok(
some(
Opt.some(
FilterServiceConf(
maxPeersToServe: b.maxPeersToServe.get(DefaultFilterMaxPeersToServe),
subscriptionTimeout: b.subscriptionTimeout.get(DefaultFilterSubscriptionTimeout),

View File

@ -1,6 +1,4 @@
import logos_delivery/waku/compat/option_valueor
import chronicles, std/options, results
import logos_delivery/waku/discovery/waku_kademlia
import chronicles, results, logos_delivery/waku/discovery/waku_kademlia
import chronos
import libp2p/[peerid, multiaddress, peerinfo]
import libp2p/protocols/kademlia/types
@ -15,16 +13,16 @@ const
DefaultServiceLookupInterval* = chronos.seconds(60)
type KademliaDiscoveryConfBuilder* = object
enabled*: Option[bool]
enabled*: Opt[bool]
bootstrapNodes*: seq[string]
randomLookupInterval*: Option[Duration]
serviceLookupInterval*: Option[Duration]
randomLookupInterval*: Opt[Duration]
serviceLookupInterval*: Opt[Duration]
proc init*(T: type KademliaDiscoveryConfBuilder): KademliaDiscoveryConfBuilder =
KademliaDiscoveryConfBuilder()
proc withEnabled*(b: var KademliaDiscoveryConfBuilder, enabled: bool) =
b.enabled = some(enabled)
b.enabled = Opt.some(enabled)
proc withBootstrapNodes*(
b: var KademliaDiscoveryConfBuilder, bootstrapNodes: seq[string]
@ -34,22 +32,22 @@ proc withBootstrapNodes*(
proc withRandomLookupInterval*(
b: var KademliaDiscoveryConfBuilder, interval: Duration
) =
b.randomLookupInterval = some(interval)
b.randomLookupInterval = Opt.some(interval)
proc withServiceLookupInterval*(
b: var KademliaDiscoveryConfBuilder, interval: Duration
) =
b.serviceLookupInterval = some(interval)
b.serviceLookupInterval = Opt.some(interval)
proc build*(
b: KademliaDiscoveryConfBuilder
): Result[Option[KademliaDiscoveryConf], string] =
): Result[Opt[KademliaDiscoveryConf], string] =
# Explicit disable wins: enabled=false disables regardless of bootstrap nodes.
if b.enabled == some(false):
return ok(none(KademliaDiscoveryConf))
if b.enabled == Opt.some(false):
return ok(Opt.none(KademliaDiscoveryConf))
# Otherwise enabled if config-enabled or any bootstrap nodes are provided.
if not b.enabled.get(DefaultKadEnabled) and b.bootstrapNodes.len == 0:
return ok(none(KademliaDiscoveryConf))
return ok(Opt.none(KademliaDiscoveryConf))
var parsedNodes: seq[(PeerId, seq[MultiAddress])]
for nodeStr in b.bootstrapNodes:
@ -58,7 +56,7 @@ proc build*(
parsedNodes.add((peerId, @[ma]))
return ok(
some(
Opt.some(
KademliaDiscoveryConf(
bootstrapNodes: parsedNodes,
randomLookupInterval: b.randomLookupInterval.get(DefaultRandomLookupInterval),

View File

@ -1,4 +1,4 @@
import chronicles, std/[net, options], results
import chronicles, std/net, results
import ../waku_conf
logScope:
@ -14,36 +14,36 @@ const
## Metrics Server Config Builder ##
###################################
type MetricsServerConfBuilder* = object
enabled*: Option[bool]
enabled*: Opt[bool]
httpAddress*: Option[IpAddress]
httpPort*: Option[Port]
logging*: Option[bool]
httpAddress*: Opt[IpAddress]
httpPort*: Opt[Port]
logging*: Opt[bool]
proc init*(T: type MetricsServerConfBuilder): MetricsServerConfBuilder =
MetricsServerConfBuilder()
proc withEnabled*(b: var MetricsServerConfBuilder, enabled: bool) =
b.enabled = some(enabled)
b.enabled = Opt.some(enabled)
proc withHttpAddress*(b: var MetricsServerConfBuilder, httpAddress: IpAddress) =
b.httpAddress = some(httpAddress)
b.httpAddress = Opt.some(httpAddress)
proc withHttpPort*(b: var MetricsServerConfBuilder, httpPort: Port) =
b.httpPort = some(httpPort)
b.httpPort = Opt.some(httpPort)
proc withHttpPort*(b: var MetricsServerConfBuilder, httpPort: uint16) =
b.httpPort = some(Port(httpPort))
b.httpPort = Opt.some(Port(httpPort))
proc withLogging*(b: var MetricsServerConfBuilder, logging: bool) =
b.logging = some(logging)
b.logging = Opt.some(logging)
proc build*(b: MetricsServerConfBuilder): Result[Option[MetricsServerConf], string] =
proc build*(b: MetricsServerConfBuilder): Result[Opt[MetricsServerConf], string] =
if not b.enabled.get(DefaultMetricsEnabled):
return ok(none(MetricsServerConf))
return ok(Opt.none(MetricsServerConf))
return ok(
some(
Opt.some(
MetricsServerConf(
httpAddress: b.httpAddress.get(DefaultMetricsHttpAddress),
httpPort: b.httpPort.get(DefaultMetricsHttpPort),

View File

@ -1,6 +1,9 @@
import logos_delivery/waku/compat/option_valueor
import chronicles, std/options, results
import libp2p/crypto/crypto, libp2p/crypto/curve25519, libp2p_mix/curve25519
import
chronicles,
results,
libp2p/crypto/crypto,
libp2p/crypto/curve25519,
libp2p_mix/curve25519
import ../waku_conf, logos_delivery/waku/waku_mix
logScope:
@ -12,35 +15,39 @@ const DefaultMixEnabled: bool = false
## Mix Config Builder ##
##################################
type MixConfBuilder* = object
enabled: Option[bool]
mixKey: Option[string]
enabled: Opt[bool]
mixKey: Opt[string]
mixNodes: seq[MixNodePubInfo]
proc init*(T: type MixConfBuilder): MixConfBuilder =
MixConfBuilder()
proc withEnabled*(b: var MixConfBuilder, enabled: bool) =
b.enabled = some(enabled)
b.enabled = Opt.some(enabled)
proc withMixKey*(b: var MixConfBuilder, mixKey: string) =
b.mixKey = some(mixKey)
b.mixKey = Opt.some(mixKey)
proc withMixNodes*(b: var MixConfBuilder, mixNodes: seq[MixNodePubInfo]) =
b.mixNodes = mixNodes
proc build*(b: MixConfBuilder): Result[Option[MixConf], string] =
proc build*(b: MixConfBuilder): Result[Opt[MixConf], string] =
if not b.enabled.get(DefaultMixEnabled):
return ok(none[MixConf]())
return ok(Opt.none(MixConf))
else:
if b.mixKey.isSome():
let mixPrivKey = intoCurve25519Key(ncrutils.fromHex(b.mixKey.get()))
let mixPubKey = public(mixPrivKey)
return ok(
some(MixConf(mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes))
Opt.some(
MixConf(mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes)
)
)
else:
let (mixPrivKey, mixPubKey) = generateKeyPair().valueOr:
return err("Generate key pair error: " & $error)
return ok(
some(MixConf(mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes))
Opt.some(
MixConf(mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes)
)
)

View File

@ -1,4 +1,4 @@
import chronicles, std/[net, options], results
import chronicles, std/net, results
import logos_delivery/waku/factory/waku_conf
logScope:
@ -11,23 +11,23 @@ const DefaultQuicPort*: Port = Port(60000)
## QUIC Config Builder ##
#########################
type QuicConfBuilder* = object
enabled*: Option[bool]
quicPort*: Option[Port]
enabled*: Opt[bool]
quicPort*: Opt[Port]
proc init*(T: type QuicConfBuilder): QuicConfBuilder =
QuicConfBuilder()
proc withEnabled*(b: var QuicConfBuilder, enabled: bool) =
b.enabled = some(enabled)
b.enabled = Opt.some(enabled)
proc withQuicPort*(b: var QuicConfBuilder, quicPort: Port) =
b.quicPort = some(quicPort)
b.quicPort = Opt.some(quicPort)
proc withQuicPort*(b: var QuicConfBuilder, quicPort: uint16) =
b.quicPort = some(Port(quicPort))
b.quicPort = Opt.some(Port(quicPort))
proc build*(b: QuicConfBuilder): Result[Option[QuicConf], string] =
proc build*(b: QuicConfBuilder): Result[Opt[QuicConf], string] =
if not b.enabled.get(false):
return ok(none(QuicConf))
return ok(Opt.none(QuicConf))
return ok(some(QuicConf(port: b.quicPort.get(DefaultQuicPort))))
return ok(Opt.some(QuicConf(port: b.quicPort.get(DefaultQuicPort))))

View File

@ -1,25 +1,23 @@
import logos_delivery/waku/compat/option_valueor
import chronicles, std/[net, options], results
import logos_delivery/waku/common/rate_limit/setting
import chronicles, std/net, results, logos_delivery/waku/common/rate_limit/setting
logScope:
topics = "waku conf builder rate limit"
type RateLimitConfBuilder* = object
strValue: Option[seq[string]]
objValue: Option[ProtocolRateLimitSettings]
strValue: Opt[seq[string]]
objValue: Opt[ProtocolRateLimitSettings]
proc init*(T: type RateLimitConfBuilder): RateLimitConfBuilder =
RateLimitConfBuilder()
proc withRateLimits*(b: var RateLimitConfBuilder, rateLimits: seq[string]) =
b.strValue = some(rateLimits)
b.strValue = Opt.some(rateLimits)
proc withRateLimitsIfNotAssigned*(
b: var RateLimitConfBuilder, rateLimits: seq[string]
) =
if b.strValue.isNone() or b.strValue.get().len == 0:
b.strValue = some(rateLimits)
b.strValue = Opt.some(rateLimits)
proc build*(b: RateLimitConfBuilder): Result[ProtocolRateLimitSettings, string] =
if b.strValue.isSome() and b.objValue.isSome():

Some files were not shown because too many files have changed in this diff Show More