Repository navigation
feat(core): implement nostream-core — event model, NIP-01 filters, crypto, NIP semantics (#863) - #907
Conversation
🦋 Changeset detectedLatest commit: 104dd61 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
[High impact] Adds cryptographic signing and core event processing to the relay. The PR appears safe to merge, with non-blocking documentation feedback on Findings
|
| /// Encrypts `plaintext` into a NIP-44 v2 payload (`nip44Encrypt`, | ||
| /// `src/utils/nip44.ts:97-110`). |
There was a problem hiding this comment.
Nonce requirements are undocumented
encrypt now requires callers to supply nonce, but its documentation never says each message needs a fresh, cryptographically random value. Reusing a nonce with the same conversation_key repeats the cipher key and its nonce, exposing relationships between encrypted messages.
Document this requirement on encrypt, including a warning against copying fixed nonces from tests into real use.
| /// Encrypts `plaintext` into a NIP-44 v2 payload (`nip44Encrypt`, | |
| /// `src/utils/nip44.ts:97-110`). | |
| /// Encrypts `plaintext` into a NIP-44 v2 payload (`nip44Encrypt`, | |
| /// `src/utils/nip44.ts:97-110`). | |
| /// | |
| /// Supply a fresh, cryptographically random 32-byte `nonce` for every message. | |
| /// Never reuse a nonce with the same `conversation_key`: doing so repeats the | |
| /// encryption stream and exposes relationships between plaintexts. Fixed nonces | |
| /// used in tests must not be copied into real use. |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Phase 1, second crate:
nostream-coreimplemented against the capture at242a3001.Factory job
006-nostream-core(.factory/jobs/006-nostream-core/), branch cut fromrewrite/main@384b283, base unchanged at merge time. Issue: #863.What landed
EventId(32 bytes),Pubkey(32 bytes),Signature(64 bytes) as validated newtypes with borrowed-hex serde (a malformed value is refused before decoding, never truncated); canonical NIP-01 serialization and SHA-256 id; BIP-340 sign/verify oversecp256k10.33.1 withsha2;sign_eventderives the author so a mismatched pubkey cannot produce a silently invalid event.Filter::matchesportssrc/utils/event.ts's truth table exactly (31 measured probes), allocation-free nibble prefix compare for ids/authors, generic tag criteria limited to single-letter keys,#gwildcard, inclusivesince/until, geohash prefix, NIP-50 search.validate_filterreproduces the capture's zod acceptance, including UTF-16 length units and the exact measured messages.expiration.rs(NIP-40, int4-bounded, reads the sameNumber()forms),pow.rs(NIP-13 leading-zero bits),nip44.rs(v2 encrypt/decrypt/validate; MAC verified over decoded bytes before decryption),time.rs(reference-time predicate),replaceable.rs(precedence derived from the capture's SQL predicate),negentropy.rs(NIP-77 seam overnegentropy0.5.2, protocol0x61).SettingsPubkey/SettingsFilterkeep the settings loader's leniency (""andabc123still load;mirroringkind ranges still load); conversion to wire types happens at use.Out of scope and still labelled stubs (17
Phase 1 stub, 18NotImplemented, 0todo!):validate_event,adaptive_pow,nip77frames, and the twelve per-NIP modules.The oracle
crates/nostream-core/tests/carries a generated differential fixture — 1,000 signed events + 50 negatives (25 id-broken, 25 content-broken) × 16 probe filters — produced bygen-ts-events.mjsfrom a real capture checkout (the generator refuses the rewrite tree) and replayed byts_parity.rs: canonical bytes, id, signature, PoW and 16,800 match decisions all identical. Regeneration is byte-identical (c7816541…), verified independently by the approver.One parity module per captured TypeScript spec: event 105, filter 9, truth table 31, filter-schema 54 (≥47), nip44 34 (≥32), pow 17, negentropy 44, ts_parity 8.
Validation
Recorded divergences (ADR 0002, each with a reversal trigger)
eventStore.backendexemption unchanged; the u64 representability cell is refused only aboveu64::MAX(an integer is carried exactly; a float at or above 2^64 is refused);negentropycrate differs from the capture on one frame-truncated bound (sessions still converge, one extra round);Reserved for the next job (recorded here and in
decision.md)Two divergences the approver accepted, both inert until NIP-40 retention or a REQ handler is wired, closed together by one explicit ECMAScript
trimset:expiration::js_numbertrims with Rust's UnicodeWhite_Spaceset, which includes U+0085 that ECMAScriptString.prototype.trimdoes not — an event the capture retains could expire early.Filter::matchessplits NIP-50 search onsplit_whitespacerather than/[\s]+/— U+FEFF and U+0085 differ in both directions.Review
Four rounds: 19 findings raised, all closed; round 4 security/code/architecture all PASS. Approver: APPROVE — it re-ran the suite, the oracle regeneration, the per-target counts,
merge-tree, and C1–C20 itself. Two reservations recorded indecision.md(both inert until NIP-40 retention or a REQ handler is wired, one ECMAScript trim set closes them).Expected cost on the job: $55 calibrated (the issue's $900 was an unreplaced Phase-1 anchor). Actual is reported on #863 when the job closes.