Руководства · Часть 4 из 6
Создайте запечатанное подтверждение существования
Обычное подтверждение существования (PoE) показывает сам факт того, что некое содержимое существовало. Запечатанное подтверждение существования доказывает то же самое, но при этом держит содержимое в тайне: вы шифруете байты для одного или нескольких ключей получателей, храните только шифртекст, а запись закрепляете в блокчейне. Любой может увидеть, что запись существует, и проверить её структуру; расшифровать полезную нагрузку способен только обладатель подходящего закрытого ключа. Формат конверта описан в разделе Sealed 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);Помощник шифрует содержимое, выгружает шифртекст, вшивает его URI ar:// и хеш
открытого текста в запись 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 запечатывает через тот же клиент шлюза. 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) в список получателей всякий раз, когда хотите сохранить доступ к тому,
что отправили.