Skip to content

Commit

Permalink
chore: simplify and fix DocsExample contract, e2e singleton + codegen…
Browse files Browse the repository at this point in the history
… to not show internal methods (AztecProtocol#4169)

DocsExample contrtc is the only place we use singleton. And it wasn't
working so fixed and massively simplified it

Also sneaked in a change where codegen doesn't show internal methods
anymore

Singleton uses `owner` and unsure if it is really needed
  • Loading branch information
rahul-kothari authored Jan 22, 2024
1 parent 144ae71 commit 38d262e
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 82 deletions.
13 changes: 13 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,17 @@ jobs:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_sandbox_example.test.ts

e2e-singleton:
docker:
- image: aztecprotocol/alpine-build-image
resource_class: small
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_singleton.test.ts

e2e-block-building:
docker:
- image: aztecprotocol/alpine-build-image
Expand Down Expand Up @@ -1228,6 +1239,7 @@ workflows:
# TODO(3458): Investigate intermittent failure
# - e2e-slow-tree: *e2e_test
- e2e-sandbox-example: *e2e_test
- e2e-singleton: *e2e_test
- e2e-block-building: *e2e_test
- e2e-nested-contract: *e2e_test
- e2e-non-contract-account: *e2e_test
Expand Down Expand Up @@ -1265,6 +1277,7 @@ workflows:
- e2e-token-contract
- e2e-blacklist-token-contract
- e2e-sandbox-example
- e2e-singleton
- e2e-block-building
- e2e-nested-contract
- e2e-non-contract-account
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/dev_docs/contracts/syntax/storage/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,15 +306,15 @@ To update the value of a `Singleton`, we can use the `replace` method. The metho

An example of this is seen in a example card game, where we create a new note (a `CardNote`) containing some new data, and replace the current note with it:

#include_code state_vars-SingletonReplace /yarn-project/noir-contracts/contracts/docs_example_contract/src/actions.nr rust
#include_code state_vars-SingletonReplace /yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr rust

If two people are trying to modify the Singleton at the same time, only one will succeed as we don't allow duplicate nullifiers! Developers should put in place appropriate access controls to avoid race conditions (unless a race is intended!).

### `get_note`

This function allows us to get the note of a Singleton, essentially reading the value.

#include_code state_vars-SingletonGet /yarn-project/noir-contracts/contracts/docs_example_contract/src/actions.nr rust
#include_code state_vars-SingletonGet /yarn-project/noir-contracts/contracts/docs_example_contract/src/main.nr rust

#### Nullifying Note reads

Expand Down
12 changes: 0 additions & 12 deletions yarn-project/end-to-end/src/e2e_lending_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,18 +250,6 @@ describe('e2e_lending_contract', () => {
.send(),
);
});
describe('failure cases', () => {
it('calling internal _deposit function directly', async () => {
// Try to call the internal `_deposit` function directly
// This should:
// - not change any storage values.
// - fail

await expect(
lendingContract.methods._deposit(lendingAccount.address.toField(), 42n, collateralAsset.address).simulate(),
).rejects.toThrow();
});
});
});

