generated at
NIP-01

> Basic protocol flow description

nostr-jpによる日本語訳

hr
基本的なプロトコルの流れの説明

Nostrの根幹となるいくつかの仕組みが定義されている

実装は、必須 mandatory とされています

仕様について

イベントと署名
各ユーザはキーペア(1組の秘密鍵公開鍵)を保有する。
署名、公開鍵暗号、エンコーディングには、secp256k1曲線を用いたシュノア署名標準を用いる。

オブジェクトの種類は、 event ただ一つである。 event は通信上、次のフォーマットに従う。
_.json
{ // シリアライズされたイベントデータのSHA-256(32バイト)を小文字の16進数で表記したもの "id": <32-bytes lowercase hex-encoded sha256 of the serialized event data>, // 公開鍵(32バイト)を小文字の16進数で表記したもの "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, // UNIXタイムスタンプ(秒単位) "created_at": <unix timestamp in seconds>, // イベントの種類 "kind": <integer>, // タグ "tags": [ ["e", <32-bytes hex of the id of another event>, <recommended relay URL>], ["p", <32-bytes hex of a pubkey>, <recommended relay URL>], ... // 他の種類のタグが後に追加される可能性がある ], // 任意の文字列 "content": <arbitrary string>, // シリアライズされたイベントデータのSHA-256(IDフィールドと同じ)に対する署名を16進数で表記したもの "sig": <64-bytes hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field> }
訳注:
32-bytes とあるが、実際には64文字(=64バイト)のIDや秘密鍵が使われている模様(nostr-tools等)
16進数表記すると1文字あたり4ビットで、32バイトのデータは64文字でエンコードされる。そう考えると辻褄が合う
後のフィルターの説明でも64文字という記述がある

event.id を算出するには、イベントをシリアライズしてそのSHA-256を計算する。
シリアライズするには、次の構造を持つUTF-8のJSONシリアライズされた文字列(空白文字や改行文字を含まない)を生成する。
_.json
[ 0, <pubkey, as a (lowercase) hex string>, <created_at, as a number>, <kind, as a number>, <tags, as an array of arrays of non-null strings>, <content, as a string> ]

タグ
各タグは、いくつかの規約を持つ任意の長さの文字列の配列。以下に例を示す:
tags.json
{ ..., "tags": [ ["e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com"], ["p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca"], ["a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com"], ["alt", "reply"], ... ], ... }

タグ配列の最初の要素はタグの 名前 または キー、その次の要素はタグのとみなされる。よって、上記イベントは "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36" (という値)がセットされた e タグ、 "reply" がセットされた alt タグを持つ、のように言うことができる。それ以降のすべての要素は慣習名を持たない。

このNIPは、すべてのイベントの種類にわたって同じ意味を持つ、3つの標準タグを定義する。以下の通り:
e タグ ... イベントを参照: ["e", <他のイベントのID(32バイト小文字16進数)>, <推奨リレーURL(任意)>]
p タグ... 他のユーザを参照: ["p", <公開鍵(32バイト小文字16進数)>, <推奨リレーURL(任意)>]
a タグ: パラメータつき上書き可能イベントを参照: ["a", <kindの整数>:<公開鍵(32バイト小文字16進数)>:<dタグの値>, <推奨リレーURL(任意)>]

慣習として、すべてのキーが1文字(英語のアルファベット文字 a-z, A-Z に限る)のタグはリレーによって索引付けられる(to be indexed)ことが期待されている。これは、例えば {"#e": "5c83da7..."} というフィルターを用いて イベント "5c83da7..." を参照するイベントを検索・購読できるようにするため。

Kind(イベントの種類)
イベントの種類 (kind) は、クライアントが各イベントやそのフィールドの意味をどう解釈すべきかを指定する(例: "r" タグはkind 1のイベントとkind 10002のイベントでは全く違う意味を持つ)。各NIPは、他で定義されていないkindの意味を定義してよい。このNIPは2つの基本的なkindを定義する:
0 : メタデータ (プロフィール)
content に次のJSONオブジェクトを文字列化して含める
{name: <username>, about: <string>, picture: <url, string>}
リレーは、同じ公開鍵に対する新しい metadata を受け取ったら、過去のものを削除してもよい。
訳注: NIP-24で含められる他の要素が定義されています
1 : テキスト投稿
content に投稿の内容となるテキストを含める
> the content is set to the text content of a note (anything the user wants to say)
パースを必要とするコンテンツ(MarkdownやHTML)を使うべきでない。クライアントもコンテンツをそのようにパースすべきでない。

さらに、実験を容易にし、リレー実装に柔軟性をもたらす、kindの範囲に関する慣習がある:
1000 <= n < 10000 : 通常イベント
リレーはこれらのイベントをすべて保存する
10000 <= n < 20000 , 0 , 3 : 置換可能(replaceable)イベント
リレーは各 pubkey kind の組み合わせについて最新のイベントのみを保存すべき(SHOULD)
古いものは破棄する
20000 <= n < 30000 : 一時(ephemeral)イベント
リレーはこれらのイベントを保存しない
30000 <= n <40000 : パラメータ付き置換可能(parametarized replaceable)イベント
リレーは各 pubkey , kind , d タグ(の値)の組み合わせについて最新のイベントのみを保存
古いものは破棄する

同じタイムスタンプを持つ上書き可能イベントが複数ある場合、最小の(辞書順で先にくる) id を持つイベントが保持され、その他は破棄される。

以上は慣習にすぎず、実際のリレー実装は異なることがある。

クライアントとリレーの通信

WebSocket接続は各リレーにつき1つだけ
クライアントはそれぞれのリレーに対し、1つを超えるWebSocket接続を行うべきではない
1つの接続上で数に限りなく購読が行えるため、クライアントはそうすべき

WebSocketステータスコードの意味
リレーがWebsocketをステータスコード4000でクローズした場合、クライアントは再接続するべきではない。
注: WebSocketのcloseコード4000は「私的用途向け」として予約されている範囲の番号。

クライアントからリレー: イベントの送信と購読の作成
クライアントは、これらの3種類のメッセージを送信できる。これはJSON文字列でなければならない。
["EVENT", イベントの内容]
イベントを投稿するときに使う
["REQ", 購読ID, フィルターのJSON]
サーバから送ってきてほしいイベントの情報を伝え、購読するために使う
購読IDは空でない、最大64文字の任意の文字列
以前は「ランダムな文字列」としか書かれていなかったが、このコミット で仕様が明確化された
実装ノート: いくつかの実装では、 profile:公開鍵 等のような一意なIDを生成して送っている
REQ を受け取ったときのサーバ側の動き
フィルターに一致するイベントを自身のデータベースからクライアントに返すべき(SHOULD)
REQ受信後に届いたイベントで、フィルターに一致するものをクライアントに転送すべき
WebSocketの通信が切れるか、 CLOSE を受け取るまで転送を続ける
同じ購読IDで新しくREQが送られた場合、過去の購読を上書きするべき
訳注: 全く同じフィルターで購読を上書きしようとするとDuplicate subscriptionとして無視する実装がある (→リレー実装による制約#6455b795a99d480000d4da27)
["CLOSE", 購読ID]
購読を停止したいときに使う
訳注: 他のメッセージとして COUNT AUTH が定義されている

フィルターは、購読にどのようなイベントが送信されるかを定めるJSONオブジェクトである。次の属性を持つことができる。
filter.json
{ // イベントのID、もしくは先頭部分(プレフィクス) "ids": <a list of event ids or prefixes>, // 公開鍵、もしくは先頭部分。イベントの公開鍵はこれらのどれかでなければならない。 "authors": <a list of pubkeys or prefixes, the pubkey of an event must be one of these>, // イベントの種類の数字のリスト "kinds": <a list of a kind numbers>, // "e"タグで参照されたイベントIDのリスト "#e": <a list of event ids that are referenced in an "e" tag>, // "p"タグで参照された公開鍵のリスト "#p": <a list of pubkeys that are referenced in a "p" tag>, // UNIXタイムスタンプ(秒単位の整数値)。通過するには、イベントの created_at がこの値以上でなければならない。 "since": <an integer unix timestamp in seconds. Events must have a created_at >= to this to pass>, // UNIXタイムスタンプ(秒単位の整数値)。通過するには、イベントの created_at がこの値以下でなければならない。 "until": <an integer unix timestamp in seconds. Events must have a created_at <= to this to pass>, // 初回の問い合わせで返されるイベントの個数の上限 "limit": <maximum number of events to be returned in the initial query> }

フィルターの属性がリストである場合( ids , kinds , #e など)
例: ["REQ", "12345", { "authors": ["abc...xyz", "123...789"] }]
一つ以上の値を含むJSONの配列として表現される
リストの中の値のいずれかが、イベント内の関連するフィールドに一致していれば、条件一致したと見なす
条件一致したと見なすには、イベントの属性が・・・
単一の値を持つ場合( kind など)
イベントに含まれる値が、リストに含まれていなければならない
複数の値を持つ場合( #e などのタグ属性)
イベントの値と条件のリストで、共通するものが少なくとも一つはなければならない
ids authors #e #p は小文字の16進数でちょうど64文字でなければならない(MUST)
since until
これらの属性で購読で返されるイベントの期間を指定できる。
フィルタが since 属性を含む場合、 created_at since 以上のイベントがフィルタに合致するとみなす。
フィルタが until 属性を含む場合、 created_at until 以下のイベントがフィルタに合致すると考える。
すなわち since <= created_at <= until であるならばイベントはそのフィルタにマッチする。
フィルターの属性を複数指定する場合
例: ["REQ", "12345", { "kinds": [...], "authors": [...] }]
すべての条件がイベントに一致すれば、条件一致したと見なす
&& (AND検索) と見做される
フィルター自体を複数指定する場合
["REQ", "12345", { "条件1": [] }, { "条件2": [] }] のような場合
複数のフィルタのうちいずれかが一致すれば、条件一致したと見なす
|| (OR検索) と見做される
limit は初回取得(リレーに保存済みのイベントをまとめて取得するフェーズ)に対してのみ有効。 limit: n が指定されている場合、初回取得で最新 n 個のイベントを返す。より新しいイベントが最初に来るべきである。同位(訳注:同じ時刻)の場合にはIDの最も小さいイベント(辞書順で最初のもの)が最初にくるべきである。指定数未満のイベントを返しても問題ないが、クライアントがデータに圧倒されないように、リレーには要求された数を(大きく)超えるイベントを返さないことが期待される。
以前は、 since until に関して、リレーの実装により差異があった( https://github.com/nostr-protocol/nips/issues/650 参照)が、2023/7/15のコミットで、 since <= created_at <= until と定義された。

リレーからクライアントへの通信
リレーは、これらの3種類のメッセージを送信できる。これはJSON文字列でなければならない。
["EVENT", <購読ID>, <イベントのJSON>]
クライアントが要求したイベントを送るために使う
クライアントが(REQメッセージを使って)前もって開始した購読に関連する購読IDと共に送信しなければならない(MUST)
["OK", <イベントID>, <true|false>, <メッセージ>]
クライアントから送られてきたイベントの受理または拒否を表す
リレーがイベントを受理した場合、3番めの引数は true とする。このとき4番目の引数は空でもよい(MAY)
リレーがイベントを拒否した場合、3番めの引数は false とし、4番目の引数は <機械向けの1単語のprefix>:<人間向けのメッセージ> という形の文字列としなければならない(MUST)。
標準の機械向けprefix: duplicate , pow , blocked , rate-limited , invalid , error (他に当てはまらない場合)
["EOSE", <subscription_id>]
End of stored events(「保存されたイベントの終わり」)を表す
新しくリアルタイムに受信されたイベントの始まりを意味する
訳注: 統合元であるNIP-15 End of Stored Events Noticeの説明も参照
["NOTICE", <message>]
human-readable(人が読める)なエラーメッセージなどをクライアントに送るために使う
このNIPでは NOTICEをどのように送信すべきで、どのように解釈されるべきかを定義しない