Commit Graph

2070 Commits

Author SHA1 Message Date
Claude 8a5d0019c1 Merge remote-tracking branch 'origin/main' into claude/nip88-polls-quartz-p5fBa 2026-05-16 19:15:32 +00:00
Vitor Pamplona 8a498695a9 v1.09.2 2026-05-16 12:06:32 -04:00
Vitor Pamplona 32b9a06612 Ignores duplicated hashtags in different char cases when processing hashtag spam 2026-05-16 11:34:11 -04:00
Vitor Pamplona 5a30b10a77 v1.09.1 2026-05-15 18:25:17 -04:00
Vitor Pamplona 74c0d6906e v1.09.0 2026-05-15 15:43:47 -04:00
davotoula 8d225c355e Code review:
- reject ill-formed localpart dots and IP literals in parse
- avoid Pair allocation in Nip05Id.parse
2026-05-15 17:17:43 +02:00
davotoula e79755dab3 fix(nip05): stop reporting verification errors as Verified 2026-05-15 17:17:43 +02:00
Claude d77f2f7333 docs(onchain-zaps): record the hand-rolled Bitcoin consensus code decision
Decision: keep the hand-rolled psbt/ + taproot/ consensus layer rather than
adopting fr.acinq.bitcoin-kmp. Rationale: a deliberately small single-key-path
P2TR subset, pinned to authoritative BIP-341/350 test vectors at every layer
(sighash, tweak, witness signature bytes, addresses, tx serialization), and
consistent with the project's minimal-dependency stance.

Recorded in amethyst/plans/2026-05-14-onchain-zaps.md with the consequence
spelled out (we own correctness; revisit if scope expands past single-key-path
P2TR). The psbt/ and taproot/ packages now carry a pointer back to that
decision so a future reader doesn't reflexively swap in a library.

Doc-comment + plan-doc only; no logic change.
2026-05-14 18:16:30 +00:00
Vitor Pamplona 76c7c1575e removes incorrect test 2026-05-14 13:13:51 -04:00
Vitor Pamplona 4a71d890cb Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-14 13:13:39 -04:00
Vitor Pamplona 5d060899c2 additional linting 2026-05-14 12:11:39 -04:00
Claude b7ead41145 fix: use lambda for generic RootIdentifierTag.match call
RootIdentifierTag is generic, so the `::match` method reference failed
JVM compilation. Call it through a lambda like the rest of CommentEvent.

https://claude.ai/code/session_01ArHkNXu1ANrVGZAyMWg4Xu
2026-05-14 15:28:53 +00:00
Claude a690777ef6 feat: show external-id scope as reply context for NIP-22 comments
A Comment event (kind 1111) scoped to an external identifier (`I` tag)
previously rendered as a bare text post and landed in the Home "New
Threads" feed. Treat it as a reply to that external scope: it now shows
in the conversations feed and renders a typed chip (hashtag, geohash,
url, or generic) above the comment text.

https://claude.ai/code/session_01ArHkNXu1ANrVGZAyMWg4Xu
2026-05-14 15:05:04 +00:00
Claude db6254872e fix(onchain-zaps): recover the OnchainZapBuilder package + re-audit fixes
The big one: the source package was named `nipBCOnchainZaps/build/`, which
the repo .gitignore (`build/`) silently matched — so OnchainZapBuilder.kt
(the main source, not just its test) was NEVER committed. The pushed branch
did not compile; the pre-commit hook only checks the working tree, so this
went unnoticed. Renamed the package `build` -> `builder` (a source package
must never be named `build`) and updated all references; the recovered files
are now actually tracked.

Re-audit findings on the previous fix commit:
- CachingOnchainBackend.txCache was unbounded -> memory leak in a long
  session. Added a bounded cache (maxCachedTxs, oldest-entry eviction).
- OnchainZapSender still trusted the signer's returned PSBT for witness
  UTXOs and tap internal keys (used to compute the sighash + the verified
  output keys). Now it copies ONLY the PSBT_IN_TAP_KEY_SIG records back onto
  the PSBT we built, so a signer can contribute signatures and nothing else;
  verification and finalization run entirely on our own PSBT.

Test coverage added:
- OnchainZapBuilderTest: confirmed-UTXO filter — unconfirmed UTXOs excluded
  by default, spendable only with allowUnconfirmed, confirmed preferred.
- CachingOnchainBackendTest: confirmed-tx cached forever, unconfirmed
  re-fetched after TTL, not-found never cached, tip/fee TTL, bounded
  eviction.

