跳至主要內容

NIP-03: OpenTimestamps

使用比特幣區塊鏈為 Nostr 事件提供不可篡改的時間戳證明

概述

NIP-03 定義了如何使用 OpenTimestamps (OTS) 協議為 Nostr 事件提供時間戳證明。 透過將事件 ID 錨定到比特幣區塊鏈,可以證明某個事件在特定時間之前就已存在, 提供不可篡改的時間證明。

OpenTimestamps 簡介

OpenTimestamps 是一個開放的時間戳標準,利用比特幣區塊鏈的不可篡改性 來證明資料在某個時間點之前就已存在。它通過將多個時間戳請求聚合到 單一交易中,大幅降低了成本。

工作原理

  1. 計算資料的雜湊值
  2. 將雜湊值提交給 OTS 日曆伺服器
  3. 日曆伺服器將多個雜湊聚合到 Merkle 樹
  4. Merkle 根被包含在比特幣交易中
  5. 交易被確認後,時間戳完成

標籤格式

["ots", "<base64-encoded-ots-proof>"]

ots 標籤包含 Base64 編碼的 OpenTimestamps 證明檔案。 這個證明可以獨立驗證,不需要信任任何第三方。

工作流程

1. 建立事件並獲取 ID

{
  "id": "a1b2c3d4...",
  "pubkey": "...",
  "created_at": 1704067200,
  "kind": 1,
  "content": "這是需要時間戳的重要內容",
  "tags": [],
  "sig": "..."
}

2. 提交 ID 到 OTS

將事件 ID(32 字節雜湊)提交給 OpenTimestamps 日曆伺服器。

3. 等待比特幣確認

通常需要幾個小時到一天,等待 OTS 將雜湊錨定到比特幣區塊。

4. 獲取並附加證明

{
  "id": "a1b2c3d4...",
  "pubkey": "...",
  "created_at": 1704067200,
  "kind": 1,
  "content": "這是需要時間戳的重要內容",
  "tags": [
    ["ots", "AE9wZW5UaW1lc3RhbXBz..."]
  ],
  "sig": "..."
}

範例

帶有 OTS 證明的事件

{
  "kind": 1,
  "content": "這份聲明發布於 2024 年 1 月 1 日",
  "tags": [
    ["ots", "AE9wZW5UaW1lc3RhbXBzIHByb29mIGRhdGEgaGVyZS4uLg=="]
  ],
  "created_at": 1704067200,
  "pubkey": "...",
  "id": "...",
  "sig": "..."
}

長文時間戳

{
  "kind": 30023,
  "content": "# 重要公告\n\n本文件的內容在此時間戳之前就已存在...",
  "tags": [
    ["d", "important-announcement"],
    ["title", "重要公告"],
    ["ots", "AE9wZW5UaW1lc3RhbXBz..."]
  ]
}

TypeScript 實作

提交時間戳請求

import OpenTimestamps from 'opentimestamps';

async function submitTimestamp(eventId: string): Promise<Uint8Array> {
  // 將事件 ID 轉換為字節
  const idBytes = hexToBytes(eventId);

  // 建立 DetachedTimestampFile
  const detached = OpenTimestamps.DetachedTimestampFile.fromHash(
    new OpenTimestamps.Ops.OpSHA256(),
    idBytes
  );

  // 提交到日曆伺服器
  await OpenTimestamps.stamp(detached);

  // 序列化證明
  const proofBytes = detached.serializeToBytes();

  return proofBytes;
}

function hexToBytes(hex: string): Uint8Array {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < hex.length; i += 2) {
    bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
  }
  return bytes;
}

// 使用範例
const proof = await submitTimestamp(event.id);
const proofBase64 = btoa(String.fromCharCode(...proof));

升級時間戳(等待確認)

async function upgradeTimestamp(
  proofBytes: Uint8Array
): Promise<Uint8Array | null> {
  // 反序列化現有證明
  const detached = OpenTimestamps.DetachedTimestampFile.deserialize(proofBytes);

  // 嘗試升級(獲取比特幣確認)
  const changed = await OpenTimestamps.upgrade(detached);

  if (changed) {
    // 返回升級後的證明
    return detached.serializeToBytes();
  }

  // 尚未確認
  return null;
}

// 輪詢升級狀態
async function waitForConfirmation(
  initialProof: Uint8Array,
  maxAttempts: number = 24,
  intervalMs: number = 3600000 // 1 小時
): Promise<Uint8Array> {
  let proof = initialProof;

  for (let i = 0; i < maxAttempts; i++) {
    const upgraded = await upgradeTimestamp(proof);
    if (upgraded) {
      return upgraded;
    }
    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }

  throw new Error('時間戳升級超時');
}

驗證時間戳

