Skip to content

Commit 357aac6

Browse files
authored
Merge pull request #229 from AztecProtocol/pod-racing
feat: add new starter example
2 parents 934573c + 59e495c commit 357aac6

File tree

11 files changed

+1381
-144
lines changed

11 files changed

+1381
-144
lines changed

Nargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "private_voting_contract"
2+
name = "pod_racing_contract"
33
type = "contract"
44
authors = [ "" ]
55
compiler_version = ">=0.18.0"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "private_voting_codespace",
2+
"name": "pod_racing_codespace",
33
"version": "1.0.0",
44
"main": "index.js",
55
"repository": "https://github.com/critesjosh/private_voting_codespace.git",

src/game_round_note.nr

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use aztec::{macros::notes::note, protocol_types::{traits::Packable, address::AztecAddress}};
2+
3+
// GameRoundNote is a private note that stores a player's point allocation for one round
4+
// These notes remain private until the player calls finish_game to reveal their totals
5+
//
6+
// Privacy model:
7+
// - Each player creates 3 of these notes (one per round) when playing
8+
// - Only the owner can read their own notes
9+
// - During finish_game, the player sums all their notes and reveals the totals publicly
10+
// - This implements a commit-reveal scheme for fair play
11+
#[derive(Eq, Packable)]
12+
#[note]
13+
pub struct GameRoundNote {
14+
// Points allocated to each of the 5 tracks in this round
15+
// Must sum to less than 10 points per round
16+
pub track1: u8,
17+
pub track2: u8,
18+
pub track3: u8,
19+
pub track4: u8,
20+
pub track5: u8,
21+
22+
// Which round this note represents (1, 2, or 3)
23+
pub round: u8,
24+
25+
// The player who created this note (only they can read it)
26+
pub owner: AztecAddress,
27+
}
28+
29+
impl GameRoundNote {
30+
// Creates a new note with the player's round choices
31+
// This note gets stored privately and can only be read by the owner
32+
pub fn new(track1: u8, track2: u8, track3: u8, track4: u8, track5: u8, round: u8, owner: AztecAddress) -> Self {
33+
Self {
34+
track1,
35+
track2,
36+
track3,
37+
track4,
38+
track5,
39+
round,
40+
owner,
41+
}
42+
}
43+
44+
// Helper method to access the note data
45+
pub fn get(self) -> Self {
46+
Self {
47+
track1: self.track1,
48+
track2: self.track2,
49+
track3: self.track3,
50+
track4: self.track4,
51+
track5: self.track5,
52+
round: self.round,
53+
owner: self.owner,
54+
}
55+
}
56+
}

src/main.nr

Lines changed: 186 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,217 @@
1+
// Pod Racing Game Contract
2+
//
3+
// This is a two-player competitive racing game where players allocate points across 5 tracks
4+
// over multiple rounds. The game flow:
5+
// 1. Player 1 creates a game with a time limit
6+
// 2. Player 2 joins the game
7+
// 3. Both players play rounds privately (allocating points across tracks)
8+
// 4. After all rounds, players reveal their total scores per track
9+
// 5. Winner is determined by who won more tracks (best of 5)
10+
//
11+
// Key mechanics:
12+
// - Each round, players distribute up to 9 points across 5 tracks
13+
// - Round choices are private until the finish phase
14+
// - The player with higher total points on a track wins that track
15+
// - The player who wins 3+ tracks wins the game
16+
117
mod test;
2-
use dep::aztec::macros::aztec;
18+
mod game_round_note;
19+
mod race;
320

4-
/**
5-
* WARNING: this is no-longer considered a good example of an Aztec contract,
6-
* because it touches low-level functions and concepts that oughtn't be
7-
* seen by a typical user.
8-
* The syntax and user-experience of Aztec contracts has since improved, so you
9-
* should seek alternative examples, please.
10-
*/
21+
use dep::aztec::macros::aztec;
1122

