-
Notifications
You must be signed in to change notification settings - Fork 116
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
Remove children from AccountUpdate: RFC and Implementation #1402
Conversation
…mmer now who will bend over backwards just to use OO idioms?
Note: I plan to add a nicer API on top of |
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.
Lots of code in this change 🎉 Question: if one was to forget to call approve
on a generated AU, what happens? Is there an error that will show up when proving?
In a case like this:
let tree = AccountUpdateTree.from(payerUpdate);
tree.approve(receiverUpdate);
this.approve(tree);
If you forget to call tree.approve
or this.approve
, what happens?
let descendants: AccountUpdate[] = []; | ||
let callDepth = this.body.callDepth; | ||
let i = accountUpdates.findIndex((a) => a.id === this.id); | ||
assert(i !== -1, 'Account update not found in transaction'); |
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.
Do you think it would be helpful to print the ID of the AU that wasn't found? Not sure if it would actually be helpful for debugging 🤷
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.
Printing the label would probably help! I use labels all the time these days for debugging
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.
I'm leaving this out for now since it's definitely not an error I expect will be encountered - we can improve the message if someone hits this
Great question! For In the case of I was going for intuitive behaviour in the sense that approving one update explicitly overrides whatever we did with it automatically before that. So in the mentioned code example, the following happens at each step (see comments): @method depositUsingTree() {
let payerUpdate = AccountUpdate.createSigned(this.sender);
// now `payerUpdate` is a child of the `depositUsingTree()` zkapp AU
let receiverUpdate = AccountUpdate.create(this.address);
// now also `receiverUpdate` is a child of the zkapp
payerUpdate.send({ to: receiverUpdate, amount: UInt64.one });
// nothing happened here, send doesn't change the layout when we pass AUs
let tree = AccountUpdateTree.from(payerUpdate);
// a tree was created but nothing else happened. `payerUpdate` is still a child of the zkapp
tree.approve(receiverUpdate);
// `payerUpdate` is still a child of the zkapp, and `receiverUpdate` is disattached (but part of the tree)
// that's because `approve` overrides the previous attachment of `receiverUpdate`
// and the tree wasn't attached anywhere yet
this.approve(tree);
// this explicitly puts the tree, containing `payerUpdate` under the zkapp, without duplicating the existing `payerUpdate` at the same level
// so we now have `zkapp` parent of `payerUpdate` parent of `receiverUpdate`
} Note that the end result wouldn't change if we had created |
On top of #1384, polish up the system introduced there for keeping track of zkapp children, use it everywhere, and rip out the previous system.
This comes with a proposal to update or remove some existing public APIs:
RFC: Refactor representation of account update layout during transaction construction
Motivation: It was a mistake to store children on account updates. Doing that entangles account updates to the specific context of a transaction, when really they should be an atomic type that can be passed between different contexts. It's cleaner if a separate, higher-level structure stores the parent-child relationships.
Remove
AccountUpdate.children
andAccountUpdate.parent
AccountUpdate
.New APIs to hold the transaction layout
Since
AccountUpdate
no longer holds information about the transaction layout, we need new APIs as a replacement.AccountUpdateTree
andAccountUpdateForest
are the new public types to represent account updates together with their children:approve()
should accept these as input to help construct the tree below an update:approve()
, anAccountUpdateLayout
, to specify the layout of children to be witnessed. Removing it will get rid of some complex code, and it should be superseded by operation on types likeAccountUpdateForest
which represent a dynamic layout of childrenNew API to have a token contract approve the results of a zkApp call
Previously, an important pattern was to call a zkApp and after that take its account update off of
zkApp.self
, and pass that to a token contract to approve. I.e.This ended up creating a tree like the following:
I called this the "manual callback" flow. It was equivalent to passing an
Experiment.Callback
with the callee method to the token contract.callee.self
no longer contains the callee's children, so we need a new API to obtain them. Maybe something like this:Where
extractTree()
collects theAccountUpdateTree
of the callee and its children, and explicitly causes them from being disattached from their current location in the transaction (previously, we had a hack that disattached account updates when they are passed to another method).Remove redundant
Callback
APIEver since #428, I didn't like the
Experimental.Callback
API because it's redundant with, and less explicit than, the approach described above. I propose to remove it. Also, removeExperimental.createChildAccountUpdate
(redundant withAccountUpdate.create()
).This is pretty much orthogonal to the rest of this RFC and will go into a separate PR.
Internal changes
Internally, #1384 introduced a global
UnfinishedForest
structure which holds a zkApp's children during the run of a smart contract method, and is updated by operations likethis.send()
. After the zkApp method runs, theUnfinishedForest
is "finalized" to anAccountUpdateForest
, whose hash is compared with the public input.This PR pulls the trigger to actually use this system, instead of the old system based on
AccountUpdate.children
, to compute the public input.Similarly, a top-level
UnfinishedForest
is maintained duringMina.transaction()
, and finalized into a flat listAccountUpdate[]
with call-depths, which is the source of truth of the overall transaction contents.TODOs
UnfinishedForest
responsible for hashingfigure out reasons for the scary amount of constraint reductionschildren
.self
and passing it to a token contract - idea: have a method return anAccountUpdateTree
as the new canonical type for an update + its children, which are the result of a zkapp call