Skip to content

Commit 82db28b

Browse files
authored
Merge pull request #1968 from o1-labs/brian/increaseFee
Add setFee and setFeePerWU
2 parents 2f4d437 + 8fba5a8 commit 82db28b

File tree

7 files changed

+165
-34
lines changed

7 files changed

+165
-34
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1717

1818
## [Unreleased](https://github.com/o1-labs/o1js/compare/b857516...HEAD)
1919

20+
### Added
21+
- `setFee` and `setFeePerSnarkCost` for `Transaction` and `PendingTransaction` https://github.com/o1-labs/o1js/pull/1968
22+
2023
### Changed
2124
- Sort order for actions now includes the transaction sequence number and the exact account id sequence https://github.com/o1-labs/o1js/pull/1917
2225

@@ -367,7 +370,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
367370
- `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
368371
- `Reducer.reduce()` requires the maximum number of actions per method as an explicit (optional) argument https://github.com/o1-labs/o1js/pull/1450
369372
- The default value is 1 and should work for most existing contracts
370-
- `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)
373+
- `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)
371374
As a replacement, `UInt64.Unsafe.fromField()` was introduced
372375
- This prevents you from accidentally creating a `UInt64` without proving that it fits in 64 bits
373376
- Equivalent changes were made to `UInt32`
@@ -1142,7 +1145,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
11421145

11431146
- **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
11441147
- Enable smart contract methods to take previous proofs as arguments, and verify them in the circuit
1145-
- 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.
1148+
- 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.
11461149
`ZkProgram` is suitable for rollup-type systems and offchain usage of Pickles + Kimchi.
11471150
- **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
11481151
- **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

flake.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/mina/local-blockchain.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ async function LocalBlockchain({
293293
status,
294294
errors,
295295
transaction: txn.transaction,
296+
setFee: txn.setFee,
297+
setFeePerSnarkCost: txn.setFeePerSnarkCost,
296298
hash,
297299
toJSON: txn.toJSON,
298300
toPretty: txn.toPretty,

src/lib/mina/mina.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ function Network(
282282
data: response?.data,
283283
errors: updatedErrors,
284284
transaction: txn.transaction,
285+
setFee : txn.setFee,
286+
setFeePerSnarkCost : txn.setFeePerSnarkCost,
285287
hash,
286288
toJSON: txn.toJSON,
287289
toPretty: txn.toPretty,

src/lib/mina/transaction-validation.ts

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export {
2626
reportGetAccountError,
2727
defaultNetworkState,
2828
verifyTransactionLimits,
29+
getTotalTimeRequired,
2930
verifyAccountUpdate,
3031
filterGroups,
3132
};
@@ -58,6 +59,41 @@ function defaultNetworkState(): NetworkValue {
5859
}
5960

6061
function verifyTransactionLimits({ accountUpdates }: ZkappCommand) {
62+
63+
let {totalTimeRequired,eventElements,authTypes} = getTotalTimeRequired(accountUpdates);
64+
65+
let isWithinCostLimit = totalTimeRequired < TransactionCost.COST_LIMIT;
66+
67+
let isWithinEventsLimit =
68+
eventElements.events <= TransactionLimits.MAX_EVENT_ELEMENTS;
69+
let isWithinActionsLimit =
70+
eventElements.actions <= TransactionLimits.MAX_ACTION_ELEMENTS;
71+
72+
let error = '';
73+
74+
if (!isWithinCostLimit) {
75+
// TODO: we should add a link to the docs explaining the reasoning behind it once we have such an explainer
76+
error += `Error: The transaction is too expensive, try reducing the number of AccountUpdates that are attached to the transaction.
77+
Each transaction needs to be processed by the snark workers on the network.
78+
Certain layouts of AccountUpdates require more proving time than others, and therefore are too expensive.
79+
80+
${JSON.stringify(authTypes)}
81+
\n\n`;
82+
}
83+
84+
if (!isWithinEventsLimit) {
85+
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`;
86+
}
87+
88+
if (!isWithinActionsLimit) {
89+
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`;
90+
}
91+
92+
if (error) throw Error('Error during transaction sending:\n\n' + error);
93+
}
94+
95+
96+
function getTotalTimeRequired(accountUpdates : AccountUpdate[]){
6197
let eventElements = { events: 0, actions: 0 };
6298

6399
let authKinds = accountUpdates.map((update) => {
@@ -83,7 +119,7 @@ function verifyTransactionLimits({ accountUpdates }: ZkappCommand) {
83119
np := proof
84120
n2 := signedPair
85121
n1 := signedSingle
86-
122+
87123
formula used to calculate how expensive a zkapp transaction is
88124
89125
10.26*np + 10.08*n2 + 9.14*n1 < 69.45
@@ -92,35 +128,8 @@ function verifyTransactionLimits({ accountUpdates }: ZkappCommand) {
92128
TransactionCost.PROOF_COST * authTypes.proof +
93129
TransactionCost.SIGNED_PAIR_COST * authTypes.signedPair +
94130
TransactionCost.SIGNED_SINGLE_COST * authTypes.signedSingle;
95-
96-
let isWithinCostLimit = totalTimeRequired < TransactionCost.COST_LIMIT;
97-
98-
let isWithinEventsLimit =
99-
eventElements.events <= TransactionLimits.MAX_EVENT_ELEMENTS;
100-
let isWithinActionsLimit =
101-
eventElements.actions <= TransactionLimits.MAX_ACTION_ELEMENTS;
102-
103-
let error = '';
104-
105-
if (!isWithinCostLimit) {
106-
// TODO: we should add a link to the docs explaining the reasoning behind it once we have such an explainer
107-
error += `Error: The transaction is too expensive, try reducing the number of AccountUpdates that are attached to the transaction.
108-
Each transaction needs to be processed by the snark workers on the network.
109-
Certain layouts of AccountUpdates require more proving time than others, and therefore are too expensive.
110-
111-
${JSON.stringify(authTypes)}
112-
\n\n`;
113-
}
114-
115-
if (!isWithinEventsLimit) {
116-
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`;
117-
}
118-
119-
if (!isWithinActionsLimit) {
120-
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`;
121-
}
122-
123-
if (error) throw Error('Error during transaction sending:\n\n' + error);
131+
// returns totalTimeRequired and additional data used by verifyTransactionLimits
132+
return {totalTimeRequired,eventElements,authTypes};
124133
}
125134

126135
function countEventElements({ data }: Events) {

src/lib/mina/transaction.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
UInt64,
3+
SmartContract,
4+
Mina,
5+
AccountUpdate,
6+
method,
7+
} from 'o1js';
8+
9+
class MyContract extends SmartContract {
10+
@method async shouldMakeCompileThrow() {
11+
this.network.blockchainLength.get();
12+
}
13+
}
14+
15+
let contractAccount: Mina.TestPublicKey;
16+
let contract: MyContract;
17+
let feePayer: Mina.TestPublicKey;
18+
19+
describe('transactions', () => {
20+
beforeAll(async () => {
21+
// set up local blockchain, create contract account keys, deploy the contract
22+
let Local = await Mina.LocalBlockchain({ proofsEnabled: false });
23+
Mina.setActiveInstance(Local);
24+
[feePayer] = Local.testAccounts;
25+
26+
contractAccount = Mina.TestPublicKey.random();
27+
contract = new MyContract(contractAccount);
28+
29+
let tx = await Mina.transaction(feePayer, async () => {
30+
AccountUpdate.fundNewAccount(feePayer);
31+
await contract.deploy();
32+
});
33+
tx.sign([feePayer.key, contractAccount.key]).send();
34+
});
35+
36+
it('setFee should not change nonce', async () => {
37+
let tx = await Mina.transaction(feePayer, async () => {
38+
contract.requireSignature();
39+
AccountUpdate.attachToTransaction(contract.self);
40+
});
41+
let nonce = tx.transaction.feePayer.body.nonce;
42+
let promise = await tx.sign([feePayer.key, contractAccount.key]).send();
43+
let new_fee = await promise.setFee(new UInt64(100));
44+
new_fee.sign([feePayer.key,contractAccount.key]);
45+
// second send is rejected for using the same nonce
46+
await expect((new_fee.send()))
47+
.rejects
48+
.toThrowError("Account_nonce_precondition_unsatisfied");
49+
// check that tx was applied, by checking nonce was incremented
50+
expect(new_fee.transaction.feePayer.body.nonce).toEqual(nonce);
51+
});
52+
53+
it('Second tx should work when first not sent', async () => {
54+
let tx = await Mina.transaction(feePayer, async () => {
55+
contract.requireSignature();
56+
AccountUpdate.attachToTransaction(contract.self);
57+
});
58+
let nonce = tx.transaction.feePayer.body.nonce;
59+
let promise = tx.sign([feePayer.key, contractAccount.key]);
60+
let new_fee = promise.setFeePerSnarkCost(42.7);
61+
await new_fee.sign([feePayer.key,contractAccount.key]);
62+
await new_fee.send();
63+
// check that tx was applied, by checking nonce was incremented
64+
expect((await new_fee).transaction.feePayer.body.nonce).toEqual(nonce);
65+
});
66+
});

src/lib/mina/transaction.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { type SendZkAppResponse, sendZkappQuery } from './graphql.js';
2323
import { type FetchMode } from './transaction-context.js';
2424
import { assertPromise } from '../util/assert.js';
2525
import { Types } from '../../bindings/mina-transaction/types.js';
26+
import { getTotalTimeRequired } from './transaction-validation.js';
2627

2728
export {
2829
Transaction,
@@ -114,6 +115,25 @@ type Transaction<
114115
* ```
115116
*/
116117
safeSend(): Promise<PendingTransaction | RejectedTransaction>;
118+
119+
/**
120+
* 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.
121+
* @returns {TransactionPromise<false,false>} The same transaction with the new fee and the proofs and signatures removed.
122+
* @example
123+
* ```ts
124+
* tx.send();
125+
* // Waits for some time and decide to resend with a higher fee
126+
*
127+
* tx.setFee(newFee);
128+
* await tx.sign([feePayerKey]));
129+
* await tx.send();
130+
* ```
131+
*/
132+
setFee(newFee:UInt64) : TransactionPromise<Proven,false>;
133+
/**
134+
* 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.
135+
*/
136+
setFeePerSnarkCost(newFeePerSnarkCost:number) : TransactionPromise<Proven,false>;
117137
} & (Proven extends false
118138
? {
119139
/**
@@ -250,6 +270,15 @@ type PendingTransaction = Pick<
250270
* ```
251271
*/
252272
errors: string[];
273+
274+
/**
275+
* setFee is the same as {@link Transaction.setFee(newFee)} but for a {@link PendingTransaction}.
276+
*/
277+
setFee(newFee:UInt64):TransactionPromise<boolean,false>;
278+
/**
279+
* setFeePerSnarkCost is the same as {@link Transaction.setFeePerSnarkCost(newFeePerSnarkCost)} but for a {@link PendingTransaction}.
280+
*/
281+
setFeePerSnarkCost(newFeePerSnarkCost:number):TransactionPromise<boolean,false>;
253282
};
254283

255284
/**
@@ -544,6 +573,26 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) {
544573
}
545574
return pendingTransaction;
546575
},
576+
setFeePerSnarkCost(newFeePerSnarkCost:number) {
577+
let {totalTimeRequired} = getTotalTimeRequired(transaction.accountUpdates);
578+
return this.setFee(new UInt64(Math.round(totalTimeRequired * newFeePerSnarkCost)));
579+
},
580+
setFee(newFee:UInt64) {
581+
return toTransactionPromise(async () =>
582+
{
583+
self = self as Transaction<false,false>;
584+
self.transaction.accountUpdates.forEach( au => {
585+
if (au.body.useFullCommitment.toBoolean())
586+
{
587+
au.authorization.signature = undefined;
588+
au.lazyAuthorization = {kind:'lazy-signature'};
589+
}
590+
});
591+
self.transaction.feePayer.body.fee = newFee;
592+
self.transaction.feePayer.lazyAuthorization = {kind : 'lazy-signature'};
593+
return self
594+
});
595+
},
547596
};
548597
return self;
549598
}

0 commit comments

Comments
 (0)