NIP-01
#NIP
Basic protocol flow description
https://github.com/nostr-protocol/nips/blob/master/01.md
基本的なプロトコルの流れの説明
Nostrの根幹となるいくつかの仕組みが定義されている
イベント と 署名
リレーとクライアントの通信
nostr-jpによる日本語訳
https://github.com/nostr-jp/nips-ja/blob/main/01.md
/icons/hr.icon
翻訳 commit=477e3df(途中)
基本となるプロトコルフローの説明
draft mandatory
このNIPは、全員が実装すべき基本プロトコルを定義する。新しいNIPは、ここで定義された構造とフローに対して、任意(あるいは必須)のフィールドやメッセージと機能を追加してもよい。
イベントと署名
各ユーザはキーペア(訳注: 1組の秘密鍵と公開鍵)を保有する。
署名、公開鍵暗号、エンコーディングには、secp256k1曲線を用いたシュノア署名標準を用いる。
オブジェクトの種類は、event ただ一つである。伝送時は次のフォーマットに従う。
code:_.jsonc
// コメントは訳注
{
// シリアライズされたイベントデータの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>,
// イベントの種類 (0 〜 65535の整数)
"kind": <integer>,
// タグ
"tags": [
// 任意の文字列
<arbitrary string>...,
// ...
],
// 任意の文字列
"content": <arbitrary string>,
// シリアライズされたイベントデータのSHA-256(IDフィールドと同じ)に対する署名を16進数で表記したもの
"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>
}
訳注:
32-bytesはバイナリ表現の話であり、16進数表記では64文字となる
4ビットが16進数表記の1文字になる
そのため、32バイトのデータは 16進数表記で 32 * 8 / 4 = 64 文字でエンコードされる
event.idを算出するには、イベントをシリアライズしてそのSHA-256を計算する。シリアライズは次の構造を持つUTF-8のJSONシリアライズされた文字列(空白文字や改行文字を含まない)に対して行う
code:_.jsonc
[
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>
]
実装の違いによって、同じイベントに対して異なるイベントIDを生成してしまうことがないように、シリアライズにあたっては次のルールに従わなければならない(MUST):
UTF-8をエンコーディングに使用すること
空白、改行、その他の不必要なフォーマッティングは、出力のJSONに含まれてはならない
contentフィールド内の文字は次に示す通りにエスケープし、他のすべての文字はそのまま含めなければならない:
改行文字(0x0A)は \n を使用
ダブルクォート(0x22)は \" を使用
バックスラッシュ(0x5C)は \\ を使用
復帰文字(0x0D)は \r を使用
タブ文字(0x09)は \t を使用
バックスペース(0x08)は \b を使用
フォームフィード(0x0C)は \f を使用
タグ
各タグは、いくつかの規約を持つ、1つ以上の文字列からなる配列である。以下の例を見てみよう:
code: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タグを持つ、と言うことができる。要素の2つ目より後の要素については、慣習的な名前はない。
この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 に限る)のすべてのタグを索引付けることが期待されている。これは、例えば {"#e": "5c83da7..."}というフィルターを用いて、イベント "5c83da7..." を参照するイベントを検索・購読できるようにするためである。
(訳注: 索引付けられるとは、リレーショナルデータベースのインデックスのことを指していると思われる)
種類
kind(イベントの種類) は、クライアントが各イベントやそのフィールドの意味をどう解釈すべきかを指定する (例: "r"タグはkind 1のイベントとkind 10002のイベントでは全く違う意味を持つ)。各NIPは、他で定義されていない、一式のkindの意味を定義してよい。例として、NIP-10はソーシャルメディア・アプリケーションのためのkind:1テキスト投稿を定義する。
このNIPは1つの基本的なkindを定義する:
0: ユーザメタデータ
content にイベントの作者を説明するJSONオブジェクトを文字列化して含める
{name: <ニックネームまたは本名>, about: <短い自己紹介>, picture: <画像のURL>}。
追加のメタデータフィールド(NIP-24)がセットできる。
リレーは、同じ公開鍵に対する新しいmetadataを受け取ったら、過去のものを削除してもよい。
さらに、実験を容易にし、リレー実装に柔軟性をもたらす、kindの範囲に関する慣習がある:
(訳注: 読みやすさのため、原文と異なるフォーマットで示している)
通常(regular) イベント
1000 <= n < 10000 || 4 <= n < 45 || n == 1 || n == 2
リレーはこれらのイベントをすべて保存する
置換可能(replaceable)イベント
10000 <= n < 20000 || n == 0 || n == 3
リレーは各pubkeyとkindの組み合わせについて最新のイベントのみを保存しなければならず(MUST)、古いものは破棄してもよい(MAY)。
一時(ephemeral)イベント
20000 <= n < 30000
リレーがこれらのイベントを保存することは期待されていない
パラメータ付き置換可能(parametarized replaceable)イベント
30000 <= n < 40000
リレーは各pubkeyとkindと dタグの値の組み合わせについて最新のイベントのみを保存しなければならず(MUST)、古いものは破棄してもよい(MAY)。
同じタイムスタンプを持つ上書き可能イベントが複数ある場合、最小の(辞書順で先にくる)idを持つイベントが保持され、その他は破棄される。
{"kinds":[0],"authors":[<hex-key]} のような置換可能イベントのREQメッセージに応答するときは、リレーが一つ以上のバージョンを保管している場合であっても、最新のものを返すべきである(SHOULD)。
以上は慣習にすぎず、実際のリレー実装は異なることがある。
クライアントとリレーの通信
リレーは、クライアントが接続できるWebSockteエンドポイントを公開する。クライアントは各リレーにつき単一のWebSocket接続を開くべきであり(SHOULD)、それをすべての購読で利用するべきである。
クライアントからリレー: イベントの送信と購読の作成
(訳注: 読みやすさのため、原文と異なるフォーマットで示している)
クライアントは、これらの3種類のメッセージを送信できる。これは以下のパターン従うJSON配列でなければならない:
["EVENT", <イベントのJSON(上で定義した通り)]
イベントを投稿するために使う
["REQ", <購読ID>, ,フィルター 1>, <フィルター 2>, ...]
イベントを要求し、新しい更新を購読するために使う
["CLOSE", 購読ID]
以前の購読を停止したいときに使う
<購読ID>は空でない、最大64文字の任意の文字列である。接続別に1つの購読を表す。リレーは各WebSocket接続の(複数の)<購読ID>を個別に管理しなければならない(MUST)。(復数の)<購読ID>がグローバルでユニークであることは保障されない。
訳注
購読IDの形式は以前は「ランダムな文字列」としか書かれていなかったが、このコミット で仕様が明確化された
実装ノート: いくつかの実装では、profile:公開鍵等のような一意なIDを生成して送っている
他のメッセージとしてCOUNTやAUTHが定義されている
https://github.com/nostr-protocol/nips/tree/master#client-to-relay
<フィルター X>は、購読に対して、どのようなイベントが送信されるかを決定するJSONオブジェクトである。次の属性を持つことができる:
code:filter.json
{
// イベントのID、もしくは先頭部分(プレフィクス)
"ids": <a list of event ids or prefixes>,
// 小文字の公開鍵のリスト。イベントの公開鍵はこれらのどれかでなければならない。
"authors": <a list of lowercase pubkeys, the pubkey of an event must be one of these>,
// イベントの種類の数字のリスト
"kinds": <a list of a kind numbers>,
// タグの値のリスト。#e:イベントIDのリスト、#p:公開鍵のリストなど
"#<single-letter (a-zA-Z)>": <a list of tag values, for #e — a list of event ids, for #p — a list of pubkeys, etc.>,
// 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>,
// リレーが初回の問い合わせで返すべき(SHOULD)、イベントの最大個数。
"limit": <maximum number of events relays SHOULD return in the initial query>
}
REQを受け取ったら、リレーはフィルターに一致するイベントを返すべきである(SHOULD)。リレーは、接続が閉じられるか、同じ購読IDでCLOSEを受け取るか、同じ購読IDを持つ新しいREQが送られる(この場合、古い購読を置き換えて、新しい購読が作られる)まで、受信したあらゆる新しいイベントを送るべきである。
訳注: REQを受け取ったときのリレー側の動きをまとめると:
リレーは、フィルターに一致するイベントをクライアントに返す
REQ受信後に届いたあらゆるイベントで、フィルターに一致するものをクライアントに転送すべき
WebSocketの通信が切れるまで
もしくは CLOSEを受け取るまで
もしくは 同じ購読IDで新しくREQが送られるまで
訳注: 全く同じフィルターで購読を上書きしようとするとDuplicate subscriptionとして無視する実装がある (→リレー実装による制約#6455b795a99d480000d4da27)
リストを含むフィルター属性(ids、authors、kinds、#eのようなタグフィルター)は、1つ以上の値を含むJSON配列である。条件が一致したとみなされるには、配列の少なくとも1つの値が、イベントの関連するフィールドに一致しなければならない。authorsやkindsのようなイベントのスカラー値の場合は、イベント中の属性がフィルターリストに含まれていなければならない。#eのようなタグ属性については、イベントが複数の値を持つ場合には、イベントとフィルター条件の間に共通する1つ以上の項目がなければならない。
ids と authors、#e、#pのフィルターリストは、正確に64文字の小文字の16進数の値を含まなければならない(MUST)。
since と until プロパティーは、購読で返されるイベントの時間範囲を指定するために使用できる。フィルターに since プロパティーが含まれる場合、created_atがsince以上であるようなイベントがフィルタに一致するとみなされる。untilプロパティは created_atが until 以下でなければならない点を除いて同じである。要するに、since <= created_at <= until が成り立つ場合、イベントはフィルタに一致する。
TBD
訳注: フィルター仕様の要約
フィルターの属性がリストである場合(ids, authors, 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 と定義された。
リレーからクライアントへの通信
https://github.com/nostr-protocol/nips/blob/master/01.md#from-relay-to-client-sending-events-and-notices
リレーは、これらの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をどのように送信すべきで、どのように解釈されるべきかを定義しない
https://iris.to/post/note1jk2f83srdvj6tsspksr3vy2jklt4dxksh3vrf40m3s3ja0034tqqzhaeej
DartでのTL閲覧コードサンプル