Nostr というのが流行っているらしい
リレーサーバー
比較
実装
※ rnostr はバグが多そう
統計情報
EVENT
[
"EVENT",
"<subscription_id>",
{<REQ で要求されたオブジェクト>}
]
OK
[
"OK",
"<id>",
true,
''
]
NOTICE
[
"NOTICE",
"<message>"
]
国内限定リレー
IP 制限がかかっています。
-
wss://relay-jp.nostr.wirednet.jp
- https://relay-jp.nostr.wirednet.jp/
- @imksoo
- CloudFront + EC2 (t3.medium)
-
wss://nostr.holybea.com
-
wss://nostr.fediverse.jp
-
wss://yabu.me
- @ikuradon
- https://grafana.gsn.im/d/d690f6e9-83af-4aa8-b6d9-c8532463fb8b
- 海外からは読み込み可だが書き込みは不可 (note)
-
wss://relay-jp.nostr.moctane.com
-
wss://nrelay-jp.c-stellar.net
ホワイトリストリレー
使用したい場合は管理者へ問い合わせてください。
-
wss://nostrja-kari.heguro.com
-
wss://nrelay.c-stellar.net
-
wss://nostr.h3z.jp
国産リレー
-
wss://relay.nostr.wirednet.jp
-
wss://nostr-relay.nokotaro.com
-
wss://nostream.ocha.one
(experimental) -
wss://relay.nostr.moctane.com
- @high_moctane
- データ保存なし (note)
運用する際の課題
- スパム対策
- スケール
- コスト
NIP-50 検索リレー
wss://relay.nostr.band
wss://nostrja-kari-nip50.heguro.com
wss://search.nos.today
検索 API
リレーの制限
対象
- kind
- pubkey
- フォロー
- フォロワー
- 独自リスト
- 課金
- 読み書き
- データの保持期間
- IP アドレス
- リクエスト全体(インフラレイヤー)
- 読み書きで個別に設定(アプリケーションレイヤー)
- 投稿内容 (content, tags)
- 正規表現
- ハッシュタグ
- 個人
- pubkey が自分
- p タグが自分宛
形式
- ホワイトリスト
- ブラックリスト
実装
- NIP-42
- 設定ファイル
kind 0, 3, 10002 専用リレー
wss://purplepag.es/
wss://user.kindpag.es/
wss://directory.yabu.me/
ブリッジ
ActivityPub <=> Nostr
その他
ActivityPub <=> Bluesky
クライアント
使用
Client | Web | PWA | iOS | Android | Note |
---|---|---|---|---|---|
Iris | 〇 | 〇 | 〇 | 〇 | |
Snort | 〇 | 〇 | × | × | リレーの情報が見れる |
Damus | × | × | 〇 | × | 使いやすい |
Amethyst | × | × | × | 〇 |
ライブラリ
※ NDK は設計が悪いので非推奨。
参考
NIP-05 Mapping Nostr keys to DNS-based internet identifiers
NIP-11 Relay Information Document
await fetch(`https://${domain}`, {
method: 'GET',
headers: {
'Accept': 'application/nostr+json',
},
});
NIP-07 window.nostr
capability for web browsers
Alby は Nostr 単体では使えない。
NIP-15 End of Stored Events Notice
データ取得をリクエストして EOSE
が来たら完了。
リレーがサポートしているかは NIP-11 の supported_nips
を確認。
EVENT
id
, pubkey
, created_at
, sig
は生成するので省略。
投稿
[
"EVENT",
{
"kind": 1,
"tags": [],
"content": "<投稿内容>",
}
]
REQ
自分に関する情報
タイムラインを取得する際に limit
フィルターを使用すると安定しないので非推奨。
代わりに since
+ until
で期間ごとに取得することを推奨。
[
"REQ",
"<subscription_id>",
{"authors":["<pubkey-hex>"]}
]
CLOSE
[
"CLOSE",
"<subscription_id>"
]
リレーリスト
Client | kind: 10002 | kind: 3 (deprecated) |
---|---|---|
Snort | ⭕️ | ⭕️ |
Iris | ❌ | ⭕️ |
Damus | ❌ | ⭕️ |
Amethyst | ❌ | ⭕️ |
kind: 10002
[
"EVENT",
"<subscription_id>",
{
"id": "<id>",
"pubkey": "<pubkey>",
"created_at": 1234567890,
"kind": 10002,
"tags": [
[
"r",
"wss://eden.nostr.land"
],
[
"r",
"wss://relay.damus.io"
],
[
"r",
"wss://relay.snort.social"
],
[
"r",
"wss://relay.current.fyi"
],
[
"r",
"wss://relay-jp.nostr.wirednet.jp"
],
[
"r",
"wss://nostr.holybea.com"
],
[
"r",
"wss://nostr-relay.nokotaro.com"
],
[
"r",
"wss://nos.lol/"
]
],
"content": "",
"sig": "<sig>"
}
]
kind: 3
[
"EVENT",
"<subscription_id>",
{
"content": "{\"wss://eden.nostr.land\":{\"read\":true,\"write\":true},\"wss://relay.damus.io\":{\"read\":true,\"write\":true},\"wss://relay.snort.social\":{\"read\":true,\"write\":true},\"wss://relay.current.fyi\":{\"read\":true,\"write\":true},\"wss://relay-jp.nostr.wirednet.jp\":{\"read\":true,\"write\":true},\"wss://nostr.holybea.com\":{\"read\":true,\"write\":true},\"wss://nostr-relay.nokotaro.com\":{\"read\":true,\"write\":true}}",
"created_at": 1234567890,
"id": "<id>",
"kind": 3,
"pubkey": "<pubkey>",
"sig": "<sig>",
"tags": [
[
"p",
"<pubkey>"
]
]
}
]
kind 0
- 複数のデータを保持しているリレーがあるので最新を採用
kind 6
リポスト。
Iris
{
"id": "3e568e5fcb2e294b32efa9064a9297725de3e80cf5b645b382ada9748ee6856a",
"pubkey": "83d52b4363d2d1bc5a098de7be67c120bfb7c0cee8efefd8eb6e42372af24689",
"created_at": 1677391085,
"kind": 6,
"tags": [
[
"e",
"c8843678966ae8d092830485111dc932a6c29e0e48343d87a98ac965b7f6c773",
"",
"mention"
],
[
"p",
"a22a2372ed6e77d2391d4392be07547b9e8ba38394cae680219781d5061a8c67"
]
],
"content": "",
"sig": "f14fd6d0081b93766105de7d97b93d3d6519466b5ff476d72595f007f41de638ed4a7b4d087b53d945f73982d4a9a33d4495c72fbf868d8a5792aa718ef37389"
}
Snort
[
"EVENT",
"23201",
{
"id": "0c26843832b742bfa56ce88d860884d402b1e0a45739a55bcafb643c2c8a38e5",
"pubkey": "83d52b4363d2d1bc5a098de7be67c120bfb7c0cee8efefd8eb6e42372af24689",
"created_at": 1677390964,
"kind": 6,
"tags": [
[
"e",
"c2e054959646f1eb4109cefc34dbb0100f6af1c7d3012a5b9a86f1c8af3ccab0"
],
[
"p",
"d1d1747115d16751a97c239f46ec1703292c3b7e9988b9ebdd4ec4705b15ed44"
]
],
"content": "{\"id\":\"c2e054959646f1eb4109cefc34dbb0100f6af1c7d3012a5b9a86f1c8af3ccab0\",\"pubkey\":\"d1d1747115d16751a97c239f46ec1703292c3b7e9988b9ebdd4ec4705b15ed44\",\"created_at\":1677390434,\"kind\":1,\"tags\":[],\"content\":\"NIP-58訳したった\\nhttps://scrapbox.io/nostr/NIP-58\",\"sig\":\"93516654ddae95be81209b90fba3523681a665500f91b4a4d6b39bc7c1ae05d14c46057ad2d709e235ec137170502fc0d64d700a672cd2ed41c1b0abb8140c2b\",\"relays\":[\"wss://nostr.holybea.com\"]}",
"sig": "e10b205653efeaecd718487142d1cdfcc1b345c56c1e8808016cfe1f761bb01519c89ff93bab5016ea88245f086e9e77e3113534627c3e984ba9056bbf82fcf1"
}
]
Damus
[
"EVENT",
"23201",
{
"id": "23c9aea1ac314eb2c23313fbc0704625d18578769075a7557f2ad494c2d90b12",
"pubkey": "83d52b4363d2d1bc5a098de7be67c120bfb7c0cee8efefd8eb6e42372af24689",
"created_at": 1677392543,
"kind": 6,
"tags":
[
[
"e",
"c30b34c922a4a9257f05816dd74ce8be06267d91deb3fea3d810d0d96c6d755c",
"",
"root",
],
[
"p",
"4d39c23b3b03bf99494df5f3a149c7908ae1bc7416807fdd6b34a31886eaae25",
],
],
"content": '{"pubkey":"4d39c23b3b03bf99494df5f3a149c7908ae1bc7416807fdd6b34a31886eaae25","content":"blastrはREST API (今の所POSTのみ) 付きのリレーだよ〜","id":"c30b34c922a4a9257f05816dd74ce8be06267d91deb3fea3d810d0d96c6d755c","created_at":1677392472,"sig":"816b67f363638ae53ca5954b6801a9fadc9f122513fc47be914b2f245c13506133cf25cd7e7a517b13a195647360f3d72489e78d6cf7ee33bf47f09565f9e279","kind":1,"tags":[]}',
"sig": "68c5b35e8f4bd299700a02e23000cb0f70ff93fdaec556473aca3977b74e4aa8dd8297ac21753ffa6f16eff1a25772aaf7efc28a00cf89c75648b9e2f65008d6",
},
]
パブリックチャットクライアント
名前 作者 |
プラットフォーム | ログイン | その他 機能 |
備考 |
---|---|---|---|---|
Amethyst Vitor |
Android | ? | TL, DM | - |
Coracle hodlbod |
Web (PC, Mobile) |
不要 | TL, DM | ログインしてないと日本人部屋が見えない |
NostrChat Talha |
Web (PC, Mobile) |
必要 | DM | 要リレー編集 部屋検索は ID のみ |
GARNET murakmii |
Web (PC) | 不要 | - | 更新停止? |
FreeFrom MAAS㈱ |
iOS, Android Web |
|||
0xchat water783 |
iOS, Android | |||
Nostri.chat pablof7z |
||||
ぱぶ茶(仮) Don |
Web (PC) | 不要 | - | WIP |
nostter 雪猫 |
Web (PC, Mobile) |
不要 | TL | ホーム TL に流れてくる |
ピン留め保存先
名前 | プラットフォーム | 保存先 |
---|---|---|
Amethyst | Android | kind 3(NIP 違反) |
Coracle | Web | |
NostrChat | Web | |
GARNET | Web | localStorage |
ぱぶ茶(仮) | Web | |
nostter | Web | kind 10001 |
主要クライアントの NIP 準拠状況
Amethyst
- ❌kind 1 をマークダウンとして描画
- ❌リレーリストを kind 3 に保存
- ❌チャンネルのピン留めを kind 3 に保存
- ❌スパム報告時にいいねにあたる kind 7 を送信
Damus
- ❌リレーリストを kind 3 に保存
- ❌ e tag の扱いがおかしい
Plebstr
Snort
- ❌kind 1 をマークダウンとして描画
- 🔺ミュートを kind 30001 に保存
- NIP-51 の先行実装のまま
Iris
Coracle
- 🔺無言引用をリポストの代わりにしている
- 一時期 NIP からリポストの仕様が消えていてそう扱うのも案の 1 つになっていた
Rabbit
nostter
- 🔺kind 6 の content が空
- 旧仕様。kind 5 と矛盾するので NIP を戻すべき
Web クライアントのフレームワーク
クライアント | フレームワーク | 備考 |
---|---|---|
nostter | Svelte | JP |
Lumilumi | Svelte | JP |
Astraea | Svelte | JP |
Rabbit | SolidJS | JP |
nosteen | React | JP |
Nemesia | Flutter | JP |
Coracle | Svelte | |
Primal | SolidJS | |
Hamstr | Vue | |
Snort / Iris | Tauri | |
旧 Iris | React | |
noStrudel | React | |
Flycat | React |
サービス
その他
NIPs
⭕️ : 必須
🔺 : 任意
-
: 不要
NIP | Relay | Client |
---|---|---|
NIP-01 | ⭕️ | ⭕️ |
NIP-02 | 🔺 | 🔺 |
NIP-03 | ||
NIP-04 | 🔺 | 🔺 |
NIP-05 | - | 🔺 |
NIP-06 | ||
NIP-07 | - | 🔺 |
NIP-08 | 🔺 | 🔺 |
NIP-09 | 🔺 | 🔺 |
NIP-10 | 🔺 | 🔺 |
NIP-11 | 🔺 | 🔺 |
NIP-12 | ||
NIP-13 | ||
NIP-14 | ||
NIP-15 | 🔺 | 🔺 |
NIP-16 | 🔺 | 🔺 |
NIP-19 | - | 🔺 |
NIP | Relay | Client |
---|---|---|
NIP-20 | ||
NIP-21 | - | 🔺 |
NIP-22 | 🔺 | 🔺 |
NIP-23 | ||
NIP-25 | ||
NIP-26 | ||
NIP-28 | ||
NIP-33 | ||
NIP-36 | ||
NIP-40 | ||
NIP-42 | ||
NIP-50 | ||
NIP-56 | ||
NIP-57 | ||
NIP-65 |
リレーから返ってくるエラー
["NOTICE","ERROR: bad req: std::get: wrong index for variant"]
送信した JSON のフォーマットがおかしい。
["NOTICE","ERROR: bad req: uneven size input to from_hex"]
npub1
形式ではなく hex で送る必要がある。
["NOTICE","ERROR: bad req: filter item too small"]
ids
などに渡す文字列が不正。(ids: [""]
など)
["NOTICE","ERROR: bad req: total filter items too large"]
["NOTICE","invalid: \"[2].authors\" must contain less than or equal to 1000 items"]
フィルターの各項目の配列を 1,000 未満にする。
雪猫
アカウント
npub1s02jksmr6tgmcksf3hnmue7pyzlm0sxwarh7lk8tdeprw2hjg6ysp7fxtw
作った
NIP-57 Lightning Zaps
流れ
TODO: invoice 生成について記載
- Zap に必要な情報を含んだ kind 9734 イベントを自分の pubkey で作成
- kind 9734 はリレーに送らずウォレットのサーバーへ送信
- ウォレットのサーバーが支払いを処理してレシートを発行
- レシート情報と元データ(kind 9734)を含んだ kind 9735 イベントをウォレットの pubkey で作成
- 元データに含まれるリレーへ kind 9735 を送信
- kind 9735 には #p(, #e) が付いているので相手に通知がいく&過去の Zap 情報も取得できる
具体的なフロー
- nip57.makeZapRequest でイベントを生成(未署名)
- 署名
- nip57.getZapEndpoint で kind 0 から LN URL を取得
- LN URL へリクエストを送る
- レスポンスの pr が invoice
- invoice を QR コードに変換して表示
WIP
参考資料
アウトボックスモデル
NIP-65
旧ゴシップモデル
課題
- デバイス毎にリレー構成を変更できない
- フォロイーの write リレー全部を購読するとパフォーマンスが悪化する(全部は購読しなくていいことになっているが適切に選ぶのは困難)
- 有料リレーしか購読していないユーザーのリレーへ書き込めない
- リレーヒントが 1 リレー分しかないのでそのリレーにアクセスできないと詰む
初心者のハードル
理解するのがやや難しい概念など。
- 秘密鍵
- リレー
- クライアントの名前がバラバラなので同じ SNS として認識しづらい
- クライアント毎に挙動が異なる場合がある
クライアントのパフォーマンス改善ポイント
- EOSE を待たずに随時反映する
- 事前にソートするのは諦めて後から挿入する
- ただし created_at が正しい保証はないので EOSE 以降の投稿は常に最新として扱う
- pubkey や tags の情報は非同期で取得して後から反映する
- リレーの Rate Limit に引っかからないように REQ をバッファする
- UI スレッドをブロックしないように注意
- キャッシュを適切に行う(バグの温床になりやすく諸刃の剣なので注意)
- 自分の Replaceable Events, Parameterized Replaceable Events
- フォロイーの kind 0
- フォロワーかどうか判定したければフォロワーの kind 3
- NIP-65 対応をするならフォロイー(+メンション等を送る可能性のある人)の kind 10002
- 過度にキャッシュしないこと(ディスク容量、更新処理の複雑化)
- 署名チェックを別スレッド (Web Worker) で行う
- 大量のイベントの verify を UI スレッドで行うと処理が終わるまで固まる
- CPU を食うのは変わらないので CPU 使用率が高いと固まるのは変わらない
- 書き込みは初回の EOSE が返ってきた時点で完了扱いにする
NIP-19
special | relay(s) | author (event.pubkey) |
kind (event.kind) |
|
---|---|---|---|---|
npub | pubkey | - | - | - |
nsec | seckey | - | - | - |
note | id | - | - | - |
nprofile | pubkey | 〇 | - | - |
nevent | id | 〇 | 〇 | 〇 |
nrelay | relay URL | - | - | - |
naddr | identifier (d tag) | 〇 | 〇 | 〇 |
Nostr 開発のメリット/デメリット
メリット
- バックエンドを用意しなくていい
- リレーをサーバー兼データベースとして扱える
- データが基本パブリック
- データを取得するだけならログイン不要
- ログインが手軽
- ログインを肩代わりしてくれるブラウザ拡張がある (
window.nostr.*
を呼ぶだけ) - ブラウザ拡張を使わなくても秘密鍵があればいいので登録フローが不要
- ログインを肩代わりしてくれるブラウザ拡張がある (
- BOT 開発が簡単
- 理由は上記の通り
- サーバー(リレー)コストが安い
- 自分で建てる必要はないが建てるとしてもそんなにコストはかからなさそう
デメリット
- あらゆるデータのバリデーションをしないといけない
- 中央サーバーがあるわけではないので不正なデータがあちこちに生成される
- 複数リレーを相手にするので UX を良くしようとすると複雑になりがち
- スパム対策が難しい
- 秘密鍵を無限に生成できるため
- プライベートなデータを扱うのはやや不得手
- 大きい数字(フォロー/フォロワー数とか)を扱うのが苦手
- フォロー数はイベントサイズの制限に、フォロワー数は REQ の制限に引っかかる
複数のリレーを扱う
Nostr は複数のリレーでイベントが冗長化されることを前提としています。
これにより耐障害性を得ることができますがクライアントでイベントを取得する際にはいくつか注意が必要です。
limit フィルター
リレーによって保存されているイベントは異なります。
そのためフィルターで limit
を指定して取得すると異なる期間のイベントが返ってきます。
これをそのままマージし次の期間のイベントを取得してしまうとイベントの取得漏れが発生します。
この問題を回避するためには limit
を使わずに since
/ until
で取得するか、すべてのリレーの EOSE を待って limit
件数を超えたものを削るかのどちらかにする必要があります。
ただどちらも一長一短あって、前者は最新のイベントが古かった場合に見つけるまでに時間がかかります。
後者はすべての EOSE を待つため時間がかかります。
前者は事前に最新のイベントを limit: 1
で取得することで緩和できますが常に REQ が倍に増えます(=時間がかかります)。
フィルターで取得可能なイベント数の上限
リレー毎に limit
の上限が設定されています。
これは limit
を指定していなくても適用されます。
そのため limit
を使わずに since
/ until
で取得する際も影響を受けます。
カウント
イベントの数(フォロワー数など)をカウントする際にすべてのイベントを取得する必要があります。
署名検証が結構重たいので数万件になるとクライアントで処理するのは事実上不可能と言っても過言ではありません。
NIP-45 Event Counts もありますがリレー毎に保存されているイベントは異なるのでまともに機能しません。(リレー毎にカウントしたい場合は有効)
id リストを返す提案をしているが動きはない模様。NIP-90 Data Vending Machine (DVM) が代替案としてあがっています。
ログイン
- npub (readonly)
- nsec
- NIP-07
- NIP-46
NIP-07
NIP-46
ライブラリ
1 行追加するだけで NIP-07 の window.nostr
をフックして NIP-46 対応してくれるライブラリ。