All quartz / commons jvmTest suites pass; quartz android + amethyst compile.
2026-05-14 13:55:18 +00:00
Claude 1d7be1d444 fix(onchain-zaps): address audit findings — fund-safety, interop, robustness
CRITICAL
- PsbtSignatureVerifier: independently verifies every key-path signature in a
  PSBT (BIP-340 sig over the BIP-341 sighash, against the tweaked output key).
- OnchainZapSender now rejects a signer that returns a different transaction
  than it was asked to sign (byte-compares the unsigned tx) and verifies all
  signatures before broadcasting — closes a substitution attack where a
  malicious/buggy external signer could redirect funds.

HIGH — break test circularity
- Pin the full BIP-341 keyPathSpending input-0 witness: signing the vector
  sighash with the vector tweaked key reproduces the vector signature
  byte-for-byte (also pins BIP-340 nonce determinism).
- Pin TaprootAddress.fromPubKey + SegwitAddress.encodeP2TR against all seven
  BIP-341 wallet-test-vector P2TR mainnet addresses.
- EsploraBackendTest: JSON-parsing coverage for /tx, /address/{}/utxo, the
  mempool.space and blockstream fee formats, and schema-fallback.

MEDIUM
- OnchainZapBuilder filters to confirmed UTXOs by default (allowUnconfirmed
  opt-in) and signals BIP-125 RBF (nSequence 0xFFFFFFFD).
- CachingOnchainBackend: TTL-caching decorator (confirmed tx forever,
  unconfirmed/tip/fees short TTL) so a feed of onchain zaps doesn't fan out
  into one HTTP request per event. Wired in AppModules.
- OnchainZapVerifier computes real confirmation depth from the chain tip and
  asserts the backend echoed the requested txid.
- EsploraBackend falls back to the standard Esplora /fee-estimates endpoint
  (blockstream.info) when /v1/fees/recommended 404s.

LOW
- BitcoinTransaction.parse caps input/output/witness-item counts to stop a
  hostile varint from triggering a giant pre-allocation.
- OnchainZapEventTest: asserts kind:8333 on-the-wire tag structure against the
  NIP-BC spec.
- alt tag now includes the amount ("Onchain zap: N sats"), matching the spec
  example.

All quartz / commons / androidHostTest suites pass; amethyst compiles.
2026-05-14 13:29:48 +00:00
Claude 5f16ebc060 feat(quartz): NIP-BC sign_psbt over NIP-55 external signer (Phase B)
Wires the NIP-55 Android external-signer contract for sign_psbt, so
NostrSignerExternal.signPsbt is real instead of a stub.

- CommandType.SIGN_PSBT ("sign_psbt") — also available in NIP-55 `perms`
  lists, since Permission wraps CommandType directly.
- SignPsbtResult result type.
- SignPsbtQuery (background ContentResolver) + SignPsbtRequest /
  SignPsbtResponse (foreground Intent), modeled on the derive_key
  string-in/string-out shape: the PSBT hex rides the `nostrsigner:` URI,
  the signed (not finalized) PSBT comes back in the `result` field.
- BackgroundRequestHandler.signPsbt / ForegroundRequestHandler.signPsbt.
- NostrSignerExternal.signPsbt now runs the background-then-foreground
  query. Signer apps that predate sign_psbt reply with no `result`, which
  surfaces as CouldNotPerformException — the send dialog shows it as a
  failure ("update your signer").

NIP-46 (NostrSignerRemote) still throws UnsupportedMethodException — the
bunker-side command isn't standardized yet.
2026-05-14 12:50:47 +00:00
Claude a42b1e0f32 feat(quartz): NIP-BC onchain zap send-side foundation (Phase A.2)
Implements the send half of the onchain zaps plan: a minimal, fund-safe
BIP-174 PSBT pipeline plus the OnchainZapBuilder and `NostrSigner.signPsbt`.
Every cryptographic step is validated against the BIP-341 wallet test
vectors.

nipBCOnchainZaps/psbt/:
- BitcoinIO: little-endian Bitcoin consensus byte reader/writer (u16/u32/u64,
  compact-size varint, var-bytes).
- BitcoinTransaction: OutPoint / TxIn / TxOut / transaction model with legacy
  and BIP-144 segwit serialization, segwit-aware parsing, and witness-stripped
  txid. Validated against the genesis coinbase tx and the BIP-341 9-input
  unsigned tx.
- TaprootSigHash: BIP-341 SigMsg + TapSighash tagged hash for key-path spends.
  All six base sighash types plus both ANYONECANPAY variants verified against
  the seven BIP-341 keyPathSpending vectors.
- Psbt: BIP-174 container subset (global unsigned-tx, per-input witness-utxo /
  tap-internal-key / tap-key-sig / sighash-type, per-output tap-internal-key).
  Unknown records round-trip verbatim. Typed accessor extensions.
