Skip to content
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

feat: add assets to a note #614

Closed
wants to merge 9 commits into from

Conversation

Dominik1999
Copy link
Collaborator

@Dominik1999 Dominik1999 commented Apr 18, 2024

Closes #596, #228, #563.

Adding the ability to add assets to a note.

  • I changed the kernel procedures for tx::create_note and I added the procedure tx::add_asset_to_note. That means, first a note is being created without any assets, then, assets can be added.
  • Also, I added the aux_data field to tx::create_note and subsequent changes.
  • Then, the way how we create OutputNotes in the Host needed to change. We now collect all the note data during execution, because we can always add new assets, and build the notes at the end.

@hackaugusto
Copy link
Contributor

hackaugusto commented Apr 18, 2024

Have you considered also doing #563 ? Another alternative would be to give an asset counter and a list of assets. I'm not sure how to provide the list of assets though, AFAIK we can only have 16 elements in the stack, so it would need to go somewhere else, one possibility is to push them to the advice provider and then give the hash of the assets to the create_note call (well, in this case with the hash, the counter can be put as value for the same key).

@Dominik1999
Copy link
Collaborator Author

That is an excellent point; maybe I should rather work on #563.

One problem is that tx::create_note calls syscall.create_note. This changes the context, and we can only add max two ASSETs since the procedure already takes [tag, note_type, RECIPIENT].

That means, instead of providing the full ASSET, we should provide the ROOT of a tree of ASSETS that go into a specific note. If we add a num_of_assets to the create_note procedure, we can iterate over the tree and use numbers as KEYs. At KEY 0 we can store the note_idx to ensure we put the correct ASSETs into the right note.

But the overall complexity grows quite a bit. I mean, you need to define before the transaction which assets you want to send with which note. That has implications for the note_args use case as well.

#! Creates a new note and returns a pointer to the memory address at which the note is stored.
#!
#! Inputs: [num_of_assets, ROOT, tag, note_type, RECIPIENT]
#! Outputs: [ptr]
#!
#! num_of_assets is how many different assets the note should contain
#! ROOT is the root of the tree of all ASSETs to send stored in memory
#! tag is the tag to be included in the note.
#! note_type is the storage type of the note
#! RECIPIENT is the recipient of the note.
#! ptr is the pointer to the memory address at which the note is stored.
export.create_note
    syscall.create_note
    # => [ptr, ZERO, ZERO, 0]

    # clear the padding from the kernel response
    movdn.8 dropw dropw swap drop
    # => [ptr]
end

@bobbinth
Copy link
Contributor

bobbinth commented Apr 19, 2024

Maybe the way to go here is to change how create_note works so that it takes no assets. Then, we could add assets one by one to the note through something like #228.

This would also address #563.

@Dominik1999
Copy link
Collaborator Author

Dominik1999 commented Apr 19, 2024

that way we could address #563 #228 and #596.

That might be the easiest to simply redefine:

#! Creates a new note and returns a pointer to the memory address at which the note is stored.
#!
#! Inputs: [aux, tag, note_type, RECIPIENT]
#! Outputs: [ptr]
#! ...
export.create_note
    syscall.create_note
    # => [ptr, ZERO, 0]
end

#! Adds an asset to a note
#!
#! Inputs: [note_idx or note_ptr, ASSET]
#! Outputs: [ptr]
#! ...
export.add_asset_to_note
    syscall.add_asset_to_note
    # => [ptr, ZERO, 0]
end

That might be quite a change. Let me check.

