ドラフト
必須
著者:fiatjaf
著者:distbit
著者:scsibug
著者:kukks
著者:jb55
著者:semisol
著者:cameri
著者:Giszmo
このNIPは、誰もが実装すべき基本的なプロトコルを定義します。新しいNIPによって、新たにオプショナルな(もしくは必須の)フィールド・メッセージ・機能が、ここで記述される構造やフローに追加される可能性があります。
ユーザーは各々が鍵ペアを持ちます。署名、公開鍵、エンコーディングはSchnorr signatures standard for the curve secp256k1
(secp256k1
曲線シュノア署名標準)に従います。
ただ1つだけ存在するオブジェクトの型が event
で、以下のようなフォーマットで伝送されます。
{
"id": <32-bytes lowercase hex-encoded sha256 of the serialized event data>,
"pubkey": <32-bytes lowercase hex-encoded public key of the event creator>,
"created_at": <unix timestamp in seconds>,
"kind": <integer between 0 and 65535>,
"tags": [
[<arbitrary string>...],
...
],
"content": <arbitrary string>,
"sig": <64-bytes lowercase hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field>
}
event.id
を得るため、そのイベントをシリアライズしてsha256
を計算します。具体的には、以下の構造をUTF-8 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": [
["e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com"],
["p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca"],
["a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com"],
["alt", "reply"],
...
],
...
}
タグ配列の最初の要素をタグの 名前 または キー と呼び、2番目をタグの 値 と呼びます。したがって、上のイベントではe
タグが"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"
に設定されていて、alt
タグが"reply"
に設定されている、などと言えます。2番目より後の要素には慣用的な呼び方はありません。
このNIPでは標準的なタグを3つ定義します。これらのタグは、あらゆるイベントのkindにおいて同じ意味で用いられます。以下の通りです。
e
タグ。イベントを参照するために用いる:["e", <他のイベントIDの32バイト小文字16進数文字列>, <おすすめのリレーURL、省略可能>]
p
タグ。別のユーザを参照するために用いる:["p", <pubkeyの32バイト小文字16進数文字列>, <おすすめのリレーURL、省略可能>]
a
タグ。(パラメータ付き・無し)置換可能イベントを参照するために用いる。- パラメータ付き置換可能イベントの場合:
["a", <kind整数>:<pubkeyの32バイト小文字16進数文字列>:<dタグの値>, <おすすめリレーURL、省略可能>]
- パラメータ無し置換可能イベントの場合:
["a", <kind整数>:<pubkeyの32バイト小文字16進数文字列>:, <おすすめリレーURL、省略可能>]
- パラメータ付き置換可能イベントの場合:
慣例として、全ての1文字(英語のアルファベットa-zとA-Zに限る)キーをもつタグは、リレーによってインデクスされることが期待されます。これにより、例えば、イベント"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"
を参照しているイベントをクエリしたり購読するために、{"#e": "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"}
フィルタを使えます。
Kindはクライアントがイベントやイベントのフィールドをどう解釈すべきかを決めます(たとえば、"r"
タグの kind 1 における意味は、kind 10002 のイベントにおける意味と全く異なる可能性があります)。それぞれのNIPにおいて、他の場所で定義されていないkindの意味を定義する可能性があります。このNIPでは、以下のように2つの基本的なkindを定義します。
0
: メタデータ:content
は文字列化されたJSONオブジェクト{name: <ユーザ名>, about: <文字列>, picture: <URL、文字列>}
で、イベントを作成したユーザのことを記述します。リレーは、同一のpubkeyから新しいイベントを受信した際、過去のイベントを削除できます。1
: テキスト投稿:content
はノート(ユーザが発言したいこと)をプレーンテキストで指定します。パースが必要なコンテンツ(マークダウンやHTMLなど)を使用すべきではありません。クライアントもコンテンツをそのようにパースをしてはいけません。
実験を容易にし、リレーの実装を柔軟にするため、kindの範囲についての以下のような慣例もあります。
- kind
n
が範囲1000 <= n < 10000
の場合は、イベントは**通常(regular)**で、全てがリレーに保存されることが期待されます。 - kind
n
が範囲10000 <= n < 20000 || n == 0 || n == 3
の場合は、イベントは**置換可能(replaceable)**で、最新のイベントだけがリレーに保持されなければなりません(MUST)。また、古いバージョンは破棄してもかまいません(MAY)。 - kind
n
が範囲20000 <= n < 30000
の場合は、イベントは**一時的(ephemeral)**で、リレーに保存されないことが期待されます。 - kind
n
が範囲30000 <= n < 40000
の場合は、イベントは**パラメータ付き置換可能(parameterized replaceable)**で、pubkey
とkind
とd
タグの最初の値の組み合わせについて、最新のイベントのみがリレーに保存されなければなりません(MUST)。また、古いバージョンは破棄してかまいません(MAY)。
置換可能イベントが同じタイムスタンプを持っている場合は、最も小さいID(辞書順で最初)のイベントが保持され、他のイベントは破棄されるべきです。
置換可能イベントに対するREQ
メッセージ(たとえば{"kinds":[0],"authors":[<hex-key>]}
)に応答する場合には、リレーが仮に複数のバージョンを保持していたとしても、最新の1つだけを返すべきです(SHOULD)。
これらは単なる慣例にすぎず、リレーの実装は異なる可能性があります。
リレーは、クライアントが接続するためのWebSocketエンドポイントを公開します。クライアントは1つのリレーに対して1つだけWebSocketコネクションを開き、全ての購読でそれを使うべきです(SHOULD)。リレーは特定のIP、クライアント、その他によってコネクション数を制限してもかまいません(MAY)。
- WebSocketがリレーからステータスコード
4000
でクローズされた場合は、クライアントは再接続を試みてはいけません。
クライアントは3種類のメッセージを送信できます。メッセージはJSON配列で、以下のパターンでなければなりません。
["EVENT", <前述のイベントJSON>]
イベントを送信するために使います。["REQ", <購読ID>, <フィルタJSON>...]
イベントを要求し、更新を購読するために使います。["CLOSE", <購読ID>]
既存の購読を中止するために使います。
<購読ID>
は任意の、空でない最大64文字の文字列で、購読を区別するために用います。リレーはWebSocket接続ごとに独立して<購読ID>
を管理しなければなりません。同一文字列の<購読ID>
であっても、コネクションごとに異なる購読として扱うべきです。
<フィルタ>
はJSONオブジェクトで、どんなイベントがその購読で転送されるかを決めます。以下の属性をもつ可能性があります。
{
"ids": <a list of event ids>,
"authors": <a list of lowercase pubkeys, the pubkey of an event must be one of these>,
"kinds": <a list of a kind numbers>,
"#<single-letter (a-zA-Z)>": <a list of tag values, for #e — a list of event ids, for #p — a list of event pubkeys etc>,
"since": <an integer unix timestamp in seconds, events must be newer than this to pass>,
"until": <an integer unix timestamp in seconds, events must be older than this to pass>,
"limit": <maximum number of events relays SHOULD return in the initial query>
}
REQ
メッセージを受信すると、リレーは内部データベースに問い合わせてフィルターにマッチするイベントを返し、そのフィルターを保存しておいて、以降に受信した全てのイベントをWebSocketがクローズされるまで同じWebSocketに送信すべきです(SHOULD)。同一の<購読ID>
を持つCLOSE
イベントを受信するか、同一の<購読ID>
を使って新たなREQ
が送信されると、リレーは既存の購読を更新しなければなりません(MUST)。
リストをとるフィルター属性(ids
、authors
、kind
と#e
のようなタグフィルタ)は、1つ以上の要素を持つJSON配列です。その条件がマッチしたとみなされるためには、配列の値のうち少なくとも1つがイベントの関連するフィールドと一致しなければなりません。authors
やkind
のようなスカラーのイベント属性の場合は、イベントの属性値がフィルターのリストに含まれなければなりません。#e
などのタグ属性のように、イベントが複数の値を持ちうる場合は、イベントとフィルタの条件値が少なくとも1つの共通する要素を持たなければなりません。
ids
、authors
、#e
、#p
のフィルタのリストが要素として含むのは、64文字の小文字の16進数文字列でなければなりません(MUST)。
since
とunitl
プロパティは、購読で返されるイベントの時刻の範囲を指定するために使用できます。もし、フィルタにsince
プロパティがあれば、since
以上のcreated_at
を持つイベントがマッチします。until
プロパティも同様で、until
以下のcreated_at
を持つイベントがマッチします。つまり、条件 since <= created_at <= until
が満たされるとき、イベントがフィルタにマッチします。
イベントがあるフィルタを通過するためには、フィルタに指定された全ての条件がマッチしなければなりません。つまり、複数の条件は&&
条件として解釈されます。
1つのREQ
メッセージに複数のフィルタを含むことができます。この場合、いずれかのフィルタにマッチするイベントが返されます。つまり、複数のフィルタは||
条件として解釈されます。
フィルタのlimit
プロパティは、初期クエリにのみ影響を及ぼし、それ以降は無視されなければなりません(MUST)。limit: n
が存在する場合、初期クエリでは最新のn
イベントがcreated_at
順に返されることが想定されます。limit
の指定よりも少ないイベントを返すことは問題ありません。しかし、クライアントに対して不必要に負荷を与えることを避けるため、リレーは要求されたよりも(はるかに)多くのイベントを返すことがないよう期待されます。
リレーは4種類のメッセージを送信できます。メッセージはJSON配列で、以下のパターンでなければなりません。
["EVENT", <購読ID>, <前述のイベントJSON>]
クライアントから要求されたイベントを送信するために使います。["OK", <イベントID>, <true|false>, <メッセージ>]
EVENT
メッセージの受理または拒否を通知するために使います。["EOSE", <購読ID>]
保存されているイベントの終わり(End Of Stored Events)、リアルタイムに受信されたイベントの開始を示します。["NOTICE", <メッセージ>]
人間可読なエラーメッセージやその他の事柄をクライアントへ送信するために使います。
このNIPではNOTICE
メッセージをどのように送信し、またどのように扱うべきかについてのルールは定義しません。
-
EVENT
メッセージは、クライアントが(前述のREQ
メッセージを用いて)以前に開始した購読IDと紐付けられたものだけが送信されなければなりません(MUST)。 -
OK
メッセージは、クライアントから受信したEVENT
メッセージに対する返信として送信されなければなりません(MUST)。3番目のパラメータは、リレーがメッセージを受理する場合はture
を、そうでなければfalse
を指定しなければなりません。4番目のパラメータも必須で(MUST)、3番目がtrue
である場合は空文字列でもかまいません(MAY)が、そうでなければ、機械可読な一語のプレフィクスに続けて:
と人間可読なメッセージを含まなければなりません(MUST)。標準化されている機械可読なプレフィクスは、duplicate
、pow
、blocked
、rate-limited
、invalid
とerror
(他のどれにも当てはまらない場合)です。例は以下の通りです。["OK", "b1a649ebe8...", true, ""]
["OK", "b1a649ebe8...", true, "pow: difficulty 25>=24"]
pow: 難易度25は24以上["OK", "b1a649ebe8...", true, "duplicate: already have this event"]
重複: すでにこのイベントは存在しています["OK", "b1a649ebe8...", false, "blocked: you are banned from posting here"]
ブロックされています: あなたはここで投稿することが禁止されています["OK", "b1a649ebe8...", false, "blocked: please register your pubkey at https://my-expensive-relay.example.com"]
ブロックされています: https://my-expensive-relay.example.comでpubkeyを登録してください["OK", "b1a649ebe8...", false, "rate-limited: slow down there chief"]
レートリミット: ゆっくりしてください["OK", "b1a649ebe8...", false, "invalid: event creation date is too far off from the current time. Is your system clock in sync?"]
無効: イベント作成日が現在時刻から大きくずれています。システムクロックは同期されていますか?["OK", "b1a649ebe8...", false, "pow: difficulty 26 is less than 30"]
pow: 難易度26は30よりも小さいです["OK", "b1a649ebe8...", false, "error: could not connect to the database"]
エラー: データベースに接続できませんでした