- PsbtSigner: signs the key-path P2TR inputs a given private key controls —
  derives the BIP-341 tweaked key, computes the all-inputs sighash, produces a
  BIP-340 Schnorr signature. Leaves inputs the key does not control untouched.
- PsbtFinalizer: moves tap-key-sigs into the witness stack, yields a
  broadcastable transaction.

nipBCOnchainZaps/build/:
- OnchainZapBuilder: largest-first coin selection + unsigned-PSBT assembly for
  a NIP-BC zap. Sender spends from / changes back to the single Taproot address
  derived from their Nostr pubkey; recipient output pays the recipient's
  derived address. Dust-aware change handling, InsufficientFundsException,
  self-zap and dust-amount guards.

nipBCOnchainZaps/taproot/:
- TaprootAddress.tweakSecretKey: BIP-341 taproot_tweak_seckey (key-path-only),
  including the odd-y internal-key negation. Validated against the BIP-341
  vector's tweakedPrivkey.

Secp256k1Instance: new `privKeyNegate` primitive across commonMain expect +
jvm / android / native actuals.

Signer hierarchy — new `NostrSigner.signPsbt(psbtHex): String`:
- NostrSignerSync / NostrSignerInternal: real implementation.
- NostrSignerWithClientTag: delegates to the wrapped signer.
- NostrSignerRemote (NIP-46) and NostrSignerExternal (NIP-55): throw
  SignerExceptions.UnsupportedMethodException — the bunker command and the
  Android Intent contract are Phase B, and depend on signer-app support.

Tests: 31 new tests across the psbt/build/signers packages, anchored on the
BIP-341 wallet-test-vectors so the tweak, sighash, signature, and finalized
transaction are all checked against an authoritative source.

Still pending: Phase B (NIP-55 sign_psbt Intent) and Phase D (send UI +
broadcast + publish kind 8333).
2026-05-14 11:29:47 +00:00
Claude 79f1d43581 feat(quartz): NIP-BC onchain zap receive-side foundation
Phase A.1 of the onchain zaps plan (amethyst/plans/2026-05-14-onchain-zaps.md):
the verify/receive path of NIP-BC. Send-side (PSBT, builder, signPsbt) is the
next phase.

Adds:
- nipBCOnchainZaps/taproot/SegwitAddress: BIP-173 / BIP-350 native segwit
  address encoder/decoder, witness v0..v16, on top of the existing Bech32 util.
- nipBCOnchainZaps/taproot/TaprootAddress: BIP-341 key-path-only derivation
  from a Nostr pubkey. Computes the TapTweak tagged hash, applies the additive
  tweak via Secp256k1Instance.pubKeyTweakAdd, and encodes as bc1p... bech32m.
- Secp256k1Instance: new pubKeyTweakAdd primitive wired through commonMain
  expect + jvm / android / native actuals.
- nipBCOnchainZaps/chain: pluggable OnchainBackend interface plus BitcoinTx,
  BitcoinTxOutput, Utxo, FeeEstimates data models.
- nipBCOnchainZaps/chain/EsploraBackend (jvmAndroid): OkHttp-based
  Esplora-compatible client (mempool.space / blockstream.info) covering
  /tx, /address/{addr}/utxo, /tx broadcast, /blocks/tip/height, and
  /v1/fees/recommended.
- nipBCOnchainZaps/verify/OnchainZapVerifier: enforces every NIP-BC client
  rule -- reject self-zap, fetch tx, derive recipient Taproot scriptPubKey,
  sum only outputs paying the recipient (excluding change back to sender),
  cap claimed amount at verified amount, mark unconfirmed as Pending.

Tests (commonTest, runs on jvmTest):
- SegwitAddressTest: BIP-350 known-good vectors for v0/v16 + round-trip for
  v1 (P2TR).
- TaprootAddressTest: BIP-341 wallet-test-vectors tagged hash + tweaked
  output key + scriptPubKey, plus address round-trip and prefix/length
  sanity.
- OnchainZapVerifierTest: confirmed / pending / self-zap / missing tx /
  zero-verified-amount / multi-output-sum / amount-capping with an
  in-memory FakeBackend wired to real TaprootAddress-derived scripts.

Plan: amethyst/plans/2026-05-14-onchain-zaps.md documents the full v1 scope,
the merge into the existing NIP-47 wallet UI, the subscription kind-list
edits, and the Note.zapsAmount fold-in approach for display.
2026-05-14 04:34:30 +00:00
Claude d095dc790d feat(quartz): add NIP-BC onchain zaps (kind 8333)
Implements NIP-BC onchain zap events analogous to NIP-57 lightning
zap receipts but for Bitcoin transactions. The recipient's Nostr
pubkey is used directly as the internal key of a BIP-341 P2TR output.