@Dominik1999 Dominik1999 force-pushed the dominik_feat_notes_without_assets branch from 1c18db8 to 380f834 Compare April 24, 2024 12:24
@Dominik1999 Dominik1999 force-pushed the dominik_feat_notes_without_assets branch 2 times, most recently from 65fb04c to c1b57df Compare April 26, 2024 09:21
@Dominik1999 Dominik1999 changed the title feat: notes without assets feat: add ability to assets to a note Apr 26, 2024
@Dominik1999 Dominik1999 force-pushed the dominik_feat_notes_without_assets branch from c1b57df to 3c5727c Compare April 26, 2024 11:48
@Dominik1999 Dominik1999 force-pushed the dominik_feat_notes_without_assets branch from 3c5727c to 1fabcc9 Compare April 26, 2024 11:59
@Dominik1999 Dominik1999 changed the title feat: add ability to assets to a note feat: add assets to a note Apr 29, 2024
#!
#! ptr is the pointer to the memory address at which the note is stored.
#! ASSET can be a fungible or non-fungible asset.
export.add_asset_to_note
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@bobbinth @hackaugusto actually here, we want to use

exec.account::remove_asset to remove the asset before we put it on the note.

Right now, this is blocked by our MockDataStore which is the basis for all our tests.

We need to refactor that first, before we can make create_note cleaner.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a question for @bobbinth . I don't know why create_note doesn't call remove_asset. It seems the user is expected to manually handle these details.

// OUTPUT NOTE BUILDER
// ================================================================================================
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct OutputNoteData {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure if this is the most elegant solution. Maybe a normal dict would have sufficed?

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean? Why do you want to use a map here?

@@ -305,6 +314,50 @@ impl<A: AdviceProvider> TransactionHost<A> {
Ok(process.get_mem_value(process.ctx(), note_address).map(NoteId::from))
}
}

/// ToDo: pretty ugly function, refactor
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It works, but is pretty ugly.

Comment on lines 532 to 534
# prepare the stack for return
#padw push.0 push.0 movup.6
# => [ptr, 0, 0, 0, 0, 0, 0]
Copy link
Contributor

@hackaugusto hackaugusto Apr 30, 2024

Choose a reason for hiding this comment

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

I think we have issues with the stack handling in this file.

Procedures inside the asm/kernels folder are what form the kernel interface. This means the procedures in this file are sycalled (create_note is called here ). Because this procedure is used via a syscall, there are exactly 16 elements in the stack as inputs, and there must be exactly 16 elements in the stack as outputs.

When entering this procedure, the stack looks like this:

[tag, aux, note_type, RECIPIENT, x, X, X]

Where x, X, X are 9 elements which may be important for the caller, so we don't want to lose them, or change their position.

Without padding the will look like this after the exec.tx::create_note

[ptr, x, X, X, 0, 0, ZERO]

The 0, 0, ZERO at the end are automatically added by the VM, because the stack always has a minimum of 16 elements. Note that the position of the x, X, X changed, that is what we want to prevent. One solution is just to move the elements back to its original position, something like:

movup.10 movup.11
# => [0, 0, ptr, x, X, X, ZERO]

swapw.2 swapw.3
# => [ZERO, X, 0, 0, ptr, x, X]

swapw swapw.2
# => [0, 0, ptr, x, ZERO, X, X]

movup.3 swap.7
# => [0, 0, ptr, ZERO, x, X, X]

movup.2
# => [ptr, 0, 0, ZERO, x, X, X]

Or, another alternative is to pad the with 0, 0, ZERO before doing exec.tx::create_note, that would be something like:

exec.authenticate_account_origin
# => [tag, aux, note_type, RECIPIENT, x, X, X]

push.0 padw
# => [ZERO, 0, tag, aux, note_type, RECIPIENT, x, X, X]

swapw.2
# => [0, tag, aux, note_type, RECIPIENT, ZERO, x, X, X]

movdn.7 push.0 movdn.7
# => [tag, aux, note_type, RECIPIENT, 0, 0, ZERO, x, X, X]

So after the exec.tx::create_note the stack will look like:

# => [ptr, 0, 0, ZERO, x, X, X]

I guess we don't have any tests that call create_note and check the stack elements that would be x, X, X, like we talked here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the in-depth explanation.

