Руководства

Руководства · Часть 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) в список получателей всякий раз, когда хотите сохранить доступ к тому, что отправили.