Newlay — Kotlin Multiplatform Nostr Relay (NIP-29 implementáció)¶
Projekt: Newlay
Szerző: opensauce (Relay Tools Code)
Forrás: https://code.relay.tools/opensauce/newlay
Verzió: 0.1.0-dev (M12 milestone — NIP-29 relay-based groups)
Létrehozva: 2026-07-11 (első commit), utolsó aktivitás 2026-07-12
Licenc: MIT
Nostr Compass projektlistában: NINCS (808 projekt, nincs benne, várhatóan a következő heti frissítésben kerül be)
1. Newlay repo áttekintés¶
A Newlay egy high-speed, high-reliability Nostr relay Kotlin Multiplatform-ban írva (JVM Linux szerverekhez / desktopokhoz, Android telefon-relay-ként). Egyetlen code base, két futtatható cél: app-jvm (CLI entrypoint) és app-android (foreground service + widget UI).
Támogatott NIP-ek¶
1, 9, 11, 29, 40, 42, 43, 45, 50, 70, 77, 86, 65535 (LIMITS) + Blossom BUD-01/02/04/06/12
Core principles¶
- Dynamic per-websocket rate limiting — tiered ACL-ok, mixed-mode NIP-42 auth (auth növeli a limitet), valós idejű capability feedback a NIP-65535 (LIMITS) sockethián.
- Egy LMDB env, egy writer, egy transaction per batch — event-ek, minden index, search, tombstones, metadata atomikusan commitálódnak. Nincs index catch-up state, soha.
- Active node — kétirányú NIP-77 negentropy sync, outbox-model spidering (követi egy owner user followjait a megfelelő relay-eken, self-re-planning), GrapeRank Web-of-Trust LMDB-n számolva → ACL tier-ekbe + spam filteringbe táplálva.
Modulok (14 darab)¶
| Modul | Tartalom |
|---|---|
core |
tiszta protokoll: event model, canonical JSON + id hash, filter matcher, tokenizer, negentropy codec |
crypto |
secp256k1-kmp Schnorr verification facade |
store |
LMDB env, indexek, query planner, single-writer pipeline, search index |
policy |
limit profile-ok, rate limiter-ek, ACL engine, group directory, LIMITS/NIP-11 renderer |
server |
Ktor CIO websocket relay, NIP-11/86, Blossom routes, NIP-29 service |
client |
outbound relay connections, sync jobs (negentropy initiator) |
spider |
outbox discovery, set-cover planning, live upstream streams |
wot |
GrapeRank edge indexek + score számítás |
apphost |
shared host glue (spider controller, status contributor, mirror fetcher) mindkét app-hoz |
app-jvm |
CLI entrypoint (config file → running relay) |
app-android |
telefon-relay: foreground service + minimal widget UI ugyanazon engine felett |
Backup & restore¶
newlay backup / newlay restore szerveren (live backup támogatott), Backup/Restore gombok a telefonon (system file picker). Hordozható archívumok verziók és platformok között is replay-elhetők — telefon ↔ szerver mindkét irányba; raw LMDB snapshot mód a nagy same-platform relay-ekhez.
Konfiguráció¶
Egyetlen TOML file (--config <path>, default ./newlay.toml). Csak a relay.url kötelező — minden más kulcsnak van értelmes defaultja, és az ismeretlen kulcsok warning-ot (nem error-t) adnak a forward compatibility kedvéért.
Top-level szekciók (a teljesség igénye nélkül): [relay], [limits_message], [cost_weights], [tier.<name>] + assignment + acl rule, [wot], [nip43] / [nip29] (mindkettőhöz identity_key_file kell), [spider], [blossom], [search], [ip_gate], [log] / [status].
A NIP-11
name/description/iconconfig értékek seed-ek: a NIP-86changerelayname/description/iconRPC LMDBrelay_metarow-okba perzisztálja az override-okat, amelyek rendereléskor előnyt élveznek (config = fallback alattuk).
2. NIP-29 spec implementáció (M12 milestone)¶
A Newlay NIP-29 implementációja a server és store modulokban található, és a canonical moderation event log = authority, a 39000–39003 state event-ek és a GroupDirectory cache derived-ek.
2.1 Kind-ok (object Nip29Kinds, a GroupMaintainer.kt-ban)¶
| Kind | Irány | Szerep |
|---|---|---|
| 9000 | user | PUT_USER (admin/moderator hozzáad egy tagot) |
| 9001 | user | REMOVE_USER |
| 9002 | user | EDIT_METADATA |
| 9005 | user | DELETE_EVENT |
| 9007 | user | CREATE_GROUP |
| 9008 | user | DELETE_GROUP |
| 9009 | user | CREATE_INVITE |
| 9021 | user | JOIN_REQUEST |
| 9022 | user | LEAVE_REQUEST |
| 39000 | relay | METADATA (addressable) |
| 39001 | relay | ADMINS (addressable) |
| 39002 | relay | MEMBERS (addressable) |
| 39003 | relay | ROLES (addressable) |
RELAY_METADATA: Set<Int> = setOf(METADATA, ADMINS, MEMBERS, ROLES)— ezek kizárólag a relay identity key által írhatók alá; nem-identity signer →restricted:reject.
2.2 GroupFlags (bitmaszk)¶
const val PRIVATE: Int = 1 shl 0 // 1 — csak tagok olvashatják az event-eket
const val CLOSED: Int = 1 shl 1 // 2 — új tag invite code-dal vagy admin approval
const val RESTRICTED: Int = 1 shl 2 // 4 — csak tagok írhatnak (más olvashat)
const val HIDDEN: Int = 1 shl 3 // 8 — 39xxx metadata sem látszik nem-tagoknak
2.3 Nip29Settings (config-ból, a [nip29] szekció)¶
data class Nip29Settings(
val enabled: Boolean = false, // master switch (no identity → off)
val allowUnmanaged: Boolean = true, // events may go to unknown group_id
val requirePrevious: Boolean = false, // enforce timeline previous refs
val minPreviousRefs: Int = 3, // min refs when requirePrevious on
val acceptForks: Boolean = false, // tolerate unseen previous refs
val latePublicationWindowSecs: Long = 3600, // 0 = disable
val defaultPrivate: Boolean = false,
val defaultClosed: Boolean = false,
val defaultRestricted: Boolean = false,
val inviteMaxUses: Int = 100, // 9009 default uses-limited
val inviteExpiryHours: Int = 48, // 9009 default expiry, 0 = never
)
A NIP-29 kikapcsol, ha nincs
identity_key_filekonfigurálva, függetlenül azenabledflagtől.
2.4 Nip29Service fő folyamata (handle())¶
suspend fun handle(event: Event): Nip29Endpoints.Outcome {
// 1. Relay-authored 39xxx: only identity may publish; else reject("restricted", ...)
if (Nip29Kinds.isRelayMetadata(event.kind)) {
return if (event.pubkey == identityPubkey) { storeRaw(event); Ok("") }
else reject("restricted", "kind X may only be published by relay identity")
}
val groupId = event.firstTagValue("h") ?: return reject("invalid", "must carry \"h\" tag")
val state = directory.group(groupId)
// 2. Unmanaged + allowUnmanaged off → reject (kivéve CREATE/JOIN bootstrap)
if (!state.managed && !settings.allowUnmanaged && kind !in bootstrapKinds)
return reject("invalid", "group X does not exist on this relay")
// 3. Late publication + previous refs guard (kivéve 39xxx)
latePublication(event)?.let { return it }
previousRefs(groupId, event)?.let { return it }
// 4. Kind dispatch
return when (event.kind) {
JOIN_REQUEST -> handleJoin(groupId, state, event)
LEAVE_REQUEST -> handleLeave(groupId, event)
CREATE_GROUP -> handleModeration(groupId, event, create = true)
DELETE_GROUP, PUT_USER, REMOVE_USER, EDIT_METADATA, DELETE_EVENT, CREATE_INVITE
-> handleModeration(groupId, event, create = false)
else -> handleUserEvent(groupId, state, event)
}
}
2.5 Closed-group held-join minta (NIP-29 design choice)¶
A handleJoin különösen szép:
- Ha a user már tag →
Duplicate("duplicate: you are already a member of this group") - Ha a group closed:
- Ha van érvényes invite code → auto-admit (relay-signed 9000 put-user szintetizálódik)
- Ha NINCS invite code → a 9021 event tárolódik (indexelve
#hszerint), és held státuszba kerül, amíg egy admin 9000-zal approve-olja vagy 9001-gyel denyolja. Admins listázzák:{"kinds":[9021],"#h":["<group>"]} - Ha a group restricted (és user nem tag) →
restricted: only members may write to group X
A
handleJoinNEM reject-eli a closed group join-t invite kód nélkül — ezt a NIP-29 spec is így írja elő, és a Newlay hűen követi.
2.6 GroupAction (authorizációs ellenőrzés)¶
enum class GroupAction { MODERATE, WRITE }
// GroupDirectory.can() — az identity kulcs MINDIG átmegy (master key)
suspend fun can(groupId: String, pubkey: PubKey, action: GroupAction): Boolean {
if (pubkey == identityPubkey) return true
val g = group(groupId)
return when (action) {
GroupAction.MODERATE -> g.roles(pubkey).any(GroupState::isPrivilegedRole)
GroupAction.WRITE -> g.isMember(pubkey)
}
}
2.7 REQ visibility gate (gateReq)¶
Kétfázisú:
- Filter-szintű check — ha a filter explicit
#htag-et tartalmaz egyprivategroupra, és az authed pubkey-ek közt nincs tag →Closed("auth-required: …")vagyClosed("restricted: …")(a wire message a NIP-01 prefixet viseli). - Per-event predicate (lock-free) —
EventVisibility { event -> directory.visibleTo(groupId, event.kind, authed) }. ArestrictedIndexMap-et a REQ time-ban rögzíti az authed setre, a fan-out során alkalmazza.
A
hiddengroup 39xxx metadata sem látszik nem-tagoknak (agateReqa relay-authored kind-ekre is alkalmazza adTag-ot groupId-ként).
2.8 Late publication guard¶
private fun latePublication(event: Event): Outcome? {
if (settings.latePublicationWindowSecs <= 0) return null
val nowSecs = nowMillis() / 1000
if (event.createdAt < nowSecs - settings.latePublicationWindowSecs)
return reject("invalid", "late publication: event is older than the group's window")
return null
}
2.9 Previous refs (timeline enforcement)¶
private suspend fun previousRefs(groupId: String, event: Event): Outcome? {
val refs = event.tags.firstOrNull { it.firstOrNull() == "previous" }?.drop(1) ?: emptyList()
val isBootstrap = event.kind in setOf(CREATE_GROUP, JOIN_REQUEST, LEAVE_REQUEST)
if (settings.requirePrevious && !isBootstrap && refs.size < settings.minPreviousRefs)
return reject("invalid", "at least ${settings.minPreviousRefs} previous refs are required")
if (settings.acceptForks) return null
for (ref in refs) {
if (!directory.previousSeen(groupId, ref))
return reject("invalid", "previous ref $ref not found in this relay's timeline")
}
return null
}
A
previoustag-ek a NIP-29 spec szerint az adott group utolsó N event id-jének first-8-hex prefix-ei. ArequirePreviousopcionális (default off), aacceptForksopcionálisan engedi a nem-látott ref-eket (group átköltözés esetén).
3. NIP-29 storage (LMDB)¶
3.1 GroupDatabases (3 LMDB DB ugyanabban az env-ben)¶
| DB | Flags | Key | Value |
|---|---|---|---|
group_meta |
— | groupId (UTF-8) | GroupMetaCodec packed record |
group_member |
— | u16(idLen) ‖ groupId ‖ pubkey(32) |
roleBits u32 ‖ since u64 |
group_invite |
— | groupId ‖ 0x00 ‖ code(UTF-8) |
expiresAt u64 BE ‖ usesRemaining u32 BE |
A
group_memberkey length-prefixed, hogy egy group id ne lehessen byte-prefixe egy másiknak. Agroup_invitekulcsában a NUL (0x00) szeparátor biztosítja, hogy(groupId, code)párok ne legyenek egymás prefixei (UTF-8-ben a NUL sosem fordul elő).
3.2 GroupMaintainer (writer-batch integráció)¶
A GroupMaintainer a store.execute { txn -> ... } single-writer batch-en belül dolgozik:
- A moderation event (9000-9009) ugyanabban a writer batch-ben alkalmazza a
group_meta/group_memberrow változásokat, mint az event store-olása — atomi commit, vagy minden commitálódik, vagy semmi. - 39xxx state event-ek nem itt jönnek létre, hanem a
Nip29Service.regenerateGroupState()hívja meg őket, ami a cache invalidation után adirectory.group(groupId)által visszaadottGroupState-ből szintetizálja a 39000-39003-at, aláírja a relay identity kulccsal, és store-olja normál ingest-en (addressable → supersede-eli az előzőt). - A
previous-ringlistener (GroupRingListener) aNotificationBus-on figyel, és minden committed#h-tagged event id-jét bejegyzi aGroupState.previousringbe (first-8-hex prefix formában).
3.3 NIP-43 invite rendszer (GroupInviteStore)¶
A 9009 create-invite → 9021 redeem flow:
- A
9009event per-code["uses","N"]és NIP-40["expiration","<ts>"]tag-ekkel override-olhatja a relay-szintű default-okat. - A
redeem()a writer batch-en belül fut → az utolsó use decrement race-free (nincs double-spend). absent/expired/exhausted→ mindfalsevisszatérés (szándékosan összemosva: a NIP-43 szerintirestricted:válasz nem szivárogtatja, hogy melyik eset volt).
4. NIP-29 szálkezelés, reconnect, reconciliation¶
4.1 GroupRingListener (NotificationBus subscriber)¶
A GroupRingListener a store.NotificationBus-ra iratkozik fel, és minden committed event-nél, ha van h tag, bejegyzi az event id first-8-hex prefixét a GroupState.previous ringbe. A subscription drop-oldest buffer-t használ (capacity = 1024), így soha nem állítja meg a writer-t; ha egy batch drop-olódik, a directory boot range-scan-je a következő aktivációkor pótolja.
4.2 Boot reconciliation¶
/** Boot reconciliation: regenerate 39xxx for every group that has `group_meta`. */
suspend fun reconcile() {
val groupIds = store.read { txn ->
val ids = ArrayList<String>()
txn.forEachFrom(groupDbs.groupMeta) { key, _ -> ids.add(key.decodeToString()); true }
ids
}
for (id in groupIds) {
directory.invalidate(id)
regenerateGroupState(id)
}
}
A canonical moderation event log + a
group_*row-ok a single source of truth. A 39xxx event-ek és aGroupDirectorycache derived. Boot során agroup_metatábla scan-ből minden ismert group 39xxx-e újraszintetizálódik és aláíródik.
4.3 Group purging (delete-group privacy)¶
A 9008 delete-group handler purgeGroupFootprint(groupId) hívással törli:
- a relay-authored 39000-39003-at (különben kliensek újra felfedeznék a groupot)
- minden #h-tagged event-et (content, join/leave request, moderation log)
A comment külön kiemeli: "the latter also closes a privacy hole, since a formerly
privategroup's content would become world-readable once the group leaves the restricted-visibility index."
5. Kompatibilitás a Concord spec-emmel (NIP-29 alternate design)¶
A Concord spec (cloud fodder, 2026-07-10) egy alapvetően más paradigmát képvisel. Az alábbi táblázat a két rendszer összehasonlítása:
| Szempont | Newlay (NIP-29 stock) | Concord (terv) |
|---|---|---|
| Trust anchor | Relay identity key (39xxx relay-authored) | Key-holders (community secret-ből derivált stream author pubkey) |
| Group identity | group_id random string, relay-scoped |
community_id = hash preimage, self-certifying, serverless |
| Identity / access split | Egységes group_member row (pubkey → roles) |
Identity key (örök, owner founder-hez kötve) + access key (eltávolítható, identitás marad) |
| Encryption | Plaintext event-ek, csak access control (private flag-gel) | Minden üzenet layer-encrypted (kind 1059 private stream, valódi npub author aláírással) |
| Forward secrecy | Nincs | Nincs epoch-on belül (szándékosan MLS ellen trade-elve) |
| Address rotáció | Nincs (fix group id) | Epoch bump-ok — removed tag rotálja a stream address-t, rotation unlinkability |
| Membership record | group_member LMDB row + moderation event log |
Nincs admission event; possession of the keys = membership (CORD-02 §7) |
| Invite rendszer | Egyféle: kind 9009 invite code, uses-limited, expiring | Három típus: Public link (CORD-05 §1-2, nincs redemption event) / Direct Invite (§6, Registry-ben nem jelenik meg) / Whisper (out-of-band key átadás, "ungateable act") |
| Tagok száma | Tag-lista a 39002 MEMBERS event-ben (replaceable) | Nincs explicit tag-lista; a key-holders száma = 0 vagy több, de a relay nem tudja |
| Admin műveletek | 9000-9009 standard NIP-29, "three removals" nincs | "Three removals" decomposition (CORD-04): identity+access+role rotációk composition |
| Relay szerepe | Szerver = moderátor, jogalkalmazó, identity signer | Szerver = postafiók (tárol + indexel), nem lát tartalmat, nem moderál |
| Tamper resistance | A relay kitilthat (put-user/remove-user), de van dispute | A relay nem tud kitiltani (only display filter a kliensen), nincs dispute |
| Push notifications | Nincs implementálva (relay nem subscriber push) | Voice broker / SFU (CORD-07) |
| Kliens oldali komplexitás | Alacsony — normál NIP-29 kliens, pl. 0xChat, Amethyst group | Magas — deriváció, decryption, edition chain kezelés, frozen label registry |
| Boot komplexitás | reconcile() 39xxx regen (cache + store) |
Cold-start kliens oldalon (community secret → community_id → regisztrációs bundle) |
Konklúzió a kompatibilitásról¶
A Newlay NIP-29 implementáció NEM kompatibilis a Concord tervvel, mert más paradigmára épül:
- Különböző trust anchor — a Newlay a relay identity kulcsot használja a 39000-39003 aláírására; a Concord-ban nincs ilyen központi kulcs, a community secret-ből deriválódik minden.
- Különböző encryption modell — a Newlay plaintext event-eket tárol (a private flag csak az access controlt szabályozza); a Concord minden üzenetet layer-encrypted formában tárol, a relay nem látja a tartalmat.
- Különböző rotáció — a Newlay-ben a group id stabil (a delete-group kivételével); a Concord-ban az epoch bump rotálja a stream address-t, és a régi address-ek "forever floodable" kockázatot jelentenek.
- Különböző invite rendszer — a Newlay egységes 9009 invite code-ot használ; a Concord három típust különböztet meg (public link, direct invite, whisper), és egyikben sincs redemption event.
- Különböző admin modell — a Newlay a 9000-9009 standard moderation event-eket használja; a Concord-ban a "three removals" decomposition az identity+access+role szétválasztására épül.
A két rendszer közötti híd gyakorlatilag lehetetlen a jelenlegi formában — a Concord-ban nincs NIP-29 moderation event (nincs 9000-9009, nincs 39000-39003). Aki Concord-ot használ, az nem NIP-29 csoportos chat-et használ, hanem egy másik, kulcs-alapú alternatívát.
6. Következtetések a Haven fork és a NIP-29 csoportos chat döntéshez¶
6.1 Newlay vs. Haven — NIP-29 szempontból¶
A Haven relay (barrydeen/haven, khatru-alapú) NEM támogatja a NIP-29-et: bár a allowedChatKinds policy elfogadja a NIP-29 event kind-okat, a group state, invite code validáció, moderation action feldolgozás nem implementált (lásd a memory: "Haven relay NIP-29 nem implementálva").
A Newlay ezzel szemben teljes NIP-29 implementációt nyújt: - 14 modul, ebből 5 dedikáltan NIP-29-hez (Nip29Service, GroupDirectory, GroupRingListener, GroupDatabases, GroupMaintainer) - Integrált LMDB single-writer atomicity (a moderation event + a group row-ok együtt commitálódnak) - Boot reconciliation - Held-join minta (admin approval closed groupokban) - Per-WS REQ visibility gate (auth-required / restricted) - 9009 invite system, writer-batch race-free redemption - Config-driven flags + late publication + previous refs
Ha a cél NIP-29 csoportos chat (klasszikus, relay-moderált), a Newlay NAGYON erős alap. A Haven fork ehhez képest minimális — a Haven NIP-29 supportjához újra kellene írni a Haven chat relay felét.
6.2 Ha a cél Concord-style (key-holder, MLS-ready) csoportos chat¶
Ebben az esetben a Newlay nem segít: a NIP-29 relay-side logika irreleváns. A Concord spec-et önállóan kellene implementálni (akár új relayként, akár a Newlay policy modulját megkerülve, a server.Nip29Wiring helyett egy ConcordService pluginnal).
6.3 Pragmatikus döntési fa¶
| Használati eset | Javasolt alap |
|---|---|
| Standard NIP-29 csoportos chat (relay moderál, plaintext, központi kulcs) | Newlay (NIP-29 teljes implementáció, M12 milestone kész) |
| Key-holder, "no relay trust" csoportos chat (Concord-style) | Saját implementáció (Newlay-ből kiindulva, de Nip29Service lecserélése ConcordService-re) |
| A meglévő Haven relay NIP-29 bővítése | Nem javasolt — a Haven chat relay más architektúrára épül (khatru, nem LMDB single-writer), a NIP-29 logika jobban illeszkedik a Newlay-be |
| Multi-platform (Android + Linux) relay gyors deployment | Newlay (Kotlin Multiplatform natív támogatás) |
7. Hivatkozások¶
- Newlay repo: https://code.relay.tools/opensauce/newlay
- Newlay GitLab API: https://code.relay.tools/api/v4/projects/opensauce%2Fnewlay
- NIP-29 spec (Relay-based groups): kind 9000-9009 moderation, 9021 join, 9022 leave, 39000-39003 relay-authored metadata
- NIP-43 spec (Relay authentication / invite codes): kind 28935 invite request, 28934 join
- Concord spec (cloud fodder): CORD-01 (private stream) / CORD-02 (membership) / CORD-03 (history) / CORD-04 (edition chains) / CORD-05 (invite types) / CORD-06 (rekey + refounding) / CORD-07 (voice broker)
- Haven relay (alternatíva, NIP-29 nélkül): barrydeen/haven, khatru-alapú
- Nostr Compass projektlista: https://nostrcompass.org/en/projects/ (808 projekt, Newlay nem szerepel)