Skip to main content

Access Control

Use allow and is_allowed to manage decryption permissions for handles.
The access control design is WIP and subject to change in future versions.

granting Access

pub fn grant_decrypt_access(
    ctx: Context<GrantAccess>,
    handle: u128,
    allowed_address: Pubkey,
) -> Result<()> {
    let cpi_ctx = CpiContext::new(
        ctx.accounts.inco_lightning_program.to_account_info(),
        Allow {
            allowance_account: ctx.accounts.allowance_account.to_account_info(),
            signer: ctx.accounts.authority.to_account_info(),
            allowed_address: ctx.accounts.allowed_address.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
        },
    );

    allow(cpi_ctx, handle, true, allowed_address)?;
    Ok(())
}

Checking Access

pub fn check_access(
    ctx: Context<CheckAccess>,
    handle: u128,
) -> Result<bool> {
    let cpi_ctx = CpiContext::new(
        ctx.accounts.inco_lightning_program.to_account_info(),
        IsAllowed {
            allowance_account: ctx.accounts.allowance_account.to_account_info(),
            allowed_address: ctx.accounts.allowed_address.to_account_info(),
        },
    );

    is_allowed(cpi_ctx, handle)
}

Access Control Functions

FunctionDescription
allowGrant or revoke decryption access for a specific address
is_allowedCheck if an address has decryption permission for a handle

Best Practice: Minimal Permissions

Grant minimal decryption permissions:
// Only allow the account owner to decrypt their balance
allow(ctx, balance_handle, true, owner_pubkey)?;
Access control is essential for privacy. Without explicit permission grants, encrypted values cannot be decrypted.

Client-Side: Simulation to Get Handles

Since encrypted operations produce new handles, you often need to simulate the transaction first to get the resulting handle before you can derive the allowance PDA and grant access in the same transaction.

Why Simulation is Needed

  1. Operations like e_add, mint_to, transfer produce new handles
  2. The allowance PDA is derived from [handle.to_le_bytes(), allowed_address]
  3. You need the handle value before you can compute the PDA address
  4. Simulation lets you “peek” at the result handle before actually submitting

Simulation Pattern

import { PublicKey } from '@solana/web3.js';

const INCO_LIGHTNING_PROGRAM_ID = new PublicKey('5sjEbPiqgZrYwR31ahR6Uk9wf5awoX61YGg7jExQSwaj');

// Helper: derive allowance PDA from handle
function getAllowancePda(handle: bigint, allowedAddress: PublicKey): [PublicKey, number] {
  const handleBuffer = Buffer.alloc(16);
  let h = handle;
  for (let i = 0; i < 16; i++) {
    handleBuffer[i] = Number(h & BigInt(0xff));
    h = h >> BigInt(8);
  }
  return PublicKey.findProgramAddressSync(
    [handleBuffer, allowedAddress.toBuffer()],
    INCO_LIGHTNING_PROGRAM_ID
  );
}

// Helper: simulate transaction and extract handle from account data
async function simulateAndGetHandle(
  connection: Connection,
  tx: Transaction,
  accountPubkey: PublicKey,
  walletKeypair: Keypair
): Promise<bigint | null> {
  const { blockhash } = await connection.getLatestBlockhash();
  tx.recentBlockhash = blockhash;
  tx.feePayer = walletKeypair.publicKey;
  tx.sign(walletKeypair);

  const simulation = await connection.simulateTransaction(tx, undefined, [accountPubkey]);
  if (simulation.value.err) return null;

  if (simulation.value.accounts?.[0]?.data) {
    const data = Buffer.from(simulation.value.accounts[0].data[0], "base64");
    // Extract handle from account data (offset depends on your account struct)
    const amountBytes = data.slice(72, 88);  // Adjust offset as needed
    let handle = BigInt(0);
    for (let i = 15; i >= 0; i--) {
      handle = handle * BigInt(256) + BigInt(amountBytes[i]);
    }
    return handle;
  }
  return null;
}

Example: Mint with Auto-Allow

// Step 1: Build transaction for simulation (without allowance accounts)
const txForSim = await program.methods
  .mintTo(hexToBuffer(encryptedAmount), inputType)
  .accounts({
    mint: mintKeypair.publicKey,
    account: ownerAccountKp.publicKey,
    mintAuthority: walletKeypair.publicKey,
    incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
    systemProgram: SystemProgram.programId,
  })
  .transaction();

// Step 2: Simulate to get the new handle
const newHandle = await simulateAndGetHandle(connection, txForSim, ownerAccountKp.publicKey, walletKeypair);

// Step 3: Derive allowance PDA from the simulated handle
const [allowancePda] = getAllowancePda(newHandle!, walletKeypair.publicKey);

// Step 4: Execute real transaction with allowance accounts
const tx = await program.methods
  .mintTo(hexToBuffer(encryptedAmount), inputType)
  .accounts({
    mint: mintKeypair.publicKey,
    account: ownerAccountKp.publicKey,
    mintAuthority: walletKeypair.publicKey,
    incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
    systemProgram: SystemProgram.programId,
  })
  .remainingAccounts([
    { pubkey: allowancePda, isSigner: false, isWritable: true },
    { pubkey: walletKeypair.publicKey, isSigner: false, isWritable: false },
  ])
  .rpc();

Example: Transfer with Auto-Allow for Both Accounts

// Step 1: Build transaction for simulation
const txForSim = await program.methods
  .transfer(hexToBuffer(encryptedAmount), inputType)
  .accounts({
    source: sourceAccountKp.publicKey,
    destination: destAccountKp.publicKey,
    authority: walletKeypair.publicKey,
    incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
    systemProgram: SystemProgram.programId,
  })
  .transaction();

// Step 2: Simulate to get both new handles
const { blockhash } = await connection.getLatestBlockhash();
txForSim.recentBlockhash = blockhash;
txForSim.feePayer = walletKeypair.publicKey;
txForSim.sign(walletKeypair);

const simulation = await connection.simulateTransaction(
  txForSim,
  undefined,
  [sourceAccountKp.publicKey, destAccountKp.publicKey]
);

// Extract handles from both accounts
const sourceHandle = extractHandleFromSimulation(simulation.value.accounts?.[0]);
const destHandle = extractHandleFromSimulation(simulation.value.accounts?.[1]);

// Step 3: Derive allowance PDAs
const [sourceAllowancePda] = getAllowancePda(sourceHandle!, walletKeypair.publicKey);
const [destAllowancePda] = getAllowancePda(destHandle!, walletKeypair.publicKey);

// Step 4: Execute with allowance accounts for both source and destination
const tx = await program.methods
  .transfer(hexToBuffer(encryptedAmount), inputType)
  .accounts({
    source: sourceAccountKp.publicKey,
    destination: destAccountKp.publicKey,
    authority: walletKeypair.publicKey,
    incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
    systemProgram: SystemProgram.programId,
  })
  .remainingAccounts([
    { pubkey: sourceAllowancePda, isSigner: false, isWritable: true },
    { pubkey: walletKeypair.publicKey, isSigner: false, isWritable: false },
    { pubkey: destAllowancePda, isSigner: false, isWritable: true },
    { pubkey: walletKeypair.publicKey, isSigner: false, isWritable: false },
  ])
  .rpc();
For complete working examples with simulation patterns, see the lightning-rod-solana repository.