Kihagyás

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/icon config értékek seed-ek: a NIP-86 changerelayname/description/icon RPC LMDB relay_meta row-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_file konfigurálva, függetlenül az enabled flagtő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 #h szerint), é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 handleJoin NEM 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ú:

  1. Filter-szintű check — ha a filter explicit #h tag-et tartalmaz egy private groupra, és az authed pubkey-ek közt nincs tag → Closed("auth-required: …") vagy Closed("restricted: …") (a wire message a NIP-01 prefixet viseli).
  2. Per-event predicate (lock-free) — EventVisibility { event -> directory.visibleTo(groupId, event.kind, authed) }. A restrictedIndex Map-et a REQ time-ban rögzíti az authed setre, a fan-out során alkalmazza.

A hidden group 39xxx metadata sem látszik nem-tagoknak (a gateReq a relay-authored kind-ekre is alkalmazza a dTag-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 previous tag-ek a NIP-29 spec szerint az adott group utolsó N event id-jének first-8-hex prefix-ei. A requirePrevious opcionális (default off), a acceptForks opcioná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_member key length-prefixed, hogy egy group id ne lehessen byte-prefixe egy másiknak. A group_invite kulcsá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_member row 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 a directory.group(groupId) által visszaadott GroupState-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-ring listener (GroupRingListener) a NotificationBus-on figyel, és minden committed #h-tagged event id-jét bejegyzi a GroupState.previous ringbe (first-8-hex prefix formában).

3.3 NIP-43 invite rendszer (GroupInviteStore)

A 9009 create-invite → 9021 redeem flow:

  • A 9009 event 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 → mind false visszatérés (szándékosan összemosva: a NIP-43 szerinti restricted: 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 a GroupDirectory cache derived. Boot során a group_meta tá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 private group'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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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)
Vissza a tetejére