指南 · 第 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)一并放进接收方列表。