事件系統詳解
深入了解 Nostr 事件的結構、類型和生命週期,掌握協議的核心數據模型。
事件是什麼?
在 Nostr 中,所有數據都是「事件」(Event)。無論是貼文、個人資料、反應還是私訊, 都是以事件的形式存在。事件是不可變的,一旦創建並簽名,就不能被修改。
設計哲學: 事件的不可變性確保了數據完整性。要「修改」內容,需要發布新事件來替代舊事件。 這與比特幣的 UTXO 模型有異曲同工之妙。
事件結構
{
"id": "32字節十六進制,事件的唯一標識符",
"pubkey": "32字節十六進制,創建者的公鑰",
"created_at": 1234567890, // Unix 時間戳(秒)
"kind": 1, // 事件類型
"tags": [ // 標籤陣列
["e", "引用的事件ID", "中繼器URL"],
["p", "提及的公鑰"],
["t", "hashtag"],
...
],
"content": "事件的主要內容",
"sig": "64字節十六進制 Schnorr 簽名"
} 欄位說明
id
事件的唯一標識符,由事件內容的 SHA256 雜湊計算得出。 這確保了事件的完整性,任何修改都會改變 ID。
pubkey
創建者的 secp256k1 公鑰(x-only 格式)。這是用戶的身份標識, 對應 bech32 編碼的 npub 格式。
created_at
事件創建的 Unix 時間戳(秒)。用於排序事件和判斷可替換事件的新舊。
kind
事件類型,決定如何解釋 content 和 tags。不同 kind 有不同的語義和處理規則。
tags
標籤陣列,每個標籤是一個字串陣列。第一個元素是標籤名稱, 後續元素是值。用於引用、分類和建立關係。
content
事件的主要內容,格式取決於 kind。可以是純文字、JSON 或空字串。
sig
對事件 ID 的 Schnorr 簽名(BIP-340),證明事件由 pubkey 擁有者創建。
事件 ID 計算
// 事件 ID 計算步驟
1. 序列化事件(不包含 id 和 sig):
[
0, // 固定值
pubkey, // 十六進制公鑰
created_at, // 整數時間戳
kind, // 整數類型
tags, // 標籤陣列
content // 內容字串
]
2. 計算 SHA256:
id = sha256(JSON.stringify(serialized))
JavaScript 範例:
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
const id = sha256(serialized); 事件類型分類
事件類型(kind)決定事件的行為和語義。根據 NIP-01,kind 數字範圍有特殊含義:
Kind 範圍分類: 0-9999: 常規事件(Regular Events) 10000-19999: 可替換事件(Replaceable Events) 20000-29999: 臨時事件(Ephemeral Events) 30000-39999: 參數化可替換事件(Addressable Events) 行為差異: - 常規事件:每個都是獨立的,永久保存 - 可替換事件:同 pubkey+kind 只保留最新 - 臨時事件:不儲存,僅轉發 - 參數化可替換:同 pubkey+kind+d標籤 只保留最新
常規事件 (Regular)
| Kind | 名稱 | 用途 |
|---|---|---|
| 1 | Short Text Note | 短文本貼文,類似推文 |
| 4 | Encrypted DM | 加密私訊(NIP-04,已不推薦) |
| 5 | Deletion | 刪除請求 |
| 6 | Repost | 轉發 |
| 7 | Reaction | 反應(點讚、表情) |
| 1984 | Report | 檢舉 |
| 9735 | Zap Receipt | Zap 收據 |
可替換事件 (Replaceable)
| Kind | 名稱 | 用途 |
|---|---|---|
| 0 | Metadata | 用戶個人資料 |
| 3 | Contacts | 關注列表 |
| 10002 | Relay List | 中繼器列表(NIP-65) |
參數化可替換事件 (Addressable)
參數化可替換事件使用 "d" 標籤作為標識符:
Kind 30023: 長篇文章
{
"kind": 30023,
"tags": [
["d", "my-article-slug"], // 文章標識符
["title", "文章標題"],
["published_at", "1234567890"]
],
"content": "文章的 Markdown 內容..."
}
同一用戶的 kind:30023 + d:"my-article-slug" 只會保留最新版本
這樣可以實現文章的更新功能
常見參數化可替換事件:
- Kind 30000: 自訂列表
- Kind 30023: 長篇文章
- Kind 30078: 應用特定數據 標籤系統
標籤是 Nostr 協議的強大功能,用於建立事件之間的關係:
常用標籤類型:
["e", "<event_id>", "<relay_url>", "<marker>"]
引用事件,marker 可以是 "reply", "root", "mention"
["p", "<pubkey>", "<relay_url>"]
提及用戶
["a", "<kind>:<pubkey>:<d-tag>", "<relay_url>"]
引用參數化可替換事件
["t", "hashtag"]
話題標籤
["d", "identifier"]
參數化可替換事件的標識符
["r", "url"]
引用外部 URL
["nonce", "<nonce>", "<difficulty>"]
工作量證明(NIP-13)
範例:回覆貼文
{
"kind": 1,
"tags": [
["e", "原始貼文ID", "wss://relay.example", "root"],
["e", "直接回覆的貼文ID", "wss://relay.example", "reply"],
["p", "原作者公鑰"]
],
"content": "我的回覆..."
} 事件生命週期
事件生命週期:
1. 創建 (Creation)
┌─────────────────────────────┐
│ 客戶端構建事件 │
│ - 設定 kind, content, tags │
│ - 計算 created_at │
└─────────────────────────────┘
↓
2. 簽名 (Signing)
┌─────────────────────────────┐
│ 計算事件 ID (SHA256) │
│ 使用私鑰簽名 ID │
│ 填入 id 和 sig 欄位 │
└─────────────────────────────┘
↓
3. 發布 (Publishing)
┌─────────────────────────────┐
│ 發送 ["EVENT", event] │
│ 到多個中繼器 │
└─────────────────────────────┘
↓
4. 驗證 (Validation)
┌─────────────────────────────┐
│ 中繼器驗證: │
│ - ID 計算正確? │
│ - 簽名有效? │
│ - 符合中繼器政策? │
└─────────────────────────────┘
↓
5. 儲存/轉發 (Storage/Relay)
┌─────────────────────────────┐
│ 回覆 ["OK", id, true, ""] │
│ 儲存事件 │
│ 轉發給訂閱者 │
└─────────────────────────────┘ 刪除事件
雖然事件是不可變的,但可以發布 kind 5 事件來請求刪除:
刪除請求(Kind 5):
{
"kind": 5,
"tags": [
["e", "要刪除的事件ID1"],
["e", "要刪除的事件ID2"],
["a", "30023:pubkey:article-id"] // 刪除參數化事件
],
"content": "刪除原因(可選)"
}
重要注意事項:
- 只能刪除自己的事件
- 中繼器可以選擇是否遵守刪除請求
- 已被其他用戶快取的事件無法真正刪除
- 這是「請求刪除」而非「強制刪除」 隱私警告: 一旦事件被發布,就無法保證完全刪除。發布前請仔細考慮內容。 Nostr 的設計是公開和永久的。
驗證事件
驗證步驟:
1. 驗證 ID
- 重新計算 SHA256
- 比對 id 欄位
2. 驗證簽名
- 使用 pubkey 驗證 Schnorr 簽名
- 簽名必須對應事件 ID
3. 驗證格式
- kind 是非負整數
- created_at 是合理的時間戳
- tags 格式正確
JavaScript 範例(使用 nostr-tools):
import { verifySignature, getEventHash } from 'nostr-tools'
function validateEvent(event) {
// 驗證 ID
const expectedId = getEventHash(event);
if (event.id !== expectedId) {
return false;
}
// 驗證簽名
return verifySignature(event);
} 最佳實踐
內容規範
- • 使用 UTF-8 編碼
- • 避免過大的內容
- • 正確使用 kind
標籤使用
- • 正確標記回覆關係
- • 使用標準標籤名稱
- • 包含中繼器 URL 提示
時間戳
- • 使用準確的時間
- • 不要設定未來時間
- • 考慮時區問題
安全性
- • 始終驗證收到的事件
- • 不信任未驗證的數據
- • 注意 content 中的 XSS
下一步: 了解 密鑰與身份 如何管理你的 Nostr 身份。