Mirrors the nip88Polls package structure with a single `zap/` subpackage:
- OnchainZapEvent (kind 8333) with profile / event / addressable-event
  zap builders
- Tags: BitcoinTxIdTag (i = bitcoin:tx:<txid>), AmountTag (sats),
  BlockTag (hash + height), ProofTag (raw-tx + merkle proof) for
  optional SPV verification
- TagArrayExt / TagArrayBuilderExt for parsing and assembly
- EventFactory wired so kind 8333 deserializes into OnchainZapEvent

Reuses standard ETag/ATag/PTag/KindTag from nip01Core. Does not yet
implement client-side transaction verification or PSBT signer methods.
2026-05-14 03:25:44 +00:00
m 2c3d006f05 docs: rename NIP-9A -> NIP-9B in code comments + user strings
The upstream NIP draft for kind:34551 community rules
(nostr-protocol/nips#2331) was renumbered from 9A to 9B per maintainer
feedback that slot 9a is already claimed by the push-notifications
draft (nostr-protocol/nips#2194):

  https://github.com/nostr-protocol/nips/pull/2331#issuecomment-4442813289

This commit updates all human-readable references in the merged
community-rules code:

- 7 Kotlin files in quartz (CommunityRulesEvent, CommunityRulesValidator,
  5 tag classes) — Kdoc comments
- 13 Kotlin files in amethyst (composer, feed filter, rules editor,
  Account, AccountSettings, tests) — code comments + Kdoc
- 1 English strings file (values/strings.xml) — 2 user-facing strings
- 54 translation strings files (values-*-r*/strings.xml) — same two
  strings, untranslated "NIP-9A" token replaced with "NIP-9B"

Kind number (34551), schema, tag names, and behaviour are unchanged.
No public API or DTO field renames. Pure docs/strings.

Verified: :quartz:spotlessCheck, :amethyst:spotlessCheck,
:commons:spotlessCheck all clean.
2026-05-14 09:29:59 +10:00
davotoula e21794e42a Manual testing fixes:
- quartz: Event.threadRootIdOrSelf() now falls back to markedReply()?.eventId
when both markedRoot() and unmarkedRoot() are absent
- amethyst: After upstream PR #2855 split the notifications feed into
notificationsFollowing and notificationsEveryone, the hiddenUsers.flow
collector still only invalidated the original notifications feed. Extends
it to invalidate all three.
- amethyst: CardFeedContentState.refreshSuspended() takes an additive-only
path when lastNotes is populated — it only adds cards for new admissions,
never removing cards for notes that no longer pass the filter. Re-muting
mid-session correctly rejected muted-thread reactions in the filter, but
the existing cards stayed in the UI as "Show Anyway" placeholders. Calls
clear() before invalidateData() on each notification feed so the refresh
hits the full-rebuild branch and removals propagate.
2026-05-13 09:06:53 +02:00
davotoula ff780bcb54 Code review:
- consolidate thread-root resolution
- consolidate mute-state writes
2026-05-13 09:06:53 +02:00
David bbb4e39465 Quartz:
- add mutedThreads/mutedThreadIdSet accessors
- MuteTag sealed interface recognizes EventTag
- add EventTag for NIP-51 mute-list e tags
- MuteListEvent round-trip and legacy-tag migration coverage
2026-05-13 09:06:53 +02:00
Vitor Pamplona 65c72605a0 function names cannot have @ in iOS 2026-05-12 22:07:23 -04:00
Vitor Pamplona c51c5f665b fix(quartz): make commonMain compile for Kotlin/Native (iOS)
@Volatile resolves via kotlin.jvm on JVM but needs an explicit
kotlin.concurrent.Volatile import on Native; Dispatchers.IO is internal
on Native, so the in-process WebSocket switches to Dispatchers.Default
(no blocking I/O on that path); and Native's LinkedHashMap is final
without removeEldestEntry, so Nip98AuthVerifier's replay cache now caps
itself with an explicit insertion-order eviction loop (access-order was
unused — the cache is write-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:03:19 -04:00
Vitor Pamplona 3751d6dc28 Fixes warnings 2026-05-12 19:27:12 -04:00
Vitor Pamplona 349f7a7989 fix(quartz): make IngestQueue writer-start KMP-safe via AtomicBoolean
Replaces JVM-only @Volatile + synchronized double-checked locking with
kotlin.concurrent.atomics.AtomicBoolean.compareAndSet so the file builds
on all commonMain targets, mirroring the pattern in BasicRelayClient.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:21:14 -04:00
Vitor Pamplona 445fbf8cda refactor(quartz, geode): derive NIP-86 publicUrl from relay.url; drop isEnabled branch
Two simplifications:

1. Derive admin URL from RelayEngine.url. NIP-86 spec mandates that
   the admin endpoint is "the same URI as ws(s)://, called via
   http(s)://" — so KtorRelay derives the NIP-98 binding URL by
   calling relay.url.toHttp() instead of accepting publicUrl as a
   separate config. Single source of truth (info.relay_url),
   accidental misconfiguration impossible, no Host-header fallback.
   StaticConfig.AdminSection.public_url is gone.

2. Uniform code path for admin-enabled vs disabled. Empty allow-list
   isn't a special case anywhere: Nip86Server.isAuthorized returns
   false for everyone, dispatch rejects, Nip86HttpHandler returns
   NotAdmin → 403. KtorRelay always assembles the Nip86HttpRoute
   and registers the POST endpoint; Nip86Server.isEnabled() and the
   "is admin on?" branches in the handler and route are deleted.

Nip86EndToEndTest's admin-disabled case is now a behavioral test:
sign a valid token, expect 403 NotAdmin (was 405 with the no-route
variant, was 403 with the fake-disabled-route variant).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:19:24 -04:00
Vitor Pamplona a53b3339b3 refactor(quartz): canonical NIP-86 HTTP flow in Nip86HttpHandler
Move the NIP-86-over-HTTP orchestration (NIP-98 verify → admin
allow-list gate → parse → dispatch → serialize) into quartz so any
relay implementation that wants the standard transport can plug its
HTTP framework in without re-deriving the sequence — and without
accidentally skipping NIP-98 verification or the admin check.

  • Nip86Server (quartz) gains allowList + isEnabled() + isAuthorized();
    dispatch becomes dispatch(pubkey, req) and enforces the allow-list
    itself, so in-process callers can't bypass it either.
  • Nip86HttpHandler (new, quartz) takes only primitives (authHeader,
    url, body) and returns a sealed Response — Disabled, PayloadTooLarge,
    MissingAuth, BadAuth, NotAdmin, BadRequest, Ok(pubkey, req, resp,
    json). Constants for the 1 MiB body cap, application/nostr+json+rpc
    content type, and WWW-Authenticate scheme live here.
  • Nip86HttpRoute (geode) shrinks to a Ktor adapter: bounded read,
    extract URL/auth, call handler, map Response → Ktor status codes.
    Audit logging stays in geode — not a quartz concern.

External relay authors: import quartz, build a Nip86HttpHandler with
their Nip86Server + Nip98AuthVerifier, and write a ~30-line adapter
for their framework. The full security-critical flow is in the box.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:38:40 -04:00
Vitor Pamplona e4e9ae7043 refactor(quartz): NIP-86 bans purge matching events via onBan(filter) hook
Nip86Server used to take an optional IEventStore and call store.delete()
only on banevent — banpubkey and disallowkind would update the BanStore
but leave existing matching events served by REQ. That's inconsistent
(an operator who bans a spam pubkey expects the spam to be gone) and
a real safety issue (banning a CSAM event wouldn't actually remove
existing copies if the store was null).

Replace the IEventStore param with onBan: suspend (Filter) -> Unit. The
dispatcher fires it after each ban method with the Filter that selects
the now-banned events:
  banpubkey   → Filter(authors = [pk])
  banevent    → Filter(ids = [id])
  disallowkind→ Filter(kinds = [k])

KtorRelay wires onBan = { f -> relay.store.delete(f) }, so the existing
SQLite delete-by-filter path handles every shape without the caller
having to know which field to populate. The default no-op keeps the
unit test for the dispatcher dependency-free.

withInt is now suspend so disallowKind's handler can call the suspending
onBan inline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:55:38 -04:00
Vitor Pamplona b0ad540453 refactor(geode): split config into StaticConfig + RuntimeConfig with seeding
Rename RelayConfig → StaticConfig and the persistence/RelayStateStore →
config/RuntimeConfig so the two configuration tiers — boot-time TOML
versus operator-mutable runtime state — are obvious from the class
names. The `persistence/` package is gone; everything related to config
lives in `config/`.

For overlapping fields (NIP-11 info doc, pubkey / kind allow-deny
lists), StaticConfig now seeds RuntimeConfig on first boot via the new
RuntimeConfig(seed = …) constructor and an AuthorizationSeed type. The
runtime file wins from then on: later edits to [authorization] in the
TOML are ignored once an admin RPC has written the snapshot. As a
consequence, KindAllowDenyPolicy and PubkeyAllowDenyPolicy are no
longer stacked in geode's policy chain — BanListPolicy already reads
the same lists from the BanStore, and double-stacking would silently
diverge after the first admin mutation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:51:19 -04:00
Vitor Pamplona 9c0873b178 fix(quartz): import RelayEngine.preload extension in jvmAndroid tests
Follow-up to the previous geode rename — quartz's jvmAndroid tests
extend the geode testFixtures `RelayClientTest` and were calling
`defaultRelay.preload(...)`. After moving `preload` off `RelayEngine`
and into an extension function in `geode.testing`, these tests need
the import explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:49:01 -04:00
davotoula 9a15a99b73 Quartz:
- Drop full event JSON from signature-verify failure logs
- Redact privKey in KeyPair.toString
2026-05-09 14:05:05 +02:00
m 3383b60315 feat(namecoin): distinguish malformed-JSON values from missing-field
When a Namecoin record's value isn't valid JSON (a real failure mode
when an operator hand-builds the value and miscounts braces), the
NIP-05 path used to silently swallow the parser exception and surface
a misleading "no nostr field" message. That sends the publisher
chasing a phantom missing field when the actual problem is the value
itself.

Concrete case that triggered this: a `name_update` published a
474-byte d/testls value with one closing brace short of balanced. The
string parses up to the missing brace, after which kotlinx.serialization
throws "Unfinished JSON term at EOF at line 1, column 474". That error
was previously dropped, leaving the operator to debug "no nostr field"
without ever seeing the underlying JSON parse failure.

Changes:

- New NamecoinResolveOutcome.MalformedRecord(name, error). Distinct
  from NoNostrField. The `error` field is the parser's own diagnostic
  (e.g. "Unfinished JSON term at EOF at line 1, column 474") so the
  publisher can locate the broken byte without spelunking.
- NamecoinNameResolver.performLookupDetailed: parse via a new
  parseValueOrError helper and surface MalformedRecord instead of
  collapsing into NoNostrField. Also rejects non-object top-level
  values (arrays, primitives, null) with a useful diagnostic
  ("top-level value is JsonArray, expected JSON object").
- DesktopSearchScreen handles the new outcome by surfacing the parser
  error verbatim in the Namecoin status banner, so the column number
  reaches the publisher's screen.

Tests (commonTest / NamecoinImportTest):

- "NIP-05 lookup surfaces MalformedRecord with parser detail when
  value is broken JSON": a deliberately one-brace-short value yields
  MalformedRecord with a non-empty diagnostic.
- "NIP-05 lookup surfaces MalformedRecord when top-level value is a
  JSON array": ensures non-object top-level values are rejected with
  a useful "expected JSON object" message rather than silently
  parsing as something unusable.

Tests don't pin the exact parser wording (kotlinx.serialization can
change it across versions); they only pin that the message is
attributed to JSON parsing rather than to a missing field.
2026-05-09 10:02:26 +10:00
Claude 667d3d51c7 docs(quartz): add parked plan for local Bitcoin headers OTS explorer
Comprehensive design doc for a headers-only Bitcoin P2P client that would
let NIP-03 OTS attestations be verified locally against the proof-of-work
chain instead of a trusted block explorer. Parked pending direction on
NIP-BC onchain-zaps verification, which has overlapping requirements.
2026-05-08 19:37:30 +00:00
Vitor Pamplona d950be6d73 Merge pull request #2792 from davotoula/fix/imeta-dangling-space
fix(quartz): drop empty/whitespace-only imeta values
2026-05-08 14:05:53 -04:00
davotoula c7ff30a5d5 fix(quartz): drop empty/whitespace-only imeta values
NIP-92 imeta tag entries are encoded as "key SPACE value" strings.
When IMetaTagBuilder.add() received an empty (or whitespace-only)
value the encoder produced "key " with a trailing space, which
schema-validating relays reject as a malformed tag value.
2026-05-08 19:28:11 +02:00
Vitor Pamplona abd565a460 Merge pull request #1914 from mstrofnone/desktop-namecoin-port
Port Namecoin NIP-05 resolution to Desktop app
2026-05-08 08:55:09 -04:00
m 23a7ac4770 fix(namecoin): accept single-identity {nostr:{pubkey,relays}} on d/ names
ifa-0001 doesn't mandate that domain records use the nostr.names
sub-dictionary. Operators who own a name outright commonly publish:

    {"nostr": {"pubkey": "<hex>", "relays": ["wss://..."]}}

(the same shape id/ records use). Before this fix d/-namespace branch
only accepted {"nostr":"<hex>"} or {"nostr":{"names":{...}}},
so a record like d/mstrofnone with the single-identity object form
silently failed with NoNostrField even though id/mstrofnone resolved
fine.

Resolution rules:
  1. nostr.names wins for any sub-identity.
  2. Root lookups fall back to bare pubkey when names["_"] is absent.
  3. Non-root lookups against names-only or single-identity records
     do NOT silently use the bare pubkey.
2026-05-08 22:33:06 +10:00
m 5efead42fb fix(quartz/electrumx): parse NAME_FIRSTUPDATE outputs alongside NAME_UPDATE
Names whose latest on-chain transaction is still the initial registration
(OP_NAME_FIRSTUPDATE = OP_2 = 0x52) were silently dropped because the
parser only matched OP_NAME_UPDATE (OP_3 = 0x53). The scripthash index
returns the FIRSTUPDATE tx in that case, so resolution looked
'unreachable' even though every server answered.

Accept both opcodes when scanning vouts and when parsing the script.
FIRSTUPDATE pushes <name> <rand> <value>, so skip the extra <rand> push
before reading the value.
2026-05-08 22:33:06 +10:00
Vitor Pamplona ab09c520ce Merge pull request #2758 from mstrofnone/feat/nip9a-community-rules
feat(quartz): NIP-9A community rules parser + validator (kind:34551)
2026-05-08 08:30:50 -04:00
Vitor Pamplona 06d6425369 Merge branch 'main' into claude/add-blossom-cache-support-ts8mK 2026-05-08 08:15:44 -04:00
Vitor Pamplona 8b1b49007c Merge pull request #2785 from greenart7c3/claude/disable-client-tags-option-Nb3b4
Add option to disable NIP-89 client tag in published events
2026-05-08 08:13:49 -04:00
Vitor Pamplona 0127ed4412 Merge pull request #2778 from nrobi144/fix/bunker-timeout-and-decrypt
feat(desktop): custom feeds system with creation, discovery, and author search
2026-05-08 08:11:55 -04:00
Claude f1650ad9ce feat(privacy): add per-account toggle to disable NIP-89 client tag
Adds a "Don't add client tag to my events" switch under Security Filters.
NostrSignerWithClientTag now consults a runtime predicate at sign-time, so
the toggle takes effect immediately on the live session without rewrapping
the signer or rebuilding Account.

https://claude.ai/code/session_01KhNVTm8LLdVphqyxVkZtDS
2026-05-08 08:44:32 +00:00
Claude 28fb3b0e83 test(quartz): make repeat-sub test tolerant of resub timing race
The mid-stream resub pattern (resub1 at events.size==1, resub2 at
events.size==5) has four receive() calls between the two subscribe
calls. On a slow CI worker that's enough time for resub1
(filtersShouldIgnore, kind 10002 limit 500) to actually reach the
relay and start streaming events before resub2 replaces it, so the
loop's second EOSE can come from filtersShouldIgnore — which can
deliver up to 50 events (the preloaded count for that kind), well
past the previous 1..11 bound.

Drop the phase-specific count asserts and verify only structural
invariants: exactly two EOSEs, the loop stopped on the second, every
non-EOSE entry is a valid 64-char id, and the total event count
stays within the worst-case envelope.

https://claude.ai/code/session_01L4qS7BhX3L7ApiS3HsCozn
2026-05-08 00:59:57 +00:00
Claude 44a8aeea28 Merge branch 'main' into claude/review-fanout-index-e1Uqq
Conflicts:
- quartz/.../LiveEventStore.kt: main added IngestQueue (group-commit)
  and a fire-and-forget submit() path; this branch replaced the
  SharedFlow live fanout with FilterIndex-driven dispatch. Resolved
  by keeping main's submit/insert pipeline shape (IngestQueue ingest
  ctor param, submit() callback, insert() wrapping submit via
  CompletableDeferred) but routing the on-Accepted fanout through
  FilterIndex.candidatesFor instead of newEventStream.tryEmit.
  Added private fanout(event) helper. Kept main's
  snapshotIdsForNegentropy method.
- geode/.../LoadBenchmark.kt: both sides added a new @Test method.
  Kept fanoutScaling (this branch) and publishPipelinedSingleClient
  (main).
2026-05-07 23:55:00 +00:00
Claude 7c4f2b720a refactor(negentropy): audit fixes for interop tests
- Delete `strfryDrivesGeodeAsServer` — boots strfry but uses
  kmp-negentropy ↔ Geode, no actual strfry-vs-geode interop.
  Duplicates `Nip77NegentropyTest.negentropyComputesSymmetricDifference`.
- Strip speculative `negentropy { enabled = ... }` and `nofiles`
  blocks from the strfry config; defaults are what we want to test.
- `InteropSyncDriver.reconcile` → `negotiate`. The function
  computes the symmetric difference; it doesn't move events.
  Convert to `suspend fun` to drop a nested `runBlocking` that
  could deadlock under dispatcher pressure.
- Inline `pullSync` helper in GeodeVsGeodeNegentropySyncTest —
  it was a one-line wrapper with a misleading name.
- Batch `relayB.preload(needFromA)` instead of looping.
- Tighten `idsOnRelay(NormalizedRelayUrl)` signature (was
  re-parsing the string on every call).
- Drop redundant `assertTrue(size > cap)` after `assertEquals(11)`.
2026-05-07 23:34:37 +00:00
Claude 4b7e0e880c feat(negentropy): strfry-parity NIP-77 reconciliation path
Implements the A+B+C+D plan in geode/plans/2026-05-07-negentropy-large-corpus.md:

- A: id-and-time-only snapshot. Adds IEventStore.snapshotIdsForNegentropy
  returning IdAndTime(createdAt, id) with no Event materialization. SQLite
  override projects directly off event_headers (~40 B/entry instead of
  ~1 KB/entry; matches strfry's MemoryView footprint).

- B: snapshot-size cap. New [negentropy] config section with
  max_sync_events=1_000_000 (mirrors strfry's relay__negentropy__maxSyncEvents).
  Overflow returns NEG-ERR "blocked: too many query results" (strfry-exact
  wording). No default since-window (strfry honors filters as-is; bounding
  silently would break interop).

- C: 500_000-byte frame cap. NegentropyServerSession.DEFAULT_FRAME_SIZE_LIMIT
  matches strfry's hard-coded Negentropy ne(storage, 500'000). Configurable
  via [negentropy].frame_size_limit.

- D: per-connection session cap = 200 (matches strfry). Overflow sends NOTICE
  "too many concurrent NEG requests" (strfry parity).

Error-string interop:
  - NEG-MSG with unknown subId → "closed: unknown subscription handle"
  - reconcile() parse failure → "PROTOCOL-ERROR"
  - snapshot overflow → "blocked: too many query results"
  - per-conn cap → NOTICE "too many concurrent NEG requests"

NegentropySettings flows: RelayConfig.NegentropySection → Relay → NostrServer
→ RelaySession → NegSessionRegistry. RelayHub takes optional settings for
tests that need to exercise the caps.

Tests:
  - SnapshotIdsForNegentropyTest covers projection correctness across all
    indexing strategies + the maxEntries+1 sentinel contract.
  - Nip77NegentropyTest gains negOpenSnapshotOverflowReturnsStrFryNegErr
    and negOpenPerConnectionCapEmitsNotice.
  - Existing negMsgWithoutOpenReturnsNegErr updated to assert strfry wording.
2026-05-07 23:34:37 +00:00
Claude 7430c2aa17 fix(quartz): audit fixes — VerifyAuthOnlyPolicy + small wins
Self-audit of the event-ingestion-batching changes turned up one
real bug + a handful of cleanups.

Fix: AUTH events skipped signature verification when
parallelVerify=true. Previous commit dropped VerifyPolicy from the
policy chain to avoid double-verifying EVENTs (the IngestQueue does
those off-thread). But VerifyPolicy.accept(AuthCmd) was the only
thing checking AUTH signatures — FullAuthPolicy verifies challenge
/ relay / expiry but trusts the sig. Removing VerifyPolicy let a
forged event mark a pubkey as authenticated.

Split VerifyPolicy into a parameterised base class
(VerifyEventsAndAuthPolicy) with two singletons:
- VerifyPolicy: verifies both EVENT and AUTH (existing default).
- VerifyAuthOnlyPolicy: verifies AUTH only — use when an
  IngestQueue does parallel EVENT verify, since AUTH commands
  bypass the queue entirely.

Geode's composePolicy now selects VerifyAuthOnlyPolicy when
parallelVerify is on, keeping AUTH signature checks intact while
still letting EVENT verify run in parallel on the writer's CPU
fan-out.

Cleanups (no behavior change):
- IngestQueue.processBatch was ~70 lines; split into verifyBatch /
  runInsertStage / dispatchOutcomes.
- Single-event verify shortcut: skip the coroutineScope + async
  dance for batch-of-1 (the common low-load case) and call the
  hook directly.
- Hoisted the "internal error: missing outcome" Rejected sentinel
  to a companion `missingOutcome` constant.
- Imported ClosedSendChannelException / ClosedReceiveChannelException
  instead of using the fully-qualified form inline.
- Dropped NostrServer's verifyEvent companion wrapper — direct
  lambda is just as cheap and the "single instance" comment was
  inaccurate.
- ObservableEventStore.batchInsert now uses requireNoNulls() rather
  than @Suppress("UNCHECKED_CAST"), since every index is provably
  populated.
2026-05-07 23:16:52 +00:00