1223
#[aztec]
13-
pub contract PrivateVoting {
24+
pub contract PodRacing {
1425
use dep::aztec::{
15-
keys::getters::get_public_keys,
26+
note::note_getter_options::NoteGetterOptions,
27+
messages::message_delivery::MessageDelivery,
1628
macros::{functions::{external, initializer, internal}, storage::storage},
1729
};
18-
use dep::aztec::protocol_types::{
19-
address::AztecAddress,
20-
hash::poseidon2_hash,
21-
traits::{Hash, ToField},
22-
};
23-
use dep::aztec::state_vars::{Map, PublicImmutable, PublicMutable};
30+
use dep::aztec::protocol_types::address::AztecAddress;
31+
use dep::aztec::state_vars::{Map, PublicMutable, PrivateSet};
32+
33+
use crate::{game_round_note::GameRoundNote, race::Race};
34+
35+
// Game configuration constants
36+
global TOTAL_ROUNDS: u8 = 3; // Each game consists of 3 rounds
37+
global GAME_LENGTH: u32 = 300; // Games expire after 300 blocks
2438

2539
#[storage]
2640
struct Storage<Context> {
27-
admin: PublicMutable<AztecAddress, Context>, // admin can end vote
28-
tally: Map<Field, PublicMutable<Field, Context>, Context>, // we will store candidate as key and number of votes as value
29-
vote_ended: PublicMutable<bool, Context>, // vote_ended is boolean
30-
active_at_block: PublicImmutable<u32, Context>, // when people can start voting
41+
// Contract administrator address
42+
admin: PublicMutable<AztecAddress, Context>,
43+
44+
// Maps game_id -> Race struct containing public game state
45+
// Stores player addresses, round progress, and final track scores
46+
races: Map<Field, PublicMutable<Race, Context>, Context>,
47+
48+
// Maps game_id -> player_address -> private notes containing that player's round choices
49+
// Each GameRoundNote stores the point allocation for one round
50+
// This data remains private until the player calls finish_game
51+
progress: Map<Field, Map<AztecAddress, PrivateSet<GameRoundNote, Context>, Context>, Context>,
52+
53+
// Maps player address -> total number of wins
54+
// Public leaderboard tracking career victories
55+
win_history: Map<AztecAddress, PublicMutable<u64, Context>, Context>,
3156
}
3257

3358
#[external("public")]
3459
#[initializer]
35-
// annotation to mark function as a constructor
3660
fn constructor(admin: AztecAddress) {
3761
storage.admin.write(admin);
38-
storage.vote_ended.write(false);
39-
storage.active_at_block.initialize(context.block_number());
4062
}
4163

64+
// Creates a new game instance
65+
// The caller becomes player1 and waits for an opponent to join
66+
// Sets the game expiration to current block + GAME_LENGTH
67+
#[external("public")]
68+
fn create_game(game_id: Field) {
69+
// Ensure this game_id hasn't been used yet (player1 must be zero address)
70+
assert(storage.races.at(game_id).read().player1.eq(AztecAddress::zero()));
71+
72+
// Initialize a new Race with the caller as player1
73+
let game = Race::new(context.msg_sender().unwrap(), TOTAL_ROUNDS, context.block_number() + GAME_LENGTH);
74+
storage.races.at(game_id).write(game);
75+
}
76+
77+
// Allows a second player to join an existing game
78+
// After joining, both players can start playing rounds
79+
#[external("public")]
80+
fn join_game(game_id: Field) {
81+
let maybe_existing_game = storage.races.at(game_id).read();
82+
83+
// Add the caller as player2 (validates that player1 exists and player2 is empty)
84+
let joined_game = maybe_existing_game.join(context.msg_sender().unwrap());
85+
storage.races.at(game_id).write(joined_game);
86+
}
87+
88+
// Plays a single round by allocating points across 5 tracks
89+
// This is a PRIVATE function - the point allocation remains hidden from the opponent
90+
// Players must play rounds sequentially (round 1, then 2, then 3)
91+
//
92+
// Parameters:
93+
// - track1-5: Points allocated to each track (must sum to less than 10)
94+
// - round: Which round this is (1, 2, or 3)
4295
#[external("private")]
43-
// annotation to mark function as private and expose private context
44-
fn cast_vote(candidate: Field) {
45-
let msg_sender_nullifier_public_key_message_hash =
46-
get_public_keys(context.msg_sender().unwrap()).npk_m.hash();
47-
48-
let secret = context.request_nsk_app(msg_sender_nullifier_public_key_message_hash); // get secret key of caller of function
49-
let nullifier = poseidon2_hash([context.msg_sender().unwrap().to_field(), secret]); // derive nullifier from sender and secret
50-
context.push_nullifier(nullifier);
51-
PrivateVoting::at(context.this_address()).add_to_tally_public(candidate).enqueue(
96+
fn play_round(game_id: Field, round: u8, track1: u8, track2: u8, track3: u8, track4: u8, track5: u8) {
97+
// Validate that total points don't exceed 9 (you can't max out all tracks)
98+
assert(track1 + track2 + track3 + track4 + track5 < 10);
99+
100+
let player = context.msg_sender().unwrap();
101+
102+
// Store the round choices privately as a note in the player's own storage
103+
// This creates a private commitment that can only be read by the player
104+
storage.progress.at(game_id).at(player).insert(GameRoundNote::new(
105+
track1,
106+
track2,
107+
track3,
108+
track4,
109+
track5,
110+
round,
111+
player,
112+
)).emit(player, MessageDelivery.CONSTRAINED_ONCHAIN);
113+
114+
// Enqueue a public function call to update the round counter
115+
// This reveals that a round was played, but not the point allocation
116+
PodRacing::at(context.this_address()).validate_and_play_round(player, game_id, round).enqueue(
52117
&mut context,
53118
);
54119
}
55120

121+
// Internal public function to validate and record that a player completed a round
122+
// Updates the public game state to track which round each player is on
123+
// Does NOT reveal the point allocation (that remains private)
56124
#[external("public")]
57125
#[internal]
58-
fn add_to_tally_public(candidate: Field) {
59-
assert(storage.vote_ended.read() == false, "Vote has ended"); // assert that vote has not ended
60-
let new_tally = storage.tally.at(candidate).read() + 1;
61-
storage.tally.at(candidate).write(new_tally);
126+
fn validate_and_play_round(player: AztecAddress, game_id: Field, round: u8) {
127+
let game_in_progress = storage.races.at(game_id).read();
128+
// Increment the player's round counter (validates sequential play)
129+
storage.races.at(game_id).write(game_in_progress.increment_player_round(player, round));
62130
}
63131

132+
// Called after all rounds are complete to reveal a player's total scores
133+
// This is PRIVATE - only the caller can read their own GameRoundNotes
134+
// The function sums up all round allocations per track and publishes totals
135+
//
136+
// This is the "reveal" phase where private choices become public
137+
#[external("private")]
138+
fn finish_game(game_id: Field) {
139+
let player = context.msg_sender().unwrap();
140+
141+
// Retrieve all private notes for this player in this game
142+
let totals = storage.progress.at(game_id).at(player).get_notes(NoteGetterOptions::new());
143+
144+
// Sum up points allocated to each track across all rounds
145+
let mut total_track1: u64= 0;
146+
let mut total_track2: u64= 0;
147+
let mut total_track3: u64= 0;
148+
let mut total_track4: u64= 0;
149+
let mut total_track5: u64= 0;
150+
151+
// Iterate through exactly TOTAL_ROUNDS notes (only this player's notes)
152+
for i in 0..TOTAL_ROUNDS {
153+
total_track1 += totals.get(i as u32).note.track1 as u64;
154+
total_track2 += totals.get(i as u32).note.track2 as u64;
155+
total_track3 += totals.get(i as u32).note.track3 as u64;
156+
total_track4 += totals.get(i as u32).note.track4 as u64;
157+
total_track5 += totals.get(i as u32).note.track5 as u64;
158+
}
159+
160+
// Enqueue public function to store the revealed totals on-chain
161+
// Now the revealing player's track totals will be publicly visible
162+
PodRacing::at(context.this_address()).validate_finish_game_and_reveal(
163+
player,
164+
game_id,
165+
total_track1,
166+
total_track2,
167+
total_track3,
168+
total_track4,
169+
total_track5,
170+
).enqueue(
171+
&mut context,
172+
);
173+
}
174+
175+
// Internal public function to store a player's revealed track totals
176+
// Validates that the player hasn't already revealed their scores (all must be 0)
177+
// After both players call finish_game, all scores are public and can be compared
64178
#[external("public")]
65-
fn end_vote() {
66-
assert(storage.admin.read().eq(context.msg_sender().unwrap()), "Only admin can end votes"); // assert that caller is admin
67-
storage.vote_ended.write(true);
179+
#[internal]
180+
fn validate_finish_game_and_reveal(
181+
player: AztecAddress,
182+
game_id: Field,
183+
total_track1: u64,
184+
total_track2: u64,
185+
total_track3: u64,
186+
total_track4: u64,
187+
total_track5: u64
188+
) {
189+
let game_in_progress = storage.races.at(game_id).read();
190+
191+
// Store the player's track totals (validates they haven't been set yet)
192+
storage.races.at(game_id).write(game_in_progress.set_player_scores(player, total_track1, total_track2, total_track3, total_track4, total_track5));
68193
}
69-
#[external("utility")]
70-
unconstrained fn get_vote(candidate: Field) -> Field {
71-
storage.tally.at(candidate).read()
194+
195+
// Determines the winner after both players have revealed their scores
196+
// Can only be called after the game's end_block (time limit expired)
197+
// Compares track totals and declares the player who won more tracks as winner
198+
//
199+
// Winner determination:
200+
// - Compare each of the 5 tracks
201+
// - Player with higher total on a track wins that track
202+
// - Player who wins 3+ tracks wins the game (best of 5)
203+
// - Updates the winner's career win count
204+
#[external("public")]
205+
fn finalize_game(
206+
game_id: Field
207+
) {
208+
let game_in_progress = storage.races.at(game_id).read();
209+
210+
// Calculate winner by comparing track scores (validates game has ended)
211+
let winner = game_in_progress.calculate_winner(context.block_number());
212+
213+
// Update the winner's total win count in the public leaderboard
214+
let previous_wins = storage.win_history.at(winner).read();
215+
storage.win_history.at(winner).write(previous_wins + 1);
72216
}
73217
}

0 commit comments

Comments
 (0)