Skip to main content

Program

This page documents the Private Raffle program’s instructions and account structures.

Account Structures

Lottery

The main raffle account:
#[account]
pub struct Lottery {
    pub authority: Pubkey,           // Raffle creator
    pub lottery_id: u64,             // Unique identifier
    pub ticket_price: u64,           // Price in lamports
    pub participant_count: u32,      // Number of tickets sold
    pub is_open: bool,               // Can still buy tickets?
    pub winning_number_handle: u128, // Encrypted winning number
    pub bump: u8,
}

Ticket

Each player’s ticket:
#[account]
pub struct Ticket {
    pub lottery: Pubkey,          // Associated raffle
    pub owner: Pubkey,            // Ticket owner
    pub guess_handle: u128,       // Encrypted guess (1-100)
    pub is_winner_handle: u128,   // Encrypted: guess == winning?
    pub prize_handle: u128,       // Encrypted prize amount
    pub claimed: bool,            // Has claimed?
    pub bump: u8,
}

Instructions

create_lottery

Creates a new raffle.
pub fn create_lottery(
    ctx: Context<CreateLottery>,
    lottery_id: u64,      // Unique raffle ID
    ticket_price: u64     // Price per ticket (lamports)
) -> Result<()>
Accounts:
#[derive(Accounts)]
#[instruction(lottery_id: u64)]
pub struct CreateLottery<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    
    #[account(
        init, 
        payer = authority, 
        space = Lottery::SIZE,
        seeds = [b"lottery", lottery_id.to_le_bytes().as_ref()], 
        bump
    )]
    pub lottery: Account<'info, Lottery>,
    
    /// CHECK: vault PDA for holding prize pool
    #[account(mut, seeds = [b"vault", lottery.key().as_ref()], bump)]
    pub vault: AccountInfo<'info>,
    
    pub system_program: Program<'info, System>,
}

buy_ticket

Purchase a ticket with an encrypted guess.
pub fn buy_ticket(
    ctx: Context<BuyTicket>,
    encrypted_guess: Vec<u8>  // Encrypted guess (1-100)
) -> Result<()>
What it does:
  1. Transfers ticket price to vault
  2. Creates encrypted handle from guess
  3. Stores ticket with encrypted guess
  4. Allows buyer to decrypt their own guess
Accounts:
#[derive(Accounts)]
pub struct BuyTicket<'info> {
    #[account(mut)]
    pub buyer: Signer<'info>,
    
    #[account(mut)]
    pub lottery: Account<'info, Lottery>,
    
    #[account(
        init, 
        payer = buyer, 
        space = Ticket::SIZE,
        seeds = [b"ticket", lottery.key().as_ref(), buyer.key().as_ref()], 
        bump
    )]
    pub ticket: Account<'info, Ticket>,
    
    /// CHECK: vault PDA
    #[account(mut, seeds = [b"vault", lottery.key().as_ref()], bump)]
    pub vault: AccountInfo<'info>,
    
    pub system_program: Program<'info, System>,
    pub inco_lightning_program: Program<'info, IncoLightning>,
}

draw_winner

Authority sets the encrypted winning number and closes the raffle.
pub fn draw_winner(
    ctx: Context<DrawWinner>,
    encrypted_winning_number: Vec<u8>  // Encrypted winning number
) -> Result<()>
What it does:
  1. Verifies caller is authority
  2. Closes the raffle (is_open = false)
  3. Stores encrypted winning number

check_winner

Compares a ticket’s guess against the winning number (encrypted comparison).
pub fn check_winner(ctx: Context<CheckWinner>) -> Result<()>
What it does:
  1. Uses e_eq to compare guess_handle with winning_number_handle
  2. Stores encrypted boolean result in is_winner_handle
  3. Allows ticket owner to decrypt the result
Key FHE operation:
// Encrypted comparison: guess == winning_number?
let is_winner: Ebool = cpi::e_eq(
    cpi_ctx,
    Euint128(ticket.guess_handle),
    Euint128(lottery.winning_number_handle),
    0,
)?;

ticket.is_winner_handle = is_winner.0;

claim_prize

Calculates the encrypted prize amount based on win/loss status.
pub fn claim_prize(ctx: Context<ClaimPrize>) -> Result<()>
What it does:
  1. Creates encrypted prize amount (vault balance)
  2. Creates encrypted zero
  3. Uses e_select: if winner, get prize; else get 0
  4. Stores encrypted prize amount
  5. Marks ticket as claimed
Key FHE operation:
// e_select: if winner, get prize; else get 0
let actual_prize: Euint128 = e_select(
    cpi_ctx,
    Ebool(ticket.is_winner_handle),  // condition
    encrypted_prize,                  // if true
    zero,                             // if false
    0,
)?;

ticket.prize_handle = actual_prize.0;

withdraw_prize

Winner withdraws their prize with on-chain signature verification.
pub fn withdraw_prize(
    ctx: Context<WithdrawPrize>,
    handle: Vec<u8>,      // Prize handle as bytes
    plaintext: Vec<u8>    // Decrypted prize amount
) -> Result<()>
What it does:
  1. Verifies ticket owner and claimed status
  2. Verifies Ed25519 signature on-chain (proves decryption is valid)
  3. Parses plaintext to get prize amount
  4. Rejects if prize is 0 (not a winner)
  5. Transfers SOL from vault to winner
Key verification:
// Verify the decryption signature on-chain
cpi::is_validsignature(
    cpi_ctx,
    1,
    Some(vec![handle]),
    Some(vec![plaintext.clone()]),
)?;

// Parse verified plaintext
let prize_amount = parse_plaintext_to_u64(&plaintext)?;
require!(prize_amount > 0, LotteryError::NotWinner);

Error Codes

#[error_code]
pub enum LotteryError {
    #[msg("Lottery is closed")]
    LotteryClosed,
    #[msg("Lottery is still open")]
    LotteryStillOpen,
    #[msg("No winning number set")]
    NoWinningNumber,
    #[msg("No participants")]
    NoParticipants,
    #[msg("Not ticket owner")]
    NotOwner,
    #[msg("Already claimed")]
    AlreadyClaimed,
    #[msg("Ticket not checked yet")]
    NotChecked,
    #[msg("Not claimed yet")]
    NotClaimed,
    #[msg("Not the winner")]
    NotWinner,
    #[msg("Unauthorized")]
    Unauthorized,
}

PDA Seeds

AccountSeeds
Lottery["lottery", lottery_id]
Vault["vault", lottery_pubkey]
Ticket["ticket", lottery_pubkey, buyer_pubkey]

Next Steps

See Client Integration for TypeScript usage examples.