NIP-26: Delegated Event Signing
允許其他密鑰代表你簽署事件的委託機制
概述
NIP-26 定義了一種委託簽名機制,允許一個密鑰(委託者)授權另一個密鑰(被委託者) 代表其簽署事件。這對於需要多設備使用、團隊管理或自動化發布等場景非常有用。
委託令牌
委託通過一個特殊的 delegation 標籤實現,包含委託者公鑰、 條件字串和簽名。
標籤格式
["delegation", "<委託者公鑰>", "<條件字串>", "<簽名>"] | 欄位 | 說明 |
|---|---|
| 委託者公鑰 | 授權委託的原始公鑰(hex 格式) |
| 條件字串 | 定義委託的限制條件 |
| 簽名 | 委託者對條件的 Schnorr 簽名 |
條件字串
條件字串定義了委託的限制,使用 & 連接多個條件:
| 條件 | 說明 | 範例 |
|---|---|---|
kind=<n> | 限制為特定事件類型 | kind=1 |
created_at<<n> | 事件時間戳必須小於 n | created_at<1700000000 |
created_at><n> | 事件時間戳必須大於 n | created_at>1600000000 |
條件範例
# 只允許發布 kind 1(短文)
kind=1
# 只允許在特定時間範圍內發布
created_at>1640000000&created_at<1700000000
# 只允許發布 kind 1,且有時間限制
kind=1&created_at>1640000000&created_at<1700000000 簽名生成
委託簽名是對以下字串的 Schnorr 簽名:
nostr:delegation:<被委託者公鑰>:<條件字串> 範例
委託事件
{
"id": "...",
"pubkey": "<被委託者公鑰>",
"created_at": 1650000000,
"kind": 1,
"tags": [
[
"delegation",
"<委託者公鑰>",
"kind=1&created_at>1640000000&created_at<1700000000",
"<委託簽名>"
]
],
"content": "這是一條由被委託者代發的訊息",
"sig": "<被委託者簽名>"
} TypeScript 實作
建立委託令牌
import { schnorr } from '@noble/curves/secp256k1';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { sha256 } from '@noble/hashes/sha256';
interface DelegationToken {
delegatorPubkey: string;
conditions: string;
signature: string;
}
interface DelegationConditions {
kind?: number;
since?: number;
until?: number;
}
function buildConditionsString(conditions: DelegationConditions): string {
const parts: string[] = [];
if (conditions.kind !== undefined) {
parts.push(`kind=${conditions.kind}`);
}
if (conditions.since !== undefined) {
parts.push(`created_at>${conditions.since}`);
}
if (conditions.until !== undefined) {
parts.push(`created_at<${conditions.until}`);
}
return parts.join('&');
}
function createDelegationToken(
delegatorSecretKey: Uint8Array,
delegateePubkey: string,
conditions: DelegationConditions
): DelegationToken {
const delegatorPubkey = bytesToHex(
schnorr.getPublicKey(delegatorSecretKey)
);
const conditionsString = buildConditionsString(conditions);
// 建立要簽名的字串
const message = `nostr:delegation:${delegateePubkey}:${conditionsString}`;
const messageHash = sha256(new TextEncoder().encode(message));
// 簽名
const signature = bytesToHex(
schnorr.sign(messageHash, delegatorSecretKey)
);
return {
delegatorPubkey,
conditions: conditionsString,
signature,
};
}
// 使用範例
const token = createDelegationToken(
delegatorSecretKey,
'被委託者公鑰hex',
{
kind: 1,
since: Math.floor(Date.now() / 1000),
until: Math.floor(Date.now() / 1000) + 86400 * 30, // 30 天
}
); 驗證委託
function verifyDelegation(
event: any,
delegationTag: string[]
): boolean {
if (delegationTag[0] !== 'delegation' || delegationTag.length !== 4) {
return false;
}
const [, delegatorPubkey, conditions, signature] = delegationTag;
// 1. 驗證條件
if (!validateConditions(event, conditions)) {
return false;
}
// 2. 驗證簽名
const message = `nostr:delegation:${event.pubkey}:${conditions}`;
const messageHash = sha256(new TextEncoder().encode(message));
try {
return schnorr.verify(
hexToBytes(signature),
messageHash,
hexToBytes(delegatorPubkey)
);
} catch {
return false;
}
}
function validateConditions(event: any, conditions: string): boolean {
const parts = conditions.split('&');
for (const part of parts) {
if (part.startsWith('kind=')) {
const kind = parseInt(part.slice(5));
if (event.kind !== kind) return false;
} else if (part.startsWith('created_at>')) {
const since = parseInt(part.slice(11));
if (event.created_at <= since) return false;
} else if (part.startsWith('created_at<')) {
const until = parseInt(part.slice(11));
if (event.created_at >= until) return false;
}
}
return true;
}
// 使用範例
const delegationTag = event.tags.find((t: string[]) => t[0] === 'delegation');
if (delegationTag && verifyDelegation(event, delegationTag)) {
console.log('委託有效,事件代表:', delegationTag[1]);
} 使用委託發布事件
import { finalizeEvent } from 'nostr-tools';
function createDelegatedEvent(
content: string,
delegateeSecretKey: Uint8Array,
delegationToken: DelegationToken
) {
const event = {
kind: 1,
content,
tags: [
[
'delegation',
delegationToken.delegatorPubkey,
delegationToken.conditions,
delegationToken.signature,
],
],
created_at: Math.floor(Date.now() / 1000),
};
return finalizeEvent(event, delegateeSecretKey);
}
// 使用範例
const delegatedEvent = createDelegatedEvent(
'這是委託發布的訊息',
delegateeSecretKey,
token
); 使用場景
多設備管理
- 主密鑰保存在冷錢包
- 為每個設備創建獨立的委託密鑰
- 設定時間限制和類型限制
團隊發布
- 品牌帳號由主密鑰控制
- 團隊成員獲得有限委託權限
- 可隨時撤銷(通過時間條件過期)
自動化發布
- 機器人使用委託密鑰
- 限制只能發布特定類型的事件
- 降低主密鑰洩露風險
安全考量
- 時間限制:始終設定
until條件,避免永久委託 - 類型限制:限制可發布的事件類型
- 密鑰隔離:委託密鑰應獨立於主密鑰
- 監控審計:定期檢查委託發布的內容
中繼器支援
支援 NIP-26 的中繼器應該:
- 驗證委託簽名的有效性
- 驗證事件是否符合條件限制
- 在查詢時,將委託事件關聯到委託者
限制與注意事項
- 委託無法撤銷,只能等待過期
- 不是所有客戶端都支援顯示委託資訊
- 某些中繼器可能不驗證委託
相關 NIPs
參考資源
已複製連結