-
Notifications
You must be signed in to change notification settings - Fork 122
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
Account / network precondition RFC #179
Comments
Thanks Gregor.
Given they're both state, how about this?
And we landed on
Alternatively for naming, would it be possible to do something like Preference for the first one b/c, although it feels long, it provides hierarchy and intellisense code suggestions would guide them to know what options are available at each depth of the hierarchy as they type out |
Thanks for the feedback @jasongitmail! My argument for Currently, this consistency is achieved by autogenerating the TS types from OCaml code. Here is the autogenerated That doesn't mean that we can never add any shortcuts / alternate ways to access things in snarkyjs. But I'm against naming tweaks like |
This is great! I agree with both @mitschabaude and @jasongitmail . Specifically, we should use the a preconditions namespace with account and network subfields and change the json, graphql, the spec, and the ocaml structure to match it! While choosing names is subjective, I think there are good arguments for using the precondition namespace and referring to the protocol ones as I can try and knock out these name changes today so it hopefully won’t slow any work down. |
Additionally, I think we need to be careful with the automatic constraint; for example, this one is would make contract interaction trivially Denial-of-service-able via MEV (front-run the transaction by sending dust to the account). A safer constraint to insert would be a |
Nice @bkase, yeah I'm perfectly fine with name changes when they happen across the project! 👌🏻 Regarding the other discussion -- should we have Personally, I'm a huge fan of separating data and functions and using plain JS objects for data.. so that an The benefit IMHO is that this makes creating these objects much nicer: let balance = { lower: UInt64.from(0), upper: UInt64.from(100e9) };
let accountPrecondition: AccountPrecondition = {
balance,
nonce,
receiptChainHash, // we have autocomplete for the property names!
delegate, // order of properties doesn't matter!
state,
sequenceState,
provedState
}; Typescript feedback is wonderful when passing objects like these around. If, on the other hand, accountPrecondition and balance have to be a custom classes with some methods on it, we are faced with less purity / more boilerplate when creating them, and it gets more error-prone, and all the classes have to be imported: let balance = new Interval(UInt64.from(0), UInt64.from(100e9)); // have to import `Interval`to do this :(
let accountPrecondition = new AccountPrecondition( // have to import `AccountPrecondition` to do this :(
balance,
nonce, // no nice autocomplete for the names here :(
delegate, // wait.. which came first again? delegate or receiptChainHash? :(
receiptChainHash,
state,
sequenceState,
provedState
); Ok, so that's my argument for the |
Finally, I think even advanced APIs should be discoverable via intellisense . I agree with you that it’s not ideal from a code organizational standpoint, but it’s one of those things where we should always to optimize for dev usability. In this case, discoverability trumps the |
Generally I'm in favour of the original proposal, with the proposed tweaks. One note:
Ideally we should be tracking the new state of the accounts part-way through transactions, so that updates from prior parties will be successful. I would expect that we should construct e.g.
if both |
I guess one other thing worth calling out: it would be nice if let balance = this.currentAccount.balance; /* has type WeakBalance */;
let balance_value = balance.value(); /* has type Balance, adds equality precondition */
balance.assertLt(foo); /* adds/refines upper bound */
balance.assertGt(bat); /* adds/refines lower bound */
this.currentAccount.balance = this.currentAccount.balance.add(1); /* doesn't add precondition, only sets delta */ (assuming this is practical to implement). This removes a potential foot-gun where users are likely to add preconditions where none were necessary or intended. Bonus points if anything that would accept a |
That's very interesting @mrmr1993! I guess this is only possible for balance, right - because that's the only field where we send the delta in the transaction and not the full new value? I agree it would be cool, but it's a bit involved and the impact not huge because it's only for balance -- so I'll save it for later as an enhancement: #205 I do think all of what you wrote is feasible |
`account_precondition` and `protocol_state_precondition` are now colocated under a `preconditions` umbrella called: `account` and `network` respectively ala the comments in o1-labs/o1js#179 At the moment, the `protocol_state_precondition` vestige is left in the code in many places. Assuming this PR is accepted, I'll open a cleanup issue to go through and rename those variables and modules throughout the codebase. As these names won't ever see the light of day from a user's perspective (neither via GraphQL nor JSON nor SnarkyJS etc) it's benign to leave those in for now.
SnarkyJS side of MinaProtocol/mina#11096 addressing the naming consistency part of #179
This sounds like giving the developer a false sense of security. An invisible precondition that will cause a transaction to fail if balance has changed (or network block height, epoch, account nonce, etc..) when all the developer means to have asserted is that the balance was less than 100 is way more confusing imo than figuring out that you need to call
This is more compelling to me. The ergonomics of making a trade at the current price +- slippage, or submitting a transaction only valid in the current epoch are way nicer than getting explicit user input. |
@qcomps That's an interesting take. But maybe the example was not ideal to make the case for an invisible precondition (I chose it to make the case for also having explicit preconditions). Let's imagine that we would expose the current balance, but not add a precondition automatically, so we would just give the user an unconstrained variable which happens to have the correct value in it. This feels like a footgun as well - how many users would understand that this variable can be anything and still make the proof succeed? If users don't do the right thing to add a precondition themselves, their smart contract could become open to attack. To me this seems worse than having a few transactions fail. So, to me the only alternative to having an easy way to access current state, and adding strict preconditions by default, is to make a less ergonomic API that makes it very obvious that you get something that's unconstrained. An example for such a less ergonomic API that actually already exists would be the following: we just have a method Would you prefer this latter way of doing it? |
Great discsussion!
@qcomps +1 from a dev experience perspective
@mitschabaude Also really important. Let's keep discussing this. Maybe the compromise position to use the magic precondition only for asserting an exact balance, to prevent any foot guns and default to safety. Then others are manual and we document this as an exception. Are there any other footguns where a default precondition could be useful for safety @mitschabaude ? |
I definitely agree that malicious behavior succeeding is worse than unintentionally wrong behavior failing. What is an example of worst case though? If I write Regardless, the same people who would shoot themselves in the foot with this will also not read through this thread and will just see transactions failing. If possible it would be preferable to fail this before deploy. Maybe
Would this actually constrain account balance or just some input UInt that ought to be account balance? I definitely prefer being able to "use" account.balance... I guess the main thing I'd prefer is to fail a lot earlier if used improperly, rather than secretly adding something the dev didn't write. |
It wouldn't constrain the account balance. It's the behavior you were arguing for, that we shouldn't constrain the balance automatically, it just makes it more obvious that it's not doing so.
The issue is that
Ok, say you write const tenPercentOfMyBalance = this.account.balance.div(10);
transfer(tenPercentOfMyBalance, caller); Then you clearly want to transfer 10% of your balance. However, if IMO, if we give users a field that says "this.account.balance", we owe them that it's actually constrained to their balance. By the way, just to make this explicit, balance is just an example in this whole thread. The same discussion applies to other account fields like state, nonce, sequenceState, provedState etc, and protocol state fields like blockchainLength, timestamp, totalCurrency etc. We already have a magical API for on-chain state. We allow users to write let x = this.x.get();
this.x.set(x.add(1)); This should mean the following: It sets your new state to +1 the old state. If
This is great feedback - errors instead of magic. I've been thinking about a new proposal which moves in that direction. Will write more soon |
Precondition RFC, version 2In light of the discussion so far, I propose an alternate API, which forces users to add preconditions themselves instead of doing it secretly. The new proposal also does away with magic property access which secretly runs logic, and instead strives to be boring and explicit, while being also fairly simple and discoverable. We consolidate account / network preconditions under a single field, let myBalance = this.account.balance.get();
// should be equal to my actual balance
this.account.balance.assertEquals(myBalance);
// ... doing arbitrary computations with the balance
let chainLength = this.network.blockchainLength.get();
// should be not more than the current length + 5
this.network.blockchainLength.assertBetween(chainLength, chainLength.add(5));
To make this not a footgun, we throw an error if the user doesn't add any explicit precondition on the field. The error is thrown when compiling, proving, or running the smart contract method in any other way. Example: @method payout(caller: PublicKey) {
let balance = this.account.balance.get();
this.transfer(balance.div(10), caller); // tentative API to send money from the zkapp account
}
// ...
MyContract.compile(zkappAddress); // throws an error! Running this would throw:
The developer would hopefully read the error message and modify their code like this .. which would fix the error, and teach them about preconditions at the same time. @method payout(caller: PublicKey) {
let balance = this.account.balance.get();
this.account.balance.assertEquals(balance);
this.transfer(balance.div(10), caller); // tentative API to send money from the zkapp account
} Closing thoughtsDoing away with the magic property access on Note that I also changed I think now that @mrmr1993's concern, about not constraining the balance when it doesn't have to be, is sufficiently addressed by having very visible methods like |
As for me, I quite like version 2! |
Love it. Intuitive API, errors catch footguns, and no unexpected magic. This is great.
+1 |
This is great, I love this! |
This is nice -- thanks for writing this up Gregor! |
Precondition RFC
If users want to add preconditions on the account or protocol state, these will most often relate to the current on-chain state of these fields. A very intuitive model is to let users just "use" the current state -- for example, let them access the "current account balance". The mental model is that this balance is a variable (not a constant!) that depends on the current chain. It can be implemented by fetching the current balance from the chain, using it in the prover as the value for the variable, and adding a precondition which fixes the balance to exactly this value. If we wouldn't add the precondition, then the balance would be unconstrained, which would be against the intuition that we prove a computation which uses the current balance as input.
Therefore, I think a good default is to just let users access the balance (and other fields) as the property of some object, and behind the scenes add the necessary precondition. We propose the following API to achieve that:
Note that to use this API intuitively, one doesn't need to understand the concept of a precondition at all. It's enough to be aware of the
this.currentAccount
property. The returnedbalance
is a plainUInt64
variable; the added precondition stays completely hidden. Similarly, we propose to have athis.currentProtocolState
property which exposes protocol state preconditions. On other parties, we would have correspondingparty.currentX
properties.However, advanced users may want to declare arbitrary preconditions. For example, in the above example, it makes sense to actually not use the current balance, but just to add a precondition directly. We propose the following API:
This works for any party, not just the
this
party. It mutatesthis.accountPrecondition.balance
, which is the precondition (represented as an object{lower: UInt64, upper: UInt64}
.The last example achieves the same as the first one -- it constrains the balance to be less than 100 MINA -- but without the fragility that the current balance needs to stay unchanged until the transaction is accepted. However, to use an API like this, you do have to understand the concept of a precondition. That's why it's OK to use more jargon ("precondition"), and make this a bit less discoverable (have to use
Party
), and let it not do any magic. Similar tothis.accountPrecondition
, we propose to exposethis.protocolStatePrecondition
, which would just be shortcuts to accessparty.body.accountPrecondition
andparty.body.protocolStatePrecondition
.In addition to
Party.assertBetween
, there is alsoParty.assertEquals
(already implemented), and we could likewise addParty.assertBelow
andParty.assertAbove
.Finally, you may want to reference the current state when declaring an arbitrary precondition. For example, possibly you want to restrict the nonce to an interval of the form
[currentNonce, currentNonce + tolerance]
. Or you may want to restrict it to be exactlycurrentNonce + 2
, because you already know that there'll be 2 earlier transactions which increment the same nonce. For this final use case, we propose the following API:This is the precisely same API as before, just making use of
this.currentAccount
. The logic would be that any explicitly set precondition would override any magically inserted precondition. So, in this case, nonce == currentNonce would be overriden to nonce == currentNonce + 2.TODO list
this.current*
API and, for other parties,party.current*
.this.*Precondition
andparty.*Precondition
.Party.assertBetween
,Party.assertEquals
The text was updated successfully, but these errors were encountered: