Personal
nostr-tools
An async Python library for the Nostr protocol that measures whether a relay can connect, read, and write instead of trusting what its NIP-11 document advertises.
The Python library underneath BigBrotr. It implements the protocol – events, filters, subscriptions, Schnorr signing – and adds the thing an observatory needs and a client does not: probing a relay to find out what it will actually let you do.
- Implements
- NIP-01 · NIP-11 · NIP-66 · BIP-340 · bech32
- Probes
- Openable, readable, writable, each with its own timing
- Compatibility
- Python 3.9–3.13
- Repository
- BigBrotr/nostr-tools
The problem
A relay’s NIP-11 document is a self-description. It lists the protocol extensions the operator believes are supported, and a measurement built on that number is measuring what operators believe, not what relays do.
The library captures the document, and then never consults it. Nothing in the probing path reads the advertised list. What a relay supports is established by asking it.
What I built
Three probes, each answering one question, each with its own timing: can a connection be opened, will a subscription return events, and will a write be accepted. The write probe publishes a parameterized-replaceable event keyed by the relay’s own URL, so repeatedly measuring the same relay updates one record rather than accumulating a trail in somebody else’s database.
Above them sits a single call that returns the NIP-11 document and the measurements together with a timestamp, which is the actual unit an observatory stores.
Around that is the ordinary protocol surface: events with their identifier and Schnorr signature, filters, subscriptions, an eighteen-type exception hierarchy, and a client that refuses to be constructed for a Tor relay without a proxy configured, so the failure happens at construction rather than at connection.
Hard parts
A measurement that half-succeeded is worse than one that failed. A latency figure alongside a verdict of “not readable” is a contradiction, and it shipped: the probe recorded a round-trip on the first message received, before checking what that message was, so a relay that answered by closing the subscription produced a timing for a read that never happened. Downstream validation rejected the record. The fix makes the invariant explicit – a failed check reports no timing at all – and it is a patch, not foresight.
Supporting Python 3.9 costs the toolchain. The floor is deliberate, for reach, and it holds the test framework several major versions back with the reason annotated next to each pin.