Skip to main content

Access control

Access control ensures only the appropriate parties have access to encrypted data. SPF manages access control during the lifecycle of a ciphertext using an access control list (ACL). The TypeScript client library provides simple functions for managing access permissions.

Ensure the client is initialized before using these functions.

Access permission types

SPF supports three types of access permissions:

  • Run access: Permission to use the ciphertext as input to a specific FHE program.
  • Decrypt access: Permission to request threshold decryption of the ciphertext.
  • Admin access: Full control over the ciphertext, including the ability to run, decrypt, and grant permissions to other addresses.

Ciphertext IDs are immutable

When access permissions are updated, SPF creates a new ciphertext with the updated ACL and returns a new ciphertext ID. The original ciphertext remains unchanged. This leads to a development flow where ciphertext operations are chained together, with each operation producing a new ciphertext ID that is used in the next operation.

Since ciphertexts are immutable, this means that the access cannot be revoked to a ciphertext once it has been granted. Carefully assign your permissions in your applications and provide only the smallest needed permissions to achieve the desired application.

Granting access permissions

The client library provides three convenience functions for common access control operations. Each function grants a single permission and returns a new ciphertext ID.

Granting run access

Grant permission to use the ciphertext as input to a specific FHE program:

export async function simpleAllowRunExample(
admin: spf.PrivateKeySigner,
ciphertextId: spf.CiphertextId,
runnerAddress: spf.Address,
libraryId: spf.LibraryId,
): Promise<spf.CiphertextId> {
// Grant run access to the runner for a specific program
const newCtId = await spf.allowRun(
admin,
ciphertextId,
runnerAddress,
libraryId,
spf.asProgramName("tally_votes"),
);

console.log("Run access granted:", newCtId);

return newCtId;
}

Parameters:

  • signer: AnySigner - Signer for authentication (must have admin access)
  • ciphertextId: CiphertextId - The ciphertext identifier
  • address: Address - Address to grant run access (use asAddress() to convert string to branded type)
  • libraryId: LibraryId - The SPF library identifier (program hash)
  • programName: ProgramName - The program entry point name
  • chainId (optional): number - Chain ID for on-chain (contract) access

Returns: Promise<CiphertextId> - The new ciphertext identifier with updated ACL

This is the most common pattern when users upload encrypted data and need to grant a specific program permission to use it.

Granting decrypt access

Grant permission to request threshold decryption of the ciphertext:

export async function simpleAllowDecryptExample(
admin: spf.PrivateKeySigner,
ciphertextId: spf.CiphertextId,
decrypterAddress: spf.Address,
): Promise<spf.CiphertextId> {
// Grant decrypt access to another address
const newCtId = await spf.allowDecrypt(admin, ciphertextId, decrypterAddress);

console.log("Decrypt access granted:", newCtId);

return newCtId;
}

Parameters:

  • signer: AnySigner - Signer for authentication (must have admin access)
  • ciphertextId: CiphertextId - The ciphertext identifier
  • address: Address - Address to grant decrypt access (use asAddress() to convert string to branded type)
  • chainId (optional): number - Chain ID for on-chain (contract) access

Returns: Promise<CiphertextId> - The new ciphertext identifier with updated ACL

Grant this permission to allow another party to view decrypted results without admin privileges.

Granting admin access

Grant full control over the ciphertext, including run, decrypt, and permission management:

export async function simpleAllowAdminExample(
admin: spf.PrivateKeySigner,
ciphertextId: spf.CiphertextId,
newAdminAddress: spf.Address,
): Promise<spf.CiphertextId> {
// Grant admin access to another address
const newCtId = await spf.allowAdmin(admin, ciphertextId, newAdminAddress);

console.log("Admin access granted:", newCtId);

return newCtId;
}

Parameters:

  • signer: AnySigner - Signer for authentication (must have admin access)
  • ciphertextId: CiphertextId - The ciphertext identifier
  • address: Address - Address to grant admin access (use asAddress() to convert string to branded type)
  • chainId (optional): number - Chain ID for on-chain (contract) access

Returns: Promise<CiphertextId> - The new ciphertext identifier with updated ACL

Admin access includes all permissions and the ability to modify the ACL further.

Common workflow: encrypt, upload, and grant access

A typical pattern is for a user to encrypt a value, upload it, and grant run access to a program runner:

export async function grantRunAccessExample(
voter: spf.PrivateKeySigner,
runnerAddress: spf.Address,
libraryId: spf.LibraryId,
): Promise<spf.CiphertextId> {
// Voter creates and uploads encrypted vote
const voteValue = 1; // Approve
const ciphertext = await spf.encryptValue(voteValue, 8);
const uploadedId = await spf.uploadCiphertext(voter, ciphertext);

// Use simple API to grant run access
const aclAppliedId = await spf.allowRun(
voter,
uploadedId,
runnerAddress,
libraryId,
spf.asProgramName("tally_votes"),
);

// Runner can now use aclAppliedId as input to the program
console.log("Vote ready for tallying:", aclAppliedId);

return aclAppliedId;
}

The user encrypts the value client-side, uploads the ciphertext, and grants run access in a single workflow. The runner can then use the ACL-applied ciphertext ID as input to the program.

Smart contract access

By default, access permissions are granted for addresses. To grant access to smart contracts, specify the chainId parameter:

