Skip to content

Commit

Permalink
refactor: showcase recursion case for transfer
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind committed Jul 2, 2024
1 parent 97d997c commit 0ed1d37
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -335,13 +335,37 @@ contract Token {
let to_ivpk = header.get_ivpk_m(&mut context, to);

let amount = U128::from_integer(amount);
storage.balances.sub(from, amount).emit(encode_and_encrypt_note_with_keys_unconstrained(&mut context, from_ovpk, from_ivpk));
let mut (change, missing) = storage.balances.odd_sub(from, amount, 2);

// If we are still missing some balance to cover the amount, call accumulate to spend more notes.
if !missing.eq(U128::zero()) {
change = Token::at(context.this_address())._accumulate(from, missing.to_field()).call(&mut context);
}

storage.balances.add(from, change).emit(encode_and_encrypt_note_with_keys_unconstrained(&mut context, from_ovpk, from_ivpk));
storage.balances.add(to, amount).emit(encode_and_encrypt_note_with_keys_unconstrained(&mut context, from_ovpk, to_ivpk));

Transfer { from: from.to_field(), to: to.to_field(), amount: amount.to_field() }.emit(encode_and_encrypt_event_with_keys_unconstrained(&mut context, from_ovpk, to_ivpk));
}
// docs:end:transfer

/**
* Accumulate value by spending notes until we hit the missing amount
* If not enough notes are available, recurse to do it again.
* Will use MORE notes than the transfer directly since there are much smaller usual overhead on this function.
* Will fail with `Cannot return zero notes` if there are not enough notes to cover the amount.
*/
#[aztec(private)]
#[aztec(internal)]
fn _accumulate(owner: AztecAddress, missing_: Field) -> U128 {
// Since we are already spending a lot of doing the call, and we don't do anything else in here, we might as well go over more notes.
let mut (change, missing) = storage.balances.odd_sub(owner, U128::from_integer(missing_), 8);
if !missing.eq(U128::zero()) {
change = Token::at(context.this_address())._accumulate(owner, missing.to_field()).call(&mut context);
}
change
}

/**
* Cancel a private authentication witness.
* @param inner_hash The inner hash of the authwit to cancel.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,43 @@ impl<T> BalancesMap<T, &mut PrivateContext> {

self.add(owner, minuend - subtrahend)
}

pub fn odd_sub<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN>(
self: Self,
owner: AztecAddress,
subtrahend: U128,
limit: u32
) -> (U128, U128) where T: NoteInterface<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN> + OwnedNote {
// docs:start:get_notes
let options = NoteGetterOptions::with_filter(filter_notes_min_sum, subtrahend).set_limit(limit);
let notes = self.map.at(owner).get_notes(options);
// docs:end:get_notes

assert(notes.len() > 0, "No notes found");

let mut minuend: U128 = U128::from_integer(0);
for i in 0..options.limit {
if i < notes.len() {
let note = notes.get_unchecked(i);

// Removes the note from the owner's set of notes.
// This will call the the `compute_nullifer` function of the `token_note`
// which require knowledge of the secret key (currently the users encryption key).
// The contract logic must ensure that the spending key is used as well.
// docs:start:remove
self.map.at(owner).remove(note);
// docs:end:remove

minuend = minuend + note.get_amount();
}
}

if minuend >= subtrahend {
(minuend - subtrahend, U128::zero())
} else {
(U128::zero(), subtrahend - minuend)
}
}
}

pub fn filter_notes_min_sum<T, T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN>(
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/circuits.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,4 @@
]
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { BatchCall, EventType, Fr } from '@aztec/aztec.js';
import { TokenContract } from '@aztec/noir-contracts.js';

import { TokenContractTest } from './token_contract_test.js';

describe('e2e_token_contract transfer private', () => {
const t = new TokenContractTest('odd_transfer_private');
let { asset, accounts, tokenSim, wallets } = t;

beforeAll(async () => {
await t.applyBaseSnapshots();
await t.applyMintSnapshot();
await t.setup();
({ asset, accounts, tokenSim, wallets } = t);
});

afterAll(async () => {
await t.teardown();
});

afterEach(async () => {
await t.tokenSim.check();
});

it('transfer full balance', async () => {
const N = 5;
// Mint 4 * N notes
// Then calls `transfer` to send all the notes to another account
// The transfer will only spend 2 notes itself, and then call `_accumulate` which will recurse
// until it have spent all the notes

let expectedNullifiers = 2; // 1 from the tx_hash and 1 from the original note in the senders possesion

for (let i = 0; i < N; i++) {
const actions = [
asset.methods.privately_mint_private_note(1n).request(),
asset.methods.privately_mint_private_note(1n).request(),
asset.methods.privately_mint_private_note(1n).request(),
asset.methods.privately_mint_private_note(1n).request(),
];
await new BatchCall(wallets[0], actions).send().wait();

expectedNullifiers += actions.length;

tokenSim.mintPrivate(BigInt(actions.length));
tokenSim.redeemShield(accounts[0].address, BigInt(actions.length));
}

const amount = await asset.methods.balance_of_private(accounts[0].address).simulate();

expect(amount).toBeGreaterThan(0n);
const tx = await asset.methods.transfer(accounts[1].address, amount).send().wait({ debug: true });
tokenSim.transferPrivate(accounts[0].address, accounts[1].address, amount);

expect(tx.debugInfo?.nullifiers.length).toBe(expectedNullifiers);

// We expect there to have been inserted a single new note.
expect(tx.debugInfo?.noteHashes.length).toBe(1);

const events = await wallets[1].getEvents(EventType.Encrypted, TokenContract.events.Transfer, tx.blockNumber!, 1);

expect(events[0]).toEqual({
from: accounts[0].address,
to: accounts[1].address,
amount: new Fr(amount),
});
});

describe('failure cases', () => {
it('transfer more than balance', async () => {
const balance0 = await asset.methods.balance_of_private(accounts[0].address).simulate();
const amount = balance0 + 1n;
expect(amount).toBeGreaterThan(0n);
await expect(asset.methods.transfer(accounts[1].address, amount).simulate()).rejects.toThrow(
"Assertion failed: Cannot return zero notes 'returned_notes.len() != 0'",
);
});
});
});

0 comments on commit 0ed1d37

Please sign in to comment.