跳至主要內容
進階

NIP-13 工作量證明

Nostr 工作量證明機制,用於防止垃圾訊息和提升事件優先級。

8 分鐘

概述

NIP-13 定義了 Nostr 事件的工作量證明(Proof of Work)機制。通過要求事件 ID 具有特定數量的前導零位元,可以增加產生事件的計算成本,從而防止垃圾訊息攻擊。 這個概念類似於比特幣的挖礦,但用於 Nostr 事件。

運作原理

Nostr 事件的 ID 是事件內容的 SHA-256 雜湊值。NIP-13 通過在事件中加入 nonce 標籤,不斷調整 nonce 值直到事件 ID 的前導零位元數達到目標難度。

難度計算

難度以前導零位元數表示。例如,難度 16 表示事件 ID 的前 16 個位元必須為零, 相當於 ID 以 0000(4 個十六進位零)開頭。

難度 前導零位元 十六進位開頭 預期嘗試次數
8 8 bits 00 ~256
16 16 bits 0000 ~65,536
24 24 bits 000000 ~16,777,216
32 32 bits 00000000 ~4,294,967,296

Nonce 標籤

事件必須包含 nonce 標籤來聲明工作量證明。標籤格式為:

{`["nonce", "", "<目標難度>"]`}

事件範例

{`{
  "id": "000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358",
  "pubkey": "a]48...",
  "created_at": 1651794653,
  "kind": 1,
  "tags": [
    ["nonce", "776797", "20"]
  ],
  "content": "這是一則帶有工作量證明的筆記",
  "sig": "..."
}`}

在這個範例中,事件 ID 以 000006 開頭,表示至少有 20 個前導零位元 (實際上有 21 個)。nonce 標籤聲明目標難度為 20,nonce 值為 776797。

實作

計算前導零位元

{`// 計算事件 ID 的前導零位元數
function countLeadingZeroBits(hex: string): number {
  let count = 0;

  for (const char of hex) {
    const nibble = parseInt(char, 16);
    if (nibble === 0) {
      count += 4;
    } else {
      // 計算這個半位元組的前導零
      count += Math.clz32(nibble) - 28;
      break;
    }
  }

  return count;
}

// 測試
console.log(countLeadingZeroBits('000006d8...')); // 21
console.log(countLeadingZeroBits('0000ffff...')); // 16
console.log(countLeadingZeroBits('00000000...')); // 32`}

挖掘事件

{`import { getEventHash, finalizeEvent, getPublicKey } from 'nostr-tools';

interface UnsignedEvent {
  kind: number;
  created_at: number;
  tags: string[][];
  content: string;
  pubkey: string;
}

// 挖掘帶有 PoW 的事件
function mineEvent(
  event: UnsignedEvent,
  targetDifficulty: number,
  maxIterations: number = 10000000
): UnsignedEvent | null {
  let nonce = 0;

  while (nonce < maxIterations) {
    // 建立帶有當前 nonce 的事件
    const eventWithNonce: UnsignedEvent = {
      ...event,
      tags: [
        ...event.tags.filter(t => t[0] !== 'nonce'),
        ['nonce', nonce.toString(), targetDifficulty.toString()],
      ],
    };

    // 計算事件 ID
    const id = getEventHash(eventWithNonce);

    // 檢查難度
    if (countLeadingZeroBits(id) >= targetDifficulty) {
      console.log(\`Found valid PoW after \${nonce} iterations\`);
      console.log(\`Event ID: \${id}\`);
      return eventWithNonce;
    }

    nonce++;

    // 每 100000 次顯示進度
    if (nonce % 100000 === 0) {
      console.log(\`Mining... \${nonce} attempts\`);
    }
  }

  console.log('Max iterations reached without finding valid PoW');
  return null;
}

// 使用範例
async function createPowEvent(
  privateKey: Uint8Array,
  content: string,
  difficulty: number
) {
  const pubkey = getPublicKey(privateKey);

  const unsignedEvent: UnsignedEvent = {
    kind: 1,
    created_at: Math.floor(Date.now() / 1000),
    tags: [],
    content,
    pubkey,
  };

  const minedEvent = mineEvent(unsignedEvent, difficulty);

  if (!minedEvent) {
    throw new Error('Failed to mine event');
  }

  return finalizeEvent(minedEvent, privateKey);
}`}

Web Worker 實作