I changed the asm/api.masm create_note procedure now to

export.create_note
    # authenticate that the procedure invocation originates from the account context
    exec.authenticate_account_origin
    # => [tag, aux, note_type, RECIPIENT]

    exec.tx::create_note
    # => [ptr]

    # prepare the stack for return
    movupw.3 movup.15 movup.15 movup.6 
    # => [ptr, 0, 0, 0, 0, 0, 0]
end

This should do the job.

When entering the procedure, we have

[tag, aux, note_type, RECIPIENT, x, X, X]

after exec.tx::create_note we should have (without padding)

[ptr, x, X, X, 0, 0, ZERO]

so we can do

movupw.3 movup.15 movup.15 movup.6 

to reach

[ptr, 0, 0, ZERO, x, X, X]

Copy link
Contributor

@hackaugusto hackaugusto May 2, 2024

Choose a reason for hiding this comment

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

I think we should document the changes to the stack in the source code too. It really helps to understand the instructions movupw.3 movup.15 movup.15 movup.6.

# => [ptr, x, X, X, 0, 0, ZERO]

movupw.3
# => [ZERO, ptr, x, X, X, 0, 0]

movup.15 movup.15
# => [0, 0, ZERO, ptr, x, X, X]

movup.6 
# => [ptr, 0, 0, ZERO, x, X, X]

Really neat solution btw.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added some comments

Really neat solution btw

thanks, ser

Comment on lines 553 to 555
# prepare stack for return
#padw movup.4
# => [ptr, 0, 0, 0, 0]
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same here, solved by

movupw.3 movup.4

Comment on lines 64 to +66
# check that amount =< max_supply - total_issuance, fails if otherwise
dup.1 gte assert.err=ERR_BASIC_FUNGIBLE_MAX_SUPPLY_OVERFLOW
# => [asset, tag, note_type, RECIPIENT, ...]
# => [asset, tag, aux, note_type, RECIPIENT, ...]
Copy link
Contributor

Choose a reason for hiding this comment

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

Not introduced by you, but this logic seems duplicated with the exec.faucet::mint below:

# prepare stack to ensure that minting the asset will not exceed the maximum
dup.7 dup exec.asset::get_fungible_asset_max_amount dup.3
# => [total_issuance, max_allowed_issuance, amount, amount, TOTAL_ISSUANCE, ASSET]
# compute difference to ensure that the total issuance will not exceed the maximum
sub lte assert.err=ERR_FAUCET_ISSUANCE_OVERFLOW
# => [amount, TOTAL_ISSUANCE, ASSET]

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

indeed, we should be able to delete that line here. Don't you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah no, I think those are two different checks.

  • The first check in the contract, checks against the max_supply, set by the user at account creation.
  • The second check, in asset.masm checks against the total max number of any asset const.FUNGIBLE_ASSET_MAX_AMOUNT=9223372036854775807

You are right, that in theory, the first check, should ensure that the max supply provided at slot 1 in the faucet is smaller than 9223372036854775807, which is checked at account creation. But an additional check doesn't hurt for faucets.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good 👍 . Lets maybe add a comment explaining the difference?

