Skip to content

Commit dcc83e9

Browse files
authored
Merge branch 'main' into feat/fix-whitelist-matching
2 parents d9150b3 + 5c12f36 commit dcc83e9

40 files changed

Lines changed: 2395 additions & 62 deletions

.changeset/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Changesets
2+
3+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4+
with multi-package repos, or single-package repos to help you version and publish your code. You can
5+
find the full documentation for it [in our repository](https://github.com/changesets/changesets).
6+
7+
We have a quick list of common questions to get you started engaging with this project in
8+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).

.changeset/config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"$schema": "https://unpkg.com/@changesets/config@latest/schema.json",
3-
"changelog": ["@changesets/changelog-github", { "repo": "cameri/nostream" }],
2+
"$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
3+
"changelog": "@changesets/cli/changelog",
44
"commit": false,
55
"fixed": [],
66
"linked": [],

.changeset/light-lilies-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
Add EWMA rate limiter with configurable strategy support
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
Add NIP-03 OpenTimestamps support for kind 1040 events: structural `.ots` validation, Bitcoin attestation requirement, digest match to the referenced `e` tag, and relay metadata updates (#105).

.changeset/slimy-bars-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
Add EWMA rate limiter with strategy support

CONFIGURATION.md

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -109,40 +109,18 @@ The settings below are listed in alphabetical order by name. Please keep this ta
109109
| | Defaults to zero. Disabled when set to zero. |
110110
| limits.event.pubkey.whitelist | List of public keys to always allow. Only public keys in this list will be able to post to this relay. Use for private relays. |
111111
| limits.event.rateLimits[].kinds | List of event kinds rate limited. Use `[min, max]` for ranges. Optional. |
112-
| limits.event.rateLimits[].period | Rate limiting period in milliseconds. |
112+
| limits.event.rateLimits[].period | Rate limiting period in milliseconds. For `sliding_window`: the time window during which requests are counted. For `ewma`: the half-life of the exponential decay — shorter values forget bursts faster, longer values are stricter on bursty clients. |
113113
| limits.event.rateLimits[].rate | Maximum number of events during period. |
114114
| limits.event.retention.kind.whitelist | Event kinds excluded from retention purge. NIP-62 `REQUEST_TO_VANISH` is always excluded from retention purge, even if not listed here. |
115115
| limits.event.retention.maxDays | Maximum number of days to retain events. Purge deletes events that are expired (`expires_at`), soft-deleted (`deleted_at`), or older than this window (`created_at`). Any non-positive value disables retention purge. |
116116
| limits.event.retention.pubkey.whitelist | Public keys excluded from retention purge. |
117117
| limits.event.whitelists.ipAddresses | List of IPs (IPv4 or IPv6) to ignore rate limits. |
118-
| limits.event.whitelists.pubkeys | List of public keys to ignore rate limits. |
119-
| limits.message.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
120-
| limits.message.rateLimits[].period | Rate limit period in milliseconds. |
118+
| limits.client.subscription.maxSubscriptions | Maximum number of subscriptions per connected client. Defaults to 10. Disabled when set to zero. |
119+
| limits.client.subscription.maxFilters | Maximum number of filters per subscription. Defaults to 10. Disabled when set to zero. |
120+
| limits.message.rateLimits[].period | Rate limiting period in milliseconds. For `sliding_window`: the time window. For `ewma`: the half-life of the decay function. |
121121
| limits.message.rateLimits[].rate | Maximum number of messages during period. |
122-
| mirroring.static[].address | Address of mirrored relay. (e.g. ws://100.100.100.100:8008) |
123-
| mirroring.static[].filters | Subscription filters used to mirror. |
124-
| mirroring.static[].limits.event | Event limit overrides for this mirror. See configurations under limits.event. |
125-
| mirroring.static[].secret | Secret to pass to relays. Nostream relays only. Optional. |
126-
| mirroring.static[].skipAdmissionCheck | Disable the admission fee check for events coming from this mirror. |
127-
| network.maxPayloadSize | Maximum number of bytes accepted per WebSocket frame |
128-
| network.remoteIpHeader | HTTP header from proxy containing IP address from client. |
129-
| nip05.domainBlacklist | List of domains blocked from NIP-05 verification. Authors with NIP-05 at these domains will be rejected. |
130-
| nip05.domainWhitelist | List of domains allowed for NIP-05 verification. If set, only authors verified at these domains can publish. |
131-
| nip05.maxConsecutiveFailures | Number of consecutive verification failures before giving up on an author. Defaults to 20. |
132-
| nip05.mode | NIP-05 verification mode: `enabled` requires verification, `passive` verifies without blocking, `disabled` does nothing. Defaults to `disabled`. |
133-
| nip05.verifyExpiration | Time in milliseconds before a successful NIP-05 verification expires and needs re-checking. Defaults to 604800000 (1 week). |
134-
| nip05.verifyUpdateFrequency | Minimum interval in milliseconds between re-verification attempts for a given author. Defaults to 86400000 (24 hours). |
135-
| paymentProcessors.lnbits.baseURL | Base URL of your Lnbits instance. |
136-
| paymentProcessors.lnbits.callbackBaseURL | Public-facing Nostream's Lnbits Callback URL. (e.g. https://relay.your-domain.com/callbacks/lnbits) |
137-
| paymentProcessors.lnurl.invoiceURL | [LUD-06 Pay Request](https://github.com/lnurl/luds/blob/luds/06.md) provider URL. (e.g. https://getalby.com/lnurlp/your-username) |
138-
| paymentProcessors.zebedee.baseURL | Zebedee's API base URL. |
139-
| paymentProcessors.zebedee.callbackBaseURL | Public-facing Nostream's Zebedee Callback URL (e.g. https://relay.your-domain.com/callbacks/zebedee) |
140-
| paymentProcessors.zebedee.ipWhitelist | List with Zebedee's API Production IPs. See [ZBD API Documentation](https://api-reference.zebedee.io/#c7e18276-6935-4cca-89ae-ad949efe9a6a) for more info. |
141-
| payments.enabled | Enabled payments. Defaults to false. |
142-
| payments.feeSchedules.admission[].amount | Admission fee amount in msats. |
143-
| payments.feeSchedules.admission[].enabled | Enables admission fee. Defaults to false. |
144-
| payments.feeSchedules.admission[].whitelists.event_kinds | List of event kinds to waive admission fee. Use `[min, max]` for ranges. |
145-
| payments.feeSchedules.admission[].whitelists.pubkeys | List of pubkeys to waive admission fee. |
146-
| payments.processor | Either `zebedee`, `lnbits`, `lnurl`. |
147-
| workers.count | Number of workers to spin up to handle incoming connections. |
148-
| | Spin workers as many CPUs are available when set to zero. Defaults to zero. |
122+
| limits.message.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
123+
| limits.admissionCheck.rateLimits[].period | Rate limiting period in milliseconds. For `sliding_window`: the time window. For `ewma`: the half-life of the decay function. |
124+
| limits.admissionCheck.rateLimits[].rate | Maximum number of admission checks during period. |
125+
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
126+
| limits.rateLimiter.strategy | Rate limiting strategy. Either `ewma` or `sliding_window`. Defaults to `ewma`. When using `ewma`, the `period` field in each rate limit serves as the half-life for the exponential decay function. Note: when switching from `sliding_window` to `ewma`, consider increasing `rate` values slightly as EWMA penalizes bursty behavior more aggressively. |

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ NIPs with a relay-specific implementation are listed here.
4747

4848
- [x] NIP-01: Basic protocol flow description
4949
- [x] NIP-02: Contact list and petnames
50+
- [x] NIP-03: OpenTimestamps Attestations for Events
5051
- [x] NIP-04: Encrypted Direct Message
5152
- [x] NIP-05: Mapping Nostr keys to DNS-based internet identifiers
5253
- [x] NIP-09: Event deletion

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"supportedNips": [
66
1,
77
2,
8+
3,
89
4,
910
9,
1011
11,
@@ -49,6 +50,7 @@
4950
"docker:build": "docker build -t nostream .",
5051
"pretest:integration": "mkdir -p .test-reports/integration",
5152
"test:load": "node -r ts-node/register ./scripts/security-load-test.ts",
53+
"smoke:nip03": "node -r ts-node/register scripts/smoke-nip03.ts",
5254
"test:integration": "cucumber-js",
5355
"cover:integration": "nyc --report-dir .coverage/integration npm run test:integration -- -p cover",
5456
"export": "node --env-file-if-exists=.env -r ts-node/register src/scripts/export-events.ts",

resources/default-settings.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ workers:
6464
mirroring:
6565
static: []
6666
limits:
67+
# strategy selection configuration for rate limiting:
68+
rateLimiter:
69+
strategy: ewma
6770
invoice:
6871
rateLimits:
6972
- period: 60000

scripts/smoke-nip03.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# NIP-03 smoke test with a real OpenTimestamps client
2+
3+
This exercises a running nostream relay against a genuine Bitcoin-attested
4+
OpenTimestamps proof that a real OTS client produced in the wild (the
5+
event used in the [NIP-03 spec example](https://github.com/nostr-protocol/nips/blob/master/03.md)
6+
on `wss://nostr-pub.wellorder.net`).
7+
8+
The relay never sees a synthetic proof here: the `.ots` blob in `content`
9+
was made by a real `ots stamp` + `ots upgrade` flow, is attested to a real
10+
Bitcoin block header, and is validated by the real `ots verify` binary
11+
against a public Esplora server before and after it round-trips through
12+
your relay.
13+
14+
## Why not generate our own proof end-to-end?
15+
16+
`ots stamp` writes a pending proof. A pending proof has to sit in a
17+
calendar server's queue for several Bitcoin blocks (typically a few
18+
hours) before `ots upgrade` can turn it into a confirmed Bitcoin
19+
attestation. Running that end-to-end in CI or on a developer machine is
20+
impractical. Re-using an already-upgraded, already-published kind 1040
21+
event is an equally honest "real client" test: we did not make the
22+
proof, and we prove its validity with the same binary a Nostr client
23+
would use.
24+
25+
## Prerequisites
26+
27+
- A running nostream relay (default `ws://127.0.0.1:8008`).
28+
- Node.js (same as the repo; the script runs via `ts-node` and uses the `ws` package).
29+
- [`opentimestamps-client`](https://github.com/opentimestamps/opentimestamps-client)
30+
for the real `ots` step (optional; the script auto-detects and skips
31+
gracefully if it's not installed):
32+
- Linux / macOS: `pipx install opentimestamps-client`, or
33+
`pip install opentimestamps-client`.
34+
- Windows: Python 3.13 has an OpenSSL compatibility bug in
35+
`python-bitcoinlib` on which the client depends. Run it inside a
36+
container instead:
37+
38+
```bash
39+
docker run --rm -v $PWD:/work python:3.11-slim \
40+
sh -c "pip install -q opentimestamps-client && ots info /work/proof.ots"
41+
```
42+
- A Bitcoin node (optional, only for `--verify`). Without one the script
43+
runs `ots info`, which parses the proof and confirms the Bitcoin
44+
attestation it terminates in. `ots verify` additionally looks up the
45+
block header on a Bitcoin node to prove the attestation is genuine; if
46+
you don't have one, `ots info` is the honest equivalent of
47+
structural-client acceptance.
48+
49+
## Running the automated script
50+
51+
```bash
52+
npm run smoke:nip03
53+
# or, with non-default relays:
54+
npx ts-node scripts/smoke-nip03.ts \
55+
--local-relay ws://127.0.0.1:8008 \
56+
--source-relay wss://nostr-pub.wellorder.net
57+
```
58+
59+
Expected output on a healthy relay with `ots` installed:
60+
61+
```
62+
NIP-03 end-to-end smoke test
63+
local relay: ws://127.0.0.1:8008
64+
source relays: wss://nos.lol, wss://relay.damus.io, wss://nostr.wine, wss://offchain.pub, wss://nostr-pub.wellorder.net
65+
66+
1) Discovering a real NIP-03 event from public relays
67+
trying wss://nos.lol for any recent kind 1040…
68+
PASS discovered 697b40df2f1c… on wss://nos.lol (pubkey=b1104a6e…, attests e=88fea43a70bd…, content=4968 chars)
69+
70+
2) Parsing OTS content with the real `ots` client
71+
PASS ots info parsed proof — BitcoinBlockHeaderAttestation(941057) (file: /tmp/nip03-XXXX/proof.ots)
72+
73+
3) Publishing the real event to the local relay
74+
PASS local relay accepted real NIP-03 event (reason="")
75+
76+
4) Round-tripping the event through the local relay
77+
PASS local relay returned the same event (id, sig, content) on REQ
78+
79+
summary: 3 passed, 0 failed
80+
```
81+
82+
Pass `--verify` (or set `OTS_VERIFY=1`) to additionally run
83+
`ots verify -d <target-event-id>` which asks a Bitcoin node to confirm
84+
the block header. Exit code is `0` iff every step passes.
85+
86+
## What each step proves
87+
88+
1. **Source discovery** — confirms a real third-party kind 1040 event
89+
exists, came from a real signed OTS client flow, and that you and the
90+
network agree on its bytes.
91+
2. **`ots info` (or `ots verify`)** — confirms the `.ots` content in
92+
`event.content` is a structurally valid OpenTimestamps proof when fed
93+
to the real reference client, and that it terminates in a Bitcoin
94+
block header attestation (which is what NIP-03 requires). If
95+
`--verify` is set, additionally walks the Bitcoin header via a
96+
configured node to prove the attested block really contains the merkle
97+
root.
98+
3. **Publish** — confirms nostream's NIP-03 strategy accepts a
99+
real-world, real-client-produced kind 1040 event (structure,
100+
`e` tag, digest match, Bitcoin attestation requirement all satisfied).
101+
4. **Round-trip** — confirms the relay persisted the event unchanged and
102+
returns the exact same id, signature, and base64 content on REQ, so
103+
downstream clients that re-run `ots verify` on the relay's output will
104+
still succeed.
105+
106+
## Manual walkthrough (if you want to stamp your own)
107+
108+
If you do have a few hours to wait and want a proof you made yourself:
109+
110+
```bash
111+
export RELAY=ws://127.0.0.1:8008
112+
export SK=$(nak key generate)
113+
export EVENT_ID=$(nak event --sec "$SK" -k 1 -c "anchor this note" "$RELAY" | jq -r '.[1].id // .id')
114+
115+
# Stamp the raw 32 bytes of the event id (not the hex string)
116+
echo -n "$EVENT_ID" | xxd -r -p > /tmp/nip03-digest.bin
117+
ots stamp /tmp/nip03-digest.bin
118+
119+
# Wait for calendars + Bitcoin confirmation, then:
120+
ots upgrade /tmp/nip03-digest.bin.ots
121+
ots verify /tmp/nip03-digest.bin.ots
122+
123+
export OTS_B64=$(base64 -w0 /tmp/nip03-digest.bin.ots)
124+
nak event --sec "$SK" -k 1040 \
125+
-t e="$EVENT_ID" \
126+
-t k=1 \
127+
-c "$OTS_B64" \
128+
"$RELAY"
129+
130+
# round-trip
131+
nak req -k 1040 -a "$(nak key public "$SK")" "$RELAY" \
132+
| jq -r '.content' | base64 -d | ots verify -
133+
```
134+
135+
Each publish attempt should come back as `["OK", "<id>", true, ""]`.
136+
137+
## Negative paths
138+
139+
Actively testing NIP-03 rejection paths (mismatched digest, uppercase
140+
`e` tag, multiple `k` tags, unsupported OTS version, garbage content)
141+
would require re-signing a mutated event, which means the proof would no
142+
longer be produced by a real OTS client. Those paths are covered in
143+
isolation by the unit tests:
144+
145+
- `test/unit/utils/nip03.spec.ts`
146+
- `test/unit/handlers/event-strategies/timestamp-event-strategy.spec.ts`

0 commit comments

Comments
 (0)