Skip to main content

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

  1. Send encrypted handles to the covalidator
  2. Covalidator decrypts and returns plaintexts with Ed25519 signatures
  3. Build a transaction with Ed25519 verification instructions
  4. Add your program instruction that uses the decrypted values
  5. 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 MessageCauseSolution
No handles provided for decryptionEmpty handles array []Pass at least one handle
Maximum 10 handles per transaction. This is a Solana transaction size limitMore than 10 handlesSplit into multiple calls of ≤10 handles
At least one handle is required for decryptionEmpty handles arrayPass at least one handle
Covalidator API request failed: <details>Covalidator errorCheck network; handle may be invalid
Covalidator returned empty plaintextMissing plaintextHandle may be invalid or not found
Covalidator signature verification failed. The response may be tamperedEd25519 verification failedResponse integrity compromised
Failed to build attested decrypt transactionTransaction construction failedCheck handles and instruction builder
Transaction failed: <details>On-chain execution failedCheck Solana explorer for details
Failed to create Ed25519 verification instruction: <details>Ed25519 instruction failedSignature format may be invalid
Attested decryption failed: <details>General failureCheck error details

When to Use Attested Decrypt vs Attested Reveal

Use CaseUse
Conditional on-chain logic based on decrypted valueAttested Decrypt
Verify decryption proof on-chainAttested Decrypt
Display balance in UIAttested Reveal
Show transaction result to userAttested Reveal