Writing the Programs
Build the Anchor programs for access control and decryption verification
Writing the Programs
In this section, you'll create two Anchor programs:
- access_control - Manages blob registration and purchases
- ace_hook - Verifies access for threshold decryption
Already Deployed Programs
The programs are already deployed on Solana testnet:
| Program | Address |
|---|---|
access_control | 6XyAbrfHK5sinJAj3nXEVG2ALzKTXQv89JLuYwXictGV |
ace_hook | 8jDv41SQVKCaVtkbFS1ZaVDDCEtKkAc7QXV3Y1psGts9 |
Feel free to use these deployed programs instead of building and deploying your own. Simply configure the program IDs in your .env file and skip to the frontend section.
Program Architecture
Our system needs two programs working together:
┌─────────────────────────────────────────────────────────────────┐
│ access_control │
│ • register_blob() - Store greenBox + price │
│ • purchase() - Transfer SOL, create receipt │
│ • BlobMetadata PDA - Stores encrypted key info │
│ • Receipt PDA - Proves purchase │
└─────────────────────────────────────────────────────────────────┘
│
│ reads
▼
┌─────────────────────────────────────────────────────────────────┐
│ ace_hook │
│ • assert_access() - Verify receipt for decryption │
│ • Called by buyers as proof-of-permission │
│ • Verified by ACE workers │
└─────────────────────────────────────────────────────────────────┘Part A: The access_control Program
This program handles the core marketplace functionality: registering encrypted files and processing purchases.
Create Program Files
Cargo.toml
Create anchor/programs/access_control/Cargo.toml:
[package]
name = "access_control"
version = "0.1.0"
description = "Token-gated access control for encrypted files on Shelby"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "access_control"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = "0.31.1"
sha3 = "0.10"lib.rs - Main Program
Create anchor/programs/access_control/src/lib.rs:
use anchor_lang::prelude::*;
declare_id!("YOUR_PROGRAM_ID_HERE");
pub mod instructions;
pub use instructions::*;
/// Metadata for an encrypted blob registered on-chain.
/// Stores the encrypted key (greenBox) and price for access.
#[account]
pub struct BlobMetadata {
/// The Solana owner who registered this blob
pub owner: Pubkey,
/// Encryption scheme used for the greenBox (2 = threshold IBE)
pub green_box_scheme: u8,
/// The encrypted cipher key (greenBox) that can be decrypted via ACE
pub green_box_bytes: Vec<u8>,
/// Sequence number for tracking updates
pub seqnum: u64,
/// Price in lamports to purchase access
pub price: u64,
}
/// Receipt proving a buyer has purchased access to a blob.
#[account]
pub struct Receipt {
/// Sequence number at time of purchase (must match blob's seqnum)
pub seqnum: u64,
}
#[program]
pub mod access_control {
use super::*;
/// Register a new encrypted blob with its greenBox and price.
/// Called by the file owner after uploading encrypted content to Shelby.
pub fn register_blob(
ctx: Context<RegisterBlob>,
storage_account_address: [u8; 32],
blob_name: String,
green_box_scheme: u8,
green_box_bytes: Vec<u8>,
price: u64,
) -> Result<()> {
msg!("register_blob: blob_name={}", blob_name);
msg!("register_blob: green_box_scheme={}", green_box_scheme);
msg!("register_blob: green_box_bytes_len={}", green_box_bytes.len());
msg!("register_blob: price={}", price);
instructions::register_blob::handler(
ctx,
storage_account_address,
blob_name,
green_box_scheme,
green_box_bytes,
price,
)
}
/// Purchase access to a blob by paying the owner.
/// Creates a receipt PDA that proves the buyer has paid.
pub fn purchase(
ctx: Context<Purchase>,
storage_account_address: [u8; 32],
blob_name: String
) -> Result<()> {
instructions::purchase::handler(ctx, storage_account_address, blob_name)
}
}instructions/mod.rs
Create anchor/programs/access_control/src/instructions/mod.rs:
pub mod register_blob;
pub mod purchase;
pub use register_blob::*;
pub use purchase::*;instructions/register_blob.rs
Create anchor/programs/access_control/src/instructions/register_blob.rs:
use anchor_lang::prelude::*;
use crate::BlobMetadata;
#[derive(Accounts)]
#[instruction(storage_account_address: [u8; 32], blob_name: String)]
pub struct RegisterBlob<'info> {
#[account(
init,
payer = owner,
// discriminator + owner + scheme + greenBox + seqnum + price
// NOTE: Solana CPI limits account creation to 10KB (10240 bytes)
// greenBox is typically ~300 bytes, allocate 1KB to be safe
space = 8 // discriminator
+ 32 // owner
+ 1 // scheme
+ 4 + 1024 // green_box_bytes vec len + data (1KB)
+ 8 // seqnum
+ 8, // price
seeds = [b"blob_metadata", storage_account_address.as_ref(), blob_name.as_bytes()],
bump
)]
pub blob_metadata: Account<'info, BlobMetadata>,
#[account(mut)]
pub owner: Signer<'info>,
pub system_program: Program<'info, System>,
}
pub fn handler(
ctx: Context<RegisterBlob>,
_storage_account_address: [u8; 32],
_blob_name: String,
green_box_scheme: u8,
green_box_bytes: Vec<u8>,
price: u64,
) -> Result<()> {
let blob_metadata = &mut ctx.accounts.blob_metadata;
blob_metadata.owner = ctx.accounts.owner.key();
blob_metadata.green_box_scheme = green_box_scheme;
blob_metadata.green_box_bytes = green_box_bytes;
blob_metadata.price = price;
blob_metadata.seqnum += 1;
Ok(())
}instructions/purchase.rs
Create anchor/programs/access_control/src/instructions/purchase.rs:
use anchor_lang::prelude::*;
use crate::{BlobMetadata, Receipt};
#[derive(Accounts)]
#[instruction(storage_account_address: [u8; 32], blob_name: String)]
pub struct Purchase<'info> {
#[account(
seeds = [b"blob_metadata", storage_account_address.as_ref(), blob_name.as_bytes()],
bump
)]
pub blob_metadata: Account<'info, BlobMetadata>,
#[account(
init,
payer = buyer,
space = 8 + 8, // discriminator + seqnum
seeds = [b"access", storage_account_address.as_ref(), blob_name.as_bytes(), buyer.key().as_ref()],
bump
)]
pub receipt: Account<'info, Receipt>,
#[account(mut)]
pub buyer: Signer<'info>,
/// CHECK: We verify this matches blob_metadata.owner
#[account(mut)]
pub owner: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
pub fn handler(
ctx: Context<Purchase>,
_storage_account_address: [u8; 32],
_blob_name: String
) -> Result<()> {
let blob_metadata = &ctx.accounts.blob_metadata;
msg!("purchase: price={}", blob_metadata.price);
msg!("purchase: seqnum={}", blob_metadata.seqnum);
// Verify the owner account matches the blob's registered owner
require!(
ctx.accounts.owner.key() == blob_metadata.owner,
PurchaseError::InvalidOwner
);
// Transfer SOL from buyer to owner
anchor_lang::solana_program::program::invoke(
&anchor_lang::solana_program::system_instruction::transfer(
ctx.accounts.buyer.key,
&ctx.accounts.owner.key(),
blob_metadata.price,
),
&[
ctx.accounts.buyer.to_account_info(),
ctx.accounts.owner.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
// Record the purchase
let receipt = &mut ctx.accounts.receipt;
receipt.seqnum = blob_metadata.seqnum;
Ok(())
}
#[error_code]
pub enum PurchaseError {
#[msg("Owner account does not match the blob's registered owner")]
InvalidOwner,
}Part B: The ace_hook Program
This program verifies that a user has purchased access to a blob. It's called by buyers to create a "proof of permission" that ACE workers verify before releasing decryption keys.
Why a Separate Program?
The ACE (Access Control Encryption) system needs to verify that a user has legitimate access before releasing decryption key shares. Workers do this by:
- Receiving a signed transaction from the buyer
- Simulating the transaction on-chain
- Verifying it calls the correct
assert_accessfunction
Having a separate program makes verification clean and auditable—workers can check the exact program ID and instruction being called.
What are ACE Workers?
ACE workers are a distributed network of nodes that collectively manage encryption keys. No single worker holds the complete decryption key—instead, the key is split across multiple workers using threshold cryptography.
When a buyer wants to decrypt a file:
- They sign a transaction calling
assert_accessto prove they have a valid receipt - They send this signed transaction to the workers as a "proof of permission"
- Each worker verifies the transaction on-chain and returns their key share
- The client combines the shares to reconstruct the decryption key
Deployed Workers (Testnet):
| Worker | Endpoint |
|---|---|
| Worker 0 | https://ace-worker-0-646682240579.europe-west1.run.app |
| Worker 1 | https://ace-worker-1-646682240579.europe-west1.run.app |
The threshold is set to 2, meaning both workers must agree to release their key shares.
Create Program Files
Cargo.toml
Create anchor/programs/ace_hook/Cargo.toml:
[package]
name = "ace_hook"
version = "0.1.0"
description = "ACE hook - allows users to prove access by signing a transaction for decryption key providers"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "ace_hook"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = "0.31.1"
access_control = { path = "../access_control", features = ["no-entrypoint"] }lib.rs - Hook Program
Create anchor/programs/ace_hook/src/lib.rs:
#![allow(unexpected_cfgs)]
use anchor_lang::prelude::*;
use access_control::{BlobMetadata, Receipt, ID as ACCESS_CONTROL_PROGRAM_ID};
// This is an ACE-specific access control program.
// Apps need to define such a callable so that consumers can prove they are allowed
// to access a given data by signing a transaction to call this callable and
// handing the signed transaction to decryption key providers.
declare_id!("YOUR_ACE_HOOK_PROGRAM_ID_HERE");
#[program]
pub mod ace_hook {
use super::*;
/// Assert that the caller has access to the specified blob.
/// This function is called by consumers who sign a transaction proving their access.
/// The signed transaction can then be presented to decryption key providers as proof of permission.
pub fn assert_access(
ctx: Context<AssertAccess>,
full_blob_name_bytes: Vec<u8>
) -> Result<()> {
// Debug: log what we're checking
msg!("blob_metadata.owner = {}", ctx.accounts.blob_metadata.owner);
msg!("receipt.owner = {}", ctx.accounts.receipt.owner);
msg!("expected = {}", ACCESS_CONTROL_PROGRAM_ID);
// Verify blob_metadata account is owned by access_control program
if *ctx.accounts.blob_metadata.owner != ACCESS_CONTROL_PROGRAM_ID {
msg!("FAIL: blob_metadata owner");
return Err(ErrorCode::InvalidAccountOwner.into());
}
// Verify receipt account is owned by access_control program
if *ctx.accounts.receipt.owner != ACCESS_CONTROL_PROGRAM_ID {
msg!("FAIL: receipt owner");
return Err(ErrorCode::InvalidAccountOwner.into());
}
// Parse full_blob_name_bytes:
// [0:2] "0x" prefix
// [2:34] owner_aptos_addr (32 bytes)
// [34] "/" separator
// [35:] blob_name
if full_blob_name_bytes.len() < 35
|| &full_blob_name_bytes[0..2] != b"0x"
|| full_blob_name_bytes[34] != b'/'
{
return Err(ErrorCode::InvalidBlobName.into());
}
let owner_aptos_addr: [u8; 32] = full_blob_name_bytes[2..34]
.try_into()
.map_err(|_| ErrorCode::InvalidBlobName)?;
let blob_name = &full_blob_name_bytes[35..];
// Derive expected PDA for blob_metadata (using access_control program's ID)
let (expected_blob_metadata_pda, _bump) = Pubkey::find_program_address(
&[
b"blob_metadata",
owner_aptos_addr.as_ref(),
blob_name,
],
&ACCESS_CONTROL_PROGRAM_ID,
);
if ctx.accounts.blob_metadata.key() != expected_blob_metadata_pda {
return Err(ErrorCode::InvalidAccountOwner.into());
}
// Derive expected PDA for receipt (using access_control program's ID)
let (expected_receipt_pda, _bump) = Pubkey::find_program_address(
&[
b"access",
owner_aptos_addr.as_ref(),
blob_name,
ctx.accounts.user.key().as_ref(),
],
&ACCESS_CONTROL_PROGRAM_ID,
);
if ctx.accounts.receipt.key() != expected_receipt_pda {
return Err(ErrorCode::InvalidAccountOwner.into());
}
// Deserialize accounts owned by access_control program
let blob_metadata_data = &ctx.accounts.blob_metadata.try_borrow_data()?;
let mut blob_metadata_slice = &blob_metadata_data[8..]; // Skip 8-byte discriminator
let blob_metadata = BlobMetadata::deserialize(&mut blob_metadata_slice)?;
let receipt_data = &ctx.accounts.receipt.try_borrow_data()?;
let mut receipt_slice = &receipt_data[8..]; // Skip 8-byte discriminator
let receipt = Receipt::deserialize(&mut receipt_slice)?;
require!(
blob_metadata.seqnum == receipt.seqnum,
ErrorCode::AccessDenied
);
Ok(())
}
}
#[derive(Accounts)]
pub struct AssertAccess<'info> {
/// CHECK: Account owned by access_control program, we verify ownership and deserialize manually
pub blob_metadata: AccountInfo<'info>,
/// CHECK: Account owned by access_control program, we verify ownership and deserialize manually
pub receipt: AccountInfo<'info>,
pub user: Signer<'info>,
}
#[error_code]
pub enum ErrorCode {
#[msg("Access denied")]
AccessDenied,
#[msg("Invalid blob name format")]
InvalidBlobName,
#[msg("Invalid account owner")]
InvalidAccountOwner,
}Part C: Build and Deploy
Prerequisites: Fund Your Solana Wallet
Before deploying to testnet, your local Solana wallet must be funded with SOL to pay for deployment fees.
# Check your current wallet address
solana address
# Request an airdrop (testnet only, may take a few tries)
solana airdrop 2 --url testnet
# Verify your balance
solana balance --url testnetTestnet airdrops are rate-limited. If the airdrop fails, wait a few minutes and try again, or use the Solana Faucet.
Build the Programs
cd anchor
anchor buildThis generates:
- Compiled programs in
target/deploy/ - IDL files in
target/idl/ - TypeScript types in
target/types/
Get Program IDs
After building, get the generated program IDs:
solana address -k target/deploy/access_control-keypair.json
solana address -k target/deploy/ace_hook-keypair.jsonUpdate Program IDs
Update the declare_id! macros in your source files with the actual addresses:
-
anchor/programs/access_control/src/lib.rs:declare_id!("YOUR_ACCESS_CONTROL_ADDRESS"); -
anchor/programs/ace_hook/src/lib.rs:declare_id!("YOUR_ACE_HOOK_ADDRESS"); -
anchor/Anchor.toml:[programs.testnet] access_control = "YOUR_ACCESS_CONTROL_ADDRESS" ace_hook = "YOUR_ACE_HOOK_ADDRESS"
Rebuild and Deploy
# Rebuild with updated IDs
anchor build
# Deploy to testnet
anchor deploy --provider.cluster testnetUpdate Environment
After deployment, add the following to your .env file:
# Program IDs (replace with your deployed addresses)
NEXT_PUBLIC_ACCESS_CONTROL_PROGRAM_ID=your-access-control-address
NEXT_PUBLIC_ACE_HOOK_PROGRAM_ID=your-ace-hook-address
# Solana RPC
NEXT_PUBLIC_SOLANA_RPC_URL=your-choice-of-solana-rpc
# ACE Workers (testnet)
NEXT_PUBLIC_ACE_WORKER_0=https://ace-worker-0-646682240579.europe-west1.run.app
NEXT_PUBLIC_ACE_WORKER_1=https://ace-worker-1-646682240579.europe-west1.run.app
NEXT_PUBLIC_ACE_THRESHOLD=2
NEXT_PUBLIC_ACE_CHAIN_NAME=testnetYour complete .env file should now look like:
# Shelby Storage (from setup step)
NEXT_PUBLIC_SHELBYNET_API_KEY=AG-your-api-key-here
# Solana RPC
NEXT_PUBLIC_SOLANA_RPC_URL=your-choice-of-solana-rpc
# Program IDs
NEXT_PUBLIC_ACCESS_CONTROL_PROGRAM_ID=your-access-control-address
NEXT_PUBLIC_ACE_HOOK_PROGRAM_ID=your-ace-hook-address
# ACE Workers
NEXT_PUBLIC_ACE_WORKER_0=https://ace-worker-0-646682240579.europe-west1.run.app
NEXT_PUBLIC_ACE_WORKER_1=https://ace-worker-1-646682240579.europe-west1.run.app
NEXT_PUBLIC_ACE_THRESHOLD=2
NEXT_PUBLIC_ACE_CHAIN_NAME=testnetUsing Pre-deployed Programs?
If you skipped deployment, use these addresses:
| Program | Address |
|---|---|
access_control | 6XyAbrfHK5sinJAj3nXEVG2ALzKTXQv89JLuYwXictGV |
ace_hook | 8jDv41SQVKCaVtkbFS1ZaVDDCEtKkAc7QXV3Y1psGts9 |
Next Steps
With the programs deployed, let's build the frontend