export async function simpleOnChainAccessExample(
signer: spf.PrivateKeySigner,
ctId: spf.CiphertextId,
libraryId: spf.LibraryId,
): Promise<{
decryptCtId: spf.CiphertextId;
runCtId: spf.CiphertextId;
}> {
// Grant on-chain decrypt access to a contract on Ethereum mainnet (chain ID 1)
const contractAddress = spf.asAddress(
"0x1234567890123456789012345678901234567890",
);
const newCtId = await spf.allowDecrypt(signer, ctId, contractAddress, 1);

// Grant on-chain run access to a contract
const newCtIdWithRun = await spf.allowRun(
signer,
ctId,
contractAddress,
libraryId,
spf.asProgramName("tally_votes"),
1,
);

return { decryptCtId: newCtId, runCtId: newCtIdWithRun };
}

When chainId is provided, the access permission is granted for on-chain use by contracts on that specific blockchain. This enables smart contracts to interact with encrypted data using the SPF network.

Checking access

To verify whether an address has specific access to a ciphertext, use the checkCiphertextAccess() function:

export async function checkAccessExample(
signer: spf.PrivateKeySigner,
ciphertextId: spf.CiphertextId,
): Promise<{
hasDecrypt: boolean;
hasAdmin: boolean;
hasRun: boolean;
}> {
// Check if an address has decrypt access
const hasDecrypt: boolean = await spf.checkCiphertextAccess(ciphertextId, {
type: "decrypt",
address: signer.getAddress(),
});

if (hasDecrypt) {
console.log("Decrypt access granted");
} else {
console.log("Decrypt access denied");
}

// Check admin access
const hasAdmin = await spf.checkCiphertextAccess(ciphertextId, {
type: "admin",
address: signer.getAddress(),
});

// Check run access for a specific program
const hasRun = await spf.checkCiphertextAccess(ciphertextId, {
type: "run",
libraryHash: spf.asLibraryId(
"0x7f7e6993da0d371c4f7f4573df315fe3638d253b74c3f98e946f9b9737b5b51b",
),
entryPoint: spf.asProgramName("tally_votes"),
address: signer.getAddress(),
});

return { hasDecrypt, hasAdmin, hasRun };
}

The function checks all three access types: decrypt, admin, and run. For run access, specify the library hash and entry point for the specific program.

Parameters:

  • ciphertextId: CiphertextId - The ciphertext identifier
  • accessType: AccessType - The type of access to check. This is an object that must include:
    • type: The access type ("admin", "decrypt", or "run")
    • address: Address - The address to check access for (required)
    • chainId (optional): number - Chain ID for checking on-chain (contract) access. Omit for external (wallet) access
    • For type: "run", also include:
      • libraryHash: LibraryId - The SPF library identifier
      • entryPoint: ProgramName - The program entry point name

Access type examples:

  • {type: "admin", address: "0x..."} - Check admin access for an address
  • {type: "decrypt", address: "0x...", chainId: 1} - Check on-chain decrypt access
  • {type: "run", address: "0x...", libraryHash: libId, entryPoint: programName} - Check run access

Returns: Promise<boolean> - true if access is granted, false otherwise

Note: This function does not require authentication. It queries the SPF network to check if a specific address has access to a ciphertext.

Checking access for other addresses

Since the address field is required in the access type, you can check access for any address without authentication:

export async function checkAccessForOtherExample(
admin: spf.PrivateKeySigner,
ciphertextId: spf.CiphertextId,
targetAddress: spf.Address,
): Promise<boolean> {
// Check if another address has decrypt access
const hasAccess = await spf.checkCiphertextAccess(ciphertextId, {
type: "decrypt",
address: targetAddress,
});

if (hasAccess) {
console.log(`Address ${targetAddress} has decrypt access`);
} else {
console.log(`Address ${targetAddress} does not have decrypt access`);
}

return hasAccess;
}

This pattern is useful to verify access permissions for specific addresses before performing operations. No special privileges are required to check access.

Advanced ACL Operations

Retrieving access signatures signed by SPF

A user can request the SPF create a cryptographic signature proving that an address has specific access to a ciphertext. Smart contracts can then use this signature for on-chain verification that a ciphertext exists with correct permissions and bit size, without querying the SPF network directly. A common use case of this verification is to verify that a ciphertext has the correct bit width and run access to a specific program before requesting a computation.

export async function getAccessSignatureExample(
signer: spf.PrivateKeySigner,
ciphertextId: spf.CiphertextId,
): Promise<spf.AclSignatureResponse> {
const result = await spf.getCiphertextAccessSignature(ciphertextId, {
type: "decrypt",
address: signer.getAddress(),
});

console.log("Signature:", result.signature);
console.log("Message that was signed:");
console.log(" - Ciphertext ID:", result.message.ciphertextId);
console.log(" - Bit width:", result.message.bitWidth);
console.log(" - Access bytes (hex):", result.message.access);
console.log("Access change:", result.accessChange);

return result;
}

Parameters:

  • ciphertextId: CiphertextId - The ciphertext identifier
  • accessType: AccessType - The type of access to verify. This is an object that must include:
    • type: The access type ("admin", "decrypt", or "run")
    • address: Address - The address to verify access for (required)
    • chainId (optional): number - Chain ID for checking on-chain access
    • For type: "run", also include libraryHash and entryPoint

Returns: Promise<AclSignatureResponse> with:

  • signature: Signature - The cryptographic signature (branded hex string with 0x prefix)
  • message: SpfCiphertextAccessConfirmation - The message that was signed, containing:
    • ciphertextId: The ciphertext identifier
    • bitWidth: The bit width of the ciphertext (useful for on-chain validation)
    • access: The raw access bytes as a hex string
  • accessChange: AccessChange - The parsed access change being verified (one of NewAdmin, NewDecrypt, or NewRun)

The response includes type guards (isNewAdmin, isNewDecrypt, isNewRun) to safely narrow the accessChange type at runtime. These enable TypeScript to provide autocomplete and type safety when inspecting the access change details.

Note: This function does not require authentication and can be called by anyone to retrieve the access signature for a given address.