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
| Function | Description |
|---|
allow | Grant or revoke decryption access for a specific address |
is_allowed | Check 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
- Operations like
e_add, mint_to, transfer produce new handles
- The allowance PDA is derived from
[handle.to_le_bytes(), allowed_address]
- You need the handle value before you can compute the PDA address
- 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();