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

Don't increment nonces on accounts that are the fee payer #637

Merged
merged 4 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 70 additions & 28 deletions src/lib/account_update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}

Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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() {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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;
}

Expand Down