diff --git a/CHANGELOG.md b/CHANGELOG.md index f51b0eabb1..649f5c976b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,16 +17,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/o1-labs/snarkyjs/compare/d880bd6e...HEAD) +### Changed + +- BREAKING CHANGE: Constraint changes in `sign()`, `requireSignature()` and `createSigned()` on `AccountUpdate` / `SmartContract`. _This means that smart contracts using these methods in their proofs won't be able to create valid proofs against old deployed verification keys._ +- New option `enforceTransactionLimits` for `LocalBlockchain` (default value: `true`), to disable the enforcement of protocol transaction limits (maximum events, maximum sequence events and enforcing certain layout of `AccountUpdate`s depending on their authorization) https://github.com/o1-labs/snarkyjs/pull/620 + +### Deprecated + +- `AccountUpdate.createSigned(privateKey: PrivateKey)` in favor of new signature `AccountUpdate.createSigned(publicKey: PublicKey)` + ### Fixed - Type inference for Structs with instance methods https://github.com/o1-labs/snarkyjs/pull/567 - also fixes `Struct.fromJSON` - `SmartContract.fetchEvents` fixed when multiple event types existed https://github.com/o1-labs/snarkyjs/issues/627 -### Changed - -- New option `enforceTransactionLimits` for `LocalBlockchain` (default value: `true`), to disable the enforcement of protocol transaction limits (maximum events, maximum sequence events and enforcing certain layout of `AccountUpdate`s depending on their authorization) https://github.com/o1-labs/snarkyjs/pull/620 - ## [0.7.3](https://github.com/o1-labs/snarkyjs/compare/5f20f496...d880bd6e) ### Fixed diff --git a/src/lib/account_update.ts b/src/lib/account_update.ts index 31edb04587..233a9134bb 100644 --- a/src/lib/account_update.ts +++ b/src/lib/account_update.ts @@ -922,18 +922,23 @@ class AccountUpdate implements Types.AccountUpdate { * Note that an account's {@link Permissions} determine which updates have to be (can be) authorized by a signature. */ requireSignature() { - let nonce = AccountUpdate.getNonce(this); - this.account.nonce.assertEquals(nonce); - this.body.incrementNonce = Bool(true); - Authorization.setLazySignature(this, {}); + this.sign(); } /** * @deprecated `.sign()` is deprecated in favor of `.requireSignature()` */ sign(privateKey?: PrivateKey) { - let nonce = AccountUpdate.getNonce(this); - this.account.nonce.assertEquals(nonce); - this.body.incrementNonce = Bool(true); + let { nonce, isSameAsFeePayer } = AccountUpdate.getSigningInfo(this); + // if this account is the same as the fee payer, we use the "full commitment" for replay protection + this.body.useFullCommitment = isSameAsFeePayer; + // otherwise, we increment the nonce + let doIncrementNonce = isSameAsFeePayer.not(); + this.body.incrementNonce = doIncrementNonce; + // in this case, we also have to set a nonce precondition + this.body.preconditions.account.nonce.isSome = doIncrementNonce; + this.body.preconditions.account.nonce.value.lower = nonce; + this.body.preconditions.account.nonce.value.upper = nonce; + // set lazy signature Authorization.setLazySignature(this, { privateKey }); } @@ -947,12 +952,25 @@ class AccountUpdate implements Types.AccountUpdate { } static getNonce(accountUpdate: AccountUpdate | FeePayerUnsigned) { - return memoizeWitness(UInt32, () => - AccountUpdate.getNonceUnchecked(accountUpdate) + return AccountUpdate.getSigningInfo(accountUpdate).nonce; + } + + private static signingInfo = provable({ + nonce: UInt32, + isSameAsFeePayer: Bool, + }); + + private static getSigningInfo( + accountUpdate: AccountUpdate | FeePayerUnsigned + ) { + return memoizeWitness(AccountUpdate.signingInfo, () => + AccountUpdate.getSigningInfoUnchecked(accountUpdate) ); } - private static getNonceUnchecked(update: AccountUpdate | FeePayerUnsigned) { + private static getSigningInfoUnchecked( + update: AccountUpdate | FeePayerUnsigned + ) { let publicKey = update.body.publicKey; let tokenId = update instanceof AccountUpdate ? update.body.tokenId : TokenId.default; @@ -962,9 +980,11 @@ class AccountUpdate implements Types.AccountUpdate { // if the fee payer is the same account update as this one, we have to start the nonce predicate at one higher, // bc the fee payer already increases its nonce let isFeePayer = Mina.currentTransaction()?.sender?.equals(publicKey); - let shouldIncreaseNonce = isFeePayer?.and(tokenId.equals(TokenId.default)); - if (shouldIncreaseNonce?.toBoolean()) nonce++; - // now, we check how often this accountUpdate already updated its nonce in this tx, and increase nonce from `getAccount` by that amount + let isSameAsFeePayer = !!isFeePayer + ?.and(tokenId.equals(TokenId.default)) + .toBoolean(); + if (isSameAsFeePayer) nonce++; + // now, we check how often this account update already updated its nonce in this tx, and increase nonce from `getAccount` by that amount CallForest.forEachPredecessor( Mina.currentTransaction.get().accountUpdates, update as AccountUpdate, @@ -976,7 +996,10 @@ class AccountUpdate implements Types.AccountUpdate { if (shouldIncreaseNonce.toBoolean()) nonce++; } ); - return UInt32.from(nonce); + return { + nonce: UInt32.from(nonce), + isSameAsFeePayer: Bool(isSameAsFeePayer), + }; } toJSON() { @@ -1006,8 +1029,6 @@ class AccountUpdate implements Types.AccountUpdate { } } - // TODO: this was only exposed to be used in a unit test - // consider removing when we have inline unit tests toPublicInput(): ZkappPublicInput { let accountUpdate = this.hash(); let calls = CallForest.hashChildren(this); @@ -1047,6 +1068,11 @@ class AccountUpdate implements Types.AccountUpdate { return { body, authorization: Ledger.dummySignature() }; } + /** + * Creates an account update. If this is inside a transaction, the account update becomes part of the transaction. + * If this is inside a smart contract method, the account update will not only become part of the transaction, but + * also becomes available for the smart contract to modify, in a way that becomes part of the proof. + */ static create(publicKey: PublicKey, tokenId?: Field) { let accountUpdate = AccountUpdate.defaultAccountUpdate(publicKey, tokenId); if (smartContractContext.has()) { @@ -1091,23 +1117,39 @@ class AccountUpdate implements Types.AccountUpdate { accountUpdate.parent === undefined; } - static createSigned(signer: PrivateKey) { - let publicKey = signer.toPublicKey(); + /** + * Creates an account update, like {@link AccountUpdate.create}, but also makes sure + * this account update will be authorized with a signature. + * + * If you use this and are not relying on a wallet to sign your transaction, then you should use the following code + * before sending your transaction: + * + * ```ts + * let tx = Mina.transaction(...); // create transaction as usual, using `createSigned()` somewhere + * tx.sign([privateKey]); // pass the private key of this account to `sign()`! + * ``` + * + * Note that an account's {@link Permissions} determine which updates have to be (can be) authorized by a signature. + */ + static createSigned(signer: PublicKey, tokenId?: Field): AccountUpdate; + /** + * @deprecated in favor of calling this function with a `PublicKey` as `signer` + */ + static createSigned(signer: PrivateKey, tokenId?: Field): AccountUpdate; + static createSigned(signer: PrivateKey | PublicKey, tokenId?: Field) { + let publicKey = + signer instanceof PrivateKey ? signer.toPublicKey() : signer; if (!Mina.currentTransaction.has()) { throw new Error( 'AccountUpdate.createSigned: Cannot run outside of a transaction' ); } - let accountUpdate = AccountUpdate.defaultAccountUpdate(publicKey); - // it's fine to compute the nonce outside the circuit, because we're constraining it with a precondition - let nonce = Circuit.witness(UInt32, () => - AccountUpdate.getNonceUnchecked(accountUpdate) - ); - accountUpdate.account.nonce.assertEquals(nonce); - accountUpdate.body.incrementNonce = Bool(true); - - Authorization.setLazySignature(accountUpdate, { privateKey: signer }); - Mina.currentTransaction.get().accountUpdates.push(accountUpdate); + let accountUpdate = AccountUpdate.create(publicKey, tokenId); + if (signer instanceof PrivateKey) { + accountUpdate.sign(signer); + } else { + accountUpdate.requireSignature(); + } return accountUpdate; }