-
Notifications
You must be signed in to change notification settings - Fork 607
Multi-Transfer (12 transfers per tx) and E2E Test #1031
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
e0de7c6
add multi transfer (initial version)
rahul-kothari 90b68f9
Fix note filtering based on pending nullifers.
suyash67 fd75e4f
Fix ctrlv mistake.
suyash67 edc0ff1
Fix isMined timeout :/:/:/
suyash67 1badb30
Add comments.
suyash67 c3af44d
Remove unnecessary fn.
suyash67 fa1f817
Simple comments.
suyash67 045d2d4
remove debug statements in nr.
suyash67 3ff9501
rearrange (rahul's suggestion)
suyash67 5e8cadb
Remove `spend_note_index`.
suyash67 bc2d61a
add note-split test.
suyash67 3afc86d
Fix errors due to new nargo version.
suyash67 e4c8882
reduce time limits.
suyash67 21793c0
remove unused accountAddresses.
suyash67 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }; | ||
|
|
||
| /** | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()`); | ||
|
rahul-kothari marked this conversation as resolved.
|
||
| const batchTransferTx = zkTokenContract.methods | ||
|
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); | ||
| }); | ||
9 changes: 9 additions & 0 deletions
9
yarn-project/noir-contracts/src/contracts/multi_transfer_contract/Nargo.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"} |
133 changes: 133 additions & 0 deletions
133
yarn-project/noir-contracts/src/contracts/multi_transfer_contract/src/main.nr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We were fetching the
newNullifiersfrom the L2 block inside the decryption logic. This means for each successfully decrypted note, we re-do the slicing onblock.newNullifiersto get the same value over and over again. I moved it outside to fetch thenewNullifiersfor a transaction just once.