diff --git a/yarn-project/aztec.js/src/contract/base_contract_interaction.ts b/yarn-project/aztec.js/src/contract/base_contract_interaction.ts index b061eec00294..be48d9bf477d 100644 --- a/yarn-project/aztec.js/src/contract/base_contract_interaction.ts +++ b/yarn-project/aztec.js/src/contract/base_contract_interaction.ts @@ -3,6 +3,7 @@ import { GasSettings } from '@aztec/circuits.js'; import { type Wallet } from '../account/wallet.js'; import { type ExecutionRequestInit, type FeeOptions } from '../entrypoint/entrypoint.js'; +import { getGasLimits } from './get_gas_limits.js'; import { SentTx } from './sent_tx.js'; /** @@ -71,7 +72,7 @@ export abstract class BaseContractInteraction { */ protected async estimateGas(txRequest: TxExecutionRequest) { const simulationResult = await this.wallet.simulateTx(txRequest, true); - const { totalGas: gasLimits, teardownGas: teardownGasLimits } = simulationResult.getGasLimits(); + const { totalGas: gasLimits, teardownGas: teardownGasLimits } = getGasLimits(simulationResult); return GasSettings.default({ gasLimits, teardownGasLimits }); } @@ -84,8 +85,7 @@ export abstract class BaseContractInteraction { const fee = request.fee; if (fee) { const txRequest = await this.wallet.createTxExecutionRequest(request); - const simulationResult = await this.wallet.simulateTx(txRequest, true); - const { totalGas: gasLimits, teardownGas: teardownGasLimits } = simulationResult.getGasLimits(); + const { gasLimits, teardownGasLimits } = await this.estimateGas(txRequest); const gasSettings = GasSettings.default({ ...fee.gasSettings, gasLimits, teardownGasLimits }); return { ...fee, gasSettings }; } diff --git a/yarn-project/aztec.js/src/contract/get_gas_limits.test.ts b/yarn-project/aztec.js/src/contract/get_gas_limits.test.ts new file mode 100644 index 000000000000..b10d8aca63b2 --- /dev/null +++ b/yarn-project/aztec.js/src/contract/get_gas_limits.test.ts @@ -0,0 +1,41 @@ +import { PublicKernelType, type SimulatedTx, mockSimulatedTx } from '@aztec/circuit-types'; +import { Gas } from '@aztec/circuits.js'; + +import { getGasLimits } from './get_gas_limits.js'; + +describe('getGasLimits', () => { + let simulatedTx: SimulatedTx; + + beforeEach(() => { + simulatedTx = mockSimulatedTx(); + simulatedTx.tx.data.publicInputs.end.gasUsed = Gas.from({ daGas: 100, l2Gas: 200 }); + simulatedTx.publicOutput!.gasUsed = { + [PublicKernelType.SETUP]: Gas.from({ daGas: 10, l2Gas: 20 }), + [PublicKernelType.APP_LOGIC]: Gas.from({ daGas: 20, l2Gas: 40 }), + [PublicKernelType.TEARDOWN]: Gas.from({ daGas: 10, l2Gas: 20 }), + }; + }); + + it('returns gas limits from private gas usage only', () => { + simulatedTx.publicOutput = undefined; + // Should be 110 and 220 but oh floating point + expect(getGasLimits(simulatedTx)).toEqual({ + totalGas: Gas.from({ daGas: 111, l2Gas: 221 }), + teardownGas: Gas.empty(), + }); + }); + + it('returns gas limits for private and public', () => { + expect(getGasLimits(simulatedTx)).toEqual({ + totalGas: Gas.from({ daGas: 154, l2Gas: 308 }), + teardownGas: Gas.from({ daGas: 11, l2Gas: 22 }), + }); + }); + + it('pads gas limits', () => { + expect(getGasLimits(simulatedTx, 1)).toEqual({ + totalGas: Gas.from({ daGas: 280, l2Gas: 560 }), + teardownGas: Gas.from({ daGas: 20, l2Gas: 40 }), + }); + }); +}); diff --git a/yarn-project/aztec.js/src/contract/get_gas_limits.ts b/yarn-project/aztec.js/src/contract/get_gas_limits.ts new file mode 100644 index 000000000000..d2fc65af17e1 --- /dev/null +++ b/yarn-project/aztec.js/src/contract/get_gas_limits.ts @@ -0,0 +1,24 @@ +import { PublicKernelType, type SimulatedTx } from '@aztec/circuit-types'; +import { Gas } from '@aztec/circuits.js'; + +/** + * Returns suggested total and teardown gas limits for a simulated tx. + * Note that public gas usage is only accounted for if the publicOutput is present. + * @param pad - Percentage to pad the suggested gas limits by, defaults to 10%. + */ +export function getGasLimits(simulatedTx: SimulatedTx, pad = 0.1) { + const privateGasUsed = simulatedTx.tx.data.publicInputs.end.gasUsed; + if (simulatedTx.publicOutput) { + const publicGasUsed = Object.values(simulatedTx.publicOutput.gasUsed) + .filter(Boolean) + .reduce((total, current) => total.add(current), Gas.empty()); + const teardownGas = simulatedTx.publicOutput.gasUsed[PublicKernelType.TEARDOWN] ?? Gas.empty(); + + return { + totalGas: privateGasUsed.add(publicGasUsed).mul(1 + pad), + teardownGas: teardownGas.mul(1 + pad), + }; + } + + return { totalGas: privateGasUsed.mul(1 + pad), teardownGas: Gas.empty() }; +} diff --git a/yarn-project/circuit-types/src/tx/simulated_tx.test.ts b/yarn-project/circuit-types/src/tx/simulated_tx.test.ts index be7f94291a39..d41ab66a881a 100644 --- a/yarn-project/circuit-types/src/tx/simulated_tx.test.ts +++ b/yarn-project/circuit-types/src/tx/simulated_tx.test.ts @@ -1,7 +1,4 @@ -import { Gas } from '@aztec/circuits.js'; - import { mockSimulatedTx } from '../mocks.js'; -import { PublicKernelType } from './processed_tx.js'; import { SimulatedTx } from './simulated_tx.js'; describe('simulated_tx', () => { @@ -22,38 +19,4 @@ describe('simulated_tx', () => { expect(SimulatedTx.fromJSON(simulatedTx.toJSON())).toEqual(simulatedTx); }); }); - - describe('getGasLimits', () => { - beforeEach(() => { - simulatedTx.tx.data.publicInputs.end.gasUsed = Gas.from({ daGas: 100, l2Gas: 200 }); - simulatedTx.publicOutput!.gasUsed = { - [PublicKernelType.SETUP]: Gas.from({ daGas: 10, l2Gas: 20 }), - [PublicKernelType.APP_LOGIC]: Gas.from({ daGas: 20, l2Gas: 40 }), - [PublicKernelType.TEARDOWN]: Gas.from({ daGas: 10, l2Gas: 20 }), - }; - }); - - it('returns gas limits from private gas usage only', () => { - simulatedTx.publicOutput = undefined; - // Should be 110 and 220 but oh floating point - expect(simulatedTx.getGasLimits()).toEqual({ - totalGas: Gas.from({ daGas: 111, l2Gas: 221 }), - teardownGas: Gas.empty(), - }); - }); - - it('returns gas limits for private and public', () => { - expect(simulatedTx.getGasLimits()).toEqual({ - totalGas: Gas.from({ daGas: 154, l2Gas: 308 }), - teardownGas: Gas.from({ daGas: 11, l2Gas: 22 }), - }); - }); - - it('pads gas limits', () => { - expect(simulatedTx.getGasLimits(1)).toEqual({ - totalGas: Gas.from({ daGas: 280, l2Gas: 560 }), - teardownGas: Gas.from({ daGas: 20, l2Gas: 40 }), - }); - }); - }); }); diff --git a/yarn-project/circuit-types/src/tx/simulated_tx.ts b/yarn-project/circuit-types/src/tx/simulated_tx.ts index 45387f1664a2..e3808a1e4632 100644 --- a/yarn-project/circuit-types/src/tx/simulated_tx.ts +++ b/yarn-project/circuit-types/src/tx/simulated_tx.ts @@ -1,6 +1,5 @@ -import { Fr, Gas } from '@aztec/circuits.js'; +import { Fr } from '@aztec/circuits.js'; -import { PublicKernelType } from './processed_tx.js'; import { type ProcessReturnValues, PublicSimulationOutput } from './public_simulation_output.js'; import { Tx } from './tx.js'; @@ -17,29 +16,6 @@ export class SimulatedTx { public publicOutput?: PublicSimulationOutput, ) {} - /** - * Returns suggested total and teardown gas limits for the simulated tx. - * Note that public gas usage is only accounted for if the publicOutput is present. - * @param pad - Percentage to pad the suggested gas limits by, defaults to 10%. - */ - public getGasLimits(pad = 0.1) { - const privateGasUsed = this.tx.data.publicInputs.end.gasUsed; - if (this.publicOutput) { - const publicGasUsed = Object.values(this.publicOutput.gasUsed).reduce( - (total, current) => total.add(current), - Gas.empty(), - ); - const teardownGas = this.publicOutput.gasUsed[PublicKernelType.TEARDOWN] ?? Gas.empty(); - - return { - totalGas: privateGasUsed.add(publicGasUsed).mul(1 + pad), - teardownGas: teardownGas.mul(1 + pad), - }; - } - - return { totalGas: privateGasUsed.mul(1 + pad), teardownGas: Gas.empty() }; - } - /** * Convert a SimulatedTx class object to a plain JSON object. * @returns A plain object with SimulatedTx properties. diff --git a/yarn-project/end-to-end/src/e2e_fees/dapp_subscription.test.ts b/yarn-project/end-to-end/src/e2e_fees/dapp_subscription.test.ts index 53702e7048cc..17027e7520af 100644 --- a/yarn-project/end-to-end/src/e2e_fees/dapp_subscription.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/dapp_subscription.test.ts @@ -44,7 +44,7 @@ describe('e2e_fees dapp_subscription', () => { beforeAll(async () => { await t.applyBaseSnapshots(); - await t.applyFundAlice(); + await t.applyFundAliceWithBananas(); await t.applySetupSubscription(); ({ diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index 39cd2308db8f..3f115a52c46d 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -203,7 +203,7 @@ export class FeesTest { ); } - public async applyFundAlice() { + public async applyFundAliceWithBananas() { await this.snapshotManager.snapshot( 'fund_alice', async () => { @@ -214,6 +214,16 @@ export class FeesTest { ); } + public async applyFundAliceWithGasToken() { + await this.snapshotManager.snapshot( + 'fund_alice_with_gas_token', + async () => { + await this.gasTokenContract.methods.mint_public(this.aliceAddress, this.INITIAL_GAS_BALANCE).send().wait(); + }, + () => Promise.resolve(), + ); + } + public async applySetupSubscription() { await this.snapshotManager.snapshot( 'setup_subscription', diff --git a/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts b/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts new file mode 100644 index 000000000000..6b90cee1fbad --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts @@ -0,0 +1,66 @@ +import { + type AccountWallet, + type AztecAddress, + type FeePaymentMethod, + NativeFeePaymentMethod, + PublicFeePaymentMethod, +} from '@aztec/aztec.js'; +import { GasFees, type GasSettings } from '@aztec/circuits.js'; +import { type TokenContract as BananaCoin, type FPCContract } from '@aztec/noir-contracts.js'; + +import { FeesTest } from './fees_test.js'; + +describe('e2e_fees gas_estimation', () => { + let aliceWallet: AccountWallet; + let aliceAddress: AztecAddress; + let bobAddress: AztecAddress; + let bananaCoin: BananaCoin; + let bananaFPC: FPCContract; + let gasSettings: GasSettings; + let teardownFixedFee: bigint; + + const t = new FeesTest('gas_estimation'); + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.applyFundAliceWithBananas(); + await t.applyFundAliceWithGasToken(); + ({ aliceWallet, aliceAddress, bobAddress, bananaCoin, bananaFPC, gasSettings } = await t.setup()); + + teardownFixedFee = gasSettings.teardownGasLimits.computeFee(GasFees.default()).toBigInt(); + }); + + afterAll(async () => { + await t.teardown(); + }); + + // Sends two tx with transfers of public tokens: one with estimateGas on, one with estimateGas off + const sendTransfers = (paymentMethod: FeePaymentMethod) => + Promise.all( + [true, false].map(estimateGas => + bananaCoin.methods + .transfer_public(aliceAddress, bobAddress, 1n, 0n) + .send({ estimateGas, fee: { gasSettings, paymentMethod } }) + .wait(), + ), + ); + + it('estimates gas with native fee payment method', async () => { + const paymentMethod = await NativeFeePaymentMethod.create(aliceWallet); + const [withEstimate, withoutEstimate] = await sendTransfers(paymentMethod); + + // Estimation should yield that teardown has no cost, so should send the tx with zero for teardown + expect(withEstimate.transactionFee! + teardownFixedFee).toEqual(withoutEstimate.transactionFee!); + }); + + it('estimates gas with public payment method', async () => { + const paymentMethod = new PublicFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet); + const [withEstimate, withoutEstimate] = await sendTransfers(paymentMethod); + + // Estimation should yield that teardown has reduced cost, but is not zero + // TODO(palla/gas): We set toBeGreaterThanOrEqual because gas in public functions is zero for now (we only meter on AVM). + // We should be able to change this to a strict equality once we meter gas in public functions or we replace brillig with the AVM. + expect(withEstimate.transactionFee!).toBeLessThan(withoutEstimate.transactionFee!); + expect(withEstimate.transactionFee! + teardownFixedFee).toBeGreaterThanOrEqual(withoutEstimate.transactionFee!); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts b/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts index ec8b0909724b..c8d76bf4474a 100644 --- a/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts @@ -27,7 +27,7 @@ describe('e2e_fees private_payment', () => { beforeAll(async () => { await t.applyBaseSnapshots(); - await t.applyFundAlice(); + await t.applyFundAliceWithBananas(); ({ aliceWallet, aliceAddress, bobAddress, sequencerAddress, gasTokenContract, bananaCoin, bananaFPC, gasSettings } = await t.setup()); });