interface VerificationResult {
  verified: boolean;
  attestations: {
    type: string;
    time: Date;
    blockHeight?: number;
  }[];
}

async function verifyTimestamp(
  eventId: string,
  proofBase64: string
): Promise<VerificationResult> {
  // 解碼證明
  const proofBytes = Uint8Array.from(atob(proofBase64), (c) => c.charCodeAt(0));

  // 反序列化
  const detached = OpenTimestamps.DetachedTimestampFile.deserialize(proofBytes);

  // 驗證事件 ID 匹配
  const idBytes = hexToBytes(eventId);
  const expectedDigest = detached.fileDigest();

  if (!arraysEqual(idBytes, expectedDigest)) {
    return { verified: false, attestations: [] };
  }

  // 驗證時間戳
  const verifyResult = await OpenTimestamps.verify(detached);

  const attestations = [];
  for (const [attestation, timestamp] of verifyResult) {
    attestations.push({
      type: attestation.constructor.name,
      time: new Date(timestamp * 1000),
      blockHeight: attestation.height,
    });
  }

  return {
    verified: attestations.length > 0,
    attestations,
  };
}

function arraysEqual(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

// 使用範例
const result = await verifyTimestamp(event.id, otsTag[1]);
if (result.verified) {
  console.log(`時間戳驗證成功!`);
  result.attestations.forEach((att) => {
    console.log(`在 ${att.time.toISOString()} 由 ${att.type} 確認`);
  });
}

建立帶時間戳的事件

import { finalizeEvent } from 'nostr-tools';

async function createTimestampedEvent(
  content: string,
  kind: number,
  secretKey: Uint8Array,
  additionalTags: string[][] = []
): Promise<{
  event: any;
  pendingProof: string;
}> {
  // 先建立不含 OTS 的事件
  const event = finalizeEvent(
    {
      kind,
      content,
      tags: additionalTags,
      created_at: Math.floor(Date.now() / 1000),
    },
    secretKey
  );

  // 提交時間戳
  const proofBytes = await submitTimestamp(event.id);
  const proofBase64 = btoa(String.fromCharCode(...proofBytes));

  return {
    event,
    pendingProof: proofBase64,
  };
}

// 更新事件的 OTS 標籤(升級後)
function addOtsTag(event: any, otsProof: string): any {
  return {
    ...event,
    tags: [...event.tags, ['ots', otsProof]],
  };
}

React 元件

interface TimestampBadgeProps {
  event: any;
}

function TimestampBadge({ event }: TimestampBadgeProps) {
  const [verification, setVerification] = useState<VerificationResult | null>(null);
  const [loading, setLoading] = useState(false);

  const otsTag = event.tags.find((t: string[]) => t[0] === 'ots');

  if (!otsTag) {
    return null;
  }

  const handleVerify = async () => {
    setLoading(true);
    try {
      const result = await verifyTimestamp(event.id, otsTag[1]);
      setVerification(result);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="timestamp-badge">
      <span className="icon">⏰</span>
      <span>OpenTimestamps</span>
      {verification === null ? (
        <button onClick={handleVerify} disabled={loading}>
          {loading ? '驗證中...' : '驗證'}
        </button>
      ) : verification.verified ? (
        <div className="verified">
          <span>✓ 已驗證</span>
          {verification.attestations.map((att, i) => (
            <div key={i} className="attestation">
              區塊 #{att.blockHeight} - {att.time.toLocaleDateString()}
            </div>
          ))}
        </div>
      ) : (
        <span className="pending">待確認</span>
      )}
    </div>
  );
}

使用場景

法律與合規

  • 證明聲明或公告的發布時間
  • 智慧財產權的時間證明
  • 合約或協議的存在證明

新聞與記錄

  • 新聞報導的時間戳
  • 歷史事件的記錄
  • 證人陳述的時間證明

學術與研究

  • 研究成果的優先權證明
  • 論文預印本的時間戳
  • 數據集的完整性證明

最佳實踐

  • 等待確認:OTS 證明需要比特幣確認才有效,通常需要數小時
  • 保存證明:證明資料應妥善保存,是驗證的必要條件
  • 定期升級:提交後定期檢查並升級證明狀態
  • 獨立驗證:任何人都可以用事件 ID 和證明獨立驗證
  • 不可更改:時間戳針對特定事件 ID,內容變更會使證明失效

限制

  • 證明需要時間完成(數小時到一天)
  • 只能證明「之前存在」,不能證明精確時間
  • 事件內容不能更改,否則 ID 會變
  • 需要額外的儲存空間保存證明
  • NIP-01:基本協議 - 事件 ID 計算
  • NIP-23:長文內容 - 重要文件時間戳
  • NIP-40:過期時間戳 - 事件時間控制

參考資源

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