Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,50 @@
use aztec::{macros::notes::note, protocol::traits::Packable};

#[derive(Eq, Packable)]
#[derive(Eq)]
#[note]
pub struct SubscriptionNote {
pub expiry_block_number: u32,
pub remaining_txs: u32,
}

impl Packable for SubscriptionNote {
let N: u32 = 1;

fn pack(self) -> [Field; Self::N] {
[(self.expiry_block_number as Field) * 2.pow_32(32) + (self.remaining_txs as Field)]
}

fn unpack(packed: [Field; Self::N]) -> Self {
let remaining_txs = packed[0] as u32;
let expiry_block_number = ((packed[0] - remaining_txs as Field) / 2.pow_32(32)) as u32;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a comment here would be nice. these kinds of unpacking functions normally rely on shifts and bitmasks, I guess we don't have yet those in Noir?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you use the same approach in the other contracts, maybe worth adding a reusable function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these kinds of unpacking functions normally rely on shifts and bitmasks, I guess we don't have yet those in Noir?

We have them in Noir but unlike in CPU instruction sets, bit shifts are super inefficient in circuits while multiplication and division are efficient (circuits have MUL and AND gates and you can perform division by multiplying by modulo inverse).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We intentionally use multiply/divide by powers of 2 rather than bit shifts — they map more efficiently to both AVM opcodes (for public functions) and proving backend primitives (for private functions). The Packable trait docs already recommend this approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered this but a reusable helper doesn't really help here — the as TargetType casts (e.g. as u32, as u64, as u128) are the core of each unpacking since they provide the circuit range constraints, and each site uses different type combinations (u32+u32, u32+u64, u128+u64, u128+bool, bools+u32s). A helper returning raw Fields would require callers to still do all the casting, resulting in more lines not fewer. The Packable trait docs already serve as the guide with a worked example.

Self { expiry_block_number, remaining_txs }
}
}