describe('Borrow', () => {
Expand Down
31 changes: 31 additions & 0 deletions yarn-project/end-to-end/src/e2e_singleton.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Fr, Wallet } from '@aztec/aztec.js';
import { DocsExampleContract } from '@aztec/noir-contracts';

import { setup } from './fixtures/utils.js';

describe('e2e_singleton', () => {
let wallet: Wallet;

let teardown: () => Promise<void>;
let contract: DocsExampleContract;

beforeAll(async () => {
({ teardown, wallet } = await setup());
contract = await DocsExampleContract.deploy(wallet).send().deployed();
// sets card value to 1 and leader to sender.
await contract.methods.initialize_private(Fr.random(), 1).send().wait();
}, 25_000);

afterAll(() => teardown());

// Singleton tests:
it('can read singleton and replace/update it in the same call', async () => {
await expect(contract.methods.update_legendary_card(Fr.random(), 0).simulate()).rejects.toThrowError(
'Assertion failed: can only update to higher value',
);

const newPoints = 3n;
await contract.methods.update_legendary_card(Fr.random(), newPoints).send().wait();
expect((await contract.methods.get_leader().view()).points).toEqual(newPoints);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ function generateAbiStatement(name: string, artifactImportPath: string) {
* @returns The corresponding ts code.
*/
export function generateTypescriptContractInterface(input: ContractArtifact, artifactImportPath?: string) {
const methods = input.functions.filter(f => f.name !== 'constructor').map(generateMethod);
const methods = input.functions.filter(f => f.name !== 'constructor' && !f.isInternal).map(generateMethod);
const deploy = artifactImportPath && generateDeploy(input);
const ctor = artifactImportPath && generateConstructor(input.name);
const at = artifactImportPath && generateAt(input.name);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,68 +1,126 @@
mod actions;
mod options;
mod types;

// Following is a very simple game to show case use of singleton in as minimalistic way as possible
// It also serves as an e2e test that you can read and then replace the singleton in the same call
// (tests ordering in the circuit)

// you have a card (singleton). Anyone can create a bigger card. Whoever is bigger will be the leader.
// it also has dummy methods and other examples used for documentation e.g.
// how to create custom notes, a custom struct for public state, a custom note that may be unencrypted
// also has `options.nr` which shows various ways of using `NoteGetterOptions` to query notes
// it also shows what our macros do behind the scenes!

contract DocsExample {
// how to import dependencies defined in your workspace
use dep::aztec::protocol_types::{
abis::function_selector::FunctionSelector,
address::AztecAddress,
};
use dep::aztec::{
context::{PrivateContext, Context},
state_vars::{
map::Map,
singleton::Singleton,
note::{
note_header::NoteHeader,
utils as note_utils,
},
context::{PrivateContext, PublicContext, Context},
state_vars::{map::Map, public_state::PublicState,singleton::Singleton},
};
use crate::actions;
// how to import methods from other files/folders within your workspace
use crate::options::create_account_card_getter_options;
use crate::types::{
card_note::{CardNote, CardNoteMethods, CARD_NOTE_LEN},
leader::{Leader, LeaderSerializationMethods, LEADER_SERIALIZED_LEN},
};

struct Storage {
// Shows how to create a custom struct in Public
leader: PublicState<Leader, LEADER_SERIALIZED_LEN>,
// docs:start:storage-singleton-declaration
legendary_card: Singleton<CardNote, CARD_NOTE_LEN>,
// docs:end:storage-singleton-declaration
// just used for docs example to show how to create a singleton map.
// docs:start:storage-map-singleton-declaration
profiles: Map<AztecAddress, Singleton<CardNote, CARD_NOTE_LEN>>,
// docs:end:storage-map-singleton-declaration
}

// docs:start:state_vars-MapSingleton
impl Storage {
fn init(context: Context) -> Self {
Storage {
leader: PublicState::new(
context,
1,
LeaderSerializationMethods,
),
// docs:start:start_vars_singleton
legendary_card: Singleton::new(context, 1, CardNoteMethods),
legendary_card: Singleton::new(context, 2, CardNoteMethods),
// docs:end:start_vars_singleton
// highlight-next-line:state_vars-MapSingleton
// just used for docs example (not for game play):
// docs:start:state_vars-MapSingleton
profiles: Map::new(
context,
2,
3,
|context, slot| {
Singleton::new(context, slot, CardNoteMethods)
},
),
// docs:end:state_vars-MapSingleton
}
}
}
// docs:end:state_vars-MapSingleton

#[aztec(private)]
fn constructor(legendary_card_secret: Field) {
let mut legendary_card = CardNote::new(0, legendary_card_secret, AztecAddress::zero());
actions::init_legendary_card(storage.legendary_card, &mut legendary_card);
fn constructor() {}

#[aztec(private)]
// msg_sender() is 0 at deploy time. So created another function
fn initialize_private(randomness: Field, points: u8) {
let mut legendary_card = CardNote::new(points, randomness, context.msg_sender());
// create and broadcast note
storage.legendary_card.initialize(&mut legendary_card, Option::none(), true);
}

#[aztec(private)]
fn update_legendary_card(new_points: u8, new_secret: Field) {
let owner = inputs.call_context.msg_sender;
let mut updated_card = CardNote::new(new_points, new_secret, owner);
actions::update_legendary_card(storage.legendary_card, &mut updated_card);
fn update_legendary_card(randomness: Field, points: u8) {
// Ensure `points` > current value
// Also serves as a e2e test that you can `get_note()` and then `replace()`

// docs:start:state_vars-SingletonGet
let card = storage.legendary_card.get_note(true);
// docs:end:state_vars-SingletonGet

assert(points > card.points, "can only update to higher value");
let mut new_card = CardNote::new(points, randomness, context.msg_sender());
// docs:start:state_vars-SingletonReplace
storage.legendary_card.replace(&mut new_card, true);
// docs:end:state_vars-SingletonReplace

context.call_public_function(
context.this_address(),
FunctionSelector::from_signature("update_leader((Field),u8)"),
[context.msg_sender().to_field(), points as Field]
);
}

unconstrained fn get_legendary_card() -> pub CardNote {
actions::get_legendary_card(storage.legendary_card)
#[aztec(public)]
internal fn update_leader(account: AztecAddress, points: u8) {
let new_leader = Leader { account, points };
storage.leader.write(new_leader);
}

unconstrained fn get_leader() -> pub Leader {
storage.leader.read()
}

// TODO: remove this placeholder once https://github.com/AztecProtocol/aztec-packages/issues/2918 is implemented
unconstrained fn compute_note_hash_and_nullifier(
contract_address: AztecAddress,
nonce: Field,
storage_slot: Field,
serialized_note: [Field; CARD_NOTE_LEN]
) -> pub [Field; 4] {
let note_header = NoteHeader::new(contract_address, nonce, storage_slot);
note_utils::compute_note_hash_and_nullifier(CardNoteMethods, note_header, serialized_note)
}

/// Macro equivalence section
Expand Down Expand Up @@ -128,21 +186,4 @@ contract DocsExample {
// ************************************************************
}
// docs:end:simple_macro_example_expanded

// Cross chain messaging section
// Demonstrates a cross chain message
// docs:start:l1_to_l2_cross_chain_message
#[aztec(private)]
fn send_to_l1() {}
// docs:end:l1_to_l2_cross_chain_message

// TODO: remove this placeholder once https://github.com/AztecProtocol/aztec-packages/issues/2918 is implemented
unconstrained fn compute_note_hash_and_nullifier(
contract_address: AztecAddress,
nonce: Field,
storage_slot: Field,
serialized_note: [Field; 0]
) -> pub [Field; 4] {
[0, 0, 0, 0]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use dep::aztec::protocol_types::{
use dep::aztec::note::note_getter_options::{NoteGetterOptions, Sort, SortOrder};
use dep::std::option::Option;

// Shows how to use NoteGetterOptions and query for notes.

// docs:start:state_vars-NoteGetterOptionsSelectSortOffset
pub fn create_account_card_getter_options(
account: AztecAddress,
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mod card_note;
mod leader;
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,37 @@ use dep::aztec::{
context::PrivateContext,
};

// Shows how to create a custom note

global CARD_NOTE_LEN: Field = 3;

// docs:start:state_vars-CardNote
struct CardNote {
points: u8,
secret: Field,
randomness: Field,
owner: AztecAddress,
header: NoteHeader,
}
// docs:end:state_vars-CardNote

impl CardNote {
pub fn new(points: u8, secret: Field, owner: AztecAddress) -> Self {
pub fn new(points: u8, randomness: Field, owner: AztecAddress) -> Self {
CardNote {
points,
secret,
randomness,
owner,
header: NoteHeader::empty(),
}
}

pub fn serialize(self) -> [Field; CARD_NOTE_LEN] {
[self.points as Field, self.secret, self.owner.to_field()]
[self.points as Field, self.randomness, self.owner.to_field()]
}

pub fn deserialize(serialized_note: [Field; CARD_NOTE_LEN]) -> Self {
CardNote {
points: serialized_note[0] as u8,
secret: serialized_note[1],
randomness: serialized_note[1],
owner: AztecAddress::from_field(serialized_note[2]),
header: NoteHeader::empty(),
}
Expand All @@ -51,7 +53,7 @@ impl CardNote {
pub fn compute_note_hash(self) -> Field {
pedersen_hash([
self.points as Field,
self.secret,
self.randomness,
self.owner.to_field(),
],0)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use dep::aztec::protocol_types::address::AztecAddress;
use dep::aztec::types::type_serialization::TypeSerializationInterface;

// Shows how to create a custom struct in Public
struct Leader {
account: AztecAddress,
points: u8,
}

global LEADER_SERIALIZED_LEN: Field = 2;

fn deserialize(fields: [Field; LEADER_SERIALIZED_LEN]) -> Leader {
Leader { account: AztecAddress::from_field(fields[0]), points: fields[1] as u8 }
}

fn serialize(leader: Leader) -> [Field; LEADER_SERIALIZED_LEN] {
[leader.account.to_field(), leader.points as Field]
}

global LeaderSerializationMethods = TypeSerializationInterface {
deserialize,
serialize,
};

0 comments on commit 38d262e

Please sign in to comment.