指南

指南 · 第 4 部分,共 6 部分

构建一份密封 PoE

普通的 PoE 证明的是某份内容曾存在过密封 PoE 在证明同一件事的同时,让内容本身保持机密:你把字节加密给一个或多个接收方公钥,只存储密文,再把记录锚定到链上。任何人都能看到这条记录存在、并验证它的结构;但只有持有匹配私钥的人才能解密载荷。信封格式参见密封 PoE,威胁模型参见密封起来,等人认领

指定接收方

接收方由一个 age 风格的字符串来标识。它有两种,靠前缀区分:

  • age1… — 经典的 X25519 密钥(32 字节)。
  • age1pqc…X-Wing 混合密钥(ML-KEM-768 + X25519,1216 字节)。

X-Wing(mlkem768x25519)是默认的 KEM——它能抵御未来的量子攻击者,而且每个身份始终都有一个 age1pqc… 地址。

接收方会通过带外渠道把字符串给你。你用 parseAgeRecipient 把它解码回密封辅助函数所需的原始公钥:

import { parseAgeRecipient } from '@cardanowall/sdk-ts';

const them = parseAgeRecipient('age1pqc…'); // { kem: 'mlkem768x25519', publicKey: Uint8Array }

如果你自己手里有一个 32 字节的种子,recipientsFromSeed 会同时给出你的两个地址——把其中一个分享出去,别人就能密封给你;同时把你自己的密钥也放进接收方列表,这样你才保得住对自己所发内容的读取权:

import { recipientsFromSeed } from '@cardanowall/sdk-ts';

const me = recipientsFromSeed(mySeed); // { age: 'age1…', age1pqc: 'age1pqc…' }

密封并发布

密封要经过网关,由它构建并广播 Cardano 交易,还替你存储密文。把客户端指向你所用的网关即可;SDK 不依赖特定网关。

publishSealed 接收的是原始接收方公钥,所以你要从每个解析出的地址里取出 publicKey。所有接收方必须共用同一个 KEM——把 age1pqc… 密钥放在一组里。先用 quote 锁定价格,再发布:

import { Label309Client, parseAgeRecipient } from '@cardanowall/sdk-ts';

const client = new Label309Client({
  baseUrl: 'https://your-gateway.example',
  apiKey: process.env.CW_API_KEY,
});

const content = new TextEncoder().encode('the secret payload');
const recipients = ['age1pqc…recipient', me.age1pqc].map((r) => parseAgeRecipient(r).publicKey);

const quote = await client.poe.quote({
  recordBytes: 512,
  recipientCount: recipients.length,
  fileBytesTotal: content.length,
});

const result = await client.poe.publishSealed({
  content,
  recipients,
  quoteId: quote.quote_id,
  // kem defaults to 'mlkem768x25519' (X-Wing); pass 'x25519' only for age1… keys.
});

console.log(result.tx_hash);

这个辅助函数会加密内容、上传密文、把密文的 ar:// URI 和明文哈希绑进一条 Label 309 记录,然后提交。你的种子和明文始终不会以明文形式离开你的机器。

使用 Python

cardanowall-sdk 是逐字节一致的孪生实现——同样的 KEM 默认值,同样的信封:

import asyncio
import os
from cardanowall import Label309Client, parse_age_recipient

async def main():
    content = b"the secret payload"
    recipients = [parse_age_recipient("age1pqc…recipient").public_key]

    async with Label309Client(
        base_url="https://your-gateway.example",
        api_key=os.environ["CW_API_KEY"],
    ) as client:
        quote = await client.poe.quote(
            record_bytes=512, recipient_count=len(recipients), file_bytes_total=len(content)
        )
        result = await client.poe.publish_sealed(
            content=content, recipients=recipients, quote_id=quote["quote_id"]
        )
        print(result["tx_hash"])

asyncio.run(main())

使用 Rust

cardanowall crate 通过同一个网关客户端来密封。parse_age_recipient 把每个地址解码成原始密钥,而 kem: None 保留 X-Wing 这个默认值:

use cardanowall::client::{
    Label309Client, Label309ClientConfig, PublishSealedInput, QuoteInput,
};
use cardanowall::recipient::parse_age_recipient;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Label309Client::new(Label309ClientConfig {
        base_url: Some("https://your-gateway.example".into()),
        api_key: std::env::var("CW_API_KEY").ok(),
    })?;

    let content = b"the secret payload".to_vec();
    let recipients = vec![parse_age_recipient("age1pqc…recipient")?.public_key];

    let quote = client.poe().quote(&QuoteInput {
        record_bytes: 512,
        recipient_count: recipients.len() as u64,
        file_bytes_total: content.len() as u64,
    })?;

    let result = client.poe().publish_sealed(&PublishSealedInput {
        content,
        recipients,
        quote_id: quote.quote_id,
        hash_alg: None,
        kem: None, // defaults to mlkem768x25519 (X-Wing); set Some for x25519
        signer: None,
        idempotency_key: None,
    })?;

    println!("{:?}", result.tx_hash);
    Ok(())
}

CLI 能发布仅含哈希的记录和 Merkle 记录;密封目前是 SDK 才有的流程。

记录最终确认后,每位接收方都能发现它,用自己的私钥解密载荷,并重新计算明文哈希来闭合整个流程——这正是验证一条记录中属于接收方的那一半。

别忘了也密封给自己

publishSealed 绝不会悄悄把你加进接收方列表。如果你不把自己的某个密钥放进去,发出的记录将永远无法被你读回。只要你想保住对自己所发内容的访问权,就把 me.age1pqc(或 me.age)一并放进接收方列表。