mod test {
use super::{Packable, SubscriptionNote};

#[test]
fn test_pack_unpack_subscription_note() {
let note = SubscriptionNote { expiry_block_number: 1000, remaining_txs: 5 };
let unpacked = SubscriptionNote::unpack(note.pack());
assert_eq(unpacked.expiry_block_number, note.expiry_block_number);
assert_eq(unpacked.remaining_txs, note.remaining_txs);
}

#[test]
fn test_pack_unpack_subscription_note_zeros() {
let note = SubscriptionNote { expiry_block_number: 0, remaining_txs: 0 };
let unpacked = SubscriptionNote::unpack(note.pack());
assert_eq(unpacked.expiry_block_number, 0);
assert_eq(unpacked.remaining_txs, 0);
}

#[test]
fn test_pack_unpack_subscription_note_max() {
let note = SubscriptionNote { expiry_block_number: 0xffffffff, remaining_txs: 0xffffffff };
let unpacked = SubscriptionNote::unpack(note.pack());
assert_eq(unpacked.expiry_block_number, note.expiry_block_number);
assert_eq(unpacked.remaining_txs, note.remaining_txs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,27 @@ use aztec::{
use field_note::FieldNote;
use std::meta::derive;

#[derive(Packable, Serialize)]
#[derive(Serialize)]
pub struct Card {
// We use u32s since u16s are unsupported
pub strength: u32,
pub points: u32,
}

impl Packable for Card {
let N: u32 = 1;

fn pack(self) -> [Field; Self::N] {
[(self.strength as Field) * 2.pow_32(32) + (self.points as Field)]
}

fn unpack(packed: [Field; Self::N]) -> Self {
let points = packed[0] as u32;
let strength = ((packed[0] - points as Field) / 2.pow_32(32)) as u32;
Self { strength, points }
}
}

impl FromField for Card {
fn from_field(field: Field) -> Card {
let value_bytes: [u8; 32] = field.to_le_bytes();
Expand All @@ -38,11 +52,39 @@ impl ToField for Card {
}
}

#[test]
fn test_to_from_field() {
let field = 1234567890;
let card = Card::from_field(field);
assert(card.to_field() == field);
mod test {
use super::{Card, FromField, Packable, ToField};

#[test]
fn test_to_from_field() {
let field = 1234567890;
let card = Card::from_field(field);
assert(card.to_field() == field);
}

#[test]
fn test_pack_unpack_card() {
let card = Card { strength: 42, points: 100 };
let unpacked = Card::unpack(card.pack());
assert_eq(unpacked.strength, card.strength);
assert_eq(unpacked.points, card.points);
}

#[test]
fn test_pack_unpack_card_zeros() {
let card = Card { strength: 0, points: 0 };
let unpacked = Card::unpack(card.pack());
assert_eq(unpacked.strength, 0);
assert_eq(unpacked.points, 0);
}

#[test]
fn test_pack_unpack_card_max() {
let card = Card { strength: 0xffffffff, points: 0xffffffff };
let unpacked = Card::unpack(card.pack());
assert_eq(unpacked.strength, card.strength);
assert_eq(unpacked.points, card.points);
}
}

pub struct CardNote {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
use crate::cards::Card;
use aztec::protocol::{address::AztecAddress, traits::{Deserialize, Packable}};
use aztec::protocol::{address::AztecAddress, traits::{Deserialize, FromField, Packable, ToField}};
use std::meta::derive;

global NUMBER_OF_PLAYERS: u32 = 2;
global NUMBER_OF_CARDS_DECK: u32 = 2;

#[derive(Deserialize, Eq, Packable)]
#[derive(Deserialize, Eq)]
pub struct PlayerEntry {
pub address: AztecAddress,
pub deck_strength: u32,
pub points: u64,
}

impl Packable for PlayerEntry {
let N: u32 = 2;

fn pack(self) -> [Field; Self::N] {
[
self.address.to_field(),
(self.deck_strength as Field) * 2.pow_32(64) + (self.points as Field),
]
}

fn unpack(packed: [Field; Self::N]) -> Self {
let address = AztecAddress::from_field(packed[0]);
let points = packed[1] as u64;
let deck_strength = ((packed[1] - points as Field) / 2.pow_32(64)) as u32;
Self { address, deck_strength, points }
}
}

impl PlayerEntry {
pub fn is_initialized(self) -> bool {
!self.address.is_zero()
Expand All @@ -20,7 +38,6 @@ impl PlayerEntry {

pub global PLAYABLE_CARDS: u32 = 4;

#[derive(Packable)]
pub struct Game {
players: [PlayerEntry; NUMBER_OF_PLAYERS],
pub rounds_cards: [Card; PLAYABLE_CARDS],
Expand All @@ -31,6 +48,65 @@ pub struct Game {
current_round: u32,
}

// Constants used just by the implementation of Packable
global PLAYERS_PACKED_LEN: u32 = NUMBER_OF_PLAYERS * <PlayerEntry as Packable>::N;
global CARDS_PACKED_LEN: u32 = PLAYABLE_CARDS * <Card as Packable>::N;
global SCALARS_OFFSET: u32 = PLAYERS_PACKED_LEN + CARDS_PACKED_LEN;

impl Packable for Game {
let N: u32 = PLAYERS_PACKED_LEN + CARDS_PACKED_LEN + 1;

fn pack(self) -> [Field; Self::N] {
let mut result = [0; Self::N];

let players_packed = self.players.pack();
for i in 0..PLAYERS_PACKED_LEN {
result[i] = players_packed[i];
}

let cards_packed = self.rounds_cards.pack();
for i in 0..CARDS_PACKED_LEN {
result[PLAYERS_PACKED_LEN + i] = cards_packed[i];
}

// Layout: [ current_round: u32 | current_player: u32 | claimed: 1b | finished: 1b | started: 1b ]
result[SCALARS_OFFSET] = (self.started as Field)
+ (self.finished as Field) * 2.pow_32(1)
+ (self.claimed as Field) * 2.pow_32(2)
+ (self.current_player as Field) * 2.pow_32(3)
+ (self.current_round as Field) * 2.pow_32(35);

result
}

fn unpack(packed: [Field; Self::N]) -> Self {
let mut players_packed = [0; PLAYERS_PACKED_LEN];
for i in 0..PLAYERS_PACKED_LEN {
players_packed[i] = packed[i];
}
let players = <[PlayerEntry; NUMBER_OF_PLAYERS]>::unpack(players_packed);

let mut cards_packed = [0; CARDS_PACKED_LEN];
for i in 0..CARDS_PACKED_LEN {
cards_packed[i] = packed[PLAYERS_PACKED_LEN + i];
}
let rounds_cards = <[Card; PLAYABLE_CARDS]>::unpack(cards_packed);

let mut tmp = packed[SCALARS_OFFSET];
let started = (tmp as u1) != 0;
tmp = (tmp - started as Field) / 2.pow_32(1);
let finished = (tmp as u1) != 0;
tmp = (tmp - finished as Field) / 2.pow_32(1);
let claimed = (tmp as u1) != 0;
tmp = (tmp - claimed as Field) / 2.pow_32(1);
let current_player = tmp as u32;
tmp = (tmp - current_player as Field) / 2.pow_32(32);
let current_round = tmp as u32;

Self { players, rounds_cards, started, finished, claimed, current_player, current_round }
}
}

impl Game {
pub fn add_player(&mut self, player_entry: PlayerEntry) -> bool {
let mut added = false;
Expand Down Expand Up @@ -116,3 +192,106 @@ impl Game {
}
}
}

mod test {
use crate::cards::Card;
use super::{AztecAddress, FromField, Game, Packable, PlayerEntry};

#[test]
fn test_pack_unpack_player_entry() {
let entry = PlayerEntry {
address: AztecAddress::from_field(0xdeadbeef),
deck_strength: 999,
points: 12345678,
};
let unpacked = PlayerEntry::unpack(entry.pack());
assert(unpacked.address.eq(entry.address));
assert_eq(unpacked.deck_strength, entry.deck_strength);
assert_eq(unpacked.points, entry.points);
}

#[test]
fn test_pack_unpack_player_entry_zeros() {
let entry =
PlayerEntry { address: AztecAddress::from_field(0), deck_strength: 0, points: 0 };
let unpacked = PlayerEntry::unpack(entry.pack());
assert(unpacked.address.eq(entry.address));
assert_eq(unpacked.deck_strength, 0);
assert_eq(unpacked.points, 0);
}

#[test]
fn test_pack_unpack_game() {
let game = Game {
players: [
PlayerEntry {
address: AztecAddress::from_field(1),
deck_strength: 100,
points: 50,
},
PlayerEntry {
address: AztecAddress::from_field(2),
deck_strength: 200,
points: 75,
},
],
rounds_cards: [
Card { strength: 10, points: 5 },
Card { strength: 20, points: 15 },
Card { strength: 30, points: 25 },
Card { strength: 40, points: 35 },
],
started: true,
finished: false,
claimed: true,
current_player: 1,
current_round: 1,
};
let unpacked = Game::unpack(game.pack());

assert(unpacked.players[0].address.eq(game.players[0].address));
assert_eq(unpacked.players[0].deck_strength, 100);
assert_eq(unpacked.players[0].points, 50);
assert(unpacked.players[1].address.eq(game.players[1].address));
assert_eq(unpacked.players[1].deck_strength, 200);
assert_eq(unpacked.players[1].points, 75);

assert_eq(unpacked.rounds_cards[0].strength, 10);
assert_eq(unpacked.rounds_cards[0].points, 5);
assert_eq(unpacked.rounds_cards[3].strength, 40);
assert_eq(unpacked.rounds_cards[3].points, 35);

assert(unpacked.started);
assert(!unpacked.finished);
assert(unpacked.claimed);
assert_eq(unpacked.current_player, 1);
assert_eq(unpacked.current_round, 1);
}

#[test]
fn test_pack_unpack_game_all_false() {
let game = Game {
players: [
PlayerEntry { address: AztecAddress::from_field(0), deck_strength: 0, points: 0 },
PlayerEntry { address: AztecAddress::from_field(0), deck_strength: 0, points: 0 },
],
rounds_cards: [
Card { strength: 0, points: 0 },
Card { strength: 0, points: 0 },
Card { strength: 0, points: 0 },
Card { strength: 0, points: 0 },
],
started: false,
finished: false,
claimed: false,
current_player: 0,
current_round: 0,
};
let unpacked = Game::unpack(game.pack());
assert(!unpacked.started);
assert(!unpacked.finished);
assert(!unpacked.claimed);
assert_eq(unpacked.current_player, 0);
assert_eq(unpacked.current_round, 0);
}
}
Loading
Loading