Attested Decrypt
Attested Decrypt allows you to decrypt encrypted handles and verify the decryption on-chain using Ed25519 signature verification. Use this when you need to perform on-chain logic based on decrypted values.
Import
import { decrypt } from '@inco/solana-sdk/attested-decrypt';
import { hexToBuffer, handleToBuffer, plaintextToBuffer } from '@inco/solana-sdk/utils';
import { Transaction, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js';
Basic Usage
import { decrypt } from '@inco/solana-sdk/attested-decrypt';
import { handleToBuffer, plaintextToBuffer } from '@inco/solana-sdk/utils';
import { Transaction, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js';
// Step 1: Decrypt handles (requires wallet signature for authentication)
const result = await decrypt(['handle1', 'handle2'], {
address: wallet.publicKey,
signMessage: wallet.signMessage,
});
// Step 2: Build program instruction
const handleBuffers = result.handles.map(h => handleToBuffer(h));
const plaintextBuffers = result.plaintexts.map(p => plaintextToBuffer(p));
const programInstruction = await program.methods
.verifyDecryption(result.handles.length, handleBuffers, plaintextBuffers)
.accounts({
authority: wallet.publicKey,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
// Step 3: Build transaction with Ed25519 instructions + program instruction
const tx = new Transaction();
result.ed25519Instructions.forEach(ix => tx.add(ix));
tx.add(programInstruction);
// Step 4: Send transaction
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = wallet.publicKey;
const signedTx = await wallet.signTransaction(tx);
const signature = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(signature, "confirmed");
Attested Decrypt uses the same decrypt() function as Attested Reveal. The difference is that you use result.ed25519Instructions to build an on-chain verification transaction.
How It Works
- Send encrypted handles to the covalidator
- Covalidator decrypts and returns plaintexts with Ed25519 signatures
- Build a transaction with Ed25519 verification instructions
- Add your program instruction that uses the decrypted values
- Submit transaction - Solana verifies signatures before executing your instruction
Example: Verify Addition Result On-Chain
import { encryptValue } from '@inco/solana-sdk/encryption';
import { decrypt } from '@inco/solana-sdk/attested-decrypt';
import { hexToBuffer, handleToBuffer, plaintextToBuffer } from '@inco/solana-sdk/utils';
import { Transaction, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js';
// Step 1: Perform encrypted addition on-chain
const encryptedA = await encryptValue(BigInt(100));
const encryptedB = await encryptValue(BigInt(50));
const addTx = await program.methods
.testAddition(hexToBuffer(encryptedA), hexToBuffer(encryptedB), 0)
.accounts({
authority: wallet.publicKey,
})
.rpc();
console.log("Addition tx:", addTx);
// Step 2: Get the result handle from logs
const resultHandle = await getHandleFromTx(addTx, "Addition result handle:");
console.log("Result handle:", resultHandle);
// Step 3: Decrypt and get Ed25519 instructions
// signMessage can be from wallet adapter (e.g., Phantom) or tweetnacl for testing
const result = await decrypt([resultHandle], {
address: wallet.publicKey,
signMessage: wallet.signMessage, // or: async (msg) => nacl.sign.detached(msg, keypair.secretKey)
});
console.log("Decrypted value:", result.plaintexts[0]); // "150"
console.log("Ed25519 instructions count:", result.ed25519Instructions.length);
// Step 4: Build program instruction
const handleBuffers = result.handles.map(h => handleToBuffer(h));
const plaintextBuffers = result.plaintexts.map(p => plaintextToBuffer(p));
const programInstruction = await program.methods
.verifyDecryption(result.handles.length, handleBuffers, plaintextBuffers)
.accounts({
authority: wallet.publicKey,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
// Step 5: Build transaction
const tx = new Transaction();
result.ed25519Instructions.forEach(ix => tx.add(ix));
tx.add(programInstruction);
// Step 6: Send transaction
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = wallet.publicKey;
const signedTx = await wallet.signTransaction(tx);
const signature = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(signature, "confirmed");
console.log("On-chain verification tx:", signature);
console.log("Verified on-chain: 100 + 50 = 150");
Example: Verify Multiple Values On-Chain
// Get handles from multiple operations
const addHandle = await getHandleFromTx(addTx, "Addition result handle:");
const subHandle = await getHandleFromTx(subTx, "Subtraction result handle:");
console.log("Add handle:", addHandle);
console.log("Sub handle:", subHandle);
// Decrypt multiple handles (requires wallet signature)
const result = await decrypt([addHandle, subHandle], {
address: wallet.publicKey,
signMessage: wallet.signMessage,
});
console.log("Decrypted values:", result.plaintexts); // ["150", "50"]
// Build program instruction
const handleBuffers = result.handles.map(h => handleToBuffer(h));
const plaintextBuffers = result.plaintexts.map(p => plaintextToBuffer(p));
const programInstruction = await program.methods
.verifyDecryption(result.handles.length, handleBuffers, plaintextBuffers)
.accounts({
authority: wallet.publicKey,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
// Build and send transaction
const tx = new Transaction();
result.ed25519Instructions.forEach(ix => tx.add(ix));
tx.add(programInstruction);
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = wallet.publicKey;
const signedTx = await wallet.signTransaction(tx);
const signature = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(signature, "confirmed");
console.log("On-chain verification tx:", signature);
console.log("Verified on-chain:");
console.log(" 100 + 50 =", result.plaintexts[0]);
console.log(" 100 - 50 =", result.plaintexts[1]);
Example: Verify e_select Result On-Chain
const encryptedA = await encryptValue(BigInt(100));
const encryptedB = await encryptValue(BigInt(50));
const encryptedIfTrue = await encryptValue(BigInt(999));
const encryptedIfFalse = await encryptValue(BigInt(0));
const selectTx = await program.methods
.testEselect(
hexToBuffer(encryptedA),
hexToBuffer(encryptedB),
hexToBuffer(encryptedIfTrue),
hexToBuffer(encryptedIfFalse),
0
)
.accounts({
authority: wallet.publicKey,
})
.rpc();
console.log("Select tx:", selectTx);
const resultHandle = await getHandleFromTx(selectTx, "Select result handle:");
console.log("Result handle:", resultHandle);
// Decrypt (requires wallet signature)
const result = await decrypt([resultHandle], {
address: wallet.publicKey,
signMessage: wallet.signMessage,
});
console.log("Decrypted select result:", result.plaintexts[0]); // "999"
// Build program instruction
const handleBuffers = result.handles.map(h => handleToBuffer(h));
const plaintextBuffers = result.plaintexts.map(p => plaintextToBuffer(p));
const programInstruction = await program.methods
.verifyDecryption(result.handles.length, handleBuffers, plaintextBuffers)
.accounts({
authority: wallet.publicKey,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
const tx = new Transaction();
result.ed25519Instructions.forEach(ix => tx.add(ix));
tx.add(programInstruction);
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = wallet.publicKey;
const signedTx = await wallet.signTransaction(tx);
const signature = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(signature, "confirmed");
console.log("On-chain verification tx:", signature);
console.log("Verified on-chain: select(100>=50, 999, 0) =", result.plaintexts[0]);
React Integration
import { useWallet, useConnection } from '@solana/wallet-adapter-react';
import { decrypt, AttestedDecryptError } from '@inco/solana-sdk/attested-decrypt';
import { handleToBuffer, plaintextToBuffer } from '@inco/solana-sdk/utils';
import { Transaction, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js';
import { useState } from 'react';
function DecryptAndVerify({ handles }: { handles: string[] }) {
const wallet = useWallet();
const { connection } = useConnection();
const [result, setResult] = useState<string[] | null>(null);
const [txSignature, setTxSignature] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleDecryptAndVerify = async () => {
if (!wallet.publicKey || !wallet.signTransaction || !wallet.signMessage) {
setError('Please connect your wallet');
return;
}
setLoading(true);
setError(null);
try {
// Step 1: Decrypt handles (requires wallet signature for authentication)
const decryptResult = await decrypt(handles, {
address: wallet.publicKey,
signMessage: wallet.signMessage!,
});
setResult(decryptResult.plaintexts);
// Step 2: Build program instruction
const handleBuffers = decryptResult.handles.map(h => handleToBuffer(h));
const plaintextBuffers = decryptResult.plaintexts.map(p => plaintextToBuffer(p));
const programInstruction = await program.methods
.verifyDecryption(decryptResult.handles.length, handleBuffers, plaintextBuffers)
.accounts({
authority: wallet.publicKey,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
// Step 3: Build transaction
const tx = new Transaction();
decryptResult.ed25519Instructions.forEach(ix => tx.add(ix));
tx.add(programInstruction);
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = wallet.publicKey;
// Step 4: Sign and send
const signedTx = await wallet.signTransaction(tx);
const signature = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(signature, 'confirmed');
setTxSignature(signature);
} catch (err) {
if (err instanceof AttestedDecryptError) {
setError(err.message);
} else {
setError('Failed to decrypt and verify');
}
console.error('Decrypt and verify failed:', err);
} finally {
setLoading(false);
}
};
return (
<div className="p-4 border rounded-lg">
<h3 className="text-lg font-semibold mb-4">Decrypt & Verify On-Chain</h3>
<button
onClick={handleDecryptAndVerify}
disabled={loading || !wallet.connected}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{loading ? 'Processing...' : 'Decrypt & Verify'}
</button>
{result && (
<div className="mt-4">
<h4 className="font-medium">Decrypted Values:</h4>
<ul className="list-disc list-inside">
{result.map((value, i) => (
<li key={i}>Handle {i + 1}: {value}</li>
))}
</ul>
</div>
)}
{txSignature && (
<p className="mt-2 text-green-600">
Verified on-chain:{' '}
<a
href={`https://explorer.solana.com/tx/${txSignature}?cluster=devnet`}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{txSignature.slice(0, 8)}...
</a>
</p>
)}
{error && <p className="mt-2 text-red-500">{error}</p>}
</div>
);
}
export default DecryptAndVerify;
Getting Handles
There are two ways to get encrypted handles for decryption:
Option 1: From Transaction Logs
When your program emits handles in logs after an operation:
async function getHandleFromTx(
txSignature: string,
logPrefix: string
): Promise<string> {
// Wait for transaction to be confirmed
await new Promise(resolve => setTimeout(resolve, 2000));
const txDetails = await connection.getTransaction(txSignature, {
commitment: "confirmed",
maxSupportedTransactionVersion: 0,
});
const logs = txDetails?.meta?.logMessages || [];
for (const log of logs) {
if (log.includes(logPrefix)) {
const match = log.match(/(\d+)/);
if (match) return match[1];
}
}
throw new Error(`Handle not found in logs for prefix: ${logPrefix}`);
}
// Usage
const handle = await getHandleFromTx(txSignature, "Addition result handle:");
Option 2: From On-Chain Account (e.g., Token Balance PDA)
When the encrypted handle is stored in an account (like a confidential token balance):
import { PublicKey } from '@solana/web3.js';
// Derive the token account PDA
const [tokenAccountPda] = PublicKey.findProgramAddressSync(
[
Buffer.from("token_account"),
mintPubkey.toBuffer(),
ownerPubkey.toBuffer(),
],
INCO_TOKEN_PROGRAM_ID
);
// Fetch the account data
const accountInfo = await connection.getAccountInfo(tokenAccountPda);
if (!accountInfo) {
throw new Error("Token account not found");
}
// Parse the account data to extract the encrypted balance handle
const accountData = program.coder.accounts.decode(
"TokenAccount",
accountInfo.data
);
// Get the encrypted balance handle (stored as u128 or similar)
const balanceHandle = accountData.encryptedBalance.toString();
// Now use the handle for attested decrypt with on-chain verification
const result = await decrypt([balanceHandle], {
address: wallet.publicKey,
signMessage: wallet.signMessage,
});
// Build verification transaction...
Example: Verify Token Balance On-Chain
import { decrypt } from '@inco/solana-sdk/attested-decrypt';
import { handleToBuffer, plaintextToBuffer } from '@inco/solana-sdk/utils';
import { PublicKey, Transaction, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js';
async function verifyTokenBalanceOnChain(
connection: Connection,
mint: PublicKey,
owner: PublicKey
): Promise<{ balance: string; txSignature: string }> {
// Derive Associated Token Account (ATA) PDA
const [ata] = PublicKey.findProgramAddressSync(
[
owner.toBuffer(),
INCO_TOKEN_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
],
ASSOCIATED_TOKEN_PROGRAM_ID
);
// Fetch and parse account
const accountInfo = await connection.getAccountInfo(ata);
if (!accountInfo) {
throw new Error("Token account not found");
}
const tokenAccount = program.coder.accounts.decode(
"IncoTokenAccount",
accountInfo.data
);
// Extract encrypted balance handle
const balanceHandle = tokenAccount.amount.toString();
// Decrypt with Ed25519 verification (requires wallet signature)
const result = await decrypt([balanceHandle], {
address: owner,
signMessage: wallet.signMessage,
});
// Build program instruction
const handleBuffers = result.handles.map(h => handleToBuffer(h));
const plaintextBuffers = result.plaintexts.map(p => plaintextToBuffer(p));
const programInstruction = await program.methods
.verifyDecryption(result.handles.length, handleBuffers, plaintextBuffers)
.accounts({
authority: owner,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
// Build and send transaction
const tx = new Transaction();
result.ed25519Instructions.forEach(ix => tx.add(ix));
tx.add(programInstruction);
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = owner;
const signedTx = await wallet.signTransaction(tx);
const signature = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(signature, "confirmed");
return {
balance: result.plaintexts[0],
txSignature: signature,
};
}
// Usage
const { balance, txSignature } = await verifyTokenBalanceOnChain(
connection,
mintPubkey,
wallet.publicKey
);
console.log("Verified balance:", balance);
console.log("Verification tx:", txSignature);
API Reference
decrypt(handles, options)
Decrypts encrypted handles and returns plaintexts with Ed25519 verification data.
Parameters:
handles: string[] - Array of encrypted handles as decimal strings (max 10)
options: DecryptOptions - Wallet authentication options
Handles must be passed as decimal string representations (e.g., "123456789012345678901234567890"), not hex strings. When reading a handle from an on-chain account (stored as u128), convert it to a decimal string using .toString().
Returns: Promise<DecryptResult>
Types
DecryptOptions
interface DecryptOptions {
address: PublicKey | string; // Wallet public key (base58 string or PublicKey)
signMessage: (message: Uint8Array) => Promise<Uint8Array>; // Message signing function
}
The signMessage function can come from:
- Wallet adapter (e.g., Phantom):
wallet.signMessage
- tweetnacl for testing:
async (msg) => nacl.sign.detached(msg, keypair.secretKey)
For complete examples, see the lightning-rod-solana tests.
DecryptResult
interface DecryptResult {
plaintexts: string[]; // Decrypted values as strings
handles: string[]; // Original handles
signatures: string[]; // Ed25519 signatures (base58)
ed25519Instructions: TransactionInstruction[]; // Instructions for on-chain verification
}
Utility Functions
import { hexToBuffer, handleToBuffer, plaintextToBuffer } from '@inco/solana-sdk/utils';
// Convert hex string to Buffer (for encrypted ciphertext)
const buffer = hexToBuffer(encryptedHex);
// Convert handle string to Buffer (for program instruction)
const handleBuf = handleToBuffer(handle);
// Convert plaintext string to Buffer (for program instruction)
const plaintextBuf = plaintextToBuffer(plaintext);
Error Handling
import { decrypt, AttestedDecryptError } from '@inco/solana-sdk/attested-decrypt';
try {
const result = await decrypt(handles, {
address: wallet.publicKey,
signMessage: wallet.signMessage,
});
} catch (error) {
if (error instanceof AttestedDecryptError) {
console.error('Attested decrypt failed:', error.message);
}
}
Errors
| Error Message | Cause | Solution |
|---|
No handles provided for decryption | Empty handles array [] | Pass at least one handle |
Maximum 10 handles per transaction. This is a Solana transaction size limit | More than 10 handles | Split into multiple calls of ≤10 handles |
At least one handle is required for decryption | Empty handles array | Pass at least one handle |
Covalidator API request failed: <details> | Covalidator error | Check network; handle may be invalid |
Covalidator returned empty plaintext | Missing plaintext | Handle may be invalid or not found |
Covalidator signature verification failed. The response may be tampered | Ed25519 verification failed | Response integrity compromised |
Failed to build attested decrypt transaction | Transaction construction failed | Check handles and instruction builder |
Transaction failed: <details> | On-chain execution failed | Check Solana explorer for details |
Failed to create Ed25519 verification instruction: <details> | Ed25519 instruction failed | Signature format may be invalid |
Attested decryption failed: <details> | General failure | Check error details |
When to Use Attested Decrypt vs Attested Reveal
| Use Case | Use |
|---|
| Conditional on-chain logic based on decrypted value | Attested Decrypt |
| Verify decryption proof on-chain | Attested Decrypt |
| Display balance in UI | Attested Reveal |
| Show transaction result to user | Attested Reveal |