Skip to content

feat(core): implement nostream-core — event model, NIP-01 filters, crypto, NIP semantics (#863) - #907

Merged
chappie-daemon merged 22 commits into
rewrite/mainfrom
factory/006-nostream-core
Oct 19, 2026
Merged

chappie-daemon merged 22 commits into
rewrite/mainfrom
factory/006-nostream-core

Conversation

@chappie-daemon

@chappie-daemon chappie-daemon commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Phase 1, second crate: nostream-core implemented against the capture at 242a3001.

Factory job 006-nostream-core (.factory/jobs/006-nostream-core/), branch cut from rewrite/main @ 384b283, base unchanged at merge time. Issue: #863.

What landed

  • Event model — 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 over secp256k1 0.33.1 with sha2; sign_event derives the author so a mismatched pubkey cannot produce a silently invalid event.
  • NIP-01 filter matching — Filter::matches ports src/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, #g wildcard, inclusive since/until, geohash prefix, NIP-50 search. validate_filter reproduces the capture's zod acceptance, including UTF-16 length units and the exact measured messages.
  • Semantics — expiration.rs (NIP-40, int4-bounded, reads the same Number() 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 over negentropy 0.5.2, protocol 0x61).
  • Settings boundary — SettingsPubkey/SettingsFilter keep the settings loader's leniency ("" and abc123 still load; mirroring kind ranges still load); conversion to wire types happens at use.

Out of scope and still labelled stubs (17 Phase 1 stub, 18 NotImplemented, 0 todo!): validate_event, adaptive_pow, nip77 frames, 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 by gen-ts-events.mjs from a real capture checkout (the generator refuses the rewrite tree) and replayed by ts_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

cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo clippy --workspace --lib --bins --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used -D clippy::todo -D clippy::unimplemented -D clippy::panic
cargo test --workspace --all-features     # 546 passed / 0 failed (base: 157)
cargo build --workspace --all-features --release

Recorded divergences (ADR 0002, each with a reversal trigger)

  • eventStore.backend exemption unchanged; the u64 representability cell is refused only above u64::MAX (an integer is carried exactly; a float at or above 2^64 is refused);
  • NIP-44 decode leniency matches Node's for the measured corpus; one size-message unit differs (both refuse);
  • the negentropy crate differs from the capture on one frame-truncated bound (sessions still converge, one extra round);
  • filter length bounds are counted in UTF-16 units as zod does.

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 trim set:

  • expiration::js_number trims with Rust's Unicode White_Space set, which includes U+0085 that ECMAScript String.prototype.trim does not — an event the capture retains could expire early.
  • Filter::matches splits NIP-50 search on split_whitespace rather 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 in decision.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.

@changeset-bot

changeset-bot Bot commented Oct 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 104dd61

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When 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

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​chacha20@​0.10.210010093100100
Addedcargo/​hex@​0.4.310010093100100
Addedcargo/​hkdf@​0.13.010010093100100
Addedcargo/​hmac@​0.13.010010093100100
Addedcargo/​negentropy@​0.5.210010093100100
Addedcargo/​secp256k1@​0.33.110010093100100
Addedcargo/​sha2@​0.11.110010093100100

View full report

@chappie-daemon
chappie-daemon merged commit b324ef5 into rewrite/main Oct 19, 2026
15 of 16 checks passed
@chappie-daemon
chappie-daemon deleted the factory/006-nostream-core branch October 19, 2026 12:23
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[High impact] Adds cryptographic signing and core event processing to the relay.

The PR appears safe to merge, with non-blocking documentation feedback on encrypt.

Findings

  1. P2 Nonce requirements are undocumented ▶

Summary

Implements the Rust core event model, hashing and signing, filter matching and validation, and NIP helpers.

  • Events now have fixed-size IDs, keys, signatures, and signing.
  • Filters now match events by prefixes, tags, time, and search.
  • Settings keep lenient pubkeys and mirror filters at the config edge.
  • NIP-44 callers can encrypt and decrypt version 2 messages.
  • Core callers can check NIP rules and compare event sets.

Acknowledgments from chappie-daemon:

  • ECMAScript whitespace differences in expiration and search are deferred until retention or REQ handling is wired.
  • Large-number representation differences and measured NIP-44 decoding differences are intentional.
  • Negentropy decoding and frame-truncation differences are recorded with revisit triggers.
  • Event validation, adaptive proof-of-work, and NIP frame handling remain intentional stubs.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Settings["Operator settings"] --> Lenient["SettingsPubkey / SettingsFilter"]
  Lenient --> Matching["Local filter matching"]
  Lenient --> Outbound["Outbound settings serialization"]
  Wire["Event JSON"] --> Event["Event / validated hex types"]
  Event --> Crypto["Canonical bytes, hash, signature"]
  Event --> Matching
  Event --> Semantics["Expiration, time, PoW, replacement"]
  Event --> Storage["Negentropy storage"]
Loading

Reviews (1) · Last reviewed commit: "docs: give the representability divergen..." · Reviewed by Greptile

Comment on lines +62 to +63
/// Encrypts `plaintext` into a NIP-44 v2 payload (`nip44Encrypt`,
/// `src/utils/nip44.ts:97-110`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
/// 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant