Skip to content

Commit

Permalink
Merge branch 'main' into feature/eddsa/sha512
Browse files Browse the repository at this point in the history
  • Loading branch information
querolita committed Jan 16, 2025
2 parents af3a21b + 82db28b commit da83ce0
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 35 deletions.
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added

- Gadgets for 224, 384 and 512 bit variants of SHA2 https://github.com/o1-labs/o1js/pull/1957
- `setFee` and `setFeePerSnarkCost` for `Transaction` and `PendingTransaction` https://github.com/o1-labs/o1js/pull/1968

### Changed

- Sort order for actions now includes the transaction sequence number and the exact account id sequence https://github.com/o1-labs/o1js/pull/1917

## [2.2.0](https://github.com/o1-labs/o1js/compare/e1bac02...b857516) - 2024-12-10
Expand Down Expand Up @@ -371,8 +373,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- `this.sender.getAndRequireSignature()` which requires a signature from the sender's public key and therefore proves that whoever created the transaction really owns the sender account
- `Reducer.reduce()` requires the maximum number of actions per method as an explicit (optional) argument https://github.com/o1-labs/o1js/pull/1450
- The default value is 1 and should work for most existing contracts
- `new UInt64()` and `UInt64.from()` no longer unsafely accept a field element as input. https://github.com/o1-labs/o1js/pull/1438 [@julio4](https://github.com/julio4)
As a replacement, `UInt64.Unsafe.fromField()` was introduced
- `new UInt64()` and `UInt64.from()` no longer unsafely accept a field element as input. https://github.com/o1-labs/o1js/pull/1438 [@julio4](https://github.com/julio4)
As a replacement, `UInt64.Unsafe.fromField()` was introduced
- This prevents you from accidentally creating a `UInt64` without proving that it fits in 64 bits
- Equivalent changes were made to `UInt32`
- Fixed vulnerability in `Field.to/fromBits()` outlined in [#1023](https://github.com/o1-labs/o1js/issues/1023) by imposing a limit of 254 bits https://github.com/o1-labs/o1js/pull/1461
Expand Down Expand Up @@ -1146,7 +1148,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- **Recursive proofs**. RFC: https://github.com/o1-labs/o1js/issues/89, PRs: https://github.com/o1-labs/o1js/pull/245 https://github.com/o1-labs/o1js/pull/250 https://github.com/o1-labs/o1js/pull/261
- Enable smart contract methods to take previous proofs as arguments, and verify them in the circuit
- Add `ZkProgram`, a new primitive which represents a collection of circuits that produce instances of the same proof. So, it's a more general version of `SmartContract`, without any of the Mina-related API.
- Add `ZkProgram`, a new primitive which represents a collection of circuits that produce instances of the same proof. So, it's a more general version of `SmartContract`, without any of the Mina-related API.
`ZkProgram` is suitable for rollup-type systems and offchain usage of Pickles + Kimchi.
- **zkApp composability** -- calling other zkApps from inside zkApps. RFC: https://github.com/o1-labs/o1js/issues/303, PRs: https://github.com/o1-labs/o1js/pull/285, https://github.com/o1-labs/o1js/pull/296, https://github.com/o1-labs/o1js/pull/294, https://github.com/o1-labs/o1js/pull/297
- **Events** support via `SmartContract.events`, `this.emitEvent`. RFC: https://github.com/o1-labs/o1js/issues/248, PR: https://github.com/o1-labs/o1js/pull/272
Expand Down
4 changes: 2 additions & 2 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/lib/mina/local-blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ async function LocalBlockchain({
status,
errors,
transaction: txn.transaction,
setFee: txn.setFee,
setFeePerSnarkCost: txn.setFeePerSnarkCost,
hash,
toJSON: txn.toJSON,
toPretty: txn.toPretty,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/mina/mina.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ function Network(
data: response?.data,
errors: updatedErrors,
transaction: txn.transaction,
setFee : txn.setFee,
setFeePerSnarkCost : txn.setFeePerSnarkCost,
hash,
toJSON: txn.toJSON,
toPretty: txn.toPretty,
Expand Down
69 changes: 39 additions & 30 deletions src/lib/mina/transaction-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export {
reportGetAccountError,
defaultNetworkState,
verifyTransactionLimits,
getTotalTimeRequired,
verifyAccountUpdate,
filterGroups,
};
Expand Down Expand Up @@ -58,6 +59,41 @@ function defaultNetworkState(): NetworkValue {
}

function verifyTransactionLimits({ accountUpdates }: ZkappCommand) {

let {totalTimeRequired,eventElements,authTypes} = getTotalTimeRequired(accountUpdates);

let isWithinCostLimit = totalTimeRequired < TransactionCost.COST_LIMIT;

let isWithinEventsLimit =
eventElements.events <= TransactionLimits.MAX_EVENT_ELEMENTS;
let isWithinActionsLimit =
eventElements.actions <= TransactionLimits.MAX_ACTION_ELEMENTS;

let error = '';

if (!isWithinCostLimit) {
// TODO: we should add a link to the docs explaining the reasoning behind it once we have such an explainer
error += `Error: The transaction is too expensive, try reducing the number of AccountUpdates that are attached to the transaction.
Each transaction needs to be processed by the snark workers on the network.
Certain layouts of AccountUpdates require more proving time than others, and therefore are too expensive.
${JSON.stringify(authTypes)}
\n\n`;
}

if (!isWithinEventsLimit) {
error += `Error: The account updates in your transaction are trying to emit too much event data. The maximum allowed number of field elements in events is ${TransactionLimits.MAX_EVENT_ELEMENTS}, but you tried to emit ${eventElements.events}.\n\n`;
}

if (!isWithinActionsLimit) {
error += `Error: The account updates in your transaction are trying to emit too much action data. The maximum allowed number of field elements in actions is ${TransactionLimits.MAX_ACTION_ELEMENTS}, but you tried to emit ${eventElements.actions}.\n\n`;
}

if (error) throw Error('Error during transaction sending:\n\n' + error);
}


function getTotalTimeRequired(accountUpdates : AccountUpdate[]){
let eventElements = { events: 0, actions: 0 };

let authKinds = accountUpdates.map((update) => {
Expand All @@ -83,7 +119,7 @@ function verifyTransactionLimits({ accountUpdates }: ZkappCommand) {
np := proof
n2 := signedPair
n1 := signedSingle
formula used to calculate how expensive a zkapp transaction is
10.26*np + 10.08*n2 + 9.14*n1 < 69.45
Expand All @@ -92,35 +128,8 @@ function verifyTransactionLimits({ accountUpdates }: ZkappCommand) {
TransactionCost.PROOF_COST * authTypes.proof +
TransactionCost.SIGNED_PAIR_COST * authTypes.signedPair +
TransactionCost.SIGNED_SINGLE_COST * authTypes.signedSingle;

let isWithinCostLimit = totalTimeRequired < TransactionCost.COST_LIMIT;

let isWithinEventsLimit =
eventElements.events <= TransactionLimits.MAX_EVENT_ELEMENTS;
let isWithinActionsLimit =
eventElements.actions <= TransactionLimits.MAX_ACTION_ELEMENTS;

let error = '';

if (!isWithinCostLimit) {
// TODO: we should add a link to the docs explaining the reasoning behind it once we have such an explainer
error += `Error: The transaction is too expensive, try reducing the number of AccountUpdates that are attached to the transaction.
Each transaction needs to be processed by the snark workers on the network.
Certain layouts of AccountUpdates require more proving time than others, and therefore are too expensive.
${JSON.stringify(authTypes)}
\n\n`;
}

if (!isWithinEventsLimit) {
error += `Error: The account updates in your transaction are trying to emit too much event data. The maximum allowed number of field elements in events is ${TransactionLimits.MAX_EVENT_ELEMENTS}, but you tried to emit ${eventElements.events}.\n\n`;
}

if (!isWithinActionsLimit) {
error += `Error: The account updates in your transaction are trying to emit too much action data. The maximum allowed number of field elements in actions is ${TransactionLimits.MAX_ACTION_ELEMENTS}, but you tried to emit ${eventElements.actions}.\n\n`;
}

if (error) throw Error('Error during transaction sending:\n\n' + error);
// returns totalTimeRequired and additional data used by verifyTransactionLimits
return {totalTimeRequired,eventElements,authTypes};
}

function countEventElements({ data }: Events) {
Expand Down
66 changes: 66 additions & 0 deletions src/lib/mina/transaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
UInt64,
SmartContract,
Mina,
AccountUpdate,
method,
} from 'o1js';

class MyContract extends SmartContract {
@method async shouldMakeCompileThrow() {
this.network.blockchainLength.get();
}
}

let contractAccount: Mina.TestPublicKey;
let contract: MyContract;
let feePayer: Mina.TestPublicKey;

describe('transactions', () => {
beforeAll(async () => {
// set up local blockchain, create contract account keys, deploy the contract
let Local = await Mina.LocalBlockchain({ proofsEnabled: false });
Mina.setActiveInstance(Local);
[feePayer] = Local.testAccounts;

contractAccount = Mina.TestPublicKey.random();
contract = new MyContract(contractAccount);

let tx = await Mina.transaction(feePayer, async () => {
AccountUpdate.fundNewAccount(feePayer);
await contract.deploy();
});
tx.sign([feePayer.key, contractAccount.key]).send();
});

it('setFee should not change nonce', async () => {
let tx = await Mina.transaction(feePayer, async () => {
contract.requireSignature();
AccountUpdate.attachToTransaction(contract.self);
});
let nonce = tx.transaction.feePayer.body.nonce;
let promise = await tx.sign([feePayer.key, contractAccount.key]).send();
let new_fee = await promise.setFee(new UInt64(100));
new_fee.sign([feePayer.key,contractAccount.key]);
// second send is rejected for using the same nonce
await expect((new_fee.send()))
.rejects
.toThrowError("Account_nonce_precondition_unsatisfied");
// check that tx was applied, by checking nonce was incremented
expect(new_fee.transaction.feePayer.body.nonce).toEqual(nonce);
});

it('Second tx should work when first not sent', async () => {
let tx = await Mina.transaction(feePayer, async () => {
contract.requireSignature();
AccountUpdate.attachToTransaction(contract.self);
});
let nonce = tx.transaction.feePayer.body.nonce;
let promise = tx.sign([feePayer.key, contractAccount.key]);
let new_fee = promise.setFeePerSnarkCost(42.7);
await new_fee.sign([feePayer.key,contractAccount.key]);
await new_fee.send();
// check that tx was applied, by checking nonce was incremented
expect((await new_fee).transaction.feePayer.body.nonce).toEqual(nonce);
});
});
49 changes: 49 additions & 0 deletions src/lib/mina/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { type SendZkAppResponse, sendZkappQuery } from './graphql.js';
import { type FetchMode } from './transaction-context.js';
import { assertPromise } from '../util/assert.js';
import { Types } from '../../bindings/mina-transaction/types.js';
import { getTotalTimeRequired } from './transaction-validation.js';

export {
Transaction,
Expand Down Expand Up @@ -114,6 +115,25 @@ type Transaction<
* ```
*/
safeSend(): Promise<PendingTransaction | RejectedTransaction>;

/**
* Modifies a transaction to set the fee to the new fee provided. Because this change invalidates proofs and signatures both are removed. The nonce is not increased so sending both transitions will not risk both being accepted.
* @returns {TransactionPromise<false,false>} The same transaction with the new fee and the proofs and signatures removed.
* @example
* ```ts
* tx.send();
* // Waits for some time and decide to resend with a higher fee
*
* tx.setFee(newFee);
* await tx.sign([feePayerKey]));
* await tx.send();
* ```
*/
setFee(newFee:UInt64) : TransactionPromise<Proven,false>;
/**
* setFeePerSnarkCost behaves identically to {@link Transaction.setFee} but the fee is given per estimated cost of snarking the transition as given by {@link getTotalTimeRequired}. This is useful because it should reflect what snark workers would charge in times of network contention.
*/
setFeePerSnarkCost(newFeePerSnarkCost:number) : TransactionPromise<Proven,false>;
} & (Proven extends false
? {
/**
Expand Down Expand Up @@ -250,6 +270,15 @@ type PendingTransaction = Pick<
* ```
*/
errors: string[];

/**
* setFee is the same as {@link Transaction.setFee(newFee)} but for a {@link PendingTransaction}.
*/
setFee(newFee:UInt64):TransactionPromise<boolean,false>;
/**
* setFeePerSnarkCost is the same as {@link Transaction.setFeePerSnarkCost(newFeePerSnarkCost)} but for a {@link PendingTransaction}.
*/
setFeePerSnarkCost(newFeePerSnarkCost:number):TransactionPromise<boolean,false>;
};

/**
Expand Down Expand Up @@ -544,6 +573,26 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) {
}
return pendingTransaction;
},
setFeePerSnarkCost(newFeePerSnarkCost:number) {
let {totalTimeRequired} = getTotalTimeRequired(transaction.accountUpdates);
return this.setFee(new UInt64(Math.round(totalTimeRequired * newFeePerSnarkCost)));
},
setFee(newFee:UInt64) {
return toTransactionPromise(async () =>
{
self = self as Transaction<false,false>;
self.transaction.accountUpdates.forEach( au => {
if (au.body.useFullCommitment.toBoolean())
{
au.authorization.signature = undefined;
au.lazyAuthorization = {kind:'lazy-signature'};
}
});
self.transaction.feePayer.body.fee = newFee;
self.transaction.feePayer.lazyAuthorization = {kind : 'lazy-signature'};
return self
});
},
};
return self;
}
Expand Down

0 comments on commit da83ce0

Please sign in to comment.