diff --git a/README.md b/README.md index 1690d05a..157f1a54 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,30 @@ Applications built on public blockchains face a fundamental limitation: all computation is transparent. These examples demonstrate how to build applications that can compute on encrypted data while preserving privacy. +All examples use Arcium's MPC protocol, which preserves privacy even with a dishonest majority -- data remains confidential as long as one node is honest. + ## Getting Started For installation instructions and setup, see the [Installation Guide](https://docs.arcium.com/developers/installation). -## Examples +Each example follows the same build/test flow: + +```bash +yarn install +arcium build +arcium test +``` -New to Arcium? Start with Coinflip and progress through the tiers in order. For conceptual background, see [Mental Model](https://docs.arcium.com/developers/arcis/mental-model). +New to Arcium? Start with Coinflip and progress through the tiers. For conceptual background, see [Mental Model](https://docs.arcium.com/developers/arcis/mental-model). + +## Examples ### Getting Started -**[Coinflip](./coinflip/)** - Generate trustless randomness using `ArcisRNG`. Stateless design, simplest example. +**[Coinflip](./coinflip/)** - Trustless randomness using `ArcisRNG`. Stateless design, simplest example. -**[Rock Paper Scissors](./rock_paper_scissors/)** - Encrypted asynchronous gameplay with hidden moves. -- [Player vs Player](./rock_paper_scissors/against-player/) - Two encrypted submissions +**[Rock Paper Scissors](./rock_paper_scissors/)** - Encrypted gameplay with hidden moves. +- [Player vs Player](./rock_paper_scissors/against-player/) - Two encrypted submissions, async commitment - [Player vs House](./rock_paper_scissors/against-house/) - Provably fair randomized opponent ### Intermediate diff --git a/blackjack/README.md b/blackjack/README.md index 69651860..940895e1 100644 --- a/blackjack/README.md +++ b/blackjack/README.md @@ -1,68 +1,24 @@ # Blackjack - Hidden Game State -Physical blackjack naturally hides the dealer's hole card. Digital blackjack has a different problem: the card's value must be stored somewhere - whether that's on a game server, in a database, or in code. Trusting a server to "hide" it just means betting they won't peek. +Physical blackjack naturally hides the dealer's hole card. Digital blackjack has a different problem: the card's value must be stored somewhere, and whoever stores it can peek. This example keeps cards encrypted until game rules require them to be revealed. -This example shows how to implement blackjack where cards remain encrypted until they need to be revealed according to game rules. +## How It Works -## Why is hidden information hard in digital card games? +1. A 52-card deck is shuffled using Arcium's cryptographic randomness +2. The entire deck, player hand, and dealer hand remain encrypted throughout gameplay +3. Players view their own cards; the dealer's hole card stays hidden +4. Game actions (hit, stand, double down) are processed against encrypted state +5. At resolution, hands are compared inside MPC and only the winner is revealed -Physical blackjack maintains three types of hidden information: the dealer's hole card, undealt cards in the deck, and random shuffle order. In digital implementations, this information must be stored as data - whether on a server or on a public blockchain - where it becomes vulnerable to inspection or manipulation. +## Implementation -Blockchain implementations face an additional challenge: transparent state. If card data is stored on-chain unencrypted, all participants can view the dealer's hidden cards and remaining deck order, completely breaking the game. +### The Size Problem and `Pack` -## How Hidden Game State Works +Encrypting 52 cards individually produces 52 x 32 = 1,664 bytes -- exceeds Solana's 1,232-byte transaction limit. Arcis `Pack` compresses byte arrays into fewer field elements: -At game initialization, a 52-card deck is shuffled using Arcium's cryptographic randomness. The entire deck, including player cards and the dealer's hole card, remains encrypted throughout gameplay. - -Information disclosure follows game rules: players view their own cards and the dealer's face-up card. Game actions (hit, stand, double down) are processed against encrypted hand values. The dealer's hole card and undealt cards remain encrypted until game resolution. - -## Running the Example - -```bash -# Install dependencies -yarn install # or npm install or pnpm install - -# Build the program -arcium build - -# Run tests -arcium test -``` - -The test suite demonstrates a complete game flow: deck shuffling with secure randomness, dealing encrypted cards, processing game actions against hidden state, and verifying final game result. - -## Technical Implementation - -The deck is stored as encrypted values, with multiple cards packed together for efficiency. Game logic processes encrypted hand values without ever decrypting them, using Arcium's confidential instructions. - -The system works through three key mechanisms: network-generated randomness creates unpredictable deck order, selective disclosure reveals only authorized information per game rules, and the MPC protocol ensures no party can manipulate game state or outcomes even with a dishonest majority—game integrity is preserved as long as one node is honest. - -## Implementation Details - -### The Encryption Size Problem - -**Requirement**: Encrypt a deck of 52 playing cards for an on-chain Blackjack game. - -**Naive Approach**: Store each card as a separate encrypted value. - -- Each card can be represented as `u8` (values 0-51) -- 52 cards = 52 `u8` values -- After encryption: each `u8` becomes a 32-byte ciphertext -- **Total size**: 52 x 32 bytes = **1,664 bytes** - -**The Problem**: Solana's transaction size limit is **1,232 bytes**, but our encrypted deck is 1,664 bytes. Whether returning the generated deck from initialization or passing it in transactions to deal more cards, the deck won't fit in a single transaction. - -### The Solution: `Pack` - -Arcis provides `Pack`, a built-in type that compresses byte arrays into fewer field elements for encryption. Instead of encrypting each card individually, `Pack` packs multiple bytes into a single field element. - -**How it works**: - -- Each field element holds up to 26 bytes (at 8 bits per byte) -- `Pack<[u8; 52]>` = 52 bytes / 26 bytes per element = **2 field elements** -- `Pack<[u8; 11]>` = 11 bytes / 26 bytes per element = **1 field element** - -**Usage in the circuit**: +- Each field element holds up to 26 bytes +- `Pack<[u8; 52]>` = 2 field elements = **64 bytes** (96% reduction) +- `Pack<[u8; 11]>` = 1 field element = **32 bytes** per hand ```rust type Deck = Pack<[u8; 52]>; @@ -72,67 +28,40 @@ let deck_packed: Deck = Pack::new(initial_deck); let deck = Mxe::get().from_arcis(deck_packed); ``` -**After encryption**: - -- 2 field elements -> 2 x 32 bytes = **64 bytes** -- **Savings**: 1,664 bytes -> 64 bytes (96% reduction) - -Accessing individual cards requires unpacking first: +Accessing individual cards requires unpacking: ```rust let deck_array = deck_ctxt.to_arcis().unpack(); let card = deck_array[index]; ``` -### Account Storage Structure +> [Best Practices](https://docs.arcium.com/developers/arcis/best-practices) + +### Account Storage -On-chain storage uses serialized byte arrays. Encrypted `Pack` values are stored as `[u8; 32]` ciphertexts: +Encrypted `Pack` values are stored as `[u8; 32]` ciphertexts: ```rust pub struct BlackjackGame { pub deck: [[u8; 32]; 2], // Pack<[u8; 52]> = 2 field elements pub player_hand: [u8; 32], // Pack<[u8; 11]> = 1 field element pub dealer_hand: [u8; 32], // Pack<[u8; 11]> = 1 field element - pub deck_nonce: u128, // Nonce for deck encryption - pub client_nonce: u128, // Nonce for player hand - pub dealer_nonce: u128, // Nonce for dealer hand - pub player_hand_size: u8, // How many cards actually in player_hand - pub dealer_hand_size: u8, // How many cards actually in dealer_hand + pub deck_nonce: u128, + pub client_nonce: u128, + pub dealer_nonce: u128, + pub player_hand_size: u8, + pub dealer_hand_size: u8, // ... other game state } ``` -**Why separate nonces?** Each encrypted value needs its own nonce for security. - -**Why track hand sizes?** The packed hand can hold up to 11 cards, but we need to know how many are actually present (could be 2, 3, 10, etc.). - -### The Result - -**Without packing**: - -- Deck: 52 encrypted values (1,664 bytes) -- Player hand: 11 encrypted values (352 bytes) -- Dealer hand: 11 encrypted values (352 bytes) -- **Total**: 2,368 bytes (won't fit in Solana transaction or account efficiently) - -**With `Pack`**: - -- Deck: 2 encrypted values (64 bytes) -- Player hand: 1 encrypted value (32 bytes) -- Dealer hand: 1 encrypted value (32 bytes) -- **Total**: 128 bytes - -**Additional benefits**: - -- Fewer MPC operations (4 encryptions vs 74) -- Faster computation (less encrypted data to process) -- Lower costs (fewer computation units) -- No manual encoding/decoding logic needed - -> For more optimization techniques, see [Best Practices](https://docs.arcium.com/developers/arcis/best-practices). - -### When to Use This Pattern +Each encrypted value needs its own nonce. Hand sizes are tracked separately because the packed array always holds 11 slots but only some contain real cards. -Use `Pack` when you have arrays of small values that are processed together. Arcis handles the packing and unpacking automatically — define a type alias, and the framework manages compression. +### Size Comparison -Examples: game pieces, map tiles, bit flags, small integers, inventory items. +| | Without Pack | With Pack | +|---|---|---| +| Deck | 1,664 bytes (52 x 32) | 64 bytes (2 x 32) | +| Player hand | 352 bytes (11 x 32) | 32 bytes (1 x 32) | +| Dealer hand | 352 bytes (11 x 32) | 32 bytes (1 x 32) | +| **Total** | **2,368 bytes** | **128 bytes** | diff --git a/blackjack/encrypted-ixs/src/lib.rs b/blackjack/encrypted-ixs/src/lib.rs index 8fb01d94..bc9b9ea2 100644 --- a/blackjack/encrypted-ixs/src/lib.rs +++ b/blackjack/encrypted-ixs/src/lib.rs @@ -72,7 +72,7 @@ mod circuits { ) } - // Returns true if the player has busted + // Checks the player's current hand value; returns true if already bust (>21) #[instruction] pub fn player_stand(player_hand_ctxt: Enc, player_hand_size: u8) -> bool { let player_hand = player_hand_ctxt.to_arcis().unpack(); @@ -80,7 +80,7 @@ mod circuits { (value > 21).reveal() } - // Returns true if the player has busted, if not, returns the new card + // Deals one card and forces stand; returns (updated hand, is_bust) #[instruction] pub fn player_double_down( deck_ctxt: Enc, diff --git a/coinflip/README.md b/coinflip/README.md index 312aa3a5..59aad4ea 100644 --- a/coinflip/README.md +++ b/coinflip/README.md @@ -1,91 +1,29 @@ # Coinflip - Trustless Randomness -Flip a coin online and you have to trust someone. Trust the server not to rig the flip, or trust yourself not to inspect the code and game the system. There's no way to prove it's actually random. - -This example shows how to generate verifiably random outcomes using distributed entropy generation. - -## Why is randomness hard to trust online? - -Traditional random number generation has a fundamental trust problem: whoever generates the random number can potentially influence or predict it. Server-side RNG can be biased by operators, client-side generation can be manipulated, and pseudorandom algorithms may have predictable patterns. The requirement is generating randomness that remains unpredictable and unbiased even when participants don't trust each other. +Flip a coin online and you have to trust someone. Trust the server not to rig the flip, or trust yourself not to inspect the code and game the system. This example generates verifiably random outcomes where no single party can predict or bias the result. ## How It Works -The coinflip follows this flow: - -1. **Player commitment**: The player's choice (heads/tails) is encrypted and submitted to the network -2. **Random generation**: Arcium nodes work together to generate a random boolean outcome -3. **Encrypted comparison**: The system compares the encrypted choice against the encrypted random result -4. **Result disclosure**: Only the win/loss outcome is revealed - -The comparison occurs on encrypted values throughout the computation. - -## Running the Example - -```bash -# Install dependencies -yarn install # or npm install or pnpm install - -# Build the program -arcium build - -# Run tests -arcium test -``` - -The test suite demonstrates the complete flow: player choice submission, secure random generation, encrypted comparison, and result verification. - -## Technical Implementation - -The player's choice is encrypted as a boolean, allowing result verification without exposing the choice prematurely. Random generation uses Arcium's cryptographic primitives, where Arcium nodes contribute entropy that no single node can predict or control. +1. Player's choice (heads/tails) is encrypted and submitted +2. Inside one MPC computation, Arcium nodes generate a random boolean, compare it against the encrypted choice, and produce the result +3. Only the win/loss outcome is revealed -## Implementation Details +## Implementation -### The Trustless Randomness Problem - -**Conceptual Challenge**: In traditional online systems, randomness comes from somewhere - a server, a third-party service, your browser's `Math.random()`. Each source requires trusting that entity: - -- **Server-generated**: Trust the operator doesn't rig outcomes -- **Third-party service**: Trust the service provider is honest -- **Client-side**: Trust the player doesn't inspect and manipulate - -**The Question**: Can we generate randomness where NO single party can predict or bias the outcome? - -### The MPC Randomness Solution - -Arcium's `ArcisRNG` generates randomness through multi-party computation: +### MPC Randomness ```rust pub fn flip(input_ctxt: Enc) -> bool { let input = input_ctxt.to_arcis(); - let toss = ArcisRNG::bool(); // MPC-generated randomness + let toss = ArcisRNG::bool(); (input.choice == toss).reveal() } ``` -> See [Arcis Primitives](https://docs.arcium.com/developers/arcis/primitives) for all randomness and cryptographic operations. - -**How it works**: +Each Arcium node contributes local entropy. The MPC protocol combines these into a final value that no single node could predict before all contributed. -1. Arcium nodes each generate local random values -2. Nodes combine their randomness using secure multi-party computation -3. Final random value is deterministic given all inputs -4. **No single node can predict the result** before all contribute -5. **No subset of nodes can bias the outcome**: The MPC protocol guarantees unbiased randomness even with a dishonest majority—the outcome remains unpredictable as long as one node is honest +> See [Arcis Primitives](https://docs.arcium.com/developers/arcis/primitives) for all randomness and cryptographic operations. ### Stateless Design -Unlike Voting or Blackjack, Coinflip has **no game state account**: - -- Receive encrypted player choice → Generate MPC random → Compare → Emit result -- Each flip is independent -- No persistent storage needed - -When randomness generation itself is the primary feature, stateless design is simplest. - -### When to Use This Pattern - -Use MPC randomness (`ArcisRNG`) when: - -- **No one should control outcome**: Lotteries, random drops, fair matchmaking -- **Platform can't be trusted**: House games where operator could cheat -- **Randomness is high-value**: Large prizes or critical game mechanics +Unlike Voting or Blackjack, Coinflip has no game state account. Each flip is independent -- receive encrypted choice, generate random, compare, emit result. diff --git a/ed25519/README.md b/ed25519/README.md index 8725ff18..83083544 100644 --- a/ed25519/README.md +++ b/ed25519/README.md @@ -1,34 +1,17 @@ -# Ed25519 Signatures - Confidential Key Management +# Ed25519 Signatures - Distributed Key Management -This example demonstrates Ed25519 signing and verification using distributed key management through multi-party computation. The private key is split across multiple nodes and never exists in a single location. +Ed25519 signing and verification using MPC. The private key is split across multiple nodes and never exists in a single location. ## How It Works -**Message Signing**: A message is sent to the Arcium network where MPC nodes collectively generate a valid Ed25519 signature using their key shares. The signature can be verified by anyone using the public key. +**Signing**: A message is sent to the Arcium network where MPC nodes collectively produce a valid Ed25519 signature using their key shares. The signature can be verified by anyone with the public key. -**Signature Verification with Confidential Public Key**: The verifying key (public key) is provided in encrypted form, and the signature is verified within MPC. Only the verification result (valid/invalid) is revealed to a designated observer. +**Verification with confidential public key**: The verifying key is provided encrypted, and verification runs inside MPC. Only the result (valid/invalid) is revealed to a designated observer. -## Running the Example - -```bash -# Install dependencies -yarn install - -# Build the program -arcium build - -# Run tests -arcium test -``` - -The test suite demonstrates both signing and verification flows with the MPC-managed key. - -## Technical Implementation +## Implementation ### MPC Signing -Arcium's `MXESigningKey` enables signing through distributed key shares: - ```rust pub fn sign_message(message: [u8; 5]) -> ArcisEd25519Signature { let signature = MXESigningKey::sign(&message); @@ -36,9 +19,9 @@ pub fn sign_message(message: [u8; 5]) -> ArcisEd25519Signature { } ``` -Each MPC node holds a share of the private key and executes a distributed signing protocol to produce a standard Ed25519 signature without reconstructing the complete key. +Each MPC node holds a key share and executes a distributed signing protocol to produce a standard Ed25519 signature without reconstructing the complete key. -> See [Arcis Primitives](https://docs.arcium.com/developers/arcis/primitives) for the full cryptographic API. +> [Arcis Primitives](https://docs.arcium.com/developers/arcis/primitives) ### Confidential Public Key Verification @@ -56,4 +39,4 @@ pub fn verify_signature( } ``` -In some scenarios, revealing which public key is being verified could leak sensitive information (identity, organizational affiliations). This pattern enables verification without public key disclosure. +Revealing which public key is being verified could leak identity or organizational affiliations. This pattern enables verification without public key disclosure. diff --git a/rock_paper_scissors/against-house/README.md b/rock_paper_scissors/against-house/README.md index 2bc2e241..65c7e26e 100644 --- a/rock_paper_scissors/against-house/README.md +++ b/rock_paper_scissors/against-house/README.md @@ -1,47 +1,16 @@ # Rock Paper Scissors vs House - Fair Gaming -Player-versus-house games require trust that the house algorithm operates fairly. In traditional implementations, the house can observe player moves before generating responses, or use biased random number generation to favor house outcomes. +In traditional player-vs-house games, the house can observe player moves before generating responses, or use biased RNG to favor house outcomes. This example demonstrates fair gaming where the house cannot access the player's move before generating its response. -This example demonstrates fair gaming where the house cannot access player moves before generating its response, and randomness generation is cryptographically secure. +## How It Works -## Why can't you trust the house? +1. Player's move is encrypted and submitted on-chain +2. Arcium nodes generate a random house move inside MPC +3. Both moves are compared in encrypted form +4. Only the game outcome (win/loss/tie) is revealed -Traditional house games have multiple trust problems: the house can see player moves before responding, bias the random number generator, or modify game behavior without transparency. Every layer requires trusting the operator not to cheat, creating information asymmetry that favors the house. +## Implementation -## How Provably Fair Gaming Works +The full game runs in a single MPC computation -- the player's encrypted move enters, the house move is generated via `ArcisRNG`, and the comparison happens without either move being exposed. -The protocol ensures fairness through cryptographic isolation: - -1. **Player encrypted submission**: The player's move is encrypted and submitted to the blockchain -2. **Random house move**: Arcium nodes generate a random house move using cryptographic randomness -3. **Encrypted comparison**: Both moves are compared in encrypted form -4. **Result disclosure**: Only the game outcome (win/loss/tie) is revealed - -Random number generation uses cryptographic primitives that no single party can predict or bias. - -## Running the Example - -```bash -# Install dependencies -yarn install # or npm install or pnpm install - -# Build the program -arcium build - -# Run tests -arcium test -``` - -The test suite demonstrates the complete protocol: player move encryption, house random response generation, encrypted comparison, and outcome verification. - -## Technical Implementation - -The player's move is encrypted on the client and stored on-chain as a ciphertext. The house move is generated using Arcium's cryptographic randomness (similar to Coinflip), where Arcium nodes contribute entropy that no single node can predict or control. - -Both moves are compared inside MPC on encrypted values. - -Key properties: - -- **Cryptographic randomness**: Arcium nodes contribute entropy; no single node or subset can predict or bias the outcome -- **Fair comparison**: Both moves processed in encrypted form throughout game resolution -- **Integrity**: The MPC protocol ensures correct game resolution even with a dishonest majority—neither the house nor the player can manipulate the outcome as long as at least one node is honest +The house move uses rejection sampling over 2-bit random candidates to ensure exactly 1/3 probability per move (a naive 2-bit mapping would bias one outcome to 50%). diff --git a/rock_paper_scissors/against-player/README.md b/rock_paper_scissors/against-player/README.md index 67c34d45..e416373d 100644 --- a/rock_paper_scissors/against-player/README.md +++ b/rock_paper_scissors/against-player/README.md @@ -1,83 +1,28 @@ # Rock Paper Scissors - Player vs Player -Player-versus-player asynchronous games require encrypted moves to prevent information advantages. Without cryptographic enforcement, players may observe opponent moves before finalizing their own, or modify submitted moves retroactively. +On public blockchains, Player 2 can see Player 1's move before submitting their own. Commit-reveal doesn't help -- with only 3 possible moves, all hashes can be precomputed. This example uses encrypted state so neither player can see the other's choice until both are submitted. -This example demonstrates a player-versus-player protocol where both players submit encrypted moves asynchronously - neither can see the other's choice until both are submitted. +## How It Works -## Why do public blockchains break asynchronous gameplay? +1. Both players submit encrypted moves on-chain (order doesn't matter) +2. Game state tracks moves as `Enc` -- network-encrypted, nobody can decrypt +3. Once both are in, Arcium nodes compare and reveal only the outcome (tie/A wins/B wins) -Blockchains make everything public - which breaks games where information needs to stay hidden. Public blockchain state allows participants to observe transaction data before submitting their own moves, creating information advantages. +## Implementation -Traditional approaches have limitations: unencrypted storage makes moves visible to all participants immediately, decryption requires someone to view moves sequentially, and trusted referees who decrypt moves can potentially favor specific players. - -The requirement is encrypted asynchronous submission where each player finalizes their choice before learning their opponent's move. - -## How Encrypted Asynchronous Gameplay Works - -The protocol enforces fairness through encrypted move submission and secure comparison: - -1. **Player 1 encrypted submission**: First player's move is encrypted and recorded on-chain -2. **Player 2 encrypted submission**: Second player's move is encrypted and recorded on-chain -3. **Verification**: System confirms both encrypted moves are submitted -4. **Encrypted comparison**: Arcium nodes jointly compute the outcome -5. **Result disclosure**: Only the game outcome (player 1 wins/player 2 wins/tie) is revealed - -The comparison occurs on encrypted values throughout. Only the final game outcome is revealed. - -## Running the Example - -```bash -# Install dependencies -yarn install # or npm install or pnpm install - -# Build the program -arcium build - -# Run tests -arcium test -``` - -The test suite simulates two-player gameplay with encrypted move submissions, demonstrating the complete protocol from move submission through outcome determination. - -## Technical Implementation - -Both player moves are encrypted and submitted on-chain. The game outcome is computed using Arcium's confidential instructions, which process encrypted moves without decryption. - -Key properties: -- **Asynchronous encrypted commitment**: Both moves locked in before comparison, despite asynchronous submission -- **Minimal revelation**: Only the game result is disclosed, not individual moves -- **Integrity**: The MPC protocol ensures correct game resolution even with a dishonest majority—neither player can manipulate the outcome as long as one node is honest - -## Implementation Details - -### The Asynchronous Information Hiding Problem - -**Conceptual Challenge**: Rock Paper Scissors requires both players to commit to moves without seeing the opponent's choice. Online, players submit asynchronously - Player 1 first, then Player 2. On public blockchains, Player 2 can see Player 1's move before submitting their own. - -**Traditional solutions**: -- **Commit-reveal**: Submit hash of move, then reveal. Problem: With only 3 options (rock/paper/scissors), attackers can precompute all possible hashes and reverse them. -- **Trusted referee**: Third party collects moves. Problem: Referee can peek or leak. -- **Time locks**: Strict submission windows. Problem: Network latency, timezone issues. - -**The Question**: Can we hide moves on a public blockchain to enable fair asynchronous gameplay? - -### The Encrypted State Pattern +### Encrypted State ```rust pub struct GameMoves { - player_a_move: u8, // 0=Rock, 1=Paper, 2=Scissors, 3=Not submitted yet + player_a_move: u8, // 0=Rock, 1=Paper, 2=Scissors, 3=Not submitted player_b_move: u8, } ``` -Stored on-chain as `Enc` - network-encrypted, no one can decrypt. +Initialized with both moves set to `3` (empty). Each `player_move` call validates the player hasn't already submitted and the move is valid (<3), then updates the encrypted state. -**Phase 1 - Initialization**: -```rust -GameMoves { player_a_move: 3, player_b_move: 3 } // Both "empty" -``` +### Move Submission (inside MPC) -**Phase 2 - Player submits move** (inside MPC): ```rust pub fn player_move( players_move_ctxt: Enc, @@ -86,26 +31,29 @@ pub fn player_move( let players_move = players_move_ctxt.to_arcis(); let mut game_moves = game_ctxt.to_arcis(); - // Validate: player hasn't moved yet (3 = invalid move, used as "empty" marker) if players_move.player == 0 && game_moves.player_a_move == 3 && players_move.player_move < 3 { - game_moves.player_a_move = players_move.player_move; // Update encrypted state + game_moves.player_a_move = players_move.player_move; } - // Similar logic for player B... + // Similar for player B... - game_ctxt.owner.from_arcis(game_moves) // Return updated encrypted moves + game_ctxt.owner.from_arcis(game_moves) } ``` -**Phase 3 - Comparison** (only after both submitted): +### Comparison (only after both submitted) + ```rust pub fn compare_moves(game_ctxt: Enc) -> u8 { let game_moves = game_ctxt.to_arcis(); - if game_moves.player_a_move == 3 || game_moves.player_b_move == 3 { - return 3; // Error: incomplete game - } + let result = if game_moves.player_a_move == 3 || game_moves.player_b_move == 3 { + 3 // Incomplete game + } else if game_moves.player_a_move == game_moves.player_b_move { + 0 // Tie + } else { + // Rock-Paper-Scissors win logic... + }; - // Determine winner based on Rock-Paper-Scissors logic... - result.reveal() // Only reveal winner (0=tie, 1=A wins, 2=B wins) + result.reveal() // 0=tie, 1=A wins, 2=B wins } ``` diff --git a/sealed_bid_auction/README.md b/sealed_bid_auction/README.md index 68b7ef6f..d6776fdc 100644 --- a/sealed_bid_auction/README.md +++ b/sealed_bid_auction/README.md @@ -1,164 +1,85 @@ # Sealed-Bid Auction - Private Bids, Fair Outcomes -Traditional auction platforms have access to all bids. Even "sealed" bids are only sealed from other bidders - the platform sees everything. This creates opportunities for bid manipulation, information leakage, and requires trusting the auctioneer not to exploit their privileged position. +Traditional auction platforms see all bids. Even "sealed" bids are only sealed from other bidders -- the platform sees everything. This example keeps bid amounts encrypted throughout the auction. Only the final winner and payment amount are revealed. -This example demonstrates sealed-bid auctions where bid amounts remain encrypted throughout the auction. The platform never sees individual bid values - only the final winner and payment amount are revealed. +## How It Works -## Why are sealed-bid auctions hard? +1. Authority creates an auction with type (first-price or Vickrey), minimum bid, and duration (end time computed on-chain) +2. Bidders encrypt their bids locally and submit to the network +3. Arcium nodes compare each new bid against encrypted auction state without decrypting +4. Highest and second-highest bids are tracked in encrypted form on-chain +5. After the auction closes, only the winner and payment amount are revealed -Transparent blockchain architectures conflict with bid privacy requirements: +## Implementation -1. **Bid visibility**: All blockchain data is publicly accessible by default - competitors see your bid -2. **Strategic manipulation**: Visible bids enable last-minute sniping and bid shading -3. **Platform trust**: Traditional sealed-bid auctions require trusting the auctioneer to not peek at bids or collude with bidders -4. **Winner determination**: Computing the highest bid without revealing all bid amounts is non-trivial -5. **Pricing mechanisms**: Supporting different auction types (first-price vs Vickrey) requires tracking both highest and second-highest bids privately - -The requirement is determining the auction winner and payment amount without revealing individual bids, while ensuring the process is verifiable and tamper-resistant. - -## How Sealed-Bid Auctions Work - -The protocol maintains bid privacy while providing accurate winner determination: - -1. **Auction creation**: Authority creates an auction specifying the type (first-price or Vickrey), minimum bid, and end time -2. **Bid encryption**: Bidders encrypt their bid amounts locally before submission using the MXE public key -3. **Encrypted comparison**: Arcium nodes compare new bids against the encrypted auction state without decrypting -4. **State update**: Highest and second-highest bids are tracked in encrypted form on-chain -5. **Winner revelation**: After the auction closes, only the winner identity and payment amount are revealed - not individual bid values -6. **Security guarantee**: Arcium's MPC protocol ensures auction integrity even with a dishonest majority - bid values remain private as long as one node is honest - -## Running the Example - -```bash -# Install dependencies -yarn install # or npm install or pnpm install - -# Build the program -arcium build - -# Run tests -arcium test -``` - -The test suite demonstrates complete auction flows for both first-price and Vickrey auction types, including auction creation, encrypted bid submission, and winner determination. - -## Technical Implementation - -Bids are encrypted using X25519 key exchange with the MXE public key before submission. The auction state stores five encrypted values on-chain: highest bid, highest bidder (as `SerializedSolanaPublicKey`), second-highest bid, and bid count. - -Key properties: - -- **Bid secrecy**: Individual bid amounts remain encrypted throughout the auction lifecycle -- **Distributed computation**: Arcium nodes jointly compare and update encrypted auction state -- **Selective revelation**: Only the winner and payment amount are revealed, not the losing bids - -## Implementation Details - -### The Sealed Bid Problem - -**Conceptual Challenge**: How do you find the maximum value in a set without revealing any individual values? - -Traditional approaches all fail: - -- **Encrypt then decrypt**: Someone holds the decryption key and can see all bids -- **Trusted auctioneer**: Requires trusting the platform not to leak or exploit bid information -- **Commit-reveal**: Bidders can see others' bids before revealing, enabling strategic behavior - -**The Question**: Can we compare encrypted bids and track the highest one without ever decrypting individual values? - -### The Encrypted Auction State Pattern - -Sealed-bid auction demonstrates storing encrypted comparison state in Anchor accounts: +### Encrypted Auction State ```rust 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 highest_bidder: SerializedSolanaPublicKey, + pub second_highest_bid: u64, pub bid_count: u16, } ``` -**Why `SerializedSolanaPublicKey`?** Solana public keys are 32 bytes, but Arcis field elements are smaller. `SerializedSolanaPublicKey` is a built-in type that handles the lo/hi u128 splitting automatically. - -**On-chain storage**: The encrypted state is stored as `[[u8; 32]; 5]` - five 32-byte ciphertexts representing each field. - -> Learn more about [Arcis Types](https://docs.arcium.com/developers/arcis/types) for encrypted value handling. +`SerializedSolanaPublicKey` splits a 32-byte pubkey into lo/hi u128 pairs to fit Arcis field elements. On-chain storage is `[[u8; 32]; 5]` -- five ciphertexts (the bidder pubkey occupies two). -### The Bid Comparison Logic +> [Arcis Types](https://docs.arcium.com/developers/arcis/types) -**MPC instruction** (runs inside encrypted computation): +### Bid Comparison ```rust pub fn place_bid( - bid_ctxt: Enc, // Bidder's encrypted bid - state_ctxt: Enc, // Current encrypted auction state + bid_ctxt: Enc, + state_ctxt: Enc, ) -> Enc { - let bid = bid_ctxt.to_arcis(); // Decrypt in MPC (never exposed) + let bid = bid_ctxt.to_arcis(); let mut state = state_ctxt.to_arcis(); if bid.amount > state.highest_bid { - // New highest bid - shift current highest to second place state.second_highest_bid = state.highest_bid; state.highest_bid = bid.amount; state.highest_bidder = bid.bidder; } else if bid.amount > state.second_highest_bid { - // New second-highest bid state.second_highest_bid = bid.amount; } state.bid_count += 1; - state_ctxt.owner.from_arcis(state) // Re-encrypt updated state + state_ctxt.owner.from_arcis(state) } ``` -**Key insight**: The comparison `bid.amount > state.highest_bid` happens inside MPC - decrypted values never leave the secure environment. +The comparison `bid.amount > state.highest_bid` happens inside MPC -- decrypted values never leave the secure environment. -### First-Price vs Vickrey Auctions +### First-Price vs Vickrey -This example supports two auction mechanisms with different economic properties: - -**First-price auction**: Winner pays their bid amount. +**First-price**: Winner pays their bid. ```rust pub fn determine_winner_first_price(state_ctxt: Enc) -> AuctionResult { let state = state_ctxt.to_arcis(); AuctionResult { winner: state.highest_bidder, - payment_amount: state.highest_bid, // Pay your bid + payment_amount: state.highest_bid, }.reveal() } ``` -**Vickrey (second-price) auction**: Winner pays the second-highest bid. +**Vickrey (second-price)**: Winner pays the second-highest bid. Bidding your true valuation is the dominant strategy -- you can't benefit from bidding lower (you might lose) or higher (you'd overpay). ```rust pub fn determine_winner_vickrey(state_ctxt: Enc) -> AuctionResult { let state = state_ctxt.to_arcis(); AuctionResult { winner: state.highest_bidder, - payment_amount: state.second_highest_bid, // Pay second-highest + payment_amount: state.second_highest_bid, }.reveal() } ``` -**Why Vickrey matters**: In a Vickrey auction, bidding your true valuation is the dominant strategy - you can't benefit from bidding lower (you might lose) or higher (you'd overpay). This incentive-compatibility property, discovered by economist William Vickrey (Nobel Prize 1996), is widely used in ad auctions (Google, Meta) and spectrum auctions. - -### When to Use This Pattern - -Apply sealed-bid auctions when: - -- **Bid privacy is critical**: Prevent competitors from seeing and reacting to bids -- **Strategic behavior is harmful**: Eliminate sniping, bid shading, and collusion -- **Verifiable fairness is required**: Prove the winner determination was correct without revealing losing bids -- **Multiple pricing mechanisms**: Need flexibility between first-price and second-price rules - -**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. +**`min_bid` not enforced against encrypted bids.** 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. +**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. `bid_count` reflects total bids, not unique bidders. diff --git a/share_medical_records/README.md b/share_medical_records/README.md index 229b83f7..26b1005c 100644 --- a/share_medical_records/README.md +++ b/share_medical_records/README.md @@ -1,76 +1,31 @@ -# Medical Records - Privacy-Preserving Healthcare Data Sharing +# Medical Records - Re-encryption -Share your medical records with a new doctor and here's the problem: the platform operator, system administrators, database engineers, and cloud infrastructure provider all have access. You can't control who sees what. +Share your medical records with a new doctor and here's the problem: the platform operator, system administrators, and cloud provider all have access. This example transfers encrypted records from one key to another inside MPC -- the platform never sees plaintext. -This example demonstrates patient-controlled selective disclosure where you specify exactly which medical information each provider can access. +## How It Works -## What's wrong with healthcare data sharing today? +1. Patient stores encrypted medical data on-chain +2. Patient initiates a share, specifying the recipient's public key +3. Arcium nodes decrypt inside MPC and re-encrypt for the recipient +4. Re-encrypted data is emitted in an event -- only the recipient can decrypt -Medical records in centralized databases face multiple problems: patients have no control over who sees what, and every centralized database is a target for breaches. The system requires trusting multiple third parties not to misuse your data. - -The challenge is enabling healthcare data sharing while giving patients control over access permissions. - -## How Selective Data Sharing Works - -The protocol enables patient-controlled data disclosure: - -1. **Encrypted storage**: Patient medical records are encrypted and stored on-chain -2. **Access control**: Patient specifies which data fields to share with which providers -3. **Selective disclosure**: Arcium network enables transfer of authorized data only -4. **Provider access**: Authorized providers receive only the specific data fields granted access - -The patient maintains granular control over data access. Providers receive only authorized information, while the platform facilitating the transfer operates on encrypted data. - -## Running the Example - -```bash -# Install dependencies -yarn install # or npm install or pnpm install - -# Build the program -arcium build - -# Run tests -arcium test -``` - -The test suite demonstrates patient data encryption, selective authorization configuration, and controlled data disclosure to authorized providers. - -## Technical Implementation - -Medical records are encrypted and stored as patient data structures. Access control is enforced through encrypted authorization logic that determines which data fields specific providers can access. - -## Implementation Details - -### The Selective Sharing Problem - -**Conceptual Challenge**: You want to share medical records with a new doctor, but you want to control exactly who can decrypt your information. - -**The Question**: Can you transfer encrypted data from your key to doctor's key without decrypting it in transit? +## Implementation ### The Re-encryption Pattern ```rust pub fn share_patient_data( - receiver: Shared, // Recipient's public key for re-encryption - input_ctxt: Enc, // Your encrypted data + receiver: Shared, + input_ctxt: Enc, ) -> Enc { - let input = input_ctxt.to_arcis(); // Decrypt inside MPC - receiver.from_arcis(input) // Re-encrypt for doctor + let input = input_ctxt.to_arcis(); + receiver.from_arcis(input) } ``` -**What happens**: +Data enters MPC encrypted under the patient's key and leaves encrypted under the doctor's key. No intermediate plaintext is exposed outside the MPC environment. -1. Your encrypted data enters MPC -2. Data is decrypted inside the MPC environment -3. Data is re-encrypted using doctor's public key -4. Re-encrypted data is emitted in event -5. Only the doctor can decrypt (they have the private key) - -**Key insight**: Data is "handed over" inside MPC—re-encrypted from one key to another without intermediate plaintext exposure. - -> See [Input/Output Patterns](https://docs.arcium.com/developers/arcis/input-output) for more on encrypted data transfer. +> [Input/Output Patterns](https://docs.arcium.com/developers/arcis/input-output) ### Multi-field Encrypted Struct @@ -86,13 +41,4 @@ pub struct PatientData { } ``` -Stored as a single encrypted data structure containing 11 fields (352 bytes total). The entire record is encrypted together for on-chain storage. - -### When to Use Re-encryption - -Apply this pattern when: - -- Data encrypted under one key needs to be accessible to another party -- No single party should see the unencrypted data during transfer -- Selective disclosure (future: share only age + blood_type, not full record) -- Examples: credential sharing, encrypted file transfer, confidential data markets +Stored on-chain as 11 encrypted fields (11 x 32 = 352 bytes). Currently the entire record is re-encrypted as a unit -- per-field selective disclosure would require separate circuits for each field combination. diff --git a/voting/README.md b/voting/README.md index 5fcd5ada..ee902fc9 100644 --- a/voting/README.md +++ b/voting/README.md @@ -1,172 +1,93 @@ # Voting - Private Ballots, Public Results -Blockchain transparency makes voting dangerous: visible votes enable vote buying (prove your vote, get paid) and coercion. Encrypt the ballots? Whoever holds the decryption key to count them can still see every vote - and potentially sell or leak that data. Traditional encryption just shifts who holds the power. +Blockchain transparency makes voting dangerous: visible votes enable vote buying and coercion. Encrypt the ballots? Whoever holds the decryption key can still see every vote. This example tallies votes without decrypting individual ballots -- only aggregate results are revealed. -This example demonstrates tallying votes without decrypting individual ballots. Votes stay encrypted throughout - only aggregate results are revealed. +## How It Works -## Why is blockchain voting hard? +1. Votes are encrypted on the client before submission +2. Arcium nodes add each encrypted vote to running tallies without decrypting +3. Only the poll authority can reveal aggregate results +4. A `VoterRecord` PDA (seeded by poll + voter) prevents double-voting -- the account is initialized on vote, so a second attempt fails -Transparent blockchain architectures conflict with ballot secrecy requirements: +## Implementation -1. **Transaction visibility**: All blockchain data is publicly accessible by default -2. **Ballot privacy**: People may not want peers, family, or colleagues knowing how they voted on sensitive issues - votes need to stay private to prevent social pressure and judgment -3. **Vote buying**: If you can prove how you voted, someone can pay you to vote a certain way and verify you followed through -4. **Public tallying**: Everyone needs to be able to check that the final count is correct, without seeing how individual people voted +### Encrypted State -The requirement is computing aggregate vote tallies without revealing individual ballots, while providing accurate and tamper-resistant final counts. - -## How Private Voting Works - -The protocol maintains ballot secrecy while providing accurate results: - -1. **Ballot encryption**: Votes are encrypted on the client's computer before submission -2. **On-chain storage**: Encrypted votes are recorded on the blockchain -3. **Secure distributed tallying**: Arcium nodes collaboratively compute aggregate totals -4. **Result publication**: Only aggregate vote counts are revealed, not individual choices -5. **Security guarantee**: Arcium's MPC protocol preserves ballot secrecy even with a dishonest majority—individual votes remain private as long as one node is honest - -## Running the Example - -```bash -# Install dependencies -yarn install # or npm install or pnpm install - -# Build the program -arcium build - -# Run tests -arcium test -``` - -The test suite demonstrates poll creation, encrypted ballot submission, secure distributed tallying, and result verification. - -## Technical Implementation - -Votes are sent as encrypted booleans and stored as encrypted vote counts on-chain (using `Enc` in the code). Arcium's confidential instructions enable aggregate computation over encrypted ballots. - -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 - -### The Private Tallying Problem - -**Conceptual Challenge**: How do you count votes without seeing individual ballots? - -Traditional approaches all fail: - -- **Encrypt then decrypt**: Someone holds the decryption key and can see votes -- **Trusted counter**: Requires trusting the tallying authority - -**The Question**: Can we compute "yes_votes + no_votes" on encrypted data without ever decrypting individual votes? - -### The Encrypted State Pattern - -Voting demonstrates storing encrypted counters directly in Anchor accounts: +Vote counters live on-chain as raw ciphertexts: ```rust -#[account] pub struct PollAccount { - pub vote_state: [[u8; 32]; 2], // Two 32-byte ciphertexts - pub nonce: u128, // Cryptographic nonce - pub authority: Pubkey, // Who can reveal results - // ... other fields + pub bump: u8, + pub vote_state: [[u8; 32]; 2], // [yes_count, no_count] + pub id: u32, + pub authority: Pubkey, + pub nonce: u128, + pub question: String, } ``` -**What's stored**: Two encrypted `u64` counters (yes, no) as raw ciphertexts. +### Byte Offsets -### Reading Encrypted Account Data - -Arx nodes need precise byte locations to read encrypted data from accounts and deserialize it into the proper MPC function arguments. - -To specify encrypted account data, provide exact byte offsets: +Arx nodes need precise byte locations to read encrypted data from accounts: ```rust -Argument::Account( +.account( ctx.accounts.poll_acc.key(), - 8 + 1, // Skip: Anchor discriminator (8 bytes) + bump (1 byte) - 64, // Read: 2 ciphertexts × 32 bytes = 64 bytes + 8 + 1, // discriminator (8) + bump (1) + 32 * 2, // 2 ciphertexts x 32 bytes ) ``` -**Memory layout**: - ``` Byte 0-7: Anchor discriminator Byte 8: bump -Byte 9-40: yes ciphertext (Enc) -Byte 41-72: no ciphertext (Enc) -Byte 73+: other fields... +Byte 9-40: yes ciphertext +Byte 41-72: no ciphertext +Byte 73+: id, authority, nonce, question ``` -### The Vote Accumulation Logic +### Vote Accumulation -**MPC instruction** (runs inside encrypted computation): +Circuit (runs inside MPC): ```rust pub fn vote( - input: Enc, // Voter's encrypted choice - votes: Enc, // Current encrypted tallies + vote_ctxt: Enc, + vote_stats_ctxt: Enc, ) -> Enc { - let input = input.to_arcis(); // Decrypt in MPC (never exposed) - let mut votes = votes.to_arcis(); // Decrypt tallies in MPC + let user_vote = vote_ctxt.to_arcis(); + let mut vote_stats = vote_stats_ctxt.to_arcis(); - if input.vote { - votes.yes += 1; // Increment happens inside MPC + if user_vote.vote { + vote_stats.yes += 1; } else { - votes.no += 1; + vote_stats.no += 1; } - votes.owner.from_arcis(votes) // Re-encrypt updated tallies + vote_stats_ctxt.owner.from_arcis(vote_stats) } ``` -**Callback** (runs on-chain after MPC completes): +Callback (writes updated ciphertexts back to the account): ```rust -pub fn vote_callback( - ctx: Context, - output: SignedComputationOutputs, -) -> Result<()> { - let o = match output.verify_output( - &ctx.accounts.cluster_account, - &ctx.accounts.computation_account, - ) { - Ok(VoteOutput { field_0 }) => field_0, - Err(_) => return Err(ErrorCode::AbortedComputation.into()), - }; - - // Save new encrypted tallies + new nonce +pub fn vote_callback(ctx: Context, output: ...) -> Result<()> { + let o = output.verify_output(...)?; ctx.accounts.poll_acc.vote_state = o.ciphertexts; ctx.accounts.poll_acc.nonce = o.nonce; Ok(()) } ``` -> Learn more: [Callback Type Generation](https://docs.arcium.com/developers/program/callback-type-generation), [Input/Output Patterns](https://docs.arcium.com/developers/arcis/input-output) +> [Callback Type Generation](https://docs.arcium.com/developers/program/callback-type-generation), [Input/Output Patterns](https://docs.arcium.com/developers/arcis/input-output) ### Revealing Results -The program restricts result revelation to the poll authority: +Only the poll authority can reveal, and only the comparison (not raw counts) is disclosed: ```rust -pub fn reveal_result(votes: Enc) -> bool { - let votes = votes.to_arcis(); - (votes.yes > votes.no).reveal() // Only reveal comparison +pub fn reveal_result(vote_stats_ctxt: Enc) -> bool { + let vote_stats = vote_stats_ctxt.to_arcis(); + (vote_stats.yes > vote_stats.no).reveal() } ``` - -### What This Example Demonstrates - -This example shows how to: - -- **Store encrypted data in Solana accounts**: Using raw bytes (`[[u8; 32]; 2]`) to persist encrypted values on-chain -- **Pass encrypted account data to MPC**: Using `Argument::Account()` with precise byte offsets to read encrypted state -- **Compute on encrypted state over time**: Accumulating encrypted values across multiple transactions (adding new votes to running tallies) - -This pattern applies to any scenario requiring private aggregation: voting, surveys, sealed-bid auctions, confidential analytics, and private leaderboards.