@Dominik1999 Dominik1999 requested a review from hackaugusto May 2, 2024 11:51
// OUTPUT NOTE BUILDER
// ================================================================================================
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct OutputNoteData {
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean? Why do you want to use a map here?

#!
#! ptr is the pointer to the memory address at which the note is stored.
#! ASSET can be a fungible or non-fungible asset.
export.add_asset_to_note
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a question for @bobbinth . I don't know why create_note doesn't call remove_asset. It seems the user is expected to manually handle these details.

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you! I left some comments with improvement suggestions inline. One thing I didn't really get deep into is the handling of the padding in various functions. I'll do a more thorough review of these on the next pass.

Comment on lines 35 to 37
#! Distributes freshly minted fungible assets to the provided recipient.
#! Inputs: [amount, tag, note_type, RECIPIENT]
#! Inputs: [amount, tag, aux, note_type, RECIPIENT]
#! Outputs: [note_ptr, 0, 0, 0, 0, 0, 0, 0, 0, ...]
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure we need to change the public interface of this file (we could pass 0 for aux field internally).


# check that amount =< max_supply - total_issuance, fails if otherwise
dup.1 gte assert.err=ERR_BASIC_FUNGIBLE_MAX_SUPPLY_OVERFLOW
# => [asset, tag, note_type, RECIPIENT, ...]
# => [asset, tag, aux, note_type, RECIPIENT, ...]
Copy link
Contributor

Choose a reason for hiding this comment

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

Should asset here be actually amount?

Comment on lines +89 to +90
exec.tx::move_asset_to_note
# => [note_ptr, ZERO, ...]
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the returned stack here correct? I think we usually try to make sure that the number of elements on the stack doesn't change (i.e., should this not be [note_ptr, ZERO, ZERO]?

Comment on lines 175 to +178
#! Creates a new note and returns a pointer to the memory address at which the note is stored.
#!
#! Inputs: [ASSET, tag, note_type, RECIPIENT]
#! Outputs: [ptr, 0, 0, 0, 0, 0, 0, 0, 0]
#! Inputs: [tag, aux, note_type, RECIPIENT]
#! Outputs: [ptr]
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the reason for removing zeros from the returned stack? Was it a mistake that we had them here before?

#! c, If there is no identical ASSET, the procedure returns the asset_ptr of the next
#! available memory location.
#!
#! Inputs: [ASSET, ptr, num_of_assets]
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I would probably rename ptr to note_ptr (here and in other places).

Comment on lines +235 to +236
#! ASSET can be a fungible or non-fungible asset.
export.move_asset_to_note
Copy link
Contributor

Choose a reason for hiding this comment

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

Now seeing how this works, I think add_asset_to_note would have been the correct name.

Comment on lines +238 to +252
movdn.4 exec.asset::validate_asset
# => [ASSET, ptr]

movdn.8 swapw padw swapw movup.12
# => [note_ptr, RECIPIENT, 0, 0, 0, 0, 0, 0, 0, 0]
# check if the note has at least one ASSET already
dup.4 exec.memory::get_created_note_num_assets dup movdn.6 neq.0
# => [num_of_assets>0?, ASSET, ptr, num_of_assets]

if.true
exec.check_if_asset_already_exists
# => [asset_ptr, ASSET'', ptr, num_of_assets]
else
# There is no asset in the note yet, so we add the asset at the first position
dup.4 exec.memory::get_created_note_asset_data_ptr
# => [asset_ptr, ASSET, ptr, num_of_assets]
end
Copy link
Contributor

Choose a reason for hiding this comment

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

I would have probably taken a slightly different approach here:

  • First, we could validate the asset (as you have now) and determine its type (fungible or non-fungible).
  • Then, for non-fungible assets, I'd create a helper procedure - e.g., add_non_fungible_asset_to_note. In this function, we could use simple eqw comparisons in a loop, and then, at the end of the loop, if nothing failed, add the asset to the note. So, the function would be relatively simple.
  • For fungible assets, I'd create a separate helper procedure - e.g., add_fungible_asset_to_note. I'd also create two other helper procedures:
    • Something like asset::get_issuer which would return the faucet ID that issued the asset.
    • Something like asset::combine_fungible_assets which would combine two fungible assets that have the same origin.
    • I would then use these functions similar to how you have now.

While this will probably result in a bit more code - I think the code would be more encapsulated and easier to follow/maintain (e.g., if the structure of assets changes, we would only need to update the relevant helper functions).

@Dominik1999
Copy link
Collaborator Author

Closing this in favor of #686 and #674

@Dominik1999 Dominik1999 deleted the dominik_feat_notes_without_assets branch May 10, 2024 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants