diff --git a/CHANGELOG.md b/CHANGELOG.md index e94808d788..97926c1d8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,13 +24,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `VerificationKey`, which is a `Struct` with auxiliary data, to pass verification keys to a `@method` - BREAKING CHANGE: Change names related to circuit types: `AsFieldsAndAux` -> `Provable`, `AsFieldElement` -> `ProvablePure`, `circuitValue` -> `provable` - BREAKING CHANGE: Change all `ofFields` and `ofBits` methods on circuit types to `fromFields` and `fromBits` -- `SmartContract.experimental.authorize` to authorize a tree of child account updates https://github.com/o1-labs/snarkyjs/pull/428 - - AccountUpdates are now valid `@method` arguments, and `authorize` is intended to be used on them when passed to a method - - Also replaces `Experimental.accountUpdateFromCallback` +- `SmartContract.experimental.authorize()` to authorize a tree of child account updates https://github.com/o1-labs/snarkyjs/pull/428 + - AccountUpdates are now valid `@method` arguments, and `authorize()` is intended to be used on them when passed to a method + - Also replaces `Experimental.accountUpdateFromCallback()` +- `Circuit.log()` to easily log Fields and other provable types inside a method, with the same API as `console.log()` +- `AccountUpdate.attachToTransaction()` for explicitly adding an account update to the current transaction. This replaces some previous behaviour where an account update got attached implicitly. ### Changed - BREAKING CHANGE: `tx.send()` is now asynchronous: old: `send(): TransactionId` new: `send(): Promise` and `tx.send()` now directly waits for the network response, as opposed to `tx.send().wait()` +- `Circuit.witness` can now be called outside circuits, where it will just directly return the callback result ### Deprecated diff --git a/src/examples/zkapps/dex/arbitrary_token_interaction.ts b/src/examples/zkapps/dex/arbitrary_token_interaction.ts new file mode 100644 index 0000000000..2fa2b26562 --- /dev/null +++ b/src/examples/zkapps/dex/arbitrary_token_interaction.ts @@ -0,0 +1,65 @@ +import { + isReady, + Mina, + AccountUpdate, + UInt64, + shutdown, + Token, +} from 'snarkyjs'; +import { TokenContract, addresses, keys, tokenIds } from './dex.js'; + +await isReady; +let doProofs = true; + +let Local = Mina.LocalBlockchain({ proofsEnabled: doProofs }); +Mina.setActiveInstance(Local); +let accountFee = Mina.accountCreationFee(); + +let [{ privateKey: userKey, publicKey: userAddress }] = Local.testAccounts; +let tx; + +console.log('-------------------------------------------------'); +console.log('TOKEN X ADDRESS\t', addresses.tokenX.toBase58()); +console.log('USER ADDRESS\t', userAddress.toBase58()); +console.log('-------------------------------------------------'); +console.log('TOKEN X ID\t', Token.Id.toBase58(tokenIds.X)); +console.log('-------------------------------------------------'); + +// compile & deploy all 5 zkApps +console.log('compile (token)...'); +await TokenContract.compile(); + +let tokenX = new TokenContract(addresses.tokenX); + +console.log('deploy & init token contracts...'); +tx = await Mina.transaction(userKey, () => { + // pay fees for creating 2 token contract accounts, and fund them so each can create 1 account themselves + let feePayerUpdate = AccountUpdate.createSigned(userKey); + feePayerUpdate.balance.subInPlace(accountFee.mul(1)); + tokenX.deploy(); +}); +await tx.prove(); +tx.sign([keys.tokenX]); +await tx.send(); + +console.log('arbitrary token minting...'); +tx = await Mina.transaction(userKey, () => { + // pay fees for creating user's token X account + AccountUpdate.createSigned(userKey).balance.subInPlace(accountFee.mul(1)); + // 😈😈😈 mint any number of tokens to our account 😈😈😈 + let tokenContract = new TokenContract(addresses.tokenX); + tokenContract.experimental.token.mint({ + address: userAddress, + amount: UInt64.from(1e18), + }); +}); +await tx.prove(); +console.log(tx.toPretty()); +await tx.send(); + +console.log( + 'User tokens: ', + Mina.getBalance(userAddress, tokenIds.X).value.toBigInt() +); + +shutdown(); diff --git a/src/examples/zkapps/dex/dex.ts b/src/examples/zkapps/dex/dex.ts index f8119ca0c7..f5db60e122 100644 --- a/src/examples/zkapps/dex/dex.ts +++ b/src/examples/zkapps/dex/dex.ts @@ -3,7 +3,6 @@ import { Bool, Circuit, DeployArgs, - Experimental, Field, Int64, isReady, @@ -18,15 +17,26 @@ import { UInt64, VerificationKey, Struct, + State, + state, } from 'snarkyjs'; export { Dex, DexTokenHolder, TokenContract, keys, addresses, tokenIds }; +class UInt64x2 extends Struct([UInt64, UInt64]) {} + class Dex extends SmartContract { // addresses of token contracts are constants tokenX = addresses.tokenX; tokenY = addresses.tokenY; + /** + * state which keeps track of total lqXY supply -- this is needed to calculate what to return when redeeming liquidity + * + * total supply is zero initially; it increases when supplying liquidity and decreases when redeeming it + */ + @state(UInt64) totalSupply = State(); + /** * Mint liquidity tokens in exchange for X and Y tokens * @param user caller address @@ -40,11 +50,7 @@ class Dex extends SmartContract { * * The transaction needs to be signed by the user's private key. */ - @method supplyLiquidityBase( - user: PublicKey, - dx: UInt64, - dy: UInt64 - ) /*: UInt64 */ { + @method supplyLiquidityBase(user: PublicKey, dx: UInt64, dy: UInt64): UInt64 { let tokenX = new TokenContract(this.tokenX); let tokenY = new TokenContract(this.tokenY); @@ -58,6 +64,7 @@ class Dex extends SmartContract { let isXZero = dexXBalance.equals(UInt64.zero); let xSafe = Circuit.if(isXZero, UInt64.one, dexXBalance); + // FIXME // Error: Constraint unsatisfied (unreduced): Equal 0 1 // dy.equals(dx.mul(dexYBalance).div(xSafe)).or(isXZero).assertTrue(); @@ -68,7 +75,12 @@ class Dex extends SmartContract { // // => maintains ratio x/l, y/l let dl = dy.add(dx); this.experimental.token.mint({ address: user, amount: dl }); - // return dl; + + // update l supply + let l = this.totalSupply.get(); + this.totalSupply.assertEquals(l); + this.totalSupply.set(l.add(dl)); + return dl; } /** @@ -83,7 +95,7 @@ class Dex extends SmartContract { * * The transaction needs to be signed by the user's private key. */ - supplyLiquidity(user: PublicKey, dx: UInt64) /*: UInt64 */ { + supplyLiquidity(user: PublicKey, dx: UInt64): UInt64 { // calculate dy outside circuit let x = Account(this.address, Token.getId(this.tokenX)).balance.get(); let y = Account(this.address, Token.getId(this.tokenY)).balance.get(); @@ -93,8 +105,7 @@ class Dex extends SmartContract { ); } let dy = dx.mul(y).div(x); - this.supplyLiquidityBase(user, dx, dy); - // return this.supplyLiquidityBase(user, dx, dy); + return this.supplyLiquidityBase(user, dx, dy); } /** @@ -105,12 +116,13 @@ class Dex extends SmartContract { * * The transaction needs to be signed by the user's private key. */ - @method redeemLiquidity(user: PublicKey, dl: UInt64): UInt64x2 { + @method redeemLiquidity(user: PublicKey, dl: UInt64) { // call the token X holder inside a token X-authorized callback let tokenX = new TokenContract(this.tokenX); let dexX = new DexTokenHolder(this.address, tokenX.experimental.token.id); let dxdy = dexX.redeemLiquidity(user, dl, this.tokenY); - tokenX.authorizeUpdate(dexX.self); + let dx = dxdy[0]; + tokenX.authorizeUpdateAndSend(dexX.self, user, dx); return dxdy; } @@ -126,7 +138,7 @@ class Dex extends SmartContract { let tokenY = new TokenContract(this.tokenY); let dexY = new DexTokenHolder(this.address, tokenY.experimental.token.id); let dy = dexY.swap(user, dx, this.tokenX); - tokenY.authorizeUpdate(dexY.self); + tokenY.authorizeUpdateAndSend(dexY.self, user, dy); return dy; } @@ -142,12 +154,30 @@ class Dex extends SmartContract { let tokenX = new TokenContract(this.tokenX); let dexX = new DexTokenHolder(this.address, tokenX.experimental.token.id); let dx = dexX.swap(user, dy, this.tokenY); - tokenX.authorizeUpdate(dexX.self); + tokenX.authorizeUpdateAndSend(dexX.self, user, dx); return dx; } -} -class UInt64x2 extends Struct([UInt64, UInt64]) {} + /** + * helper method to authorize burning of user's liquidity. + * this just burns user tokens, so there is no incentive to call this directly. + * instead, the dex token holders call this and in turn pay back tokens. + * + * @param user caller address + * @param dl input amount of lq tokens + * @returns total supply of lq tokens _before_ burning dl, so that caller can calculate how much dx / dx to returns + * + * The transaction needs to be signed by the user's private key. + */ + @method burnLiquidity(user: PublicKey, dl: UInt64): UInt64 { + // this makes sure there is enough l to burn (user balance stays >= 0), so l stays >= 0, so l was >0 before + this.experimental.token.burn({ address: user, amount: dl }); + let l = this.totalSupply.get(); + this.totalSupply.assertEquals(l); + this.totalSupply.set(l.sub(dl)); + return l; + } +} class DexTokenHolder extends SmartContract { // simpler circuit for redeeming liquidity -- direct trade between our token and lq token @@ -155,20 +185,20 @@ class DexTokenHolder extends SmartContract { // see the more complicated method `redeemLiquidity` below which gives back both tokens, by calling this method, // for the other token, in a callback @method redeemLiquidityPartial(user: PublicKey, dl: UInt64): UInt64x2 { - let dex = AccountUpdate.create(this.address); - let l = dex.account.balance.get(); - dex.account.balance.assertEquals(l); - - // user sends dl to dex - let idlXY = Token.getId(this.address); - let userUpdate = AccountUpdate.create(user, idlXY); - userUpdate.balance.subInPlace(dl); + // user burns dl, authorized by the Dex main contract + let dex = new Dex(addresses.dex); + let l = dex.burnLiquidity(user, dl); // in return, we give dy back let y = this.account.balance.get(); this.account.balance.assertEquals(y); + // we can safely divide by l here because the Dex contract logic wouldn't allow burnLiquidity if not l>0 let dy = y.mul(dl).div(l); - this.send({ to: user, amount: dy }); + // just subtract the balance, user gets their part one level higher + this.balance.subInPlace(dy); + + // this can't be a delegate call, or it won't be authorized by the token owner + this.self.isDelegateCall = Bool(false); // return l, dy so callers don't have to walk their child account updates to get it return [l, dy]; @@ -186,13 +216,17 @@ class DexTokenHolder extends SmartContract { let result = dexY.redeemLiquidityPartial(user, dl); let l = result[0]; let dy = result[1]; - tokenY.authorizeUpdate(dexY.self); + tokenY.authorizeUpdateAndSend(dexY.self, user, dy); // in return for dl, we give back dx, the X token part let x = this.account.balance.get(); this.account.balance.assertEquals(x); let dx = x.mul(dl).div(l); - this.send({ to: user, amount: dx }); + // just subtract the balance, user gets their part one level higher + this.balance.subInPlace(dx); + + // this can't be a delegate call, or it won't be authorized by the token owner + this.self.isDelegateCall = Bool(false); return [dx, dy]; } @@ -206,20 +240,18 @@ class DexTokenHolder extends SmartContract { // we're writing this as if our token == y and other token == x let dx = otherTokenAmount; let tokenX = new TokenContract(otherTokenAddress); - // send x from user to us (i.e., to the same address as this but with the other token) - let dexX = tokenX.experimental.token.send({ - from: user, - to: this.address, - amount: dx, - }); // get balances - let x = dexX.account.balance.get(); - dexX.account.balance.assertEquals(x); + let x = tokenX.getBalance(this.address); let y = this.account.balance.get(); this.account.balance.assertEquals(y); + // send x from user to us (i.e., to the same address as this but with the other token) + tokenX.transfer(user, this.address, dx); // compute and send dy let dy = y.mul(dx).div(x.add(dx)); - this.send({ to: user, amount: dy }); + // just subtract dy balance and let adding balance be handled one level higher + this.balance.subInPlace(dy); + // not be a delegate call + this.self.isDelegateCall = Bool(false); return dy; } } @@ -235,7 +267,8 @@ class TokenContract extends SmartContract { super.deploy(args); this.setPermissions({ ...Permissions.default(), - send: Permissions.proofOrSignature(), + send: Permissions.proof(), + receive: Permissions.proof(), }); } @method init() { @@ -265,21 +298,19 @@ class TokenContract extends SmartContract { zkapp.sign(); } - // let a zkapp do whatever it wants, as long as the token supply stays constant - @method authorizeUpdate(zkappUpdate: AccountUpdate) { - // adopt this account update as a child, allowing a certain layout for its own children - // we allow 10 child account updates, in a left-biased tree of width 3 - let { NoChildren, StaticChildren } = AccountUpdate.Layout; - let layout = StaticChildren( - StaticChildren(StaticChildren(3), NoChildren, NoChildren), - NoChildren, - NoChildren - ); - this.experimental.authorize(zkappUpdate, layout); - - // walk account updates to see if balances for this token cancel - let balance = balanceSum(zkappUpdate, this.experimental.token.id); - balance.assertEquals(Int64.zero); + // let a zkapp send tokens to someone, provided the token supply stays constant + @method authorizeUpdateAndSend( + zkappUpdate: AccountUpdate, + to: PublicKey, + amount: UInt64 + ) { + this.experimental.authorize(zkappUpdate); + + // see if balance change cancels the amount sent + let balanceChange = Int64.fromObject(zkappUpdate.body.balanceChange); + balanceChange.assertEquals(Int64.from(amount).neg()); + // add same amount of tokens to the receiving address + this.experimental.token.mint({ address: to, amount }); } @method transfer(from: PublicKey, to: PublicKey, value: UInt64) { @@ -315,7 +346,7 @@ function balanceSum(accountUpdate: AccountUpdate, tokenId: Field) { let myBalance = Int64.fromObject(accountUpdate.body.balanceChange); let balance = Circuit.if(myTokenId.equals(tokenId), myBalance, Int64.zero); for (let child of accountUpdate.children.accountUpdates) { - balance.add(balanceSum(child, tokenId)); + balance = balance.add(balanceSum(child, tokenId)); } return balance; } @@ -331,6 +362,7 @@ function randomAccounts( 'EKFE2UKugtoVMnGTxTakF2M9wwL9sp4zrxSLhuzSn32ZAYuiKh5R', 'EKEn2s1jSNADuC8CmvCQP5CYMSSoNtx5o65H7Lahqkqp2AVdsd12', 'EKE21kTAb37bekHbLvQpz2kvDYeKG4hB21x8VTQCbhy6m2BjFuxA', + 'EKF9JA8WiEAk7o3ENnvgMHg5XKwgQfyMowNFFrEDCevoSozSgLTn', ]; let keys = Object.fromEntries( diff --git a/src/examples/zkapps/dex/run.ts b/src/examples/zkapps/dex/run.ts index ae29f19fdb..8fb6151f6d 100644 --- a/src/examples/zkapps/dex/run.ts +++ b/src/examples/zkapps/dex/run.ts @@ -1,4 +1,11 @@ -import { isReady, Mina, AccountUpdate, UInt64, shutdown } from 'snarkyjs'; +import { + isReady, + Mina, + AccountUpdate, + UInt64, + shutdown, + Token, +} from 'snarkyjs'; import { Dex, DexTokenHolder, @@ -9,9 +16,9 @@ import { } from './dex.js'; await isReady; -let doProofs = true; +let doProofs = false; -let Local = Mina.LocalBlockchain(); +let Local = Mina.LocalBlockchain({ proofsEnabled: doProofs }); Mina.setActiveInstance(Local); let accountFee = Mina.accountCreationFee(); @@ -25,26 +32,26 @@ console.log('TOKEN Y ADDRESS\t', addresses.tokenY.toBase58()); console.log('DEX ADDRESS\t', addresses.dex.toBase58()); console.log('USER ADDRESS\t', addresses.user.toBase58()); console.log('-------------------------------------------------'); +console.log('TOKEN X ID\t', Token.Id.toBase58(tokenIds.X)); +console.log('TOKEN Y ID\t', Token.Id.toBase58(tokenIds.Y)); +console.log('-------------------------------------------------'); // analyze methods for quick error feedback TokenContract.analyzeMethods(); DexTokenHolder.analyzeMethods(); Dex.analyzeMethods(); -if (doProofs) { - // compile & deploy all 5 zkApps - console.log('compile (token)...'); - await TokenContract.compile(); - console.log('compile (dex token holder)...'); - await DexTokenHolder.compile(); - console.log('compile (dex main contract)...'); - await Dex.compile(); -} +// compile & deploy all 5 zkApps +console.log('compile (token)...'); +await TokenContract.compile(); +console.log('compile (dex token holder)...'); +await DexTokenHolder.compile(); +console.log('compile (dex main contract)...'); +await Dex.compile(); + let tokenX = new TokenContract(addresses.tokenX); let tokenY = new TokenContract(addresses.tokenY); let dex = new Dex(addresses.dex); -let dexX = new DexTokenHolder(addresses.dex, tokenIds.X); -let dexY = new DexTokenHolder(addresses.dex, tokenIds.Y); console.log('deploy & init token contracts...'); tx = await Mina.transaction({ feePayerKey }, () => { @@ -61,7 +68,6 @@ tx = await Mina.transaction({ feePayerKey }, () => { }); await tx.prove(); tx.sign([keys.tokenX, keys.tokenY]); -console.log(tx.toPretty()); await tx.send(); console.log( @@ -81,10 +87,9 @@ tx = await Mina.transaction(feePayerKey, () => { }); await tx.prove(); tx.sign([keys.dex]); -console.log(tx.toPretty()); await tx.send(); -console.log('transfer X and Y tokens to a user...'); +console.log('transfer tokens to user'); tx = await Mina.transaction({ feePayerKey, fee: accountFee.mul(1) }, () => { AccountUpdate.createSigned(feePayerKey).balance.subInPlace( Mina.accountCreationFee().mul(2) @@ -106,6 +111,7 @@ console.log( console.log('user supply liquidity -- base'); tx = await Mina.transaction({ feePayerKey, fee: accountFee.mul(1) }, () => { + // needed because the user account for the liquidity token is created AccountUpdate.createSigned(feePayerKey).balance.subInPlace( Mina.accountCreationFee().mul(1) ); @@ -113,8 +119,7 @@ tx = await Mina.transaction({ feePayerKey, fee: accountFee.mul(1) }, () => { }); await tx.prove(); - -tx.sign([keys.dex, keys.user, keys.tokenX]); +tx.sign([keys.user]); await tx.send(); console.log( @@ -133,8 +138,26 @@ tx = await Mina.transaction({ feePayerKey, fee: accountFee.mul(1) }, () => { }); await tx.prove(); +tx.sign([keys.user]); +await tx.send(); -tx.sign([keys.dex, keys.user, keys.tokenX]); +console.log( + 'DEX liquidity (X, Y): ', + Mina.getBalance(addresses.dex, tokenIds.X).value.toBigInt(), + Mina.getBalance(addresses.dex, tokenIds.Y).value.toBigInt() +); + +console.log( + 'user DEX tokens: ', + Mina.getBalance(addresses.user, tokenIds.lqXY).value.toBigInt() +); + +console.log('user redeem liquidity'); +tx = await Mina.transaction({ feePayerKey, fee: accountFee.mul(1) }, () => { + dex.redeemLiquidity(addresses.user, UInt64.from(100)); +}); +await tx.prove(); +tx.sign([keys.user]); await tx.send(); console.log( @@ -147,5 +170,26 @@ console.log( 'user DEX tokens: ', Mina.getBalance(addresses.user, tokenIds.lqXY).value.toBigInt() ); +console.log( + 'User tokens X: ', + Mina.getBalance(addresses.user, tokenIds.X).value.toBigInt(), + '\nUser tokens Y: ', + Mina.getBalance(addresses.user, tokenIds.Y).value.toBigInt() +); + +console.log('swap 10 X for Y'); +tx = await Mina.transaction({ feePayerKey, fee: accountFee.mul(1) }, () => { + dex.swapX(addresses.user, UInt64.from(10)); +}); +await tx.prove(); +tx.sign([keys.user]); +await tx.send(); + +console.log( + 'User tokens X: ', + Mina.getBalance(addresses.user, tokenIds.X).value.toBigInt(), + '\nUser tokens Y: ', + Mina.getBalance(addresses.user, tokenIds.Y).value.toBigInt() +); shutdown(); diff --git a/src/lib/account_update.ts b/src/lib/account_update.ts index f9ea36f2d5..eb0a6b0756 100644 --- a/src/lib/account_update.ts +++ b/src/lib/account_update.ts @@ -4,7 +4,6 @@ import { cloneCircuitValue, memoizationContext, memoizeWitness, - witness, } from './circuit_value.js'; import { Field, Bool, Ledger, Circuit, Pickles, Provable } from '../snarky.js'; import { jsLayout, Types } from '../snarky/types.js'; @@ -678,9 +677,7 @@ class AccountUpdate implements Types.AccountUpdate { authorization, accountUpdate.isSelf ); - cloned.lazyAuthorization = cloneCircuitValue( - accountUpdate.lazyAuthorization - ); + cloned.lazyAuthorization = accountUpdate.lazyAuthorization; cloned.children.callsType = accountUpdate.children.callsType; cloned.children.accountUpdates = accountUpdate.children.accountUpdates.map( AccountUpdate.clone @@ -821,12 +818,13 @@ class AccountUpdate implements Types.AccountUpdate { ).add(amount); } - authorize(childUpdate: AccountUpdate, layout?: AccountUpdatesLayout) { + authorize( + childUpdate: AccountUpdate, + layout: AccountUpdatesLayout = AccountUpdate.Layout.NoDelegation + ) { makeChildAccountUpdate(this, childUpdate); this.isDelegateCall = Bool(false); - if (layout !== undefined) { - AccountUpdate.witnessChildren(childUpdate, layout, { skipCheck: true }); - } + AccountUpdate.witnessChildren(childUpdate, layout, { skipCheck: true }); } get balance() { @@ -1028,6 +1026,17 @@ class AccountUpdate implements Types.AccountUpdate { } return accountUpdate; } + static attachToTransaction(accountUpdate: AccountUpdate) { + if (smartContractContext.has()) { + smartContractContext.get().this.self.authorize(accountUpdate); + } else { + if (!Mina.currentTransaction.has()) return; + let updates = Mina.currentTransaction.get().accountUpdates; + if (!updates.find((update) => update.id === accountUpdate.id)) { + updates.push(accountUpdate); + } + } + } static createSigned(signer: PrivateKey) { let publicKey = signer.toPublicKey(); @@ -1092,7 +1101,7 @@ class AccountUpdate implements Types.AccountUpdate { callsType: { type: 'None' }, accountUpdates: [], }; - let lazyAuthorization = a && cloneCircuitValue(a.lazyAuthorization); + let lazyAuthorization = a && a.lazyAuthorization; if (a) { children.callsType = a.children.callsType; children.accountUpdates = a.children.accountUpdates.map( @@ -1151,6 +1160,11 @@ class AccountUpdate implements Types.AccountUpdate { accountUpdate.children.callsType = { type: 'Witness' }; return; } + if (childLayout === AccountUpdate.Layout.NoDelegation) { + accountUpdate.children.callsType = { type: 'Witness' }; + accountUpdate.isDelegateCall.assertFalse(); + return; + } let childArray: AccountUpdatesLayout[] = typeof childLayout === 'number' ? Array(childLayout).fill(AccountUpdate.Layout.NoChildren) @@ -1211,7 +1225,7 @@ class AccountUpdate implements Types.AccountUpdate { * ```ts * let { NoChildren, AnyChildren, StaticChildren } = AccounUpdate.Layout; * - * NoChildren // an account updates with no children + * NoChildren // an account update with no children * AnyChildren // an account update with arbitrary children * StaticChildren(NoChildren) // an account update with 1 child, which doesn't have children itself * StaticChildren(1) // shortcut for StaticChildren(NoChildren) @@ -1232,7 +1246,8 @@ class AccountUpdate implements Types.AccountUpdate { (...args: AccountUpdatesLayout[]): AccountUpdatesLayout; }, NoChildren: 0, - AnyChildren: null, + AnyChildren: 'AnyChildren' as const, + NoDelegation: 'NoDelegation' as const, }; toPretty() { @@ -1290,6 +1305,9 @@ class AccountUpdate implements Types.AccountUpdate { if (body.update?.permissions) { body.update.permissions = JSON.stringify(body.update.permissions) as any; } + if (body.update?.appState) { + body.update.appState = JSON.stringify(body.update.appState) as any; + } if ( jsonUpdate.authorization !== undefined || body.authorizationKind !== 'None_given' @@ -1305,7 +1323,11 @@ class AccountUpdate implements Types.AccountUpdate { } } -type AccountUpdatesLayout = number | null | AccountUpdatesLayout[]; +type AccountUpdatesLayout = + | number + | 'AnyChildren' + | 'NoDelegation' + | AccountUpdatesLayout[]; const CallForest = { // similar to Mina_base.ZkappCommand.Call_forest.to_account_updates_list @@ -1338,10 +1360,10 @@ const CallForest = { // compute hash outside the circuit if callsType is "Witness" // i.e., allowing accountUpdates with arbitrary children if (callsType.type === 'Witness') { - return witness(Field, () => CallForest.hashChildrenBase(update)); + return Circuit.witness(Field, () => CallForest.hashChildrenBase(update)); } let calls = CallForest.hashChildrenBase(update); - if (callsType.type === 'Equals') { + if (callsType.type === 'Equals' && inCheckedComputation()) { calls.assertEquals(callsType.value); } return calls; @@ -1461,6 +1483,12 @@ function makeChildAccountUpdate(parent: AccountUpdate, child: AccountUpdate) { if (i !== undefined && i !== -1) { topLevelUpdates!.splice(i, 1); } + } else { + let siblings = child.parent.children.accountUpdates; + let i = siblings?.findIndex((update) => update.id === child.id); + if (i !== undefined && i !== -1) { + siblings!.splice(i, 1); + } } child.parent = parent; } @@ -1629,14 +1657,14 @@ async function addMissingProofs( async function addProof(index: number, accountUpdate: AccountUpdate) { accountUpdate = AccountUpdate.clone(accountUpdate); - if (!proofsEnabled) { - Authorization.setProof(accountUpdate, Pickles.dummyBase64Proof()); + if (accountUpdate.lazyAuthorization?.kind !== 'lazy-proof') { return { accountUpdateProved: accountUpdate as AccountUpdateProved, proof: undefined, }; } - if (accountUpdate.lazyAuthorization?.kind !== 'lazy-proof') { + if (!proofsEnabled) { + Authorization.setProof(accountUpdate, Pickles.dummyBase64Proof()); return { accountUpdateProved: accountUpdate as AccountUpdateProved, proof: undefined, @@ -1670,7 +1698,16 @@ async function addMissingProofs( () => memoizationContext.runWithAsync( { memoized, currentIndex: 0, blindingValue }, - () => provers[i](publicInputFields, previousProofs) + async () => { + try { + return await provers[i](publicInputFields, previousProofs); + } catch (err) { + console.error( + `Error when proving ${ZkappClass.name}.${methodName}()` + ); + throw err; + } + } ) ); Authorization.setProof( diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index d921f49e09..6f599512b1 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -29,7 +29,6 @@ export { memoizeWitness, getBlindingValue, toConstant, - witness, InferCircuitValue, Provables, HashInput, @@ -928,7 +927,7 @@ Circuit.witness = function = Provable>( compute: () => T ) { let proverValue: T | undefined; - let fields = Circuit._witness(type, () => { + let createFields = () => { proverValue = compute(); let fields = type.toFields(proverValue); // TODO: enable this check @@ -941,7 +940,14 @@ Circuit.witness = function = Provable>( // ); // } return fields; - }); + }; + let ctx = snarkContext.get(); + let fields = + inCheckedComputation() && !ctx.inWitnessBlock + ? snarkContext.runWith({ ...ctx, inWitnessBlock: true }, () => + Circuit._witness(type, createFields) + )[1] + : createFields(); let aux = type.toAuxiliary(proverValue); let value = type.fromFields(fields, aux); type.check(value); @@ -1003,6 +1009,23 @@ Circuit.constraintSystem = function (f: () => T) { return result; }; +Circuit.log = function (...args: any) { + Circuit.asProver(() => { + let prettyArgs = []; + for (let arg of args) { + if (arg?.toPretty !== undefined) prettyArgs.push(arg.toPretty()); + else { + try { + prettyArgs.push(JSON.parse(JSON.stringify(arg))); + } catch { + prettyArgs.push(arg); + } + } + } + console.log(...prettyArgs); + }); +}; + function auxiliary(type: Provable, compute: () => any[]) { let aux; if (inCheckedComputation()) Circuit.asProver(() => (aux = compute())); @@ -1010,11 +1033,6 @@ function auxiliary(type: Provable, compute: () => any[]) { return aux ?? type.toAuxiliary(); } -// TODO: very likely, this is how Circuit.witness should behave -function witness(type: Provable, compute: () => T) { - return inCheckedComputation() ? Circuit.witness(type, compute) : compute(); -} - let memoizationContext = Context.create<{ memoized: { fields: Field[]; aux: any[] }[]; currentIndex: number; @@ -1026,7 +1044,7 @@ let memoizationContext = Context.create<{ * for reuse by the prover. This is needed to witness non-deterministic values. */ function memoizeWitness(type: Provable, compute: () => T) { - return witness(type, () => { + return Circuit.witness(type, () => { if (!memoizationContext.has()) return compute(); let context = memoizationContext.get(); let { memoized, currentIndex } = context; diff --git a/src/lib/encryption.ts b/src/lib/encryption.ts index 385860cf4b..0598ae6b77 100644 --- a/src/lib/encryption.ts +++ b/src/lib/encryption.ts @@ -11,9 +11,7 @@ type CipherText = { function encrypt(message: Field[], otherPublicKey: PublicKey) { // key exchange - let privateKey = Circuit.inCheckedComputation() - ? Circuit.witness(Scalar, () => Scalar.random()) - : Scalar.random(); + let privateKey = Circuit.witness(Scalar, () => Scalar.random()); let publicKey = Group.generator.scale(privateKey); let sharedSecret = otherPublicKey.toGroup().scale(privateKey); diff --git a/src/lib/mina.ts b/src/lib/mina.ts index dfc92d9f3e..6c580f62af 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -227,21 +227,6 @@ interface Mina { ) => { hash: string; actions: string[][] }[]; } -interface MockMina extends Mina { - addAccount(publicKey: PublicKey, balance: string): void; - /** - * An array of 10 test accounts that have been pre-filled with - * 30000000000 units of currency. - */ - testAccounts: Array<{ publicKey: PublicKey; privateKey: PrivateKey }>; - applyJsonTransaction: (tx: string) => void; - setTimestamp: (ms: UInt64) => void; - setGlobalSlot: (slot: UInt32) => void; - setGlobalSlotSinceHardfork: (slot: UInt32) => void; - setBlockchainLength: (height: UInt32) => void; - setTotalCurrency: (currency: UInt64) => void; -} - const defaultAccountCreationFee = 1_000_000_000; /** @@ -250,7 +235,7 @@ const defaultAccountCreationFee = 1_000_000_000; function LocalBlockchain({ accountCreationFee = defaultAccountCreationFee as string | number, proofsEnabled = true, -} = {}): MockMina { +} = {}) { const msPerSlot = 3 * 60 * 1000; const startTime = new Date().valueOf(); @@ -466,6 +451,10 @@ function LocalBlockchain({ ); }, addAccount, + /** + * An array of 10 test accounts that have been pre-filled with + * 30000000000 units of currency. + */ testAccounts, setTimestamp(ms: UInt64) { networkState.timestamp = ms; @@ -482,6 +471,9 @@ function LocalBlockchain({ setTotalCurrency(currency: UInt64) { networkState.totalCurrency = currency; }, + setProofsEnabled(newProofsEnabled: boolean) { + proofsEnabled = newProofsEnabled; + }, }; } diff --git a/src/lib/precondition.test.ts b/src/lib/precondition.test.ts index 0abcc740f4..f770202b38 100644 --- a/src/lib/precondition.test.ts +++ b/src/lib/precondition.test.ts @@ -48,6 +48,7 @@ describe('preconditions', () => { await expect( Mina.transaction(feePayer, () => { precondition().get(); + AccountUpdate.attachToTransaction(zkapp.self); }) ).rejects.toThrow(/precondition/); } @@ -69,6 +70,7 @@ describe('preconditions', () => { let p = precondition().get(); precondition().assertEquals(p as any); } + AccountUpdate.attachToTransaction(zkapp.self); }); await tx.send(); // check that tx was applied, by checking nonce was incremented @@ -81,6 +83,7 @@ describe('preconditions', () => { Mina.transaction(feePayer, () => { let p = precondition(); p.assertEquals(p.get() as any); + AccountUpdate.attachToTransaction(zkapp.self); }) ).rejects.toThrow(/not implemented/); } @@ -94,6 +97,7 @@ describe('preconditions', () => { precondition().assertBetween(p.constructor.zero, p); } zkapp.sign(zkappKey); + AccountUpdate.attachToTransaction(zkapp.self); }); await tx.send(); // check that tx was applied, by checking nonce was incremented @@ -108,6 +112,7 @@ describe('preconditions', () => { precondition().assertNothing(); } zkapp.sign(zkappKey); + AccountUpdate.attachToTransaction(zkapp.self); }); await tx.send(); // check that tx was applied, by checking nonce was incremented @@ -134,6 +139,7 @@ describe('preconditions', () => { zkapp.self.body.preconditions.network.totalCurrency.value.upper = UInt64.from(1e9 * 1e9); zkapp.sign(zkappKey); + AccountUpdate.attachToTransaction(zkapp.self); }); await tx.send(); // check that tx was applied, by checking nonce was incremented @@ -146,6 +152,7 @@ describe('preconditions', () => { let tx = await Mina.transaction(feePayer, () => { let p = precondition().get(); precondition().assertEquals(p.add(1) as any); + AccountUpdate.attachToTransaction(zkapp.self); }); await expect(tx.send()).rejects.toThrow(/unsatisfied/); @@ -157,6 +164,7 @@ describe('preconditions', () => { let tx = await Mina.transaction(feePayer, () => { let p = precondition().get(); precondition().assertEquals(p.not()); + AccountUpdate.attachToTransaction(zkapp.self); }); await expect(tx.send()).rejects.toThrow(/unsatisfied/); } @@ -165,6 +173,7 @@ describe('preconditions', () => { it('unsatisfied assertEquals should be rejected (public key)', async () => { let tx = await Mina.transaction(feePayer, () => { zkapp.account.delegate.assertEquals(PublicKey.empty()); + AccountUpdate.attachToTransaction(zkapp.self); }); await expect(tx.send()).rejects.toThrow(/unsatisfied/); }); @@ -174,6 +183,7 @@ describe('preconditions', () => { let tx = await Mina.transaction(feePayer, () => { let p: any = precondition().get(); precondition().assertBetween(p.add(20), p.add(30)); + AccountUpdate.attachToTransaction(zkapp.self); }); await expect(tx.send()).rejects.toThrow(/unsatisfied/); } @@ -186,6 +196,7 @@ describe('preconditions', () => { let tx = await Mina.transaction(feePayer, () => { zkapp.account.nonce.assertEquals(UInt32.from(1e8)); zkapp.sign(zkappKey); + AccountUpdate.attachToTransaction(zkapp.self); }); expect(() => tx.send()).toThrow(); }); diff --git a/src/lib/precondition.ts b/src/lib/precondition.ts index 2dc5a9d1fd..a8260d93de 100644 --- a/src/lib/precondition.ts +++ b/src/lib/precondition.ts @@ -1,5 +1,5 @@ import { Provable, Bool, Field } from '../snarky.js'; -import { circuitValueEquals, witness } from './circuit_value.js'; +import { circuitValueEquals, Circuit } from './circuit_value.js'; import * as Mina from './mina.js'; import { SequenceEvents, @@ -54,8 +54,8 @@ let unimplementedPreconditions: LongKey[] = [ 'account.provedState', ]; -type BaseType = 'UInt64' | 'UInt32' | 'Field' | 'Bool'; -let baseMap = { UInt64, UInt32, Field, Bool }; +type BaseType = 'UInt64' | 'UInt32' | 'Field' | 'Bool' | 'PublicKey'; +let baseMap = { UInt64, UInt32, Field, Bool, PublicKey }; function preconditionClass( layout: Layout, @@ -126,6 +126,9 @@ function preconditionSubclass< fieldType: Provable, context: PreconditionContext ) { + if (fieldType === undefined) { + throw Error(`this.${longKey}: fieldType undefined`); + } return { get() { if (unimplementedPreconditions.includes(longKey)) { @@ -169,7 +172,7 @@ function getVariable( longKey: K, fieldType: Provable ): U { - return witness(fieldType, () => { + return Circuit.witness(fieldType, () => { let [accountOrNetwork, ...rest] = longKey.split('.'); let key = rest.join('.'); let value: U; diff --git a/src/lib/proof_system.ts b/src/lib/proof_system.ts index a2f6f79dda..86eff38c8a 100644 --- a/src/lib/proof_system.ts +++ b/src/lib/proof_system.ts @@ -47,6 +47,7 @@ type SnarkContext = { inCheckedComputation?: boolean; inAnalyze?: boolean; inRunAndCheck?: boolean; + inWitnessBlock?: boolean; }; let snarkContext = Context.create({ default: {} }); diff --git a/src/lib/state.ts b/src/lib/state.ts index 7a5790875a..97be6f9459 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -1,5 +1,5 @@ import { Field, ProvablePure } from '../snarky.js'; -import { circuitArray, witness } from './circuit_value.js'; +import { circuitArray, Circuit } from './circuit_value.js'; import { AccountUpdate, TokenId } from './account_update.js'; import { PublicKey } from './signature.js'; import * as Mina from './mina.js'; @@ -201,7 +201,7 @@ function createState(): InternalStateType { let contract = this._contract; let inProver_ = inProver(); let stateFieldsType = circuitArray(Field, layout.length); - let stateAsFields = witness(stateFieldsType, () => { + let stateAsFields = Circuit.witness(stateFieldsType, () => { let account: Account; try { account = Mina.getAccount( @@ -215,10 +215,10 @@ function createState(): InternalStateType { } throw Error( `${contract.key}.get() failed, either:\n` + - `1. We can't find this zkapp account in the ledger\n` + - `2. Because the zkapp account was not found in the cache. ` + + `1. We can't find this zkapp account in the ledger\n` + + `2. Because the zkapp account was not found in the cache. ` + `Try calling \`await fetchAccount(zkappAddress)\` first.\n` + - `If none of these are the case, then please reach out on Discord at #zkapp-developers and/or open an issue to tell us!` + `If none of these are the case, then please reach out on Discord at #zkapp-developers and/or open an issue to tell us!` ); } if (account.appState === undefined) { diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index f935af8dc3..8db4121079 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -702,6 +702,7 @@ class SmartContract { } this.setValue(this.self.update.permissions, Permissions.default()); this.sign(zkappKey); + AccountUpdate.attachToTransaction(this.self); } sign(zkappKey?: PrivateKey) { @@ -736,13 +737,9 @@ class SmartContract { ) { return executionState.accountUpdate; } - // TODO: here, we are creating a new account update & attaching it implicitly - // we should refactor some methods which rely on that, such as `deploy()`, - // to do at least the attaching explicitly, and remove implicit attaching - // also, implicit creation is questionable - let transaction = Mina.currentTransaction.get(); + // if in a transaction, but outside a @method call, we implicitly create an account update + // which is stable during the current transaction -- as long as it doesn't get overridden by a method call let accountUpdate = selfAccountUpdate(this); - transaction.accountUpdates.push(accountUpdate); this._executionState = { transactionId, accountUpdate }; return accountUpdate; } diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 949e1d829a..9c112b563b 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -587,6 +587,8 @@ declare class Circuit { static inProver(): boolean; static inCheckedComputation(): boolean; + + static log(...args: any): void; } declare class Scalar {