Skip to content

deed reaches relays, and keeps what it finds - #17

Merged
sepehr-safari merged 2 commits into
mainfrom
deed-reaches-relays
Sep 22, 2026
Merged

sepehr-safari merged 2 commits into
mainfrom
deed-reaches-relays

Conversation

@sepehr-safari

Copy link
Copy Markdown
Contributor

Closes #16. This is v0.2.0.

Three verbs and a store, which the milestone named as one release because they are one idea.

req builds a subscription and runs it
fetch gets the events a NIP-19 code names, using the hints in the code
publish offers signed events and reports what each relay said
--store keeps what arrives, and --local answers from it with nothing dialled

The store is the point

deed req -k 1 -l 50 --store ~/.deed/db wss://relay.example   # once, over the network
deed req -k 1 -l 50 --store ~/.deed/db --local               # again, dialling nothing

I ran exactly that: 8 events over the network, 8 out of the store, identical id sets, and deed verify accepts them from the store as readily as from the wire, because what is stored is what was signed.

The claim that this is a real gap is checkable rather than rhetorical. nak keeps its local database behind //go:build linux && !riscv64 && !arm64, so on a Mac or an ARM machine every run starts from nothing, and even where it is compiled in there is no write anywhere in its req or fetch path.

Three decisions, all read rather than reasoned

Events are checked before they are kept or printed. A relay can send anything, so a bad signature is dropped and so is an event that does not answer the question that was asked. nak does both by default, and a tool whose output people pipe into other tools has to: this is the last place a forgery can be stopped.

A query gives up rather than hanging. nak per-relay wait has no timer in it at all, so a relay that accepts a subscription and then says nothing holds the process open indefinitely. Survivable at a prompt, not in a script. Thirty seconds by default, --timeout to change.

Publishing succeeds when any relay accepts. An event one relay holds is published, so partial failure exits 0 with every refusal on stderr and only total failure exits 1. Reporting failure after reaching the network would have scripts retry something that already happened. nak draws the line in the same place.

Relays are also dialled before stdin is read, so a run that cannot reach anybody says so without having consumed the events it was given.

Run, not just compiled

req ... wss://nos.lol returns events that deed verify accepts
fetch <id> returns that id
fetch <npub> returns one kind:0, and 0 for an author with no profile, matching the equivalent req
store round trip 8 in, 8 out, identical

The publish accept path is the one thing not exercised against a real relay, because that means posting to somebody relay. Every other publish path is: no relay named, unreachable relay, unknown option, empty input.

Two things it does not do, in the README rather than left to be found

A code with no relay hints is not chased through the author relay list: that is a second round trip with its own staleness rules, and doing it badly is worse than saying so. Windows still does not build (nostr#59).

Also

A leak the tests caught, in production code: decodeNaddr allocates the d value the filter points at, and only the relay hints were being freed, so every naddr fetched leaked one string.

68 tests, up from 53.

The first half of the network verbs: the filter, and the envelope it becomes.

`deed req` given no relay prints what it would send and stops. That is nak's default too, and it is the right one for a tool whose point is that you can read what it is about to ask before anybody is asked. `--bare` drops the envelope and leaves the filter, which is the form that goes into a config or another tool.

The flags are the ones every nostr filter has: kinds, authors, ids, `e`, `p` and `t` tags, a limit and a time window. Authors and ids are taken as bech32 or as hex and mean the same thing either way, because a reader has whichever one they were given.

The exit codes follow the contract the rest of the tool keeps: a value that will not parse is 1, because the command was understood and the value in it was wrong; an unknown flag is 2, because nothing was attempted.

`relayset.zig` is the other half, and it is structure only so far: dialling a set of relays, asking each the same question, deduplicating events by id across them and writing each one out as a line. Nothing calls it yet. The policy it needs, when a one-shot query should stop and what a partial failure means for the exit code, is the part worth reading other implementations for rather than deciding from first principles, so it is not decided here.

It does compile, which took proving. `_ = @import("relayset.zig")` imports a file without analysing a function body nobody references, so the build went green over code that did not compile: `Io.Clock` has no `.monotonic`, the monotonic clock is `.awake`, and `Clock.Duration` wraps the `Io.Duration` that carries the unit accessors. A `refAllDecls` test in the file is what forced the compiler to look.
Three verbs and a store, which the milestone named as one release because they are one idea.

`req` builds a subscription and runs it. `fetch` gets the events a NIP-19 code names, using the relay hints the code carries, which is the whole difference between it and `req` with an id. `publish` offers signed events to relays and reports what each one said.

`--store` is the point. A run that fetches can keep what it fetched, and a later run answers the same question from the store with no socket open at all. The events that come back are the same events and still verify, because what is stored is what was signed. `--local` is the half that makes keeping worth doing: a store nothing reads back is a log.

This is the gap the README claims deed exists for, and the claim is checkable. nak keeps its local database behind `//go:build linux && !riscv64 && !arm64`, so on a Mac or an ARM machine every run starts from nothing, and even where it is compiled in there is no write anywhere in its req or fetch path.

Three decisions worth naming, all of them read rather than reasoned.

Events are checked before they are kept or printed. A relay can send anything, so a signature that does not verify is dropped, and so is an event that does not answer the question that was asked. nak does both by default and a tool whose output people pipe into other tools has to: this is the last place a forgery can be stopped.

A query gives up rather than hanging. nak's per-relay wait has no timer in it at all, so a relay that accepts a subscription and then says nothing holds the process open for as long as somebody lets it. That is survivable at a prompt and not in a script, which is where a command line lives.

Publishing succeeds when any relay accepts. An event one relay holds is published, so a partial failure exits 0 with every refusal named on stderr and only a total failure exits 1. Reporting failure after reaching the network would have scripts retrying something that already happened. nak draws the line in the same place.

Relays are dialled before stdin is read, so a run that cannot reach anybody says so without having consumed the events it was given.

Two things this does not do, said in the README rather than left to be discovered. A code with no relay hints is not chased through the author's relay list, because that is a second round trip with its own staleness rules. Windows still does not build.

68 tests. The verbs were run against a live relay rather than only compiled: a query returns events that `deed verify` accepts, a fetch by id returns that id, a bare npub returns one kind:0, and a store filled over the network answers the same filter offline with an identical set. The publish accept path is the one thing not exercised against a real relay, because doing that means posting to somebody's relay.

Closes #16.
@sepehr-safari
sepehr-safari merged commit 31ac3af into main Sep 22, 2026
2 checks passed
@sepehr-safari
sepehr-safari deleted the deed-reaches-relays branch September 22, 2026 16:54
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.

deed cannot reach a relay, or keep anything

1 participant