對於較高難度的 PoW,建議使用 Web Worker 避免阻塞主線程:

{`// pow-worker.ts
self.onmessage = (e) => {
  const { event, targetDifficulty, startNonce, endNonce } = e.data;

  for (let nonce = startNonce; nonce < endNonce; nonce++) {
    const eventWithNonce = {
      ...event,
      tags: [
        ...event.tags.filter((t: string[]) => t[0] !== 'nonce'),
        ['nonce', nonce.toString(), targetDifficulty.toString()],
      ],
    };

    const id = getEventHash(eventWithNonce);

    if (countLeadingZeroBits(id) >= targetDifficulty) {
      self.postMessage({ found: true, event: eventWithNonce, nonce });
      return;
    }

    // 定期回報進度
    if (nonce % 10000 === 0) {
      self.postMessage({ found: false, progress: nonce });
    }
  }

  self.postMessage({ found: false, exhausted: true });
};

// 主線程使用多個 worker 並行挖掘
async function mineWithWorkers(
  event: UnsignedEvent,
  difficulty: number,
  workerCount: number = 4
) {
  const workers: Worker[] = [];
  const chunkSize = 1000000;

  return new Promise((resolve, reject) => {
    for (let i = 0; i < workerCount; i++) {
      const worker = new Worker('pow-worker.ts');

      worker.onmessage = (e) => {
        if (e.data.found) {
          // 終止所有 worker
          workers.forEach(w => w.terminate());
          resolve(e.data.event);
        }
      };

      worker.postMessage({
        event,
        targetDifficulty: difficulty,
        startNonce: i * chunkSize,
        endNonce: (i + 1) * chunkSize,
      });

      workers.push(worker);
    }
  });
}`}

驗證

{`// 驗證事件的工作量證明
function verifyPow(event: Event): { valid: boolean; difficulty: number } {
  // 找到 nonce 標籤
  const nonceTag = event.tags.find(t => t[0] === 'nonce');

  if (!nonceTag) {
    return { valid: false, difficulty: 0 };
  }

  const targetDifficulty = parseInt(nonceTag[2], 10);
  const actualDifficulty = countLeadingZeroBits(event.id);

  return {
    valid: actualDifficulty >= targetDifficulty,
    difficulty: actualDifficulty,
  };
}

// 使用範例
const result = verifyPow(event);
if (result.valid) {
  console.log(\`Valid PoW with difficulty \${result.difficulty}\`);
} else {
  console.log('Invalid PoW');
}`}

中繼器應用

中繼器可以通過 NIP-11 聲明對 PoW 的要求:

{`{
  "limitation": {
    "min_pow_difficulty": 16
  }
}`}

查詢帶有 PoW 的事件

{`import { SimplePool } from 'nostr-tools';

const pool = new SimplePool();
const relays = ['wss://relay.damus.io'];

// 查詢所有事件,然後篩選高 PoW
async function getHighPowEvents(minDifficulty: number) {
  const events = await pool.querySync(relays, {
    kinds: [1],
    limit: 100,
  });

  return events.filter(event => {
    const { valid, difficulty } = verifyPow(event);
    return valid && difficulty >= minDifficulty;
  });
}`}

應用場景

  • 垃圾訊息防護:中繼器可要求最低 PoW 難度
  • 優先顯示:客戶端可優先顯示高 PoW 事件
  • 付費替代:用計算成本替代付費訂閱
  • 信譽系統:累積的 PoW 可作為用戶信譽指標
  • 重要公告:用高 PoW 標記重要訊息

注意事項

  • 時間戳承諾:PoW 與 created_at 綁定,事件時間無法修改
  • 設備差異:不同設備計算能力差異大,難度設定需考慮普通用戶
  • 電池消耗:移動設備上的 PoW 會消耗大量電池
  • 難度通膨:隨著硬體進步,相同難度的成本會降低

綁定 PoW

為防止 PoW 被重複使用在不同事件上,nonce 標籤可以包含對特定欄位的承諾:

{`{
  "tags": [
    ["nonce", "776797", "20", "pubkey,created_at"]
  ]
}`}

這表示 PoW 是針對特定的 pubkey 和 created_at 計算的, 無法被複製到其他用戶或時間的事件上。

  • NIP-01 - 基本協議與事件結構
  • NIP-11 - 中繼器資訊(PoW 要求聲明)

參考資源

已複製連結
已複製到剪貼簿