Skip to content

feat: initial escrowing example#6632

Closed
LHerskind wants to merge 9 commits into
masterfrom
lh/5864-escrow-token
Closed

feat: initial escrowing example#6632
LHerskind wants to merge 9 commits into
masterfrom
lh/5864-escrow-token

Conversation

@LHerskind

@LHerskind LHerskind commented May 23, 2024

Copy link
Copy Markdown
Contributor

Initial escrow flow showcasing one way to address #5864

Replaces the blacklist token with an escrowable blacklist token. Additional tests cases of escrowing should be created to showcase the usage better in separate PR's.

LHerskind commented May 23, 2024

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Join @LHerskind and the rest of your teammates on Graphite Graphite

@LHerskind LHerskind force-pushed the lh/5864-escrow-token branch 2 times, most recently from a6494b7 to 5a1438b Compare May 24, 2024 12:15
@AztecBot

AztecBot commented May 24, 2024

Copy link
Copy Markdown
Collaborator

Benchmark results

No metrics with a significant change found.

Detailed results

All benchmarks are run on txs on the Benchmarking contract on the repository. Each tx consists of a batch call to create_note and increment_balance, which guarantees that each tx has a private call, a nested private call, a public call, and a nested public call, as well as an emitted private note, an unencrypted log, and public storage read and write.

This benchmark source data is available in JSON format on S3 here.

Proof generation

Each column represents the number of threads used in proof generation.

Metric 1 threads 4 threads 16 threads 32 threads 64 threads
proof_construction_time_sha256_ms 5,722 1,556 (+1%) 708 763 (+1%) 772
proof_construction_time_sha256_30_ms 11,768 3,157 1,407 1,437 (+1%) 1,472
proof_construction_time_sha256_100_ms 44,831 11,721 5,402 (-1%) 5,429 (+1%) 5,351
proof_construction_time_poseidon_hash_ms 78.0 34.0 34.0 57.0 88.0 (+1%)
proof_construction_time_poseidon_hash_30_ms 1,517 420 204 (+1%) 228 (-1%) 267 (-1%)
proof_construction_time_poseidon_hash_100_ms 5,750 1,563 720 (-1%) 769 (-1%) 791 (-1%)

L2 block published to L1

Each column represents the number of txs on an L2 block published to L1.

Metric 4 txs 8 txs
l1_rollup_calldata_size_in_bytes 1,412 1,412
l1_rollup_calldata_gas 9,464 9,462
l1_rollup_execution_gas 608,000 607,998
l2_block_processing_time_in_ms 699 (+1%) 1,300
l2_block_building_time_in_ms 22,423 46,417 (+2%)
l2_block_rollup_simulation_time_in_ms 22,204 46,065 (+2%)
l2_block_public_tx_process_time_in_ms 19,055 42,610 (+2%)

L2 chain processing

Each column represents the number of blocks on the L2 chain where each block has 8 txs.

Metric 3 blocks 5 blocks
node_history_sync_time_in_ms 5,940 8,644 (+1%)
node_database_size_in_bytes 9,478,224 12,742,736
pxe_database_size_in_bytes 7,774 15,127

Circuits stats

Stats on running time and I/O sizes collected for every kernel circuit run across all benchmarks.

Circuit simulation_time_in_ms witness_generation_time_in_ms proving_time_in_ms input_size_in_bytes output_size_in_bytes proof_size_in_bytes num_public_inputs size_in_gates
private-kernel-init 137 (+1%) 497 (-1%) 11,873 (-1%) 20,634 64,614 89,536 2,731 524,288
private-kernel-inner 410 1,068 (+6%) 45,463 92,326 64,614 89,536 2,731 2,097,152
private-kernel-tail 586 (+1%) 2,820 47,744 (+1%) 96,545 68,261 11,648 297 2,097,152
base-parity 6.66 (+1%) 1,765 2,548 (-2%) 128 64.0 2,208 2.00 131,072
root-parity 50.3 (-5%) 46.7 (-6%) 34,320 27,100 64.0 2,720 18.0 2,097,152
base-rollup 5,991 2,331 65,349 119,738 756 3,648 47.0 4,194,304
root-rollup 112 65.4 (+7%) 19,455 25,309 620 3,456 41.0 1,048,576
public-kernel-app-logic 575 (+1%) 3,576 (+2%) 39,008 (+1%) 108,073 86,550 116,768 3,582 2,097,152
public-kernel-tail 1,156 (+1%) 23,417 (-2%) 154,825 403,238 7,646 11,648 297 8,388,608
private-kernel-reset-small 595 (+1%) 2,066 (-3%) 40,714 120,737 64,614 89,536 2,731 2,097,152
public-kernel-setup 678 (+1%) 2,681 (+1%) 37,580 108,073 86,550 116,768 3,582 2,097,152
public-kernel-teardown 582 (+2%) 3,591 (+2%) 39,016 108,073 86,550 116,768 3,582 2,097,152
merge-rollup 30.3 (+1%) N/A N/A 16,542 756 N/A N/A N/A
private-kernel-tail-to-public N/A 8,658 (-1%) 87,366 N/A N/A 116,768 3,582 4,194,304

Stats on running time collected for app circuits

Function input_size_in_bytes output_size_in_bytes witness_generation_time_in_ms proof_size_in_bytes proving_time_in_ms size_in_gates num_public_inputs
ContractClassRegisterer:register 1,344 9,944 470 N/A N/A N/A N/A
ContractInstanceDeployer:deploy 1,408 9,944 42.1 N/A N/A N/A N/A
MultiCallEntrypoint:entrypoint 1,920 9,944 1,802 (+1%) N/A N/A N/A N/A
SchnorrAccount:constructor 1,312 9,944 1,469 (+1%) N/A N/A N/A N/A
SchnorrAccount:entrypoint 2,304 9,944 2,841 16,768 50,299 2,097,152 457
Token:privately_mint_private_note 1,280 9,944 1,648 N/A N/A N/A N/A
FPC:fee_entrypoint_public 1,344 9,944 1,128 16,768 10,130 524,288 457
Token:transfer 1,376 9,944 5,472 (+1%) 16,768 47,829 (-1%) 2,097,152 457
Benchmarking:create_note 1,344 9,944 1,397 (+1%) N/A N/A N/A N/A
SchnorrAccount:spend_private_authwit 1,280 9,944 77.3 N/A N/A N/A N/A
Token:unshield 1,376 9,944 3,929 N/A N/A N/A N/A
FPC:fee_entrypoint_private 1,376 9,944 4,796 (-1%) N/A N/A N/A N/A

Tree insertion stats

The duration to insert a fixed batch of leaves into each tree type.

Metric 1 leaves 16 leaves 64 leaves 128 leaves 256 leaves 512 leaves 1024 leaves 32 leaves
batch_insert_into_append_only_tree_16_depth_ms 10.5 17.1 N/A N/A N/A N/A N/A N/A
batch_insert_into_append_only_tree_16_depth_hash_count 16.7 31.7 N/A N/A N/A N/A N/A N/A
batch_insert_into_append_only_tree_16_depth_hash_ms 0.608 0.523 N/A N/A N/A N/A N/A N/A
batch_insert_into_append_only_tree_32_depth_ms N/A N/A 49.0 76.3 (+1%) 133 247 475 N/A
batch_insert_into_append_only_tree_32_depth_hash_count N/A N/A 95.9 159 288 543 1,055 N/A
batch_insert_into_append_only_tree_32_depth_hash_ms N/A N/A 0.500 0.470 (+1%) 0.456 0.449 (+1%) 0.444 N/A
batch_insert_into_indexed_tree_20_depth_ms N/A N/A 59.1 (+1%) 113 (+1%) 185 (+1%) 353 (+1%) 633 (+1%) N/A
batch_insert_into_indexed_tree_20_depth_hash_count N/A N/A 107 208 355 686 1,263 N/A
batch_insert_into_indexed_tree_20_depth_hash_ms N/A N/A 0.510 (+1%) 0.506 (+1%) 0.489 0.482 (+1%) 0.473 (+1%) N/A
batch_insert_into_indexed_tree_40_depth_ms N/A N/A N/A N/A N/A N/A N/A 59.5 (+1%)
batch_insert_into_indexed_tree_40_depth_hash_count N/A N/A N/A N/A N/A N/A N/A 101
batch_insert_into_indexed_tree_40_depth_hash_ms N/A N/A N/A N/A N/A N/A N/A 0.555 (+1%)

Miscellaneous

Transaction sizes based on how many contract classes are registered in the tx.

Metric 0 registered classes 1 registered classes
tx_size_in_bytes 79,470 665,267

Transaction size based on fee payment method

| Metric | |
| - | |

@LHerskind LHerskind force-pushed the lh/5864-escrow-token branch 4 times, most recently from 99a70a6 to 7df928c Compare May 30, 2024 07:54
* Escrowable Token (partial implementation)
*
* The following is a partial implementation of an escrowable token.
* Partial in the sense that it do not (yet) support all the features of a full token, but just

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.

Suggested change
* Partial in the sense that it do not (yet) support all the features of a full token, but just
* Partial in the sense that it does not (yet) support all the features of a full token, but just

* Escrowable here being that the "owner" of a "balance" might be different from the actor
* who is actually able to use the funds.
*
* In public state, example on this is uniswap most other contracts. The contract escrow funds

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.

Sentence does not make sense

* it defines the rules for how it is spend (still following the tokens own rules).
*
* This is all good and dandy, but when moving to private state, this becomes significantly more tricky.
* Recall in private you have both an "owner" as specified by the business logic, but also a actor

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.

Suggested change
* Recall in private you have both an "owner" as specified by the business logic, but also a actor
* Recall in private you have both an "owner" as specified by the business logic, but also an actor

* However, if escrowing, this might no longer be the case.
*
* An example would be a `vault` such as the `SimpleEscrow` contract.
* The contract "owns" the tokens, but it don't have any nullifier keys itself, and rely on `alice` to provide them.

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.

Suggested change
* The contract "owns" the tokens, but it don't have any nullifier keys itself, and rely on `alice` to provide them.
* The contract "owns" the tokens, but it doesn't have any nullifier keys itself, and relies on `alice` to provide them.

* be double spent, we need to support the case where `vault` is the `owner` but `alice`s keys are used for `npk_m_hash`
*
* Furthermore, to support the `incoming` and `outgoing` logs, we need to provie values for these. Note that
* want to use the address that maps to `npk_m_hash` for the `ivpk` as well, since we want to ensure that the

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.

Suggested change
* want to use the address that maps to `npk_m_hash` for the `ivpk` as well, since we want to ensure that the
* we want to use the address that maps to `npk_m_hash` for the `ivpk` as well, since we want to ensure that the

*
* Furthermore, to support the `incoming` and `outgoing` logs, we need to provie values for these. Note that
* want to use the address that maps to `npk_m_hash` for the `ivpk` as well, since we want to ensure that the
* actor that can nullify the note, will also learn about the notes existence - otherwise it don't matter much.

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.

Suggested change
* actor that can nullify the note, will also learn about the notes existence - otherwise it don't matter much.
* actor that can nullify the note, will also learn about the notes existence - otherwise it doesn't matter much.

* want to use the address that maps to `npk_m_hash` for the `ivpk` as well, since we want to ensure that the
* actor that can nullify the note, will also learn about the notes existence - otherwise it don't matter much.
*
* The address mapping to `ovpk` is a bit more tricky, since it don't necessarily match any of the other addresses.

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.

Suggested change
* The address mapping to `ovpk` is a bit more tricky, since it don't necessarily match any of the other addresses.
* The address mapping to `ovpk` is a bit more tricky, since it doesn't necessarily match any of the other addresses.

* In most "direct" user to user transfers, it will belong to the `msg_sender`, but for escrowing, it might be someone
* else. It needs to be provided.
*
* - `incoming_viewer_and_nullifier` to refer to the address that is used to fetch `ivpk` and `npk` related values

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.

I see that you don't like our nullificator friend

image

He will remember

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.

270a4fa5ac7035fdd19f65d8d3a657b0

fn mint_private(
to: AztecAddress,
amount: Field,
incoming_viewer_and_nullifier: AztecAddressOption,

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.

Great idea using option here 👍

@LHerskind LHerskind force-pushed the lh/5864-escrow-token branch from 7df928c to fab4d38 Compare May 30, 2024 11:49
@LHerskind LHerskind changed the base branch from master to lh/token-simulator-cleanup May 30, 2024 11:49
Base automatically changed from lh/token-simulator-cleanup to master May 30, 2024 15:06
@LHerskind LHerskind force-pushed the lh/5864-escrow-token branch 2 times, most recently from 293aebe to 0c04aaa Compare May 31, 2024 12:49
@AztecBot

AztecBot commented May 31, 2024

Copy link
Copy Markdown
Collaborator

Docs Preview

Hey there! 👋 You can check your preview at https://665f23d0839c0f1860d222da--aztec-docs-dev.netlify.app


let amount = U128::from_integer(amount);

storage.balances.sub(

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.

imagine a staking contract.
When user stakes there: tokenNote should still belong to user. But balance should be increased of staking contract
i.e. balances.at(staking_contract) += amount
SAID ANOTHER WAY: balance[staking_contract] = TokenNote<owner = user. amount = 100>

Later, when a user unstakes:
the staking contract should reduce its balance. Crucuially though, get_notes should be called on the user and not the staking contract since the note.owner = user.

So there should be a from_nullifioor. My hypothesis is there is never a case when from_nullifioor != outgoing_viewoor so you should get_notes() from that party and not from

) 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);
let maybe_notes = self.map.at(owner).get_notes(options);

@rahul-kothari rahul-kothari May 31, 2024

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.

As discussed A potential issue not with this contract but PXE i guess:
Assume owner (person with balance) is different to address that gives nullifier key. E..g in escrow or staking contract:

balance[staking_contract] = [ TokenNote< { rahul, amount = 100},   TokenNote {lasse, amount = 50} ]

i.e. both rahul and lasse stake some tokens [the balance of staking contract increases (150) but lasse and rahul still own their note.

Later, when a user unstakes, the staking contract should reduce its balance. Crucuially though, get_notes should filter for notes with the right user.

Currently if whoever is calling the tx somehow has both Lasse and Rahul's notes, both get fetched and there is a non-deterministic outcome i.e. sometimes lasse's notes could be nullified even though I wanted to unstake rahul's notes.

Ideally the pxe can just filter based on the account address in the note and only return those notes

@LHerskind LHerskind marked this pull request as ready for review May 31, 2024 16:26
@rahul-kothari

Copy link
Copy Markdown
Contributor

https://link.excalidraw.com/l/918xh05mbiS/AIKmHUXcmiH

For those trying to understand what's happening with all these comments and scenarious

@LHerskind LHerskind force-pushed the lh/5864-escrow-token branch from 0c04aaa to 9274fef Compare June 3, 2024 13:57
@LHerskind LHerskind requested a review from benesjan June 3, 2024 14:20

@benesjan benesjan left a comment

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.

Looks good. Feel free to merge once you address my comments

});

tokenSim.addAccount(walletGroup2[0].getAddress());
tokenSim.setLookupProvider(walletGroup2[0].getAddress(), walletGroup2[0]);

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.

Calling this addWallet would be much clearer. Lookup provider is too cryptic.

amount,
nonce,
toAddressOption(accounts[0].address),
toAddressOption(wallets[0].getAddress()),

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.

Is there some reason for why here we get the address from wallet and in the other 2 cases from accounts? The values should be the same, no?


expect(wallets[0].getAddress()).toEqual(accounts[0].address);

const action = asset

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.

Suggested change
const action = asset
// First we transfer funds to escrow and authorize it to donate on behalf of account 0
const action = asset

Some comments would help here.

);
await wallets[0].createAuthWit({ caller: escrowContracts[0].address, action });

await escrowContracts[0].withWallet(wallets[0]).methods.donate(amount, nonce).send().wait();

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.

Suggested change
await escrowContracts[0].withWallet(wallets[0]).methods.donate(amount, nonce).send().wait();
// Now we check that donating from escrow works
await escrowContracts[0].withWallet(wallets[0]).methods.donate(amount, nonce).send().wait();


describe('failure cases', () => {
it('FAIL: direct transfer to vault fails', async () => {
const amount = 10n;

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.

Suggested change
const amount = 10n;
// This test should fail because the escrow does not have any keys registered and hence obtaining them fails.
const amount = 10n;

Am I correct here that this is the cause of the issue?

});
});

it('sending funds but using recipient as escrow, 🏦🤫💰', async () => {

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.

This is a fun one. But the contracts could add checks to prevent this.

it('sending funds but using recipient as escrow, 🏦🤫💰', async () => {
// For this test, I'm doing something quite strange, I am sending funds to Bob, but I am not allowing him to actually spend them
// I am still the "incoming_viewer_and_nullifier". This is a really annoying example as it will look like loss of funds to the users, and to get the funds
// back, or spend them, I need to give Bob some secrets 💀

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.

Suggested change
// back, or spend them, I need to give Bob some secrets 💀
// back, or spend them, I need to give Bob my `nsk_app` and the `ivsk` (or give the note itself instead of `ivsk`).💀

Being more specific here is helpful.

// In practice, the funds are still possible to rescue, but this makes our account a bit simpler.

{
const balance1 = await asset.methods.balance_of_private(accounts[1].address).simulate();

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.

Is accounts[1].address equal to walletGroup2[0].getAddress() here?

naming the addresses in this test would help.

tokenSim.transferPrivate(accounts[0].address, AztecAddress.ZERO, amount);
// In practice, the funds are still possible to rescue, but this makes our account a bit simpler.

{

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.

Suggested change
{
// Now we try to obtain the balance. It should fail with zero notes error because the recipient's PXE is expected to not have decrypted any notes.
{

import { EscrowTokenContractTest, toAddressOption } from './escrowable_token_contract_test.js';

// @todo For this test to be truly meaningful we need to run every actor with a separate PXE.
// when they are using the same we don't actually know if the default values are correct or

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.

Suggested change
// when they are using the same we don't actually know if the default values are correct or
// When they are using the same we don't actually know if the default values are correct or

@LHerskind LHerskind force-pushed the lh/5864-escrow-token branch 2 times, most recently from 797d971 to 449052a Compare June 4, 2024 14:09
@LHerskind LHerskind force-pushed the lh/5864-escrow-token branch from 449052a to 0d8ca72 Compare June 6, 2024 17:36
@LHerskind LHerskind marked this pull request as draft June 10, 2024 22:45
@LHerskind

Copy link
Copy Markdown
Contributor Author

The consensus seems to be that we won't be following this direction as it can be handled at the app-layer instead and that it might just introduce "too" much complexity for usual transfers.

@LHerskind LHerskind closed this Jun 11, 2024
@ludamad ludamad deleted the lh/5864-escrow-token branch December 17, 2024 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants