The diagram below shows the on-chain secret flow: allocate a vault, encrypt the
secret locally with TDH2, and write the ciphertext to the vault.
The high-level uploadCDR method handles vault allocation, encryption, and writing in a single call.
import { initWasm } from "@piplabs/cdr-sdk";import { encodeAbiParameters } from "viem";await initWasm();// Assumes `client` and `walletClient` are already created (see Setup)const { uploader, observer } = client;const walletAddress = walletClient.account!.address;const OWNER_CONDITION = "0x4C9bFC96d7092b590D497A191826C3dA2277c34B";const ownerConditionData = encodeAbiParameters( [{ type: "address" }], [walletAddress],);// Pure read: fetch the DKG global public keyconst globalPubKey = await observer.getGlobalPubKey();// Encode your secret as bytesconst secret = "my confidential data";const dataKey = new TextEncoder().encode(secret);// Sends 2 transactions: allocate + writeconst { uuid, ciphertext, txHashes } = await uploader.uploadCDR({ dataKey, globalPubKey, updatable: false, writeConditionAddr: OWNER_CONDITION, readConditionAddr: OWNER_CONDITION, writeConditionData: ownerConditionData, readConditionData: ownerConditionData, accessAuxData: "0x",});console.log(`Vault created with UUID: ${uuid}`);console.log(`Allocate tx: ${txHashes.allocate}`);console.log(`Write tx: ${txHashes.write}`);
uploadCDR automatically queries the allocate and write fees. You can skip
those queries by passing allocateFeeOverride and writeFeeOverride.
The value of the transaction must be exactly the same as the fee.
dataKey is the historical parameter name. In uploadCDR() it can be any
secret bytes, not just a cryptographic key.
Vault encrypted data is limited to 1024 bytes on Aeneid
(maxEncryptedDataSize). TDH2 adds overhead, so the maximum plaintext is
smaller. For larger content, use uploadFile() so only a small {cid, key}
payload is written to the vault.
Decryption requires submitting a read request on-chain, collecting partial
decryptions from validators, and combining them client-side.
const { consumer } = client;// Sends 1 transaction, then collects partials and combines them locallyconst { dataKey, txHash } = await consumer.accessCDR({ uuid, accessAuxData: "0x", timeoutMs: 120_000, // wait up to 2 minutes for validators});const secret = new TextDecoder().decode(dataKey);console.log(`Read tx: ${txHash}`);console.log(`Decrypted secret: ${secret}`);
In v0.1.1, accessCDR() can auto-generate the ephemeral keypair and
auto-query globalPubKey / threshold when you omit them.
The timeout of the request on the server side is 200 blocks, which is
approximately 7 minutes. If you’re not able to collect enough partials within
this timeout, try another read request.
Use the file workflow when the encrypted payload should live off-chain and only
the encrypted file key plus pointer should be stored in the vault.Upload happens once by the data owner. Download happens later by an authorized
reader who recovers the vault payload and then decrypts the stored file.
import { HeliaProvider } from "@piplabs/cdr-sdk";import { readFile, writeFile } from "node:fs/promises";import { createHelia } from "helia";import { unixfs } from "@helia/unixfs";import { CID } from "multiformats/cid";import { encodeAbiParameters } from "viem";const walletAddress = walletClient.account!.address;const OWNER_CONDITION = "0x4C9bFC96d7092b590D497A191826C3dA2277c34B";const ownerConditionData = encodeAbiParameters( [{ type: "address" }], [walletAddress],);// Pure readconst globalPubKey = await client.observer.getGlobalPubKey();const helia = await createHelia();const storage = new HeliaProvider({ helia, unixfs: unixfs(helia), CID: (s) => CID.parse(s),});const sourceFile = await readFile("./example.pdf");// Off-chain upload + 2 on-chain transactionsconst { uuid, cid } = await client.uploader.uploadFile({ content: new Uint8Array(sourceFile), storageProvider: storage, globalPubKey, updatable: false, writeConditionAddr: OWNER_CONDITION, readConditionAddr: OWNER_CONDITION, writeConditionData: ownerConditionData, readConditionData: ownerConditionData, accessAuxData: "0x",});// 1 on-chain read transaction + off-chain download + local AES decryptionconst { content, txHash } = await client.consumer.downloadFile({ uuid, accessAuxData: "0x", storageProvider: storage, timeoutMs: 120_000,});console.log(`Stored at CID: ${cid}`);console.log(`Read tx: ${txHash}`);await writeFile("./example.decrypted.pdf", Buffer.from(content));console.log("Decrypted file written to ./example.decrypted.pdf");
HeliaProvider is the only storage backend fully tested on Aeneid in the
current release, and it requires Node.js 22+.
uploadFile() and downloadFile() work with raw file bytes. In a browser,
start from a File object and convert it with new Uint8Array(await file.arrayBuffer()).
If you need more control over the process, you can call each step individually.
These snippets continue from the variables in the examples above:
walletClient, globalPubKey, threshold, requesterPubKey,
recipientPrivKey, and dataKey.