From 51775406802a8a26e0e3e98388da2b60023f4b34 Mon Sep 17 00:00:00 2001 From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:44:34 +0530 Subject: [PATCH 01/10] fix(examples): correct blackjack scoring/bust logic and enforce patient PDA ownership --- blackjack/encrypted-ixs/src/lib.rs | 86 +++++++++---------- blackjack/programs/blackjack/src/lib.rs | 11 ++- blackjack/tests/blackjack.ts | 12 +-- .../against-house/encrypted-ixs/src/lib.rs | 39 ++++++--- .../programs/share_medical_records/src/lib.rs | 4 + 5 files changed, 89 insertions(+), 63 deletions(-) diff --git a/blackjack/encrypted-ixs/src/lib.rs b/blackjack/encrypted-ixs/src/lib.rs index 50f7cc6c..5f740489 100644 --- a/blackjack/encrypted-ixs/src/lib.rs +++ b/blackjack/encrypted-ixs/src/lib.rs @@ -175,20 +175,20 @@ mod circuits { let mut player_hand = player_hand_ctxt.to_arcis().to_array(); - let player_hand_value = calculate_hand_value(&player_hand, player_hand_size); - - let is_bust = player_hand_value > 21; - - let new_card = if !is_bust { + let can_draw = (player_hand_size as usize) < 11; + if can_draw { let card_index = (player_hand_size + dealer_hand_size) as usize; + let new_card = deck[card_index]; + player_hand[player_hand_size as usize] = new_card; + } - // Get the next card from the deck - deck[card_index] + let hand_len = if can_draw { + player_hand_size + 1 } else { - 53 + player_hand_size }; - player_hand[player_hand_size as usize] = new_card; + let is_bust = calculate_hand_value(&player_hand, hand_len) > 21; ( player_hand_ctxt @@ -219,20 +219,20 @@ mod circuits { let mut player_hand = player_hand_ctxt.to_arcis().to_array(); - let player_hand_value = calculate_hand_value(&player_hand, player_hand_size); - - let is_bust = player_hand_value > 21; - - let new_card = if !is_bust { + let can_draw = (player_hand_size as usize) < 11; + if can_draw { let card_index = (player_hand_size + dealer_hand_size) as usize; + let new_card = deck_array[card_index]; + player_hand[player_hand_size as usize] = new_card; + } - // Get the next card from the deck - deck_array[card_index] + let hand_len = if can_draw { + player_hand_size + 1 } else { - 53 + player_hand_size }; - player_hand[player_hand_size as usize] = new_card; + let is_bust = calculate_hand_value(&player_hand, hand_len) > 21; ( player_hand_ctxt @@ -251,15 +251,15 @@ mod circuits { player_hand_size: u8, dealer_hand_size: u8, ) -> (Enc, Enc, u8) { - let deck = deck_ctxt.to_arcis(); - let deck_array = deck.to_array(); + let deck_array = deck_ctxt.to_arcis().to_array(); let mut dealer = dealer_hand_ctxt.to_arcis().to_array(); let mut size = dealer_hand_size as usize; - for _ in 0..7 { + // Dealer can draw at most 9 additional cards (starting from 2, capped at 11). + for _ in 0..9 { let val = calculate_hand_value(&dealer, size as u8); - if val < 17 { - let idx = (player_hand_size as usize + size) as usize; + if val < 17 && size < 11 { + let idx = player_hand_size as usize + size; dealer[size] = deck_array[idx]; size += 1; } @@ -284,35 +284,31 @@ mod circuits { /// # Returns /// The total value of the hand (1-21, or >21 if busted) fn calculate_hand_value(hand: &[u8; 11], hand_length: u8) -> u8 { - let mut value = 0; - let mut has_ace = false; + let mut value: u8 = 0; + let mut ace_count: u8 = 0; - // Process each card in the hand for i in 0..11 { - let rank = if i < hand_length as usize { - hand[i] % 13 // Card rank (0=Ace, 1-9=pip cards, 10-12=face cards) - } else { - 0 - }; - if i < hand_length as usize { - if rank == 0 { - // Ace: start with value of 11 - value += 11; - has_ace = true; - } else if rank > 10 { - // Face cards (Jack, Queen, King): value of 10 - value += 10; - } else { - // Pip cards (2-10): face value (rank 1-9 becomes value 1-9) - value += rank; + let card = hand[i]; + if card <= 51 { + let rank = card % 13; // 0=Ace, 1=2, ..., 9=10, 10=J, 11=Q, 12=K + if rank == 0 { + value += 11; + ace_count += 1; + } else if rank <= 9 { + value += rank + 1; + } else { + value += 10; + } } } } - // Convert Ace from 11 to 1 if hand would bust with 11 - if value > 21 && has_ace { - value -= 10; + for _ in 0..11 { + if value > 21 && ace_count > 0 { + value -= 10; + ace_count -= 1; + } } value diff --git a/blackjack/programs/blackjack/src/lib.rs b/blackjack/programs/blackjack/src/lib.rs index 6b680a81..0609e402 100644 --- a/blackjack/programs/blackjack/src/lib.rs +++ b/blackjack/programs/blackjack/src/lib.rs @@ -190,6 +190,10 @@ pub mod blackjack { !ctx.accounts.blackjack_game.player_has_stood, ErrorCode::InvalidMove ); + require!( + ctx.accounts.blackjack_game.player_hand_size < 11, + ErrorCode::InvalidMove + ); let args = ArgBuilder::new() // Deck @@ -254,6 +258,7 @@ pub mod blackjack { let blackjack_game = &mut ctx.accounts.blackjack_game; blackjack_game.player_hand = player_hand; blackjack_game.client_nonce = client_nonce; + blackjack_game.player_hand_size += 1; if is_bust { blackjack_game.game_state = GameState::DealerTurn; @@ -268,7 +273,6 @@ pub mod blackjack { client_nonce, game_id: blackjack_game.game_id, }); - blackjack_game.player_hand_size += 1; } Ok(()) @@ -294,6 +298,10 @@ pub mod blackjack { !ctx.accounts.blackjack_game.player_has_stood, ErrorCode::InvalidMove ); + require!( + ctx.accounts.blackjack_game.player_hand_size < 11, + ErrorCode::InvalidMove + ); let args = ArgBuilder::new() // Deck @@ -358,6 +366,7 @@ pub mod blackjack { let blackjack_game = &mut ctx.accounts.blackjack_game; blackjack_game.player_hand = player_hand; blackjack_game.client_nonce = client_nonce; + blackjack_game.player_hand_size += 1; blackjack_game.player_has_stood = true; if is_bust { diff --git a/blackjack/tests/blackjack.ts b/blackjack/tests/blackjack.ts index 3ef4912f..aba79e6b 100644 --- a/blackjack/tests/blackjack.ts +++ b/blackjack/tests/blackjack.ts @@ -288,7 +288,9 @@ describe("Blackjack", () => { // Basic Strategy: Hit on 16 or less, Stand on 17 or more. Hit soft 17. let action: "hit" | "stand" = "stand"; - if (playerValue < 17 || (playerValue === 17 && playerIsSoft)) { + if (gameState.playerHandSize >= 11) { + action = "stand"; + } else if (playerValue < 17 || (playerValue === 17 && playerIsSoft)) { action = "hit"; } @@ -367,10 +369,10 @@ describe("Blackjack", () => { ); if (playerValue > 21) { - console.error( - "ERROR: Bust detected after PlayerHitEvent, expected PlayerBustEvent!" - ); - playerBusted = true; + expect( + playerValue, + "Bust detected after PlayerHitEvent, expected PlayerBustEvent" + ).to.be.at.most(21); } } else { console.log("Received PlayerBustEvent."); diff --git a/rock_paper_scissors/against-house/encrypted-ixs/src/lib.rs b/rock_paper_scissors/against-house/encrypted-ixs/src/lib.rs index 7698b1ef..5ac336e7 100644 --- a/rock_paper_scissors/against-house/encrypted-ixs/src/lib.rs +++ b/rock_paper_scissors/against-house/encrypted-ixs/src/lib.rs @@ -13,20 +13,35 @@ mod circuits { pub fn play_rps(player_move_ctxt: Enc) -> u8 { let player_move = player_move_ctxt.to_arcis(); - let first_bit = ArcisRNG::bool(); - let second_bit = ArcisRNG::bool(); + // Sample a near-uniform house move in {0,1,2} using rejection sampling over 2 random bits. + // 00 -> 0, 01 -> 1, 10 -> 2, 11 -> reject and resample. + // Fixed iterations bound runtime; the fallback introduces negligible bias (<= (1/4)^16). + let mut house_move: u8 = 0; + let mut selected = false; - let house_move = if first_bit { - if second_bit { - 0 + for _ in 0..16 { + let b0 = ArcisRNG::bool(); + let b1 = ArcisRNG::bool(); + + // Map (b0,b1) to 0..3 uniformly. + let candidate: u8 = if b0 { + if b1 { + 3 + } else { + 2 + } + } else if b1 { + 1 } else { - 2 - } - } else if second_bit { - 1 - } else { - 0 - }; + 0 + }; + + let candidate_valid = candidate < 3; + let take = (!selected) & candidate_valid; + + house_move = if take { candidate } else { house_move }; + selected = selected | candidate_valid; + } // 0 - tie, 1 - player wins, 2 - house wins, 3 - invalid move let result = if player_move.player_move > 2 { diff --git a/share_medical_records/programs/share_medical_records/src/lib.rs b/share_medical_records/programs/share_medical_records/src/lib.rs index b96ab0c8..61da17cb 100644 --- a/share_medical_records/programs/share_medical_records/src/lib.rs +++ b/share_medical_records/programs/share_medical_records/src/lib.rs @@ -208,6 +208,10 @@ pub struct SharePatientData<'info> { pub clock_account: Account<'info, ClockAccount>, pub system_program: Program<'info, System>, pub arcium_program: Program<'info, Arcium>, + #[account( + seeds = [b"patient_data", payer.key().as_ref()], + bump, + )] pub patient_data: Account<'info, PatientData>, } From 47e53d34abe6cb3eca2170befb5014e48f99eee3 Mon Sep 17 00:00:00 2001 From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com> Date: Thu, 18 Dec 2025 22:23:05 +0530 Subject: [PATCH 02/10] fix --- ed25519/tests/ed_25519.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ed25519/tests/ed_25519.ts b/ed25519/tests/ed_25519.ts index f0fcf735..0f8e56cb 100644 --- a/ed25519/tests/ed_25519.ts +++ b/ed25519/tests/ed_25519.ts @@ -56,6 +56,12 @@ describe("Ed25519", () => { it("sign and verify with MPC Ed25519", async () => { const owner = readKpJson(`${os.homedir()}/.config/solana/id.json`); + const mxePublicKey = await getMXEPublicKeyWithRetry( + provider as anchor.AnchorProvider, + program.programId + ); + console.log("MXE x25519 pubkey:", mxePublicKey); + console.log("Initializing computation definitions"); const initSMSig = await initSignMessageCompDef(program, owner, false, false); console.log("Sign message computation definition initialized with signature", initSMSig); @@ -63,13 +69,6 @@ describe("Ed25519", () => { const initVSSig = await initVerifySignatureCompDef(program, owner, false, false); console.log("Verify signature computation definition initialized with signature", initVSSig); - const mxePublicKey = await getMXEPublicKeyWithRetry( - provider as anchor.AnchorProvider, - program.programId - ); - - console.log("MXE x25519 pubkey:", mxePublicKey); - console.log("\nSigning message with MPC Ed25519"); let message = new TextEncoder().encode('hello'); From bef4d2932bbb47dcd3eb2c3ba71ca00c4b121284 Mon Sep 17 00:00:00 2001 From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:13:38 +0530 Subject: [PATCH 03/10] fix: logic --- blackjack/programs/blackjack/src/lib.rs | 11 +- sealed_bid_auction/Anchor.toml | 2 +- .../programs/sealed_bid_auction/src/lib.rs | 35 ++++-- voting/programs/voting/src/lib.rs | 106 +++++++++++++++++- 4 files changed, 138 insertions(+), 16 deletions(-) diff --git a/blackjack/programs/blackjack/src/lib.rs b/blackjack/programs/blackjack/src/lib.rs index 0609e402..6ba5b525 100644 --- a/blackjack/programs/blackjack/src/lib.rs +++ b/blackjack/programs/blackjack/src/lib.rs @@ -453,8 +453,8 @@ pub mod blackjack { blackjack_game.player_has_stood = true; if is_bust { - // This should never happen - blackjack_game.game_state = GameState::PlayerTurn; + // Player bust edge case + blackjack_game.game_state = GameState::DealerTurn; emit!(PlayerBustEvent { client_nonce: blackjack_game.client_nonce, game_id: blackjack_game.game_id, @@ -838,6 +838,7 @@ pub struct PlayerHit<'info> { mut, seeds = [b"blackjack_game".as_ref(), _game_id.to_le_bytes().as_ref()], bump = blackjack_game.bump, + constraint = blackjack_game.player_pubkey == payer.key() @ ErrorCode::NotAuthorized, )] pub blackjack_game: Account<'info, BlackjackGame>, } @@ -946,6 +947,7 @@ pub struct PlayerDoubleDown<'info> { mut, seeds = [b"blackjack_game".as_ref(), _game_id.to_le_bytes().as_ref()], bump = blackjack_game.bump, + constraint = blackjack_game.player_pubkey == payer.key() @ ErrorCode::NotAuthorized, )] pub blackjack_game: Account<'info, BlackjackGame>, } @@ -1054,6 +1056,7 @@ pub struct PlayerStand<'info> { mut, seeds = [b"blackjack_game".as_ref(), _game_id.to_le_bytes().as_ref()], bump = blackjack_game.bump, + constraint = blackjack_game.player_pubkey == payer.key() @ ErrorCode::NotAuthorized, )] pub blackjack_game: Account<'info, BlackjackGame>, } @@ -1162,6 +1165,7 @@ pub struct DealerPlay<'info> { mut, seeds = [b"blackjack_game".as_ref(), _game_id.to_le_bytes().as_ref()], bump = blackjack_game.bump, + constraint = blackjack_game.player_pubkey == payer.key() @ ErrorCode::NotAuthorized, )] pub blackjack_game: Account<'info, BlackjackGame>, } @@ -1270,6 +1274,7 @@ pub struct ResolveGame<'info> { mut, seeds = [b"blackjack_game".as_ref(), _game_id.to_le_bytes().as_ref()], bump = blackjack_game.bump, + constraint = blackjack_game.player_pubkey == payer.key() @ ErrorCode::NotAuthorized, )] pub blackjack_game: Account<'info, BlackjackGame>, } @@ -1429,4 +1434,6 @@ pub enum ErrorCode { InvalidDealerClientPubkey, #[msg("Cluster not set")] ClusterNotSet, + #[msg("Not authorized to perform this action")] + NotAuthorized, } diff --git a/sealed_bid_auction/Anchor.toml b/sealed_bid_auction/Anchor.toml index 39464266..b5f82846 100644 --- a/sealed_bid_auction/Anchor.toml +++ b/sealed_bid_auction/Anchor.toml @@ -6,7 +6,7 @@ resolution = true skip-lint = false [programs.localnet] -sealed_bid_auction = "659TqLmsCXCHotnPZmKaPo91vAV7a1VFqsmD5GTfyttk" +sealed_bid_auction = "CHFR2eD8dmZ5NM7UbwM7nWFVTfWPpdtKfv6H4Bgtha3e" [registry] url = "https://api.apr.dev" diff --git a/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs b/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs index 7f261ab3..d2e1be1c 100644 --- a/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs +++ b/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs @@ -8,7 +8,7 @@ const COMP_DEF_OFFSET_DETERMINE_WINNER_FIRST_PRICE: u32 = comp_def_offset("determine_winner_first_price"); const COMP_DEF_OFFSET_DETERMINE_WINNER_VICKREY: u32 = comp_def_offset("determine_winner_vickrey"); -declare_id!("659TqLmsCXCHotnPZmKaPo91vAV7a1VFqsmD5GTfyttk"); +declare_id!("CHFR2eD8dmZ5NM7UbwM7nWFVTfWPpdtKfv6H4Bgtha3e"); #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, InitSpace)] pub enum AuctionType { @@ -142,11 +142,15 @@ pub mod sealed_bid_auction { auction.status == AuctionStatus::Open, ErrorCode::AuctionNotOpen ); + require!( + Clock::get()?.unix_timestamp < auction.end_time, + ErrorCode::AuctionEnded + ); ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account; - // Account offset: 8 (discriminator) + 1 + 32 + 1 + 1 + 8 + 8 + 1 + 16 = 76 - const ENCRYPTED_STATE_OFFSET: u32 = 76; + // Account offset: 8 (discriminator) + 1 + 32 + 1 + 1 + 8 + 8 + 2 + 16 = 77 + const ENCRYPTED_STATE_OFFSET: u32 = 77; const ENCRYPTED_STATE_SIZE: u32 = 32 * 5; let args = ArgBuilder::new() @@ -200,7 +204,10 @@ pub mod sealed_bid_auction { let auction = &mut ctx.accounts.auction; auction.encrypted_state = o.ciphertexts; auction.state_nonce = o.nonce; - auction.bid_count += 1; + auction.bid_count = auction + .bid_count + .checked_add(1) + .ok_or(ErrorCode::BidCountOverflow)?; emit!(BidPlacedEvent { auction: auction_key, @@ -216,6 +223,10 @@ pub mod sealed_bid_auction { auction.status == AuctionStatus::Open, ErrorCode::AuctionNotOpen ); + require!( + Clock::get()?.unix_timestamp >= auction.end_time, + ErrorCode::AuctionNotEnded + ); auction.status = AuctionStatus::Closed; emit!(AuctionClosedEvent { @@ -242,7 +253,7 @@ pub mod sealed_bid_auction { ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account; - const ENCRYPTED_STATE_OFFSET: u32 = 8 + 1 + 32 + 1 + 1 + 8 + 8 + 1 + 16; + const ENCRYPTED_STATE_OFFSET: u32 = 8 + 1 + 32 + 1 + 1 + 8 + 8 + 2 + 16; const ENCRYPTED_STATE_SIZE: u32 = 32 * 5; let args = ArgBuilder::new() @@ -329,7 +340,7 @@ pub mod sealed_bid_auction { ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account; - const ENCRYPTED_STATE_OFFSET: u32 = 8 + 1 + 32 + 1 + 1 + 8 + 8 + 1 + 16; + const ENCRYPTED_STATE_OFFSET: u32 = 8 + 1 + 32 + 1 + 1 + 8 + 8 + 2 + 16; const ENCRYPTED_STATE_SIZE: u32 = 32 * 5; let args = ArgBuilder::new() @@ -410,7 +421,7 @@ pub struct Auction { pub status: AuctionStatus, pub min_bid: u64, pub end_time: i64, - pub bid_count: u8, + pub bid_count: u16, pub state_nonce: u128, pub encrypted_state: [[u8; 32]; 5], } @@ -736,13 +747,13 @@ pub struct AuctionCreatedEvent { #[event] pub struct BidPlacedEvent { pub auction: Pubkey, - pub bid_count: u8, + pub bid_count: u16, } #[event] pub struct AuctionClosedEvent { pub auction: Pubkey, - pub bid_count: u8, + pub bid_count: u16, } #[event] @@ -767,4 +778,10 @@ pub enum ErrorCode { WrongAuctionType, #[msg("Unauthorized")] Unauthorized, + #[msg("Auction has ended")] + AuctionEnded, + #[msg("Auction has not ended yet")] + AuctionNotEnded, + #[msg("Bid count overflow")] + BidCountOverflow, } diff --git a/voting/programs/voting/src/lib.rs b/voting/programs/voting/src/lib.rs index 10fe1d99..fbf9d9e8 100644 --- a/voting/programs/voting/src/lib.rs +++ b/voting/programs/voting/src/lib.rs @@ -1,4 +1,6 @@ use anchor_lang::prelude::*; +use anchor_lang::solana_program::rent::Rent; +use anchor_lang::solana_program::sysvar::Sysvar; use arcium_anchor::prelude::*; use arcium_client::idl::arcium::types::CallbackAccount; @@ -124,7 +126,31 @@ pub mod voting { ) .build(); + if !ctx.accounts.voter_record.data_is_empty() { + return Err(ErrorCode::AlreadyVoted.into()); + } + + let rent = Rent::get()?; + let space = 8 + VoterRecord::INIT_SPACE; + let lamports_needed = rent.minimum_balance(space); + let current_lamports = ctx.accounts.voter_record.lamports(); + + if current_lamports < lamports_needed { + let transfer_amount = lamports_needed - current_lamports; + anchor_lang::system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: ctx.accounts.payer.to_account_info(), + to: ctx.accounts.voter_record.to_account_info(), + }, + ), + transfer_amount, + )?; + } + ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account; + let voter_record_key = ctx.accounts.voter_record.key(); queue_computation( ctx.accounts, @@ -134,10 +160,20 @@ pub mod voting { vec![VoteCallback::callback_ix( computation_offset, &ctx.accounts.mxe_account, - &[CallbackAccount { - pubkey: ctx.accounts.poll_acc.key(), - is_writable: true, - }], + &[ + CallbackAccount { + pubkey: ctx.accounts.poll_acc.key(), + is_writable: true, + }, + CallbackAccount { + pubkey: ctx.accounts.payer.key(), // Original voter pubkey + is_writable: false, + }, + CallbackAccount { + pubkey: voter_record_key, // Pre-funded VoterRecord + is_writable: true, + }, + ], )?], 1, 0, @@ -161,6 +197,39 @@ pub mod voting { ctx.accounts.poll_acc.vote_state = o.ciphertexts; ctx.accounts.poll_acc.nonce = o.nonce; + let voter_record_info = ctx.accounts.voter_record.to_account_info(); + let voter = &ctx.accounts.voter; + let poll = &ctx.accounts.poll_acc; + + if !voter_record_info.data_is_empty() { + return Ok(()); + } + + require!( + *voter_record_info.owner == anchor_lang::system_program::ID, + ErrorCode::InvalidVoterRecord + ); + + let (expected_pda, bump) = Pubkey::find_program_address( + &[b"voter", poll.key().as_ref(), voter.key().as_ref()], + ctx.program_id, + ); + require!( + voter_record_info.key() == expected_pda, + ErrorCode::InvalidVoterRecord + ); + + let space = 8 + VoterRecord::INIT_SPACE; + voter_record_info.resize(space)?; + + { + let mut data = voter_record_info.try_borrow_mut_data()?; + data[..8].copy_from_slice(&VoterRecord::DISCRIMINATOR); + data[8] = bump; + } + + voter_record_info.assign(&crate::ID); + let clock = Clock::get()?; let current_timestamp = clock.unix_timestamp; @@ -422,6 +491,15 @@ pub struct Vote<'info> { has_one = authority )] pub poll_acc: Account<'info, PollAccount>, + /// CHECK: VoterRecord PDA - checked manually in vote(). + /// Pre-funded here, initialized atomically in vote_callback(). + /// This ensures voter isn't locked out if callback fails. + #[account( + mut, + seeds = [b"voter", poll_acc.key().as_ref(), payer.key().as_ref()], + bump, + )] + pub voter_record: UncheckedAccount<'info>, } #[callback_accounts("vote")] @@ -447,6 +525,12 @@ pub struct VoteCallback<'info> { pub instructions_sysvar: AccountInfo<'info>, #[account(mut)] pub poll_acc: Account<'info, PollAccount>, + /// CHECK: Original voter's pubkey, passed from vote() for PDA verification + pub voter: UncheckedAccount<'info>, + /// CHECK: Pre-funded VoterRecord PDA to be initialized + #[account(mut)] + pub voter_record: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, } #[init_computation_definition_accounts("vote", payer)] @@ -591,6 +675,16 @@ pub struct PollAccount { pub question: String, } +/// Tracks that a voter has already voted on a specific poll. +/// The PDA seeds [b"voter", poll.key(), voter.key()] ensure each voter +/// can only have one record per poll, preventing double-voting. +#[account] +#[derive(InitSpace)] +pub struct VoterRecord { + /// PDA bump seed + pub bump: u8, +} + #[error_code] pub enum ErrorCode { #[msg("Invalid authority")] @@ -599,6 +693,10 @@ pub enum ErrorCode { AbortedComputation, #[msg("Cluster not set")] ClusterNotSet, + #[msg("Already voted on this poll")] + AlreadyVoted, + #[msg("Invalid voter record PDA")] + InvalidVoterRecord, } #[event] From 80e0e9535e35945f54e56398a5f5c680d8ffa6d8 Mon Sep 17 00:00:00 2001 From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:15:52 +0530 Subject: [PATCH 04/10] fix: correct test logic for blackjack bust handling, auction timing, and voting PDAs --- blackjack/tests/blackjack.ts | 33 ++++++++++++++----- .../tests/sealed_bid_auction.ts | 12 ++++--- voting/tests/voting.ts | 16 +++++++++ 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/blackjack/tests/blackjack.ts b/blackjack/tests/blackjack.ts index aba79e6b..7a6b8e20 100644 --- a/blackjack/tests/blackjack.ts +++ b/blackjack/tests/blackjack.ts @@ -388,6 +388,7 @@ describe("Blackjack", () => { console.log("Player decides to STAND."); const playerStandComputationOffset = new anchor.BN(randomBytes(8)); const playerStandEventPromise = awaitEvent("playerStandEvent"); + const playerBustOnStandEventPromise = awaitEvent("playerBustEvent"); const playerStandSig = await program.methods .playerStand( @@ -426,16 +427,30 @@ describe("Blackjack", () => { finalizeStandSig ); - const playerStandEvent = await playerStandEventPromise; - console.log( - `Received PlayerStandEvent. Is Bust reported? ${playerStandEvent.isBust}` - ); - expect(playerStandEvent.isBust).to.be.false; + // Handle both stand and bust events (player might bust during stand calculation) + const standEvent = await Promise.race([ + playerStandEventPromise, + playerBustOnStandEventPromise, + ]); - playerStood = true; gameState = await program.account.blackjackGame.fetch(blackjackGamePDA); - expect(gameState.gameState).to.deep.equal({ dealerTurn: {} }); - console.log("Player stands. Proceeding to Dealer's Turn."); + + if ("isBust" in standEvent) { + // PlayerStandEvent + console.log( + `Received PlayerStandEvent. Is Bust reported? ${standEvent.isBust}` + ); + expect(standEvent.isBust).to.be.false; + playerStood = true; + expect(gameState.gameState).to.deep.equal({ dealerTurn: {} }); + console.log("Player stands. Proceeding to Dealer's Turn."); + } else { + // PlayerBustEvent - player busted during stand calculation + console.log("Received PlayerBustEvent during stand."); + playerBusted = true; + expect(gameState.gameState).to.deep.equal({ dealerTurn: {} }); + console.log("Player BUSTED during stand!"); + } } if (!playerBusted && !playerStood) { @@ -446,7 +461,7 @@ describe("Blackjack", () => { // --- Dealer's Turn --- gameState = await program.account.blackjackGame.fetch(blackjackGamePDA); - if (gameState.gameState.hasOwnProperty("dealerTurn")) { + if (gameState.gameState.hasOwnProperty("dealerTurn") && !playerBusted) { console.log("Dealer's Turn..."); const dealerPlayComputationOffset = new anchor.BN(randomBytes(8)); const dealerPlayNonce = randomBytes(16); diff --git a/sealed_bid_auction/tests/sealed_bid_auction.ts b/sealed_bid_auction/tests/sealed_bid_auction.ts index 424aa251..62b1337c 100644 --- a/sealed_bid_auction/tests/sealed_bid_auction.ts +++ b/sealed_bid_auction/tests/sealed_bid_auction.ts @@ -141,7 +141,7 @@ describe("SealedBidAuction", () => { createComputationOffset, { firstPrice: {} }, // AuctionType::FirstPrice new anchor.BN(100), // min_bid: 100 lamports - new anchor.BN(Date.now() / 1000 + 3600), // end_time: 1 hour from now + new anchor.BN(Math.floor(Date.now() / 1000) + 5), // end_time: 5 seconds from now new anchor.BN(deserializeLE(createNonce).toString()) // nonce for MXE ) .accountsPartial({ @@ -240,7 +240,9 @@ describe("SealedBidAuction", () => { expect(bidPlacedEvent.bidCount).to.equal(1); // Step 3: Close auction - console.log("\nStep 3: Closing auction..."); + console.log("\nStep 3: Waiting for auction to end..."); + await new Promise((resolve) => setTimeout(resolve, 6000)); + console.log("Closing auction..."); const auctionClosedPromise = awaitEvent("auctionClosedEvent"); const closeSig = await program.methods @@ -366,7 +368,7 @@ describe("SealedBidAuction", () => { createComputationOffset, { vickrey: {} }, // AuctionType::Vickrey new anchor.BN(50), // min_bid: 50 lamports - new anchor.BN(Date.now() / 1000 + 3600), + new anchor.BN(Math.floor(Date.now() / 1000) + 5), // end_time: 5 seconds from now new anchor.BN(deserializeLE(vickreyCreateNonce).toString()) // nonce for MXE ) .accountsPartial({ @@ -518,7 +520,9 @@ describe("SealedBidAuction", () => { expect(bidPlaced2Event.bidCount).to.equal(2); // Step 4: Close auction - console.log("\nStep 4: Closing Vickrey auction..."); + console.log("\nStep 4: Waiting for auction to end..."); + await new Promise((resolve) => setTimeout(resolve, 6000)); + console.log("Closing Vickrey auction..."); const auctionClosedPromise = awaitEvent("auctionClosedEvent"); const closeSig = await program.methods diff --git a/voting/tests/voting.ts b/voting/tests/voting.ts index 3992c55f..e637cbc4 100644 --- a/voting/tests/voting.ts +++ b/voting/tests/voting.ts @@ -158,6 +158,20 @@ describe("Voting", () => { console.log(`Voting for poll ${POLL_ID}`); + // Derive poll PDA (poll_id uses little-endian u32) + const pollIdBuffer = Buffer.alloc(4); + pollIdBuffer.writeUInt32LE(POLL_ID); + const [pollPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("poll"), owner.publicKey.toBuffer(), pollIdBuffer], + program.programId + ); + + // Derive voter_record PDA + const [voterRecordPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("voter"), pollPDA.toBuffer(), owner.publicKey.toBuffer()], + program.programId + ); + const voteComputationOffset = new anchor.BN(randomBytes(8), "hex"); const queueVoteSig = await program.methods @@ -182,6 +196,8 @@ describe("Voting", () => { Buffer.from(getCompDefAccOffset("vote")).readUInt32LE() ), authority: owner.publicKey, + pollAcc: pollPDA, + voterRecord: voterRecordPDA, }) .rpc({ skipPreflight: true, commitment: "confirmed" }); console.log(`Queue vote for poll ${POLL_ID} sig is `, queueVoteSig); From ffb8f41aaf6818257f9f8016531ae447793e4dca Mon Sep 17 00:00:00 2001 From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:36:24 -0700 Subject: [PATCH 05/10] fix: correct game logic, harden security, and prevent double-voting across examples Blackjack: fix hand value calculation (ace downgrade, pip values, face card threshold), deal card before bust check, transition to Resolving on bust, add player ownership constraints, bound hand size to 11. Sealed bid auction: enforce auction timing (no bids after end, no close before end), widen bid_count to u16 with checked_add, require bids before resolving, document known limitations. Voting: add VoterRecord PDA for double-vote prevention with idempotent callback, add PDA derivation validation, add double-vote rejection test. Misc: constrain patient_data PDA in share_medical_records, reorder getMXEPublicKeyWithRetry before comp def init in ed25519/blackjack tests. --- blackjack/Anchor.toml | 4 +- blackjack/encrypted-ixs/src/lib.rs | 32 ++------- blackjack/programs/blackjack/src/lib.rs | 67 ++++++------------- blackjack/tests/blackjack.ts | 35 ++++------ sealed_bid_auction/README.md | 8 ++- sealed_bid_auction/encrypted-ixs/src/lib.rs | 2 +- .../programs/sealed_bid_auction/src/lib.rs | 18 +++-- .../tests/sealed_bid_auction.ts | 8 +-- voting/README.md | 11 +++ voting/programs/voting/src/lib.rs | 33 +++++---- voting/tests/voting.ts | 58 ++++++++++++++++ 11 files changed, 147 insertions(+), 129 deletions(-) diff --git a/blackjack/Anchor.toml b/blackjack/Anchor.toml index 581a6deb..592fb98a 100644 --- a/blackjack/Anchor.toml +++ b/blackjack/Anchor.toml @@ -6,7 +6,7 @@ resolution = true skip-lint = false [programs.localnet] -blackjack = "7fJeDSDS5dnCYQWAA4K6FjVyew3c2fH5Ba2cQ9KGcjoo" +blackjack = "Ku4ygyvbN7UbezR3eNGBJMM5iGdM5dPtb23czFuenMK" [registry] url = "https://api.apr.dev" @@ -20,3 +20,5 @@ test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 \"tests/**/*.ts\"" [test] startup_wait = 20000 +shutdown_wait = 2000 +upgradeable = false diff --git a/blackjack/encrypted-ixs/src/lib.rs b/blackjack/encrypted-ixs/src/lib.rs index a625bd20..eb0dfb5f 100644 --- a/blackjack/encrypted-ixs/src/lib.rs +++ b/blackjack/encrypted-ixs/src/lib.rs @@ -61,20 +61,10 @@ mod circuits { let mut player_hand = player_hand_ctxt.to_arcis().unpack(); - let can_draw = (player_hand_size as usize) < 11; - if can_draw { - let card_index = (player_hand_size + dealer_hand_size) as usize; - let new_card = deck[card_index]; - player_hand[player_hand_size as usize] = new_card; - } - - let hand_len = if can_draw { - player_hand_size + 1 - } else { - player_hand_size - }; + let card_index = (player_hand_size + dealer_hand_size) as usize; + player_hand[player_hand_size as usize] = deck[card_index]; - let is_bust = calculate_hand_value(&player_hand, hand_len) > 21; + let is_bust = calculate_hand_value(&player_hand, player_hand_size + 1) > 21; ( player_hand_ctxt.owner.from_arcis(Pack::new(player_hand)), @@ -102,20 +92,10 @@ mod circuits { let mut player_hand = player_hand_ctxt.to_arcis().unpack(); - let can_draw = (player_hand_size as usize) < 11; - if can_draw { - let card_index = (player_hand_size + dealer_hand_size) as usize; - let new_card = deck_array[card_index]; - player_hand[player_hand_size as usize] = new_card; - } - - let hand_len = if can_draw { - player_hand_size + 1 - } else { - player_hand_size - }; + let card_index = (player_hand_size + dealer_hand_size) as usize; + player_hand[player_hand_size as usize] = deck_array[card_index]; - let is_bust = calculate_hand_value(&player_hand, hand_len) > 21; + let is_bust = calculate_hand_value(&player_hand, player_hand_size + 1) > 21; ( player_hand_ctxt.owner.from_arcis(Pack::new(player_hand)), diff --git a/blackjack/programs/blackjack/src/lib.rs b/blackjack/programs/blackjack/src/lib.rs index f4d0ea1e..3cafd6ca 100644 --- a/blackjack/programs/blackjack/src/lib.rs +++ b/blackjack/programs/blackjack/src/lib.rs @@ -9,7 +9,7 @@ const COMP_DEF_OFFSET_PLAYER_STAND: u32 = comp_def_offset("player_stand"); const COMP_DEF_OFFSET_DEALER_PLAY: u32 = comp_def_offset("dealer_play"); const COMP_DEF_OFFSET_RESOLVE_GAME: u32 = comp_def_offset("resolve_game"); -declare_id!("7fJeDSDS5dnCYQWAA4K6FjVyew3c2fH5Ba2cQ9KGcjoo"); +declare_id!("Ku4ygyvbN7UbezR3eNGBJMM5iGdM5dPtb23czFuenMK"); #[arcium_program] pub mod blackjack { @@ -47,11 +47,6 @@ pub mod blackjack { blackjack_game.bump = ctx.bumps.blackjack_game; blackjack_game.game_id = game_id; blackjack_game.player_pubkey = ctx.accounts.payer.key(); - blackjack_game.player_hand = [0; 32]; - blackjack_game.dealer_hand = [0; 32]; - blackjack_game.deck_nonce = 0; - blackjack_game.client_nonce = 0; - blackjack_game.dealer_nonce = 0; blackjack_game.player_enc_pubkey = client_pubkey; blackjack_game.game_state = GameState::Initial; blackjack_game.player_hand_size = 0; @@ -254,7 +249,7 @@ pub mod blackjack { blackjack_game.player_hand_size += 1; if is_bust { - blackjack_game.game_state = GameState::DealerTurn; + blackjack_game.game_state = GameState::Resolving; emit!(PlayerBustEvent { client_nonce, game_id: blackjack_game.game_id, @@ -362,7 +357,7 @@ pub mod blackjack { blackjack_game.player_has_stood = true; if is_bust { - blackjack_game.game_state = GameState::DealerTurn; + blackjack_game.game_state = GameState::Resolving; emit!(PlayerBustEvent { client_nonce, game_id: blackjack_game.game_id, @@ -445,7 +440,7 @@ pub mod blackjack { if is_bust { // Player bust edge case - blackjack_game.game_state = GameState::DealerTurn; + blackjack_game.game_state = GameState::Resolving; emit!(PlayerBustEvent { client_nonce: blackjack_game.client_nonce, game_id: blackjack_game.game_id, @@ -453,7 +448,6 @@ pub mod blackjack { } else { blackjack_game.game_state = GameState::DealerTurn; emit!(PlayerStandEvent { - is_bust, game_id: blackjack_game.game_id }); } @@ -617,41 +611,21 @@ pub mod blackjack { Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; - if result == 0 { - // Player busts (dealer wins) - emit!(ResultEvent { - winner: "Dealer".to_string(), - game_id: ctx.accounts.blackjack_game.game_id, - }); - } else if result == 1 { - // Dealer busts (player wins) - emit!(ResultEvent { - winner: "Player".to_string(), - game_id: ctx.accounts.blackjack_game.game_id, - }); - } else if result == 2 { - // Player wins - emit!(ResultEvent { - winner: "Player".to_string(), - game_id: ctx.accounts.blackjack_game.game_id, - }); - } else if result == 3 { - // Dealer wins - emit!(ResultEvent { - winner: "Dealer".to_string(), - game_id: ctx.accounts.blackjack_game.game_id, - }); - } else { - // Push (tie) - emit!(ResultEvent { - winner: "Tie".to_string(), - game_id: ctx.accounts.blackjack_game.game_id, - }); - } + let winner = match result { + 0 | 3 => "Dealer", + 1 | 2 => "Player", + _ => "Tie", + }; let blackjack_game = &mut ctx.accounts.blackjack_game; + blackjack_game.game_result = result; blackjack_game.game_state = GameState::Resolved; + emit!(ResultEvent { + winner: winner.to_string(), + game_id: blackjack_game.game_id, + }); + Ok(()) } } @@ -837,7 +811,7 @@ pub struct PlayerHit<'info> { bump = blackjack_game.bump, constraint = blackjack_game.player_pubkey == payer.key() @ ErrorCode::NotAuthorized, )] - pub blackjack_game: Account<'info, BlackjackGame>, + pub blackjack_game: Box>, } #[callback_accounts("player_hit")] @@ -953,7 +927,7 @@ pub struct PlayerDoubleDown<'info> { bump = blackjack_game.bump, constraint = blackjack_game.player_pubkey == payer.key() @ ErrorCode::NotAuthorized, )] - pub blackjack_game: Account<'info, BlackjackGame>, + pub blackjack_game: Box>, } #[callback_accounts("player_double_down")] @@ -1069,7 +1043,7 @@ pub struct PlayerStand<'info> { bump = blackjack_game.bump, constraint = blackjack_game.player_pubkey == payer.key() @ ErrorCode::NotAuthorized, )] - pub blackjack_game: Account<'info, BlackjackGame>, + pub blackjack_game: Box>, } #[callback_accounts("player_stand")] @@ -1185,7 +1159,7 @@ pub struct DealerPlay<'info> { bump = blackjack_game.bump, constraint = blackjack_game.player_pubkey == payer.key() @ ErrorCode::NotAuthorized, )] - pub blackjack_game: Account<'info, BlackjackGame>, + pub blackjack_game: Box>, } #[callback_accounts("dealer_play")] @@ -1301,7 +1275,7 @@ pub struct ResolveGame<'info> { bump = blackjack_game.bump, constraint = blackjack_game.player_pubkey == payer.key() @ ErrorCode::NotAuthorized, )] - pub blackjack_game: Account<'info, BlackjackGame>, + pub blackjack_game: Box>, } #[callback_accounts("resolve_game")] @@ -1429,7 +1403,6 @@ pub struct PlayerDoubleDownEvent { #[event] pub struct PlayerStandEvent { - pub is_bust: bool, pub game_id: u64, } diff --git a/blackjack/tests/blackjack.ts b/blackjack/tests/blackjack.ts index 22babdb0..78e3038b 100644 --- a/blackjack/tests/blackjack.ts +++ b/blackjack/tests/blackjack.ts @@ -109,6 +109,12 @@ describe("Blackjack", () => { it("Should play a full blackjack game with state awareness", async () => { console.log("Owner address:", owner.publicKey.toBase58()); + // Wait for MXE account to be ready before initializing comp defs + const mxePublicKey = await getMXEPublicKeyWithRetry( + provider as anchor.AnchorProvider, + program.programId + ); + // --- Initialize Computation Definitions --- console.log("Initializing computation definitions..."); await Promise.all([ @@ -137,10 +143,6 @@ describe("Blackjack", () => { // --- Setup Game Cryptography --- const privateKey = x25519.utils.randomSecretKey(); const publicKey = x25519.getPublicKey(privateKey); - const mxePublicKey = await getMXEPublicKeyWithRetry( - provider as anchor.AnchorProvider, - program.programId - ); console.log("MXE x25519 pubkey is", mxePublicKey); const sharedSecret = x25519.getSharedSecret(privateKey, mxePublicKey); @@ -353,7 +355,7 @@ describe("Blackjack", () => { } else { console.log("Received PlayerBustEvent."); playerBusted = true; - expect(gameState.gameState).to.deep.equal({ dealerTurn: {} }); + expect(gameState.gameState).to.deep.equal({ resolving: {} }); console.log("Player BUSTED!"); } } catch (e) { @@ -413,12 +415,9 @@ describe("Blackjack", () => { gameState = await program.account.blackjackGame.fetch(blackjackGamePDA); - if ("isBust" in standEvent) { - // PlayerStandEvent - console.log( - `Received PlayerStandEvent. Is Bust reported? ${standEvent.isBust}` - ); - expect(standEvent.isBust).to.be.false; + if (!("clientNonce" in standEvent)) { + // PlayerStandEvent (no clientNonce field, unlike PlayerBustEvent) + console.log("Received PlayerStandEvent."); playerStood = true; expect(gameState.gameState).to.deep.equal({ dealerTurn: {} }); console.log("Player stands. Proceeding to Dealer's Turn."); @@ -426,7 +425,7 @@ describe("Blackjack", () => { // PlayerBustEvent - player busted during stand calculation console.log("Received PlayerBustEvent during stand."); playerBusted = true; - expect(gameState.gameState).to.deep.equal({ dealerTurn: {} }); + expect(gameState.gameState).to.deep.equal({ resolving: {} }); console.log("Player BUSTED during stand!"); } } @@ -439,7 +438,7 @@ describe("Blackjack", () => { // --- Dealer's Turn --- gameState = await program.account.blackjackGame.fetch(blackjackGamePDA); - if (gameState.gameState.hasOwnProperty("dealerTurn") && !playerBusted) { + if (gameState.gameState.hasOwnProperty("dealerTurn")) { console.log("Dealer's Turn..."); const dealerPlayComputationOffset = new anchor.BN(randomBytes(8)); const dealerPlayNonce = randomBytes(16); @@ -504,18 +503,10 @@ describe("Blackjack", () => { ); gameState = await program.account.blackjackGame.fetch(blackjackGamePDA); expect(gameState.gameState).to.deep.equal({ resolving: {} }); - } else if (playerBusted) { - console.log("Player busted, skipping Dealer's Turn."); - console.log( - "Manually considering state as Resolving for test flow after player bust." - ); } gameState = await program.account.blackjackGame.fetch(blackjackGamePDA); - if ( - gameState.gameState.hasOwnProperty("resolving") || - (playerBusted && gameState.gameState.hasOwnProperty("dealerTurn")) - ) { + if (gameState.gameState.hasOwnProperty("resolving")) { console.log("Resolving Game..."); const resolveComputationOffset = new anchor.BN(randomBytes(8)); const resultEventPromise = awaitEvent("resultEvent"); diff --git a/sealed_bid_auction/README.md b/sealed_bid_auction/README.md index 5a5757ac..68b7ef6f 100644 --- a/sealed_bid_auction/README.md +++ b/sealed_bid_auction/README.md @@ -75,7 +75,7 @@ pub struct AuctionState { pub highest_bid: u64, pub highest_bidder: SerializedSolanaPublicKey, // Winner pubkey (lo/hi u128 pair) pub second_highest_bid: u64, // Required for Vickrey auctions - pub bid_count: u8, + pub bid_count: u16, } ``` @@ -156,3 +156,9 @@ Apply sealed-bid auctions when: **Example applications**: NFT auctions, ad bidding systems, procurement contracts, treasury bond auctions, spectrum license sales. This pattern extends to any scenario requiring private comparison and selection: hiring decisions, grant allocations, or matching markets where selection criteria should remain confidential. + +## Known Limitations + +**`min_bid` not enforced against encrypted bids.** The `min_bid` field is stored on-chain but never checked -- on-chain validation is impossible (the bid is encrypted), and circuit-side validation would require passing `min_bid` as a plaintext argument. For production, pass `min_bid` into the circuit and compare before updating state. + +**No per-bidder deduplication.** A bidder can submit multiple bids. This is non-exploitable: in Vickrey mode, duplicate bids can only increase the second-highest price (hurting the bidder). In first-price mode, the bidder always pays their highest bid regardless. The `bid_count` field reflects total bids, not unique bidders. diff --git a/sealed_bid_auction/encrypted-ixs/src/lib.rs b/sealed_bid_auction/encrypted-ixs/src/lib.rs index de65096d..2200faa5 100644 --- a/sealed_bid_auction/encrypted-ixs/src/lib.rs +++ b/sealed_bid_auction/encrypted-ixs/src/lib.rs @@ -13,7 +13,7 @@ mod circuits { pub highest_bid: u64, pub highest_bidder: SerializedSolanaPublicKey, pub second_highest_bid: u64, - pub bid_count: u8, + pub bid_count: u16, } pub struct AuctionResult { diff --git a/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs b/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs index 34fbf390..079b08c2 100644 --- a/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs +++ b/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs @@ -8,6 +8,10 @@ const COMP_DEF_OFFSET_DETERMINE_WINNER_FIRST_PRICE: u32 = comp_def_offset("determine_winner_first_price"); const COMP_DEF_OFFSET_DETERMINE_WINNER_VICKREY: u32 = comp_def_offset("determine_winner_vickrey"); +// Auction account byte offset: 8 (discriminator) + 1 + 32 + 1 + 1 + 8 + 8 + 2 + 16 = 77 +const ENCRYPTED_STATE_OFFSET: u32 = 77; +const ENCRYPTED_STATE_SIZE: u32 = 32 * 5; + declare_id!("CHFR2eD8dmZ5NM7UbwM7nWFVTfWPpdtKfv6H4Bgtha3e"); #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, InitSpace)] @@ -146,10 +150,6 @@ pub mod sealed_bid_auction { ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account; - // Account offset: 8 (discriminator) + 1 + 32 + 1 + 1 + 8 + 8 + 2 + 16 = 77 - const ENCRYPTED_STATE_OFFSET: u32 = 77; - const ENCRYPTED_STATE_SIZE: u32 = 32 * 5; - let args = ArgBuilder::new() .x25519_pubkey(bidder_pubkey) .plaintext_u128(nonce) @@ -246,12 +246,10 @@ pub mod sealed_bid_auction { auction.auction_type == AuctionType::FirstPrice, ErrorCode::WrongAuctionType ); + require!(auction.bid_count > 0, ErrorCode::NoBids); ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account; - const ENCRYPTED_STATE_OFFSET: u32 = 8 + 1 + 32 + 1 + 1 + 8 + 8 + 2 + 16; - const ENCRYPTED_STATE_SIZE: u32 = 32 * 5; - let args = ArgBuilder::new() .plaintext_u128(auction.state_nonce) .account( @@ -335,12 +333,10 @@ pub mod sealed_bid_auction { auction.auction_type == AuctionType::Vickrey, ErrorCode::WrongAuctionType ); + require!(auction.bid_count > 0, ErrorCode::NoBids); ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account; - const ENCRYPTED_STATE_OFFSET: u32 = 8 + 1 + 32 + 1 + 1 + 8 + 8 + 2 + 16; - const ENCRYPTED_STATE_SIZE: u32 = 32 * 5; - let args = ArgBuilder::new() .plaintext_u128(auction.state_nonce) .account( @@ -816,4 +812,6 @@ pub enum ErrorCode { AuctionNotEnded, #[msg("Bid count overflow")] BidCountOverflow, + #[msg("No bids placed")] + NoBids, } diff --git a/sealed_bid_auction/tests/sealed_bid_auction.ts b/sealed_bid_auction/tests/sealed_bid_auction.ts index 4e8fc50c..ed8f9e37 100644 --- a/sealed_bid_auction/tests/sealed_bid_auction.ts +++ b/sealed_bid_auction/tests/sealed_bid_auction.ts @@ -135,7 +135,7 @@ describe("SealedBidAuction", () => { createComputationOffset, { firstPrice: {} }, // AuctionType::FirstPrice new anchor.BN(100), // min_bid: 100 lamports - new anchor.BN(Math.floor(Date.now() / 1000) + 5) // end_time: 5 seconds from now + new anchor.BN(Math.floor(Date.now() / 1000) + 10) // end_time: 10 seconds from now ) .accountsPartial({ authority: owner.publicKey, @@ -242,7 +242,7 @@ describe("SealedBidAuction", () => { // Step 3: Close auction console.log("\nStep 3: Waiting for auction to end..."); - await new Promise((resolve) => setTimeout(resolve, 6000)); + await new Promise((resolve) => setTimeout(resolve, 12000)); console.log("Closing auction..."); const auctionClosedPromise = awaitEvent("auctionClosedEvent"); @@ -370,7 +370,7 @@ describe("SealedBidAuction", () => { createComputationOffset, { vickrey: {} }, // AuctionType::Vickrey new anchor.BN(50), // min_bid: 50 lamports - new anchor.BN(Math.floor(Date.now() / 1000) + 5) // end_time: 5 seconds from now + new anchor.BN(Math.floor(Date.now() / 1000) + 10) // end_time: 10 seconds from now ) .accountsPartial({ authority: vickreyAuthority.publicKey, @@ -534,7 +534,7 @@ describe("SealedBidAuction", () => { // Step 4: Close auction console.log("\nStep 4: Waiting for auction to end..."); - await new Promise((resolve) => setTimeout(resolve, 6000)); + await new Promise((resolve) => setTimeout(resolve, 12000)); console.log("Closing Vickrey auction..."); const auctionClosedPromise = awaitEvent("auctionClosedEvent"); diff --git a/voting/README.md b/voting/README.md index 3766b37c..5e2e0525 100644 --- a/voting/README.md +++ b/voting/README.md @@ -140,9 +140,20 @@ pub fn vote_callback( Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; + // Prevent double-voting: check if VoterRecord PDA is already initialized + let voter_record_info = ctx.accounts.voter_record.to_account_info(); + if !voter_record_info.data_is_empty() { + return Ok(()); + } + + // ... (PDA ownership and derivation validation) + // Save new encrypted tallies + new nonce ctx.accounts.poll_acc.vote_state = o.ciphertexts; ctx.accounts.poll_acc.nonce = o.nonce; + + // Initialize VoterRecord PDA to mark this voter as having voted + // ... (VoterRecord initialization code) Ok(()) } ``` diff --git a/voting/programs/voting/src/lib.rs b/voting/programs/voting/src/lib.rs index fe97dc7c..0d8dcfac 100644 --- a/voting/programs/voting/src/lib.rs +++ b/voting/programs/voting/src/lib.rs @@ -189,22 +189,12 @@ pub mod voting { Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; - ctx.accounts.poll_acc.vote_state = o.ciphertexts; - ctx.accounts.poll_acc.nonce = o.nonce; - + // Check VoterRecord BEFORE writing state to prevent double-counting + // from racing callbacks let voter_record_info = ctx.accounts.voter_record.to_account_info(); let voter = &ctx.accounts.voter; let poll = &ctx.accounts.poll_acc; - if !voter_record_info.data_is_empty() { - return Ok(()); - } - - require!( - *voter_record_info.owner == anchor_lang::system_program::ID, - ErrorCode::InvalidVoterRecord - ); - let (expected_pda, bump) = Pubkey::find_program_address( &[b"voter", poll.key().as_ref(), voter.key().as_ref()], ctx.program_id, @@ -214,6 +204,18 @@ pub mod voting { ErrorCode::InvalidVoterRecord ); + if !voter_record_info.data_is_empty() { + msg!("vote_callback: VoterRecord already initialized, skipping"); + return Ok(()); + } + + // Safe to write state — VoterRecord is uninitialized, so this is + // the first callback for this voter + ctx.accounts.poll_acc.vote_state = o.ciphertexts; + ctx.accounts.poll_acc.nonce = o.nonce; + + voter_record_info.assign(&crate::ID); + let space = 8 + VoterRecord::INIT_SPACE; voter_record_info.resize(space)?; @@ -223,13 +225,10 @@ pub mod voting { data[8] = bump; } - voter_record_info.assign(&crate::ID); - let clock = Clock::get()?; - let current_timestamp = clock.unix_timestamp; emit!(VoteEvent { - timestamp: current_timestamp, + timestamp: clock.unix_timestamp, }); Ok(()) @@ -492,7 +491,7 @@ pub struct Vote<'info> { bump = poll_acc.bump, has_one = authority )] - pub poll_acc: Account<'info, PollAccount>, + pub poll_acc: Box>, /// CHECK: VoterRecord PDA - checked manually in vote(). /// Pre-funded here, initialized atomically in vote_callback(). /// This ensures voter isn't locked out if callback fails. diff --git a/voting/tests/voting.ts b/voting/tests/voting.ts index 16745c7f..d5246538 100644 --- a/voting/tests/voting.ts +++ b/voting/tests/voting.ts @@ -234,6 +234,64 @@ describe("Voting", () => { ); } + // Test double-vote prevention: attempt to vote again on the first poll + console.log("\n--- Testing double-vote prevention ---"); + const DOUBLE_VOTE_POLL_ID = POLL_IDS[0]; + const doubleVoteNonce = randomBytes(16); + const doubleVoteCiphertext = cipher.encrypt([BigInt(true)], doubleVoteNonce); + + const doubleVotePollIdBuffer = Buffer.alloc(4); + doubleVotePollIdBuffer.writeUInt32LE(DOUBLE_VOTE_POLL_ID); + const [doubleVotePollPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("poll"), owner.publicKey.toBuffer(), doubleVotePollIdBuffer], + program.programId + ); + const [doubleVoteVoterRecordPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("voter"), doubleVotePollPDA.toBuffer(), owner.publicKey.toBuffer()], + program.programId + ); + + const doubleVoteComputationOffset = new anchor.BN(randomBytes(8), "hex"); + + try { + await program.methods + .vote( + doubleVoteComputationOffset, + DOUBLE_VOTE_POLL_ID, + Array.from(doubleVoteCiphertext[0]), + Array.from(publicKey), + new anchor.BN(deserializeLE(doubleVoteNonce).toString()) + ) + .accountsPartial({ + computationAccount: getComputationAccAddress( + arciumEnv.arciumClusterOffset, + doubleVoteComputationOffset + ), + clusterAccount: clusterAccount, + mxeAccount: getMXEAccAddress(program.programId), + mempoolAccount: getMempoolAccAddress(arciumEnv.arciumClusterOffset), + executingPool: getExecutingPoolAccAddress( + arciumEnv.arciumClusterOffset + ), + compDefAccount: getCompDefAccAddress( + program.programId, + Buffer.from(getCompDefAccOffset("vote")).readUInt32LE() + ), + authority: owner.publicKey, + pollAcc: doubleVotePollPDA, + voterRecord: doubleVoteVoterRecordPDA, + }) + .rpc({ + preflightCommitment: "confirmed", + commitment: "confirmed", + }); + + expect.fail("Double vote should have been rejected"); + } catch (error) { + console.log("Double vote correctly rejected:", error.message); + expect(error.message).to.include("AlreadyVoted"); + } + // Reveal results for each poll for (let i = 0; i < POLL_IDS.length; i++) { const POLL_ID = POLL_IDS[i]; From f0f13c2ee6e70595039316d29f54deb946f3e4b5 Mon Sep 17 00:00:00 2001 From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:58:39 +0530 Subject: [PATCH 06/10] refactor: simplify voting with Anchor init and clean up comments --- blackjack/encrypted-ixs/src/lib.rs | 1 - blackjack/programs/blackjack/src/lib.rs | 6 +- blackjack/tests/blackjack.ts | 3 - .../against-house/encrypted-ixs/src/lib.rs | 4 - voting/README.md | 12 +-- voting/programs/voting/src/lib.rs | 100 ++---------------- voting/tests/voting.ts | 5 +- 7 files changed, 18 insertions(+), 113 deletions(-) diff --git a/blackjack/encrypted-ixs/src/lib.rs b/blackjack/encrypted-ixs/src/lib.rs index eb0dfb5f..63e0dc1a 100644 --- a/blackjack/encrypted-ixs/src/lib.rs +++ b/blackjack/encrypted-ixs/src/lib.rs @@ -116,7 +116,6 @@ mod circuits { let mut dealer = dealer_hand_ctxt.to_arcis().unpack(); let mut size = dealer_hand_size as usize; - // Dealer can draw at most 9 additional cards (starting from 2, capped at 11). for _ in 0..9 { let val = calculate_hand_value(&dealer, size as u8); if val < 17 && size < 11 { diff --git a/blackjack/programs/blackjack/src/lib.rs b/blackjack/programs/blackjack/src/lib.rs index 3cafd6ca..573f3ee9 100644 --- a/blackjack/programs/blackjack/src/lib.rs +++ b/blackjack/programs/blackjack/src/lib.rs @@ -439,7 +439,6 @@ pub mod blackjack { blackjack_game.player_has_stood = true; if is_bust { - // Player bust edge case blackjack_game.game_state = GameState::Resolving; emit!(PlayerBustEvent { client_nonce: blackjack_game.client_nonce, @@ -614,7 +613,8 @@ pub mod blackjack { let winner = match result { 0 | 3 => "Dealer", 1 | 2 => "Player", - _ => "Tie", + 4 => "Tie", + _ => return Err(ErrorCode::InvalidGameResult.into()), }; let blackjack_game = &mut ctx.accounts.blackjack_game; @@ -1440,4 +1440,6 @@ pub enum ErrorCode { ClusterNotSet, #[msg("Not authorized to perform this action")] NotAuthorized, + #[msg("Unexpected game result value from computation")] + InvalidGameResult, } diff --git a/blackjack/tests/blackjack.ts b/blackjack/tests/blackjack.ts index 78e3038b..03ca1dde 100644 --- a/blackjack/tests/blackjack.ts +++ b/blackjack/tests/blackjack.ts @@ -407,7 +407,6 @@ describe("Blackjack", () => { finalizeStandSig ); - // Handle both stand and bust events (player might bust during stand calculation) const standEvent = await Promise.race([ playerStandEventPromise, playerBustOnStandEventPromise, @@ -416,13 +415,11 @@ describe("Blackjack", () => { gameState = await program.account.blackjackGame.fetch(blackjackGamePDA); if (!("clientNonce" in standEvent)) { - // PlayerStandEvent (no clientNonce field, unlike PlayerBustEvent) console.log("Received PlayerStandEvent."); playerStood = true; expect(gameState.gameState).to.deep.equal({ dealerTurn: {} }); console.log("Player stands. Proceeding to Dealer's Turn."); } else { - // PlayerBustEvent - player busted during stand calculation console.log("Received PlayerBustEvent during stand."); playerBusted = true; expect(gameState.gameState).to.deep.equal({ resolving: {} }); diff --git a/rock_paper_scissors/against-house/encrypted-ixs/src/lib.rs b/rock_paper_scissors/against-house/encrypted-ixs/src/lib.rs index ee5cdc96..751c101a 100644 --- a/rock_paper_scissors/against-house/encrypted-ixs/src/lib.rs +++ b/rock_paper_scissors/against-house/encrypted-ixs/src/lib.rs @@ -13,9 +13,6 @@ mod circuits { pub fn play_rps(player_move_ctxt: Enc) -> u8 { let player_move = player_move_ctxt.to_arcis(); - // Sample a near-uniform house move in {0,1,2} using rejection sampling over 2 random bits. - // 00 -> 0, 01 -> 1, 10 -> 2, 11 -> reject and resample. - // Fixed iterations bound runtime; the fallback introduces negligible bias (<= (1/4)^16). let mut house_move: u8 = 0; let mut selected = false; @@ -23,7 +20,6 @@ mod circuits { let b0 = ArcisRNG::bool(); let b1 = ArcisRNG::bool(); - // Map (b0,b1) to 0..3 uniformly. let candidate: u8 = if b0 { if b1 { 3 diff --git a/voting/README.md b/voting/README.md index 5e2e0525..1bfaa74d 100644 --- a/voting/README.md +++ b/voting/README.md @@ -49,6 +49,7 @@ Key properties: - **Ballot secrecy**: Individual votes remain encrypted throughout the tallying process - **Distributed computation**: Arcium nodes jointly compute aggregate tallies - **Result accuracy**: Aggregate totals are computed correctly despite processing only encrypted data +- **Double-vote prevention**: A `VoterRecord` PDA (seeded by poll + voter keys) is initialized via Anchor's `init` constraint in the `vote` instruction — a second vote from the same voter fails because the account already exists ## Implementation Details @@ -140,20 +141,9 @@ pub fn vote_callback( Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; - // Prevent double-voting: check if VoterRecord PDA is already initialized - let voter_record_info = ctx.accounts.voter_record.to_account_info(); - if !voter_record_info.data_is_empty() { - return Ok(()); - } - - // ... (PDA ownership and derivation validation) - // Save new encrypted tallies + new nonce ctx.accounts.poll_acc.vote_state = o.ciphertexts; ctx.accounts.poll_acc.nonce = o.nonce; - - // Initialize VoterRecord PDA to mark this voter as having voted - // ... (VoterRecord initialization code) Ok(()) } ``` diff --git a/voting/programs/voting/src/lib.rs b/voting/programs/voting/src/lib.rs index 0d8dcfac..2ec09b5f 100644 --- a/voting/programs/voting/src/lib.rs +++ b/voting/programs/voting/src/lib.rs @@ -1,6 +1,4 @@ use anchor_lang::prelude::*; -use anchor_lang::solana_program::rent::Rent; -use anchor_lang::solana_program::sysvar::Sysvar; use arcium_anchor::prelude::*; use arcium_client::idl::arcium::types::CallbackAccount; @@ -122,31 +120,9 @@ pub mod voting { ) .build(); - if !ctx.accounts.voter_record.data_is_empty() { - return Err(ErrorCode::AlreadyVoted.into()); - } - - let rent = Rent::get()?; - let space = 8 + VoterRecord::INIT_SPACE; - let lamports_needed = rent.minimum_balance(space); - let current_lamports = ctx.accounts.voter_record.lamports(); - - if current_lamports < lamports_needed { - let transfer_amount = lamports_needed - current_lamports; - anchor_lang::system_program::transfer( - CpiContext::new( - ctx.accounts.system_program.to_account_info(), - anchor_lang::system_program::Transfer { - from: ctx.accounts.payer.to_account_info(), - to: ctx.accounts.voter_record.to_account_info(), - }, - ), - transfer_amount, - )?; - } + ctx.accounts.voter_record.bump = ctx.bumps.voter_record; ctx.accounts.sign_pda_account.bump = ctx.bumps.sign_pda_account; - let voter_record_key = ctx.accounts.voter_record.key(); queue_computation( ctx.accounts, @@ -155,20 +131,10 @@ pub mod voting { vec![VoteCallback::callback_ix( computation_offset, &ctx.accounts.mxe_account, - &[ - CallbackAccount { - pubkey: ctx.accounts.poll_acc.key(), - is_writable: true, - }, - CallbackAccount { - pubkey: ctx.accounts.payer.key(), // Original voter pubkey - is_writable: false, - }, - CallbackAccount { - pubkey: voter_record_key, // Pre-funded VoterRecord - is_writable: true, - }, - ], + &[CallbackAccount { + pubkey: ctx.accounts.poll_acc.key(), + is_writable: true, + }], )?], 1, 0, @@ -189,42 +155,9 @@ pub mod voting { Err(_) => return Err(ErrorCode::AbortedComputation.into()), }; - // Check VoterRecord BEFORE writing state to prevent double-counting - // from racing callbacks - let voter_record_info = ctx.accounts.voter_record.to_account_info(); - let voter = &ctx.accounts.voter; - let poll = &ctx.accounts.poll_acc; - - let (expected_pda, bump) = Pubkey::find_program_address( - &[b"voter", poll.key().as_ref(), voter.key().as_ref()], - ctx.program_id, - ); - require!( - voter_record_info.key() == expected_pda, - ErrorCode::InvalidVoterRecord - ); - - if !voter_record_info.data_is_empty() { - msg!("vote_callback: VoterRecord already initialized, skipping"); - return Ok(()); - } - - // Safe to write state — VoterRecord is uninitialized, so this is - // the first callback for this voter ctx.accounts.poll_acc.vote_state = o.ciphertexts; ctx.accounts.poll_acc.nonce = o.nonce; - voter_record_info.assign(&crate::ID); - - let space = 8 + VoterRecord::INIT_SPACE; - voter_record_info.resize(space)?; - - { - let mut data = voter_record_info.try_borrow_mut_data()?; - data[..8].copy_from_slice(&VoterRecord::DISCRIMINATOR); - data[8] = bump; - } - let clock = Clock::get()?; emit!(VoteEvent { @@ -492,15 +425,14 @@ pub struct Vote<'info> { has_one = authority )] pub poll_acc: Box>, - /// CHECK: VoterRecord PDA - checked manually in vote(). - /// Pre-funded here, initialized atomically in vote_callback(). - /// This ensures voter isn't locked out if callback fails. #[account( - mut, + init, + payer = payer, + space = 8 + VoterRecord::INIT_SPACE, seeds = [b"voter", poll_acc.key().as_ref(), payer.key().as_ref()], bump, )] - pub voter_record: UncheckedAccount<'info>, + pub voter_record: Account<'info, VoterRecord>, } #[callback_accounts("vote")] @@ -526,12 +458,6 @@ pub struct VoteCallback<'info> { pub instructions_sysvar: AccountInfo<'info>, #[account(mut)] pub poll_acc: Account<'info, PollAccount>, - /// CHECK: Original voter's pubkey, passed from vote() for PDA verification - pub voter: UncheckedAccount<'info>, - /// CHECK: Pre-funded VoterRecord PDA to be initialized - #[account(mut)] - pub voter_record: UncheckedAccount<'info>, - pub system_program: Program<'info, System>, } #[init_computation_definition_accounts("vote", payer)] @@ -689,9 +615,7 @@ pub struct PollAccount { pub question: String, } -/// Tracks that a voter has already voted on a specific poll. -/// The PDA seeds [b"voter", poll.key(), voter.key()] ensure each voter -/// can only have one record per poll, preventing double-voting. +/// Per-poll voter deduplication record. #[account] #[derive(InitSpace)] pub struct VoterRecord { @@ -707,10 +631,6 @@ pub enum ErrorCode { AbortedComputation, #[msg("Cluster not set")] ClusterNotSet, - #[msg("Already voted on this poll")] - AlreadyVoted, - #[msg("Invalid voter record PDA")] - InvalidVoterRecord, } #[event] diff --git a/voting/tests/voting.ts b/voting/tests/voting.ts index d5246538..da34a008 100644 --- a/voting/tests/voting.ts +++ b/voting/tests/voting.ts @@ -177,7 +177,6 @@ describe("Voting", () => { program.programId ); - // Derive voter_record PDA const [voterRecordPDA] = PublicKey.findProgramAddressSync( [Buffer.from("voter"), pollPDA.toBuffer(), owner.publicKey.toBuffer()], program.programId @@ -289,7 +288,9 @@ describe("Voting", () => { expect.fail("Double vote should have been rejected"); } catch (error) { console.log("Double vote correctly rejected:", error.message); - expect(error.message).to.include("AlreadyVoted"); + expect(error.message).to.satisfy( + (msg: string) => msg.includes("already in use") || msg.includes("0x0") + ); } // Reveal results for each poll From 3d3da799c6fa7bc0e64cc85ad6a923dae98cbb12 Mon Sep 17 00:00:00 2001 From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:09:58 +0530 Subject: [PATCH 07/10] refactor: deduplicate PDA derivation and fix naming --- blackjack/encrypted-ixs/src/lib.rs | 4 ++-- voting/tests/voting.ts | 23 ++++++++++------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/blackjack/encrypted-ixs/src/lib.rs b/blackjack/encrypted-ixs/src/lib.rs index 63e0dc1a..8fb01d94 100644 --- a/blackjack/encrypted-ixs/src/lib.rs +++ b/blackjack/encrypted-ixs/src/lib.rs @@ -88,12 +88,12 @@ mod circuits { player_hand_size: u8, dealer_hand_size: u8, ) -> (Enc, bool) { - let deck_array = deck_ctxt.to_arcis().unpack(); + let deck = deck_ctxt.to_arcis().unpack(); let mut player_hand = player_hand_ctxt.to_arcis().unpack(); let card_index = (player_hand_size + dealer_hand_size) as usize; - player_hand[player_hand_size as usize] = deck_array[card_index]; + player_hand[player_hand_size as usize] = deck[card_index]; let is_bust = calculate_hand_value(&player_hand, player_hand_size + 1) > 21; diff --git a/voting/tests/voting.ts b/voting/tests/voting.ts index da34a008..371c969a 100644 --- a/voting/tests/voting.ts +++ b/voting/tests/voting.ts @@ -157,6 +157,8 @@ describe("Voting", () => { // Cast votes for each poll with different outcomes const voteOutcomes = [true, false, true]; // Different outcomes for each poll + let firstPollPDA: PublicKey; + let firstVoterRecordPDA: PublicKey; for (let i = 0; i < POLL_IDS.length; i++) { const POLL_ID = POLL_IDS[i]; const vote = BigInt(voteOutcomes[i]); @@ -182,6 +184,11 @@ describe("Voting", () => { program.programId ); + if (i === 0) { + firstPollPDA = pollPDA; + firstVoterRecordPDA = voterRecordPDA; + } + const voteComputationOffset = new anchor.BN(randomBytes(8), "hex"); const queueVoteSig = await program.methods @@ -234,22 +241,12 @@ describe("Voting", () => { } // Test double-vote prevention: attempt to vote again on the first poll + // Reuse firstPollPDA and firstVoterRecordPDA derived during the voting loop console.log("\n--- Testing double-vote prevention ---"); const DOUBLE_VOTE_POLL_ID = POLL_IDS[0]; const doubleVoteNonce = randomBytes(16); const doubleVoteCiphertext = cipher.encrypt([BigInt(true)], doubleVoteNonce); - const doubleVotePollIdBuffer = Buffer.alloc(4); - doubleVotePollIdBuffer.writeUInt32LE(DOUBLE_VOTE_POLL_ID); - const [doubleVotePollPDA] = PublicKey.findProgramAddressSync( - [Buffer.from("poll"), owner.publicKey.toBuffer(), doubleVotePollIdBuffer], - program.programId - ); - const [doubleVoteVoterRecordPDA] = PublicKey.findProgramAddressSync( - [Buffer.from("voter"), doubleVotePollPDA.toBuffer(), owner.publicKey.toBuffer()], - program.programId - ); - const doubleVoteComputationOffset = new anchor.BN(randomBytes(8), "hex"); try { @@ -277,8 +274,8 @@ describe("Voting", () => { Buffer.from(getCompDefAccOffset("vote")).readUInt32LE() ), authority: owner.publicKey, - pollAcc: doubleVotePollPDA, - voterRecord: doubleVoteVoterRecordPDA, + pollAcc: firstPollPDA, + voterRecord: firstVoterRecordPDA, }) .rpc({ preflightCommitment: "confirmed", From 166b8e33b10d2d332f401ca78497d4e033d05893 Mon Sep 17 00:00:00 2001 From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:15:06 +0530 Subject: [PATCH 08/10] refactor: clean up test infrastructure and harden configurations - Remove redundant preflightCommitment when skipPreflight is true - Add event timeout handling to prevent tests hanging indefinitely - Add shutdown_wait and upgradeable=false to Anchor.toml configs - Box large account types to reduce stack usage - Sequentialize blackjack comp def inits and remove unnecessary sleep --- blackjack/tests/blackjack.ts | 88 ++++++++++++------- coinflip/Anchor.toml | 5 ++ coinflip/tests/coinflip.ts | 13 ++- ed25519/Anchor.toml | 2 + ed25519/tests/ed_25519.ts | 23 +++-- rock_paper_scissors/against-house/Anchor.toml | 5 ++ .../tests/rock_paper_scissors_against_rng.ts | 17 +++- .../against-player/Anchor.toml | 5 ++ .../tests/rock_paper_scissors.ts | 26 ++---- sealed_bid_auction/Anchor.toml | 5 ++ .../programs/sealed_bid_auction/src/lib.rs | 8 +- .../tests/sealed_bid_auction.ts | 18 ++-- share_medical_records/Anchor.toml | 5 ++ .../tests/share_medical_records.ts | 12 ++- voting/Anchor.toml | 5 ++ voting/programs/voting/src/lib.rs | 2 +- voting/tests/voting.ts | 20 +++-- 17 files changed, 172 insertions(+), 87 deletions(-) diff --git a/blackjack/tests/blackjack.ts b/blackjack/tests/blackjack.ts index 03ca1dde..b68c5995 100644 --- a/blackjack/tests/blackjack.ts +++ b/blackjack/tests/blackjack.ts @@ -117,28 +117,19 @@ describe("Blackjack", () => { // --- Initialize Computation Definitions --- console.log("Initializing computation definitions..."); - await Promise.all([ - initShuffleAndDealCardsCompDef(program as any, owner).then((sig) => - console.log("Shuffle/Deal CompDef Init Sig:", sig) - ), - initPlayerHitCompDef(program as any, owner).then((sig) => - console.log("Player Hit CompDef Init Sig:", sig) - ), - initPlayerStandCompDef(program as any, owner).then((sig) => - console.log("Player Stand CompDef Init Sig:", sig) - ), - initPlayerDoubleDownCompDef(program as any, owner).then((sig) => - console.log("Player DoubleDown CompDef Init Sig:", sig) - ), - initDealerPlayCompDef(program as any, owner).then((sig) => - console.log("Dealer Play CompDef Init Sig:", sig) - ), - initResolveGameCompDef(program as any, owner).then((sig) => - console.log("Resolve Game CompDef Init Sig:", sig) - ), - ]); + const inits = [ + { name: "Shuffle/Deal", fn: initShuffleAndDealCardsCompDef }, + { name: "Player Hit", fn: initPlayerHitCompDef }, + { name: "Player Stand", fn: initPlayerStandCompDef }, + { name: "Player DoubleDown", fn: initPlayerDoubleDownCompDef }, + { name: "Dealer Play", fn: initDealerPlayCompDef }, + { name: "Resolve Game", fn: initResolveGameCompDef }, + ]; + for (const { name, fn } of inits) { + const sig = await fn(program as any, owner); + console.log(`${name} CompDef Init Sig:`, sig); + } console.log("All computation definitions initialized."); - await new Promise((res) => setTimeout(res, 2000)); // --- Setup Game Cryptography --- const privateKey = x25519.utils.randomSecretKey(); @@ -198,7 +189,10 @@ describe("Blackjack", () => { blackjackGame: blackjackGamePDA, }) .signers([owner]) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); console.log("Initialize game TX Signature:", initGameSig); console.log("Waiting for shuffle/deal computation finalization..."); @@ -303,7 +297,10 @@ describe("Blackjack", () => { payer: owner.publicKey, }) .signers([owner]) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); console.log("Player Hit TX Signature:", playerHitSig); console.log("Waiting for player hit computation finalization..."); @@ -392,7 +389,10 @@ describe("Blackjack", () => { payer: owner.publicKey, }) .signers([owner]) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); console.log("Player Stand TX Signature:", playerStandSig); console.log("Waiting for player stand computation finalization..."); @@ -465,7 +465,10 @@ describe("Blackjack", () => { blackjackGame: blackjackGamePDA, }) .signers([owner]) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); console.log("Dealer Play TX Signature:", dealerPlaySig); console.log("Waiting for dealer play computation finalization..."); @@ -529,7 +532,10 @@ describe("Blackjack", () => { payer: owner.publicKey, }) .signers([owner]) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); console.log("Resolve Game TX Signature:", resolveSig); console.log("Waiting for resolve game computation finalization..."); @@ -600,7 +606,10 @@ describe("Blackjack", () => { mxeAccount, addressLookupTable: lutAddress, }) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); const rawCircuit = fs.readFileSync("build/shuffle_and_deal_cards.arcis"); await uploadCircuit( @@ -652,7 +661,10 @@ describe("Blackjack", () => { mxeAccount, addressLookupTable: lutAddress, }) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); const rawCircuit = fs.readFileSync("build/player_hit.arcis"); await uploadCircuit( @@ -704,7 +716,10 @@ describe("Blackjack", () => { mxeAccount, addressLookupTable: lutAddress, }) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); const rawCircuit = fs.readFileSync("build/player_stand.arcis"); await uploadCircuit( @@ -756,7 +771,10 @@ describe("Blackjack", () => { mxeAccount, addressLookupTable: lutAddress, }) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); const rawCircuit = fs.readFileSync("build/player_double_down.arcis"); await uploadCircuit( @@ -808,7 +826,10 @@ describe("Blackjack", () => { mxeAccount, addressLookupTable: lutAddress, }) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); const rawCircuit = fs.readFileSync("build/dealer_play.arcis"); await uploadCircuit( @@ -860,7 +881,10 @@ describe("Blackjack", () => { mxeAccount, addressLookupTable: lutAddress, }) - .rpc({ commitment: "confirmed", preflightCommitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); const rawCircuit = fs.readFileSync("build/resolve_game.arcis"); await uploadCircuit( diff --git a/coinflip/Anchor.toml b/coinflip/Anchor.toml index 1eac2dba..dfcdb6d2 100644 --- a/coinflip/Anchor.toml +++ b/coinflip/Anchor.toml @@ -17,3 +17,8 @@ wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 \"tests/**/*.ts\"" + +[test] +startup_wait = 20000 +shutdown_wait = 2000 +upgradeable = false diff --git a/coinflip/tests/coinflip.ts b/coinflip/tests/coinflip.ts index 7563d5db..060c6c7e 100644 --- a/coinflip/tests/coinflip.ts +++ b/coinflip/tests/coinflip.ts @@ -34,16 +34,22 @@ describe("Coinflip", () => { type Event = anchor.IdlEvents<(typeof program)["idl"]>; const awaitEvent = async ( - eventName: E + eventName: E, + timeoutMs = 120000 ): Promise => { let listenerId: number; - const event = await new Promise((res) => { + let timeoutId: NodeJS.Timeout; + const event = await new Promise((res, rej) => { listenerId = program.addEventListener(eventName, (event) => { + clearTimeout(timeoutId); res(event); }); + timeoutId = setTimeout(() => { + program.removeEventListener(listenerId); + rej(new Error(`Event ${eventName} timed out after ${timeoutMs}ms`)); + }, timeoutMs); }); await program.removeEventListener(listenerId); - return event; }; @@ -107,7 +113,6 @@ describe("Coinflip", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); console.log("Queue sig is ", queueSig); diff --git a/ed25519/Anchor.toml b/ed25519/Anchor.toml index 31d2d431..9b76f0cd 100644 --- a/ed25519/Anchor.toml +++ b/ed25519/Anchor.toml @@ -20,3 +20,5 @@ test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 \"tests/**/*.ts\"" [test] startup_wait = 20000 +shutdown_wait = 2000 +upgradeable = false diff --git a/ed25519/tests/ed_25519.ts b/ed25519/tests/ed_25519.ts index 7483adf0..60e853f4 100644 --- a/ed25519/tests/ed_25519.ts +++ b/ed25519/tests/ed_25519.ts @@ -38,16 +38,22 @@ describe("Ed25519", () => { type Event = anchor.IdlEvents<(typeof program)["idl"]>; const awaitEvent = async ( - eventName: E + eventName: E, + timeoutMs = 120000 ): Promise => { let listenerId: number; - const event = await new Promise((res) => { + let timeoutId: NodeJS.Timeout; + const event = await new Promise((res, rej) => { listenerId = program.addEventListener(eventName, (event) => { + clearTimeout(timeoutId); res(event); }); + timeoutId = setTimeout(() => { + program.removeEventListener(listenerId); + rej(new Error(`Event ${eventName} timed out after ${timeoutMs}ms`)); + }, timeoutMs); }); await program.removeEventListener(listenerId); - return event; }; @@ -100,7 +106,10 @@ describe("Ed25519", () => { Buffer.from(getCompDefAccOffset("sign_message")).readUInt32LE() ), }) - .rpc({ skipPreflight: true, preflightCommitment: "confirmed", commitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); await awaitComputationFinalization( provider as anchor.AnchorProvider, @@ -211,7 +220,10 @@ describe("Ed25519", () => { Buffer.from(getCompDefAccOffset("verify_signature")).readUInt32LE() ), }) - .rpc({ skipPreflight: true, preflightCommitment: "confirmed", commitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); await awaitComputationFinalization( provider as anchor.AnchorProvider, @@ -345,7 +357,6 @@ async function getMXEPublicKeyWithRetry( maxRetries: number = 20, retryDelayMs: number = 500 ): Promise { - console.log(""); for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const mxePublicKey = await getMXEPublicKey(provider, programId); diff --git a/rock_paper_scissors/against-house/Anchor.toml b/rock_paper_scissors/against-house/Anchor.toml index 0b6a1f37..813a84fe 100644 --- a/rock_paper_scissors/against-house/Anchor.toml +++ b/rock_paper_scissors/against-house/Anchor.toml @@ -17,3 +17,8 @@ wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 \"tests/**/*.ts\"" + +[test] +startup_wait = 20000 +shutdown_wait = 2000 +upgradeable = false diff --git a/rock_paper_scissors/against-house/tests/rock_paper_scissors_against_rng.ts b/rock_paper_scissors/against-house/tests/rock_paper_scissors_against_rng.ts index a2c16d7d..95517355 100644 --- a/rock_paper_scissors/against-house/tests/rock_paper_scissors_against_rng.ts +++ b/rock_paper_scissors/against-house/tests/rock_paper_scissors_against_rng.ts @@ -34,16 +34,22 @@ describe("RockPaperScissorsAgainstRng", () => { type Event = anchor.IdlEvents<(typeof program)["idl"]>; const awaitEvent = async ( - eventName: E + eventName: E, + timeoutMs = 120000 ): Promise => { let listenerId: number; - const event = await new Promise((res) => { + let timeoutId: NodeJS.Timeout; + const event = await new Promise((res, rej) => { listenerId = program.addEventListener(eventName, (event) => { + clearTimeout(timeoutId); res(event); }); + timeoutId = setTimeout(() => { + program.removeEventListener(listenerId); + rej(new Error(`Event ${eventName} timed out after ${timeoutMs}ms`)); + }, timeoutMs); }); await program.removeEventListener(listenerId); - return event; }; @@ -105,7 +111,10 @@ describe("RockPaperScissorsAgainstRng", () => { Buffer.from(getCompDefAccOffset("play_rps")).readUInt32LE() ), }) - .rpc({ preflightCommitment: "confirmed", commitment: "confirmed" }); + .rpc({ + skipPreflight: true, + commitment: "confirmed", + }); console.log("Queue sig is ", queueSig); const finalizeSig = await awaitComputationFinalization( diff --git a/rock_paper_scissors/against-player/Anchor.toml b/rock_paper_scissors/against-player/Anchor.toml index 29bef737..f9040959 100644 --- a/rock_paper_scissors/against-player/Anchor.toml +++ b/rock_paper_scissors/against-player/Anchor.toml @@ -17,3 +17,8 @@ wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 \"tests/**/*.ts\"" + +[test] +startup_wait = 20000 +shutdown_wait = 2000 +upgradeable = false diff --git a/rock_paper_scissors/against-player/tests/rock_paper_scissors.ts b/rock_paper_scissors/against-player/tests/rock_paper_scissors.ts index 5b3d6a4b..95e4fa41 100644 --- a/rock_paper_scissors/against-player/tests/rock_paper_scissors.ts +++ b/rock_paper_scissors/against-player/tests/rock_paper_scissors.ts @@ -36,16 +36,22 @@ describe("RockPaperScissors", () => { type Event = anchor.IdlEvents<(typeof program)["idl"]>; const awaitEvent = async ( - eventName: E + eventName: E, + timeoutMs = 120000 ): Promise => { let listenerId: number; - const event = await new Promise((res) => { + let timeoutId: NodeJS.Timeout; + const event = await new Promise((res, rej) => { listenerId = program.addEventListener(eventName, (event) => { + clearTimeout(timeoutId); res(event); }); + timeoutId = setTimeout(() => { + program.removeEventListener(listenerId); + rej(new Error(`Event ${eventName} timed out after ${timeoutMs}ms`)); + }, timeoutMs); }); await program.removeEventListener(listenerId); - return event; }; @@ -142,7 +148,6 @@ describe("RockPaperScissors", () => { .signers([owner]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -219,7 +224,6 @@ describe("RockPaperScissors", () => { .signers([playerA]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -296,7 +300,6 @@ describe("RockPaperScissors", () => { .signers([playerB]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -345,7 +348,6 @@ describe("RockPaperScissors", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -412,7 +414,6 @@ describe("RockPaperScissors", () => { .signers([owner]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -489,7 +490,6 @@ describe("RockPaperScissors", () => { .signers([unauthorizedPlayer]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -567,7 +567,6 @@ describe("RockPaperScissors", () => { .signers([owner]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -627,7 +626,6 @@ describe("RockPaperScissors", () => { .signers([playerA]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -687,7 +685,6 @@ describe("RockPaperScissors", () => { .signers([playerB]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -736,7 +733,6 @@ describe("RockPaperScissors", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -808,7 +804,6 @@ describe("RockPaperScissors", () => { .signers([owner]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -870,7 +865,6 @@ describe("RockPaperScissors", () => { .signers([playerA]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -929,7 +923,6 @@ describe("RockPaperScissors", () => { .signers([playerB]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -975,7 +968,6 @@ describe("RockPaperScissors", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); diff --git a/sealed_bid_auction/Anchor.toml b/sealed_bid_auction/Anchor.toml index b5f82846..ae95393c 100644 --- a/sealed_bid_auction/Anchor.toml +++ b/sealed_bid_auction/Anchor.toml @@ -17,3 +17,8 @@ wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 \"tests/**/*.ts\"" + +[test] +startup_wait = 20000 +shutdown_wait = 2000 +upgradeable = false diff --git a/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs b/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs index 079b08c2..52b2b55d 100644 --- a/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs +++ b/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs @@ -435,7 +435,7 @@ pub struct CreateAuction<'info> { seeds = [b"auction", authority.key().as_ref()], bump, )] - pub auction: Account<'info, Auction>, + pub auction: Box>, #[account( init_if_needed, space = 9, @@ -496,7 +496,7 @@ pub struct PlaceBid<'info> { #[account(mut)] pub bidder: Signer<'info>, #[account(mut)] - pub auction: Account<'info, Auction>, + pub auction: Box>, #[account( init_if_needed, space = 9, @@ -568,7 +568,7 @@ pub struct DetermineWinnerFirstPrice<'info> { #[account(mut)] pub authority: Signer<'info>, #[account(mut, has_one = authority @ ErrorCode::Unauthorized)] - pub auction: Account<'info, Auction>, + pub auction: Box>, #[account( init_if_needed, space = 9, @@ -629,7 +629,7 @@ pub struct DetermineWinnerVickrey<'info> { #[account(mut)] pub authority: Signer<'info>, #[account(mut, has_one = authority @ ErrorCode::Unauthorized)] - pub auction: Account<'info, Auction>, + pub auction: Box>, #[account( init_if_needed, space = 9, diff --git a/sealed_bid_auction/tests/sealed_bid_auction.ts b/sealed_bid_auction/tests/sealed_bid_auction.ts index ed8f9e37..592157d0 100644 --- a/sealed_bid_auction/tests/sealed_bid_auction.ts +++ b/sealed_bid_auction/tests/sealed_bid_auction.ts @@ -52,13 +52,20 @@ describe("SealedBidAuction", () => { type Event = anchor.IdlEvents<(typeof program)["idl"]>; const awaitEvent = async ( - eventName: E + eventName: E, + timeoutMs = 120000 ): Promise => { let listenerId: number; - const event = await new Promise((res) => { + let timeoutId: NodeJS.Timeout; + const event = await new Promise((res, rej) => { listenerId = program.addEventListener(eventName, (event) => { + clearTimeout(timeoutId); res(event); }); + timeoutId = setTimeout(() => { + program.removeEventListener(listenerId); + rej(new Error(`Event ${eventName} timed out after ${timeoutMs}ms`)); + }, timeoutMs); }); await program.removeEventListener(listenerId); return event; @@ -159,7 +166,6 @@ describe("SealedBidAuction", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -222,7 +228,6 @@ describe("SealedBidAuction", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -288,7 +293,6 @@ describe("SealedBidAuction", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -395,7 +399,6 @@ describe("SealedBidAuction", () => { .signers([vickreyAuthority]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -454,7 +457,6 @@ describe("SealedBidAuction", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -515,7 +517,6 @@ describe("SealedBidAuction", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -582,7 +583,6 @@ describe("SealedBidAuction", () => { .signers([vickreyAuthority]) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); diff --git a/share_medical_records/Anchor.toml b/share_medical_records/Anchor.toml index 6594643c..3d096e36 100644 --- a/share_medical_records/Anchor.toml +++ b/share_medical_records/Anchor.toml @@ -17,3 +17,8 @@ wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 \"tests/**/*.ts\"" + +[test] +startup_wait = 20000 +shutdown_wait = 2000 +upgradeable = false diff --git a/share_medical_records/tests/share_medical_records.ts b/share_medical_records/tests/share_medical_records.ts index 7a36e8c4..e129632c 100644 --- a/share_medical_records/tests/share_medical_records.ts +++ b/share_medical_records/tests/share_medical_records.ts @@ -36,16 +36,22 @@ describe("ShareMedicalRecords", () => { type Event = anchor.IdlEvents<(typeof program)["idl"]>; const awaitEvent = async ( - eventName: E + eventName: E, + timeoutMs = 120000 ): Promise => { let listenerId: number; - const event = await new Promise((res) => { + let timeoutId: NodeJS.Timeout; + const event = await new Promise((res, rej) => { listenerId = program.addEventListener(eventName, (event) => { + clearTimeout(timeoutId); res(event); }); + timeoutId = setTimeout(() => { + program.removeEventListener(listenerId); + rej(new Error(`Event ${eventName} timed out after ${timeoutMs}ms`)); + }, timeoutMs); }); await program.removeEventListener(listenerId); - return event; }; diff --git a/voting/Anchor.toml b/voting/Anchor.toml index 540f6c18..57c188a6 100644 --- a/voting/Anchor.toml +++ b/voting/Anchor.toml @@ -17,3 +17,8 @@ wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 2000000 \"tests/**/*.ts\"" + +[test] +startup_wait = 20000 +shutdown_wait = 2000 +upgradeable = false diff --git a/voting/programs/voting/src/lib.rs b/voting/programs/voting/src/lib.rs index 2ec09b5f..9480f8be 100644 --- a/voting/programs/voting/src/lib.rs +++ b/voting/programs/voting/src/lib.rs @@ -432,7 +432,7 @@ pub struct Vote<'info> { seeds = [b"voter", poll_acc.key().as_ref(), payer.key().as_ref()], bump, )] - pub voter_record: Account<'info, VoterRecord>, + pub voter_record: Box>, } #[callback_accounts("vote")] diff --git a/voting/tests/voting.ts b/voting/tests/voting.ts index 371c969a..1a1dfae4 100644 --- a/voting/tests/voting.ts +++ b/voting/tests/voting.ts @@ -57,16 +57,22 @@ describe("Voting", () => { type Event = anchor.IdlEvents<(typeof program)["idl"]>; const awaitEvent = async ( - eventName: E + eventName: E, + timeoutMs = 120000 ): Promise => { let listenerId: number; - const event = await new Promise((res) => { + let timeoutId: NodeJS.Timeout; + const event = await new Promise((res, rej) => { listenerId = program.addEventListener(eventName, (event) => { + clearTimeout(timeoutId); res(event); }); + timeoutId = setTimeout(() => { + program.removeEventListener(listenerId); + rej(new Error(`Event ${eventName} timed out after ${timeoutMs}ms`)); + }, timeoutMs); }); await program.removeEventListener(listenerId); - return event; }; @@ -140,7 +146,6 @@ describe("Voting", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); @@ -220,7 +225,6 @@ describe("Voting", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); console.log(`Queue vote for poll ${POLL_ID} sig is `, queueVoteSig); @@ -245,7 +249,10 @@ describe("Voting", () => { console.log("\n--- Testing double-vote prevention ---"); const DOUBLE_VOTE_POLL_ID = POLL_IDS[0]; const doubleVoteNonce = randomBytes(16); - const doubleVoteCiphertext = cipher.encrypt([BigInt(true)], doubleVoteNonce); + const doubleVoteCiphertext = cipher.encrypt( + [BigInt(true)], + doubleVoteNonce + ); const doubleVoteComputationOffset = new anchor.BN(randomBytes(8), "hex"); @@ -319,7 +326,6 @@ describe("Voting", () => { }) .rpc({ skipPreflight: true, - preflightCommitment: "confirmed", commitment: "confirmed", }); console.log(`Reveal queue for poll ${POLL_ID} sig is `, revealQueueSig); From 7af91dab101a7a6c91d10bf542d0572e76960365 Mon Sep 17 00:00:00 2001 From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:20:07 +0530 Subject: [PATCH 09/10] fix: resolve voting stack overflow and auction test timing Box MXEAccount in Vote struct to prevent stack overflow. Increase auction end_time to 60s and wait to 65s for CI reliability. --- sealed_bid_auction/tests/sealed_bid_auction.ts | 8 ++++---- voting/programs/voting/src/lib.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sealed_bid_auction/tests/sealed_bid_auction.ts b/sealed_bid_auction/tests/sealed_bid_auction.ts index 592157d0..643fa7ea 100644 --- a/sealed_bid_auction/tests/sealed_bid_auction.ts +++ b/sealed_bid_auction/tests/sealed_bid_auction.ts @@ -142,7 +142,7 @@ describe("SealedBidAuction", () => { createComputationOffset, { firstPrice: {} }, // AuctionType::FirstPrice new anchor.BN(100), // min_bid: 100 lamports - new anchor.BN(Math.floor(Date.now() / 1000) + 10) // end_time: 10 seconds from now + new anchor.BN(Math.floor(Date.now() / 1000) + 60) // end_time: 60 seconds from now ) .accountsPartial({ authority: owner.publicKey, @@ -247,7 +247,7 @@ describe("SealedBidAuction", () => { // Step 3: Close auction console.log("\nStep 3: Waiting for auction to end..."); - await new Promise((resolve) => setTimeout(resolve, 12000)); + await new Promise((resolve) => setTimeout(resolve, 65000)); console.log("Closing auction..."); const auctionClosedPromise = awaitEvent("auctionClosedEvent"); @@ -374,7 +374,7 @@ describe("SealedBidAuction", () => { createComputationOffset, { vickrey: {} }, // AuctionType::Vickrey new anchor.BN(50), // min_bid: 50 lamports - new anchor.BN(Math.floor(Date.now() / 1000) + 10) // end_time: 10 seconds from now + new anchor.BN(Math.floor(Date.now() / 1000) + 60) // end_time: 60 seconds from now ) .accountsPartial({ authority: vickreyAuthority.publicKey, @@ -535,7 +535,7 @@ describe("SealedBidAuction", () => { // Step 4: Close auction console.log("\nStep 4: Waiting for auction to end..."); - await new Promise((resolve) => setTimeout(resolve, 12000)); + await new Promise((resolve) => setTimeout(resolve, 65000)); console.log("Closing Vickrey auction..."); const auctionClosedPromise = awaitEvent("auctionClosedEvent"); diff --git a/voting/programs/voting/src/lib.rs b/voting/programs/voting/src/lib.rs index 9480f8be..ecbeae6b 100644 --- a/voting/programs/voting/src/lib.rs +++ b/voting/programs/voting/src/lib.rs @@ -374,7 +374,7 @@ pub struct Vote<'info> { #[account( address = derive_mxe_pda!() )] - pub mxe_account: Account<'info, MXEAccount>, + pub mxe_account: Box>, #[account( mut, address = derive_mempool_pda!(mxe_account, ErrorCode::ClusterNotSet) From 0a2e5bdd11acfaf487e3a256d846bb998c9e61ba Mon Sep 17 00:00:00 2001 From: Arihant Bansal <17180950+arihantbansal@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:08:10 +0530 Subject: [PATCH 10/10] fix: compute auction end_time on-chain from duration --- .../programs/sealed_bid_auction/src/lib.rs | 5 +- .../tests/sealed_bid_auction.ts | 95 +++++++++++++++---- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs b/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs index 52b2b55d..0cfc6dfb 100644 --- a/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs +++ b/sealed_bid_auction/programs/sealed_bid_auction/src/lib.rs @@ -60,7 +60,7 @@ pub mod sealed_bid_auction { computation_offset: u64, auction_type: AuctionType, min_bid: u64, - end_time: i64, + duration: i64, ) -> Result<()> { let auction = &mut ctx.accounts.auction; auction.bump = ctx.bumps.auction; @@ -68,7 +68,8 @@ pub mod sealed_bid_auction { auction.auction_type = auction_type; auction.status = AuctionStatus::Open; auction.min_bid = min_bid; - auction.end_time = end_time; + let clock = Clock::get()?; + auction.end_time = clock.unix_timestamp + duration; auction.bid_count = 0; auction.encrypted_state = [[0u8; 32]; 5]; diff --git a/sealed_bid_auction/tests/sealed_bid_auction.ts b/sealed_bid_auction/tests/sealed_bid_auction.ts index 643fa7ea..855160d0 100644 --- a/sealed_bid_auction/tests/sealed_bid_auction.ts +++ b/sealed_bid_auction/tests/sealed_bid_auction.ts @@ -51,17 +51,39 @@ describe("SealedBidAuction", () => { const provider = anchor.getProvider(); type Event = anchor.IdlEvents<(typeof program)["idl"]>; + + async function getValidatorTimestamp( + connection: anchor.web3.Connection + ): Promise { + const slot = await connection.getSlot("confirmed"); + const blockTime = await connection.getBlockTime(slot); + if (blockTime === null) { + throw new Error("Could not fetch block time from validator"); + } + return blockTime; + } + const awaitEvent = async ( eventName: E, + auctionKey?: PublicKey, timeoutMs = 120000 ): Promise => { let listenerId: number; let timeoutId: NodeJS.Timeout; const event = await new Promise((res, rej) => { - listenerId = program.addEventListener(eventName, (event) => { - clearTimeout(timeoutId); - res(event); - }); + listenerId = program.addEventListener( + eventName, + (event: Record) => { + if ( + auctionKey && + event.auction instanceof PublicKey && + !event.auction.equals(auctionKey) + ) + return; + clearTimeout(timeoutId); + res(event as Event[E]); + } + ); timeoutId = setTimeout(() => { program.removeEventListener(listenerId); rej(new Error(`Event ${eventName} timed out after ${timeoutMs}ms`)); @@ -129,7 +151,6 @@ describe("SealedBidAuction", () => { // Step 1: Create First-Price Auction console.log("Step 1: Creating first-price auction..."); - const auctionCreatedPromise = awaitEvent("auctionCreatedEvent"); const createComputationOffset = new anchor.BN(randomBytes(8), "hex"); const [auctionPDA] = PublicKey.findProgramAddressSync( @@ -137,12 +158,17 @@ describe("SealedBidAuction", () => { program.programId ); + const auctionCreatedPromise = awaitEvent( + "auctionCreatedEvent", + auctionPDA + ); + const createSig = await program.methods .createAuction( createComputationOffset, { firstPrice: {} }, // AuctionType::FirstPrice new anchor.BN(100), // min_bid: 100 lamports - new anchor.BN(Math.floor(Date.now() / 1000) + 60) // end_time: 60 seconds from now + new anchor.BN(120) // duration: 120 seconds ) .accountsPartial({ authority: owner.publicKey, @@ -189,7 +215,7 @@ describe("SealedBidAuction", () => { // Step 2: Place a bid console.log("\nStep 2: Placing bid of 500 lamports..."); - const bidPlacedPromise = awaitEvent("bidPlacedEvent"); + const bidPlacedPromise = awaitEvent("bidPlacedEvent", auctionPDA); const bidComputationOffset = new anchor.BN(randomBytes(8), "hex"); const bidAmount = BigInt(500); @@ -247,9 +273,19 @@ describe("SealedBidAuction", () => { // Step 3: Close auction console.log("\nStep 3: Waiting for auction to end..."); - await new Promise((resolve) => setTimeout(resolve, 65000)); + const auctionAccount = await program.account.auction.fetch(auctionPDA); + const endTime = auctionAccount.endTime.toNumber(); + while (true) { + const currentTime = await getValidatorTimestamp(provider.connection); + if (currentTime >= endTime) break; + const remaining = endTime - currentTime; + console.log( + ` Validator clock: ${currentTime}, end_time: ${endTime}, waiting ${remaining}s...` + ); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } console.log("Closing auction..."); - const auctionClosedPromise = awaitEvent("auctionClosedEvent"); + const auctionClosedPromise = awaitEvent("auctionClosedEvent", auctionPDA); const closeSig = await program.methods .closeAuction() @@ -266,7 +302,10 @@ describe("SealedBidAuction", () => { // Step 4: Determine winner (first-price) console.log("\nStep 4: Determining winner (first-price)..."); - const auctionResolvedPromise = awaitEvent("auctionResolvedEvent"); + const auctionResolvedPromise = awaitEvent( + "auctionResolvedEvent", + auctionPDA + ); const resolveComputationOffset = new anchor.BN(randomBytes(8), "hex"); const resolveSig = await program.methods @@ -361,7 +400,6 @@ describe("SealedBidAuction", () => { // Step 1: Create Vickrey Auction console.log("Step 1: Creating Vickrey auction..."); - const auctionCreatedPromise = awaitEvent("auctionCreatedEvent"); const createComputationOffset = new anchor.BN(randomBytes(8), "hex"); const [vickreyAuctionPDA] = PublicKey.findProgramAddressSync( @@ -369,12 +407,17 @@ describe("SealedBidAuction", () => { program.programId ); + const auctionCreatedPromise = awaitEvent( + "auctionCreatedEvent", + vickreyAuctionPDA + ); + const createSig = await program.methods .createAuction( createComputationOffset, { vickrey: {} }, // AuctionType::Vickrey new anchor.BN(50), // min_bid: 50 lamports - new anchor.BN(Math.floor(Date.now() / 1000) + 60) // end_time: 60 seconds from now + new anchor.BN(120) // duration: 120 seconds ) .accountsPartial({ authority: vickreyAuthority.publicKey, @@ -420,7 +463,7 @@ describe("SealedBidAuction", () => { // Step 2: Place first bid (1000 lamports) console.log("\nStep 2: Placing first bid of 1000 lamports..."); - const bidPlaced1Promise = awaitEvent("bidPlacedEvent"); + const bidPlaced1Promise = awaitEvent("bidPlacedEvent", vickreyAuctionPDA); const bid1ComputationOffset = new anchor.BN(randomBytes(8), "hex"); const bid1Amount = BigInt(1000); @@ -474,7 +517,7 @@ describe("SealedBidAuction", () => { // Step 3: Place second bid (700 lamports) - this becomes second-highest console.log("\nStep 3: Placing second bid of 700 lamports..."); - const bidPlaced2Promise = awaitEvent("bidPlacedEvent"); + const bidPlaced2Promise = awaitEvent("bidPlacedEvent", vickreyAuctionPDA); const bid2ComputationOffset = new anchor.BN(randomBytes(8), "hex"); // Use same bidder but different bid amount @@ -535,9 +578,24 @@ describe("SealedBidAuction", () => { // Step 4: Close auction console.log("\nStep 4: Waiting for auction to end..."); - await new Promise((resolve) => setTimeout(resolve, 65000)); + const vickreyAuctionAccount = await program.account.auction.fetch( + vickreyAuctionPDA + ); + const vickreyEndTime = vickreyAuctionAccount.endTime.toNumber(); + while (true) { + const currentTime = await getValidatorTimestamp(provider.connection); + if (currentTime >= vickreyEndTime) break; + const remaining = vickreyEndTime - currentTime; + console.log( + ` Validator clock: ${currentTime}, end_time: ${vickreyEndTime}, waiting ${remaining}s...` + ); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } console.log("Closing Vickrey auction..."); - const auctionClosedPromise = awaitEvent("auctionClosedEvent"); + const auctionClosedPromise = awaitEvent( + "auctionClosedEvent", + vickreyAuctionPDA + ); const closeSig = await program.methods .closeAuction() @@ -555,7 +613,10 @@ describe("SealedBidAuction", () => { // Step 5: Determine winner (Vickrey) console.log("\nStep 5: Determining winner (Vickrey)..."); - const auctionResolvedPromise = awaitEvent("auctionResolvedEvent"); + const auctionResolvedPromise = awaitEvent( + "auctionResolvedEvent", + vickreyAuctionPDA + ); const resolveComputationOffset = new anchor.BN(randomBytes(8), "hex"); const resolveSig = await program.methods