Skip to content
Merged
13 changes: 13 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,17 @@ jobs:
command: ./scripts/cond_run_script end-to-end $JOB_NAME ./scripts/run_tests_local e2e_private_token_contract.test.ts
working_directory: yarn-project/end-to-end

e2e-multi-transfer-contract:
docker:
- image: aztecprotocol/alpine-build-image
resource_class: small
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_spot_run_tests end-to-end e2e_multi_transfer.test.ts

e2e-block-building:
machine:
image: ubuntu-2004:202010-01
Expand Down Expand Up @@ -1312,6 +1323,7 @@ workflows:
- e2e-deploy-contract: *e2e_test
- e2e-lending-contract: *e2e_test
- e2e-zk-token-contract: *e2e_test
- e2e-multi-transfer-contract: *e2e_test
- e2e-block-building: *e2e_test
- e2e-nested-contract: *e2e_test
- e2e-non-contract-account: *e2e_test
Expand Down Expand Up @@ -1339,6 +1351,7 @@ workflows:
- e2e-deploy-contract
- e2e-lending-contract
- e2e-zk-token-contract
- e2e-multi-transfer-contract
- e2e-block-building
- e2e-nested-contract
- e2e-non-contract-account
Expand Down
8 changes: 4 additions & 4 deletions yarn-project/aztec-rpc/src/note_processor/note_processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ export class NoteProcessor {
indexOfTxInABlock * MAX_NEW_COMMITMENTS_PER_TX,
(indexOfTxInABlock + 1) * MAX_NEW_COMMITMENTS_PER_TX,
);
const newNullifiers = block.newNullifiers.slice(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

for my understanding - why did we move this earlier?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We were fetching the newNullifiers from the L2 block inside the decryption logic. This means for each successfully decrypted note, we re-do the slicing on block.newNullifiers to get the same value over and over again. I moved it outside to fetch the newNullifiers for a transaction just once.

indexOfTxInABlock * MAX_NEW_NULLIFIERS_PER_TX,
(indexOfTxInABlock + 1) * MAX_NEW_NULLIFIERS_PER_TX,
);
// Note: Each tx generates a `TxL2Logs` object and for this reason we can rely on its index corresponding
// to the index of a tx in a block.
const txFunctionLogs = txLogs[indexOfTxInABlock].functionLogs;
Expand All @@ -117,10 +121,6 @@ export class NoteProcessor {
const noteSpendingInfo = NoteSpendingInfo.fromEncryptedBuffer(logs, privateKey, curve);
if (noteSpendingInfo) {
// We have successfully decrypted the data.
const newNullifiers = block.newNullifiers.slice(
indexOfTxInABlock * MAX_NEW_NULLIFIERS_PER_TX,
(indexOfTxInABlock + 1) * MAX_NEW_NULLIFIERS_PER_TX,
);
try {
const { index, nonce, siloedNullifier } = await this.findNoteIndexAndNullifier(
dataStartIndexForTx,
Expand Down
195 changes: 195 additions & 0 deletions yarn-project/end-to-end/src/e2e_multi_transfer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { AztecNodeService } from '@aztec/aztec-node';
import { AztecRPCServer } from '@aztec/aztec-rpc';
import { AztecAddress, Contract, Fr, Wallet } from '@aztec/aztec.js';
import { DebugLogger } from '@aztec/foundation/log';
import { PrivateTokenAirdropContract } from '@aztec/noir-contracts/types';
import { MultiTransferContract } from '@aztec/noir-contracts/types';
import { AztecRPC, CompleteAddress } from '@aztec/types';

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

/**
* Multi-transfer payments is an example application to demonstrate how a payroll application could be built using aztec.
* In the current version of aztec, each multi-transfer can support only 12 recipients per transaction. The sender
* can decide which note can be spent.
*/
describe('multi-transfer payments', () => {
const numberOfAccounts = 12;

let aztecNode: AztecNodeService | undefined;
let aztecRpcServer: AztecRPC;
let wallet: Wallet;
let logger: DebugLogger;
let ownerAddress: AztecAddress;
const recipients: AztecAddress[] = [];
let initialBalance: bigint;

let zkTokenContract: PrivateTokenAirdropContract;
let multiTransferContract: MultiTransferContract;

beforeEach(async () => {
let accounts: CompleteAddress[];
({ aztecNode, aztecRpcServer, accounts, logger, wallet } = await setup(numberOfAccounts + 1)); // 1st being the `owner`
ownerAddress = accounts[0].address;

for (let i = 1; i < accounts.length; i++) {
const account = accounts[i].address;
recipients.push(account);
}

logger(`Deploying zk token contract...`);
initialBalance = 1000n;
await deployZkTokenContract(initialBalance, ownerAddress);

logger(`Deploying multi-transfer contract...`);
await deployMultiTransferContract();
}, 100_000);

afterEach(async () => {
await aztecNode?.stop();
if (aztecRpcServer instanceof AztecRPCServer) {
await aztecRpcServer?.stop();
}
}, 30_000);

const deployZkTokenContract = async (initialBalance: bigint, owner: AztecAddress) => {
logger(`Deploying zk token contract...`);
zkTokenContract = await PrivateTokenAirdropContract.deploy(wallet, initialBalance, owner).send().deployed();
logger(`zk token contract deployed at ${zkTokenContract.address}`);
};

const deployMultiTransferContract = async () => {
logger(`Deploying multi-transfer contract...`);
multiTransferContract = await MultiTransferContract.deploy(wallet).send().deployed();
logger(`multi-transfer contract deployed at ${multiTransferContract.address}`);
};

const expectBalance = async (tokenContract: Contract, owner: AztecAddress, expectedBalance: bigint) => {
const balance = await tokenContract.methods.getBalance(owner).view({ from: owner });
logger(`Account ${owner} balance: ${balance}`);
expect(balance).toBe(expectedBalance);
};

/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

love this - thank you for adding it!!!

* Payroll example
*
* Transaction 1:
* The sender first splits 1000 to create new notes (for himself) with values 100, 200, 300, 400:
* 0: sender: [1000]
* |
* +-- [100 (change), 200, 300, 400]
*
* Transaction 2:
* In the next transaction, the sender wants to spend all four notes created in the previous transaction:
* index: [0 1 2 3 4 5 6 7]
* 0: sender: [100, 200, 300, 400]
* |
* +-- [25 (change), 20, 25, 30] // first batchTx call
*
* index: [0 1 2 3 4 5 6 7]
* 1: sender: [200, 300, 400, 25]
* |
* +-- [50 (change), 40, 50, 60] // second batchTx call
*
* index: [0 1 2 3 4 5 6 7]
* 2: sender: [300, 400, 25, 50]
* |
* +-- [60 (change), 75, 80, 85] // third batchTx call
*
* index: [0 1 2 3 4 5 6 7]
* 3: sender: [400, 25, 50, 60]
* |
* +-- [50 (change), 100, 120, 130] // fourth batchTx call
*
*/
it('12 transfers per transactions should work', async () => {
// Transaction 1
logger(`self batchTransfer()`);
Comment thread
rahul-kothari marked this conversation as resolved.
const batchTransferTx = zkTokenContract.methods
Comment thread
rahul-kothari marked this conversation as resolved.
.batchTransfer(ownerAddress, [200n, 300n, 400n], [ownerAddress, ownerAddress, ownerAddress], 0)
.send({ origin: ownerAddress });
await batchTransferTx.isMined();
const batchTransferTxReceipt = await batchTransferTx.getReceipt();
logger(`consumption Receipt status: ${batchTransferTxReceipt.status}`);
await expectBalance(zkTokenContract, ownerAddress, initialBalance);
await expectsNumOfEncryptedLogsInTheLastBlockToBe(aztecNode, 4);

const amounts: bigint[] = [20n, 25n, 30n, 40n, 50n, 60n, 75n, 80n, 85n, 100n, 120n, 130n];
const amountSum = amounts.reduce((a, b) => a + b, 0n);
const noteOffsets: bigint[] = [0n, 0n, 0n, 0n];

// Transaction 2
logger(`multiTransfer()...`);
const multiTransferTx = multiTransferContract.methods
.multiTransfer(
zkTokenContract.address.toField(),
recipients,
amounts,
ownerAddress,
Fr.fromBuffer(zkTokenContract.methods.batchTransfer.selector),
noteOffsets,
)
.send({ origin: ownerAddress });
await multiTransferTx.isMined({ timeout: 1000 }); // mining timeout ≥ time needed for the test to finish.
const multiTransferTxReceipt = await multiTransferTx.getReceipt();
logger(`Consumption Receipt status: ${multiTransferTxReceipt.status}`);

await expectBalance(zkTokenContract, ownerAddress, initialBalance - amountSum);
for (let index = 0; index < numberOfAccounts; index++) {
await expectBalance(zkTokenContract, recipients[index], amounts[index]);
}
await expectsNumOfEncryptedLogsInTheLastBlockToBe(aztecNode, 16);
}, 100_000);

/**
* Creating change notes for self.
*
* Transaction 1: Splits the 1000 note to create 12 notes x 50 each.
* index: [0 1 2 3 4 5 6 7]
* 0: sender: [1000]
* |
* +-- [850, 50, 50, 50]
*
* index: [0 1 2 3 4 5 6 7]
* 1: sender: [850, 50, 50, 50]
* |
* +-- [700, 50, 50, 50]
*
* index: [0 1 2 3 4 5 6 7]
* 2: sender: [50, 50, 50, 700, 50, 50, 50]
* |
* +-- [550, 50, 50, 50]
*
* index: [0 1 2 3 4 5 6 7]
* 3: sender: [50, 50, 50, 50, 50, 50, 550, 50, 50, 50]
* |
* +-- [400, 50, 50, 50]
*
* End state:
* sender: [50, 50, 50, 50, 50, 50, 50, 50, 50, 400, 50, 50, 50]
*/
it('create 12 small notes out of 1 large note', async () => {
// Transaction 1
const amounts: bigint[] = [50n, 50n, 50n, 50n, 50n, 50n, 50n, 50n, 50n, 50n, 50n, 50n];
const noteOffsets: bigint[] = [0n, 0n, 3n, 6n];
const repeatedSelfAdddress: AztecAddress[] = Array(12).fill(ownerAddress);

logger(`split multiTransfer()...`);
const multiTransferTx = multiTransferContract.methods
.multiTransfer(
zkTokenContract.address.toField(),
repeatedSelfAdddress,
amounts,
ownerAddress,
Fr.fromBuffer(zkTokenContract.methods.batchTransfer.selector),
noteOffsets,
)
.send({ origin: ownerAddress });
await multiTransferTx.isMined({ timeout: 100 }); // mining timeout ≥ time needed for the test to finish.
const multiTransferTxReceipt = await multiTransferTx.getReceipt();
logger(`Consumption Receipt status: ${multiTransferTxReceipt.status}`);

await expectBalance(zkTokenContract, ownerAddress, initialBalance);
await expectsNumOfEncryptedLogsInTheLastBlockToBe(aztecNode, 16);
}, 100_000);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "multi_transfer_contract"
authors = [""]
compiler_version = "0.1"
type = "contract"

[dependencies]
aztec = { path = "../../../../noir-libs/noir-aztec" }
value_note = { path = "../../../../noir-libs/value-note"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Demonstrates how to perform 4 x 4 = 16 transfers in one transaction. Uses the private airdrop contract in the backend.
contract MultiTransfer {
use dep::aztec::abi;
use dep::aztec::abi::PrivateContextInputs;
use dep::aztec::abi::PublicContextInputs;
use dep::aztec::context::PrivateContext;
use dep::aztec::oracle::public_call;
use dep::aztec::private_call_stack_item::PrivateCallStackItem;
use dep::aztec::public_call_stack_item::PublicCallStackItem;
use dep::aztec::types::point::Point;

// Libs
use dep::value_note::{
value_note::{VALUE_NOTE_LEN, ValueNoteMethods},
};
use dep::aztec::note::{
note_header::NoteHeader,
utils as note_utils,
};

fn constructor(
inputs: PrivateContextInputs
) -> distinct pub abi::PrivateCircuitPublicInputs {
PrivateContext::new(inputs, 0).finish()
}

// Transfers 12 amounts to 12 recipients.
// multiTransfer() => 4 calls to batchTransfer() on the private airdrop contract.
// Each batchTransfer() call allows sending new notes to 3 recipients, so 3 x 4 = 12 recipients in total.
// Note that all the notes stay on the airdrop contract, the multi transfer contract must interact with
// methods in the private airdrop contract to initiate multiple transfers in one transaction.
fn multiTransfer(
inputs: PrivateContextInputs,
asset: Field, // Asset to distribute
addresses: [Field; 12], // Addresses to distribute to
amounts: [Field; 12], // Amounts to distribute
owner: Field, // Owner of the asset
batch_transfer_selector: Field, // Function selector for transfer
note_offsets: [Field; 4], // Offsets from which 4 notes of the owner would be read.
) -> distinct pub abi::PrivateCircuitPublicInputs {
let mut context = PrivateContext::new(inputs, abi::hash_args([
asset,
addresses[0],
addresses[1],
addresses[2],
addresses[3],
addresses[4],
addresses[5],
addresses[6],
addresses[7],
addresses[8],
addresses[9],
addresses[10],
addresses[11],
amounts[0],
amounts[1],
amounts[2],
amounts[3],
amounts[4],
amounts[5],
amounts[6],
amounts[7],
amounts[8],
amounts[9],
amounts[10],
amounts[11],
owner,
batch_transfer_selector,
note_offsets[0],
note_offsets[1],
note_offsets[2],
note_offsets[3],
]));

// First batch transfer call
let return_values_1 = context.call_private_function(asset, batch_transfer_selector, [
owner,
amounts[0],
amounts[1],
amounts[2],
addresses[0],
addresses[1],
addresses[2],
note_offsets[0],
]);
let result1 = return_values_1[0];
context.return_values.push(result1);

// Second batch transfer call
let return_values_2 = context.call_private_function(asset, batch_transfer_selector, [
owner,
amounts[3],
amounts[4],
amounts[5],
addresses[3],
addresses[4],
addresses[5],
note_offsets[1],
]);
let result2 = return_values_2[0];
context.return_values.push(result2);

// Third batch transfer call
let return_values_3 = context.call_private_function(asset, batch_transfer_selector, [
owner,
amounts[6],
amounts[7],
amounts[8],
addresses[6],
addresses[7],
addresses[8],
note_offsets[2],
]);
let result3 = return_values_3[0];
context.return_values.push(result3);

// Fourth batch transfer call
let return_values_4 = context.call_private_function(asset, batch_transfer_selector, [
owner,
amounts[9],
amounts[10],
amounts[11],
addresses[9],
addresses[10],
addresses[11],
note_offsets[3],
]);
let result4 = return_values_4[0];
context.return_values.push(result4);

context.finish()
}
}
Loading