diff --git a/noir-projects/aztec-nr/aztec/src/context/avm_context.nr b/noir-projects/aztec-nr/aztec/src/context/avm_context.nr index cb2c3d26e89c..016e4860c77c 100644 --- a/noir-projects/aztec-nr/aztec/src/context/avm_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/avm_context.nr @@ -78,6 +78,11 @@ impl PublicContextInterface for AvmContext { 0 } + fn transaction_fee(self) -> Field { + assert(false, "'transaction_fee' not implemented!"); + 0 + } + fn nullifier_exists(self, unsiloed_nullifier: Field, address: AztecAddress) -> bool { nullifier_exists(unsiloed_nullifier, address.to_field()) == 1 } diff --git a/noir-projects/aztec-nr/aztec/src/context/interface.nr b/noir-projects/aztec-nr/aztec/src/context/interface.nr index 175d93cc2c44..5051d98511b7 100644 --- a/noir-projects/aztec-nr/aztec/src/context/interface.nr +++ b/noir-projects/aztec-nr/aztec/src/context/interface.nr @@ -54,6 +54,7 @@ trait PublicContextInterface { args: [Field] ) -> FunctionReturns; fn nullifier_exists(self, unsiloed_nullifier: Field, address: AztecAddress) -> bool; + fn transaction_fee(self) -> Field; } struct PrivateCallInterface { diff --git a/noir-projects/aztec-nr/aztec/src/context/public_context.nr b/noir-projects/aztec-nr/aztec/src/context/public_context.nr index 33765f8f3ec5..0e7e9435105d 100644 --- a/noir-projects/aztec-nr/aztec/src/context/public_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/public_context.nr @@ -239,6 +239,10 @@ impl PublicContextInterface for PublicContext { self.inputs.public_global_variables.gas_fees.fee_per_l2_gas } + fn transaction_fee(self) -> Field { + self.inputs.transaction_fee + } + fn nullifier_exists(self, unsiloed_nullifier: Field, address: AztecAddress) -> bool { // Current public can only check for settled nullifiers, so we always silo. let siloed_nullifier = silo_nullifier(address, unsiloed_nullifier); diff --git a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr index c3233b2d55de..f049473ea572 100644 --- a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr @@ -31,6 +31,7 @@ contract AppSubscription { subscription_price: SharedImmutable, subscriptions: Map>, gas_token_address: SharedImmutable, + gas_token_limit_per_tx: SharedImmutable, } global SUBSCRIPTION_DURATION_IN_BLOCKS = 5; @@ -47,7 +48,8 @@ contract AppSubscription { note.remaining_txs -= 1; storage.subscriptions.at(user_address).replace(&mut note, true); - GasToken::at(storage.gas_token_address.read_private()).pay_fee(42).enqueue(&mut context); + let gas_limit = storage.gas_token_limit_per_tx.read_private(); + GasToken::at(storage.gas_token_address.read_private()).pay_fee(gas_limit).enqueue(&mut context); context.end_setup(); @@ -63,14 +65,15 @@ contract AppSubscription { subscription_recipient_address: AztecAddress, subscription_token_address: AztecAddress, subscription_price: Field, - gas_token_address: AztecAddress + gas_token_address: AztecAddress, + gas_token_limit_per_tx: Field ) { storage.target_address.initialize(target_address); storage.subscription_token_address.initialize(subscription_token_address); storage.subscription_recipient_address.initialize(subscription_recipient_address); storage.subscription_price.initialize(subscription_price); - storage.gas_token_address.initialize(gas_token_address); + storage.gas_token_limit_per_tx.initialize(gas_token_limit_per_tx); } #[aztec(public)] diff --git a/noir-projects/noir-contracts/contracts/gas_token_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/gas_token_contract/src/lib.nr index 9e1afebac298..e3a5fc12684f 100644 --- a/noir-projects/noir-contracts/contracts/gas_token_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/gas_token_contract/src/lib.nr @@ -2,8 +2,8 @@ use dep::aztec::prelude::{AztecAddress, EthAddress}; use dep::aztec::context::interface::PublicContextInterface; use dep::aztec::protocol_types::hash::sha256_to_field; -pub fn calculate_fee(_context: TPublicContext) -> U128 where TPublicContext: PublicContextInterface { - U128::from_integer(1) +pub fn calculate_fee(context: TPublicContext) -> Field where TPublicContext: PublicContextInterface { + context.transaction_fee() } pub fn get_bridge_gas_msg_hash(owner: AztecAddress, amount: Field) -> Field { diff --git a/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr index fd2bb0356eca..3b46f9b53fad 100644 --- a/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr @@ -54,7 +54,11 @@ contract GasToken { #[aztec(public)] fn pay_fee(fee_limit: Field) -> Field { let fee_limit_u128 = U128::from_integer(fee_limit); - let fee = calculate_fee(context); + let fee = U128::from_integer(calculate_fee(context)); + dep::aztec::oracle::debug_log::debug_log_format( + "Gas token: paying fee {0} (limit {1})", + [fee.to_field(), fee_limit] + ); assert(fee <= fee_limit_u128, "Fee too high"); let sender_new_balance = storage.balances.at(context.msg_sender()).read() - fee; diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 3bf5b97ab46f..343130a1b38c 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -676,7 +676,7 @@ export class AztecNodeService implements AztecNode { this.log.warn(`Simulated tx ${tx.getTxHash()} reverts: ${reverted[0].revertReason}`); throw reverted[0].revertReason; } - this.log.info(`Simulated tx ${tx.getTxHash()} succeeds`); + this.log.debug(`Simulated tx ${tx.getTxHash()} succeeds`); const [processedTx] = processedTxs; return { constants: processedTx.data.constants, diff --git a/yarn-project/circuit-types/src/interfaces/configs.ts b/yarn-project/circuit-types/src/interfaces/configs.ts index 2698d7aeb78f..fe44947209bd 100644 --- a/yarn-project/circuit-types/src/interfaces/configs.ts +++ b/yarn-project/circuit-types/src/interfaces/configs.ts @@ -33,10 +33,8 @@ export interface SequencerConfig { acvmWorkingDirectory?: string; /** The path to the ACVM binary */ acvmBinaryPath?: string; - /** The list of functions calls allowed to run in setup */ allowedFunctionsInSetup?: AllowedFunction[]; - /** The list of functions calls allowed to run teardown */ allowedFunctionsInTeardown?: AllowedFunction[]; } diff --git a/yarn-project/end-to-end/src/client_prover_integration/client_prover_test.ts b/yarn-project/end-to-end/src/client_prover_integration/client_prover_test.ts index d511f5ce7164..346147d92d0e 100644 --- a/yarn-project/end-to-end/src/client_prover_integration/client_prover_test.ts +++ b/yarn-project/end-to-end/src/client_prover_integration/client_prover_test.ts @@ -19,9 +19,10 @@ import * as fs from 'fs/promises'; import { waitRegisteredAccountSynced } from '../benchmarks/utils.js'; import { - SnapshotManager, + type ISnapshotManager, type SubsystemsContext, addAccounts, + createSnapshotManager, publicDeployAccounts, } from '../fixtures/snapshot_manager.js'; import { getBBConfig, setupPXEService } from '../fixtures/utils.js'; @@ -42,7 +43,7 @@ export class ClientProverTest { static TOKEN_NAME = 'Aztec Token'; static TOKEN_SYMBOL = 'AZT'; static TOKEN_DECIMALS = 18n; - private snapshotManager: SnapshotManager; + private snapshotManager: ISnapshotManager; logger: DebugLogger; keys: Array<[Fr, Fq]> = []; wallets: AccountWalletWithSecretKey[] = []; @@ -59,7 +60,7 @@ export class ClientProverTest { constructor(testName: string) { this.logger = createDebugLogger(`aztec:client_prover_test:${testName}`); - this.snapshotManager = new SnapshotManager(`client_prover_integration/${testName}`, dataPath); + this.snapshotManager = createSnapshotManager(`client_prover_integration/${testName}`, dataPath); } /** diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts index afbb66309f82..708f1cbfb3dd 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts @@ -13,9 +13,10 @@ import { import { DocsExampleContract, TokenBlacklistContract, type TokenContract } from '@aztec/noir-contracts.js'; import { - SnapshotManager, + type ISnapshotManager, type SubsystemsContext, addAccounts, + createSnapshotManager, publicDeployAccounts, } from '../fixtures/snapshot_manager.js'; import { TokenSimulator } from '../simulators/token_simulator.js'; @@ -54,7 +55,7 @@ export class BlacklistTokenContractTest { // This value MUST match the same value that we have in the contract static DELAY = 2; - private snapshotManager: SnapshotManager; + private snapshotManager: ISnapshotManager; logger: DebugLogger; wallets: AccountWallet[] = []; accounts: CompleteAddress[] = []; @@ -68,7 +69,7 @@ export class BlacklistTokenContractTest { constructor(testName: string) { this.logger = createDebugLogger(`aztec:e2e_blacklist_token_contract:${testName}`); - this.snapshotManager = new SnapshotManager(`e2e_blacklist_token_contract/${testName}`, dataPath); + this.snapshotManager = createSnapshotManager(`e2e_blacklist_token_contract/${testName}`, dataPath); } async mineBlocks(amount: number = BlacklistTokenContractTest.DELAY) { diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts index b003195c3933..ab535e3e61ab 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts @@ -14,7 +14,7 @@ describe('e2e_blacklist_token_contract mint', () => { await t.setup(); // Have to destructure again to ensure we have latest refs. ({ asset, tokenSim, wallets, blacklisted } = t); - }, 200_000); + }, 300_000); afterAll(async () => { await t.teardown(); diff --git a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts deleted file mode 100644 index 109c89c181d9..000000000000 --- a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { - type AccountWalletWithSecretKey, - type AztecAddress, - type AztecNode, - type DebugLogger, - type DeployL1Contracts, - type FeePaymentMethod, - Fr, - type PXE, - PrivateFeePaymentMethod, - PublicFeePaymentMethod, - SentTx, -} from '@aztec/aztec.js'; -import { GasSettings } from '@aztec/circuits.js'; -import { DefaultDappEntrypoint } from '@aztec/entrypoints/dapp'; -import { - AppSubscriptionContract, - TokenContract as BananaCoin, - CounterContract, - FPCContract, - GasTokenContract, -} from '@aztec/noir-contracts.js'; -import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; - -import { type BalancesFn, expectMapping, getBalancesFn, publicDeployAccounts, setup } from './fixtures/utils.js'; - -const TOKEN_NAME = 'BananaCoin'; -const TOKEN_SYMBOL = 'BC'; -const TOKEN_DECIMALS = 18n; - -describe('e2e_dapp_subscription', () => { - let pxe: PXE; - let logger: DebugLogger; - - let aliceWallet: AccountWalletWithSecretKey; - let bobWallet: AccountWalletWithSecretKey; - let aliceAddress: AztecAddress; // Dapp subscriber. - let bobAddress: AztecAddress; // Dapp owner. - let sequencerAddress: AztecAddress; - // let gasTokenContract: GasTokenContract; - let bananaCoin: BananaCoin; - let counterContract: CounterContract; - let subscriptionContract: AppSubscriptionContract; - let gasTokenContract: GasTokenContract; - let bananaFPC: FPCContract; - let gasBalances: BalancesFn; - let bananasPublicBalances: BalancesFn; - let bananasPrivateBalances: BalancesFn; - - const SUBSCRIPTION_AMOUNT = BigInt(100e9); - const INITIAL_GAS_BALANCE = BigInt(1000e9); - const PUBLICLY_MINTED_BANANAS = BigInt(500e9); - const PRIVATELY_MINTED_BANANAS = BigInt(600e9); - - const FEE_AMOUNT = 1n; - const MAX_FEE = BigInt(20e9); - - const GAS_SETTINGS = GasSettings.default(); - - beforeAll(async () => { - process.env.PXE_URL = ''; - process.env.ENABLE_GAS ??= '1'; - - expect(GAS_SETTINGS.getFeeLimit().toBigInt()).toEqual(MAX_FEE); - - let wallets: AccountWalletWithSecretKey[]; - let aztecNode: AztecNode; - let deployL1ContractsValues: DeployL1Contracts; - ({ wallets, aztecNode, deployL1ContractsValues, logger, pxe } = await setup(3, {}, {}, true)); - - await publicDeployAccounts(wallets[0], wallets); - - // this should be a SignerlessWallet but that can't call public functions directly - gasTokenContract = await GasTokenContract.at( - getCanonicalGasTokenAddress(deployL1ContractsValues.l1ContractAddresses.gasPortalAddress), - wallets[0], - ); - - aliceAddress = wallets[0].getAddress(); - bobAddress = wallets[1].getAddress(); - sequencerAddress = wallets[2].getAddress(); - - await aztecNode.setConfig({ - feeRecipient: sequencerAddress, - }); - - [aliceWallet, bobWallet] = wallets; - - bananaCoin = await BananaCoin.deploy(aliceWallet, aliceAddress, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS) - .send() - .deployed(); - bananaFPC = await FPCContract.deploy(aliceWallet, bananaCoin.address, gasTokenContract.address).send().deployed(); - - counterContract = await CounterContract.deploy(bobWallet, 0, bobAddress).send().deployed(); - - subscriptionContract = await AppSubscriptionContract.deploy( - bobWallet, - counterContract.address, - bobAddress, - // anyone can purchase a subscription for 100 test tokens - bananaCoin.address, - SUBSCRIPTION_AMOUNT, - gasTokenContract.address, - ) - .send() - .deployed(); - - // mint some test tokens for Alice - // she'll pay for the subscription with these - await bananaCoin.methods.privately_mint_private_note(PRIVATELY_MINTED_BANANAS).send().wait(); - await bananaCoin.methods.mint_public(aliceAddress, PUBLICLY_MINTED_BANANAS).send().wait(); - await gasTokenContract.methods.mint_public(subscriptionContract.address, INITIAL_GAS_BALANCE).send().wait(); - await gasTokenContract.methods.mint_public(bananaFPC.address, INITIAL_GAS_BALANCE).send().wait(); - - gasBalances = getBalancesFn('⛽', gasTokenContract.methods.balance_of_public, logger); - bananasPublicBalances = getBalancesFn('Public 🍌', bananaCoin.methods.balance_of_public, logger); - bananasPrivateBalances = getBalancesFn('Private 🍌', bananaCoin.methods.balance_of_private, logger); - - await expectMapping( - gasBalances, - [aliceAddress, sequencerAddress, subscriptionContract.address, bananaFPC.address], - [0n, 0n, INITIAL_GAS_BALANCE, INITIAL_GAS_BALANCE], - ); - }); - - it('should allow Alice to subscribe by paying privately with bananas', async () => { - /** - PRIVATE SETUP - we first unshield `MAX_FEE` BC from alice's private balance to the FPC's public balance - - PUBLIC APP LOGIC - we then privately transfer `SUBSCRIPTION_AMOUNT` BC from alice to bob's subscription contract - - PUBLIC TEARDOWN - then the FPC calls `pay_fee`, reducing its gas balance by `FEE_AMOUNT`, and increasing the sequencer's gas balance by `FEE_AMOUNT` - the FPC also publicly sends `REFUND` BC to alice - */ - - await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), MAX_FEE); - - await expectMapping( - bananasPrivateBalances, - [aliceAddress, bobAddress, bananaFPC.address], - [PRIVATELY_MINTED_BANANAS - SUBSCRIPTION_AMOUNT - MAX_FEE, SUBSCRIPTION_AMOUNT, 0n], - ); - - await expectMapping( - bananasPublicBalances, - [aliceAddress, bobAddress, bananaFPC.address], - // refund is done via a transparent note for now - [PUBLICLY_MINTED_BANANAS, 0n, FEE_AMOUNT], - ); - - await expectMapping( - gasBalances, - // note the subscription contract hasn't paid any fees yet - [bananaFPC.address, subscriptionContract.address, sequencerAddress], - [INITIAL_GAS_BALANCE - FEE_AMOUNT, INITIAL_GAS_BALANCE, FEE_AMOUNT], - ); - - // REFUND_AMOUNT is a transparent note note - }); - - it('should allow Alice to subscribe by paying with bananas in public', async () => { - /** - PRIVATE SETUP - we publicly transfer `MAX_FEE` BC from alice's public balance to the FPC's public balance - - PUBLIC APP LOGIC - we then privately transfer `SUBSCRIPTION_AMOUNT` BC from alice to bob's subscription contract - - PUBLIC TEARDOWN - then the FPC calls `pay_fee`, reducing its gas balance by `FEE_AMOUNT`, and increasing the sequencer's gas balance by `FEE_AMOUNT` - the FPC also publicly sends `REFUND` BC to alice - */ - await subscribe(new PublicFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), MAX_FEE); - - await expectMapping( - bananasPrivateBalances, - [aliceAddress, bobAddress, bananaFPC.address], - // we pay the fee publicly, but the subscription payment is still private. - // Also, minus 1 x MAX_FEE as leftover from the previous test, since we paid publicly this time - [PRIVATELY_MINTED_BANANAS - 2n * SUBSCRIPTION_AMOUNT - MAX_FEE, 2n * SUBSCRIPTION_AMOUNT, 0n], - ); - - await expectMapping( - bananasPublicBalances, - [aliceAddress, bobAddress, bananaFPC.address], - [ - // we have the refund from the previous test, - // but since we paid publicly this time, the refund should have been "squashed" - PUBLICLY_MINTED_BANANAS - FEE_AMOUNT, - 0n, // Bob still has no public bananas - 2n * FEE_AMOUNT, // because this is the second time we've used the FPC - ], - ); - - await expectMapping( - gasBalances, - [subscriptionContract.address, bananaFPC.address, sequencerAddress], - [INITIAL_GAS_BALANCE, INITIAL_GAS_BALANCE - 2n * FEE_AMOUNT, 2n * FEE_AMOUNT], - ); - }); - - it('should call dapp subscription entrypoint', async () => { - const dappPayload = new DefaultDappEntrypoint(aliceAddress, aliceWallet, subscriptionContract.address); - const action = counterContract.methods.increment(bobAddress).request(); - const txExReq = await dappPayload.createTxExecutionRequest({ calls: [action] }); - const tx = await pxe.proveTx(txExReq, true); - const sentTx = new SentTx(pxe, pxe.sendTx(tx)); - await sentTx.wait(); - - expect(await counterContract.methods.get_counter(bobAddress).simulate()).toBe(1n); - - await expectMapping( - gasBalances, - [subscriptionContract.address, bananaFPC.address, sequencerAddress], - [INITIAL_GAS_BALANCE - FEE_AMOUNT, INITIAL_GAS_BALANCE - 2n * FEE_AMOUNT, FEE_AMOUNT * 3n], - ); - }); - - it('should reject after the sub runs out', async () => { - // subscribe again. This will overwrite the subscription - await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), MAX_FEE, 0); - await expect(dappIncrement()).rejects.toThrow( - "Failed to solve brillig function '(context.block_number()) as u64 < expiry_block_number as u64'", - ); - }); - - it('should reject after the txs run out', async () => { - // subscribe again. This will overwrite the subscription - await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), FEE_AMOUNT, 5, 1); - await expect(dappIncrement()).resolves.toBeDefined(); - await expect(dappIncrement()).rejects.toThrow(/note.remaining_txs as u64 > 0/); - }); - - async function subscribe( - paymentMethod: FeePaymentMethod, - maxFee: bigint, - blockDelta: number = 5, - txCount: number = 4, - ) { - const nonce = Fr.random(); - const action = bananaCoin.methods.transfer(aliceAddress, bobAddress, SUBSCRIPTION_AMOUNT, nonce); - await aliceWallet.createAuthWit({ caller: subscriptionContract.address, action }); - - return subscriptionContract - .withWallet(aliceWallet) - .methods.subscribe(aliceAddress, nonce, (await pxe.getBlockNumber()) + blockDelta, txCount) - .send({ fee: { gasSettings: GAS_SETTINGS, paymentMethod } }) - .wait(); - } - - async function dappIncrement() { - const dappEntrypoint = new DefaultDappEntrypoint(aliceAddress, aliceWallet, subscriptionContract.address); - const action = counterContract.methods.increment(bobAddress).request(); - const txExReq = await dappEntrypoint.createTxExecutionRequest({ calls: [action] }); - const tx = await pxe.proveTx(txExReq, true); - const sentTx = new SentTx(pxe, pxe.sendTx(tx)); - return sentTx.wait(); - } -}); diff --git a/yarn-project/end-to-end/src/e2e_delegate_calls/delegate_calls_test.ts b/yarn-project/end-to-end/src/e2e_delegate_calls/delegate_calls_test.ts index 457408a05f7a..0911bd0bc80a 100644 --- a/yarn-project/end-to-end/src/e2e_delegate_calls/delegate_calls_test.ts +++ b/yarn-project/end-to-end/src/e2e_delegate_calls/delegate_calls_test.ts @@ -2,12 +2,17 @@ import { getSchnorrAccount } from '@aztec/accounts/schnorr'; import { type AccountWallet, type DebugLogger, createDebugLogger } from '@aztec/aztec.js'; import { DelegatedOnContract, DelegatorContract } from '@aztec/noir-contracts.js'; -import { SnapshotManager, type SubsystemsContext, addAccounts } from '../fixtures/snapshot_manager.js'; +import { + type ISnapshotManager, + type SubsystemsContext, + addAccounts, + createSnapshotManager, +} from '../fixtures/snapshot_manager.js'; const { E2E_DATA_PATH: dataPath } = process.env; export class DelegateCallsTest { - private snapshotManager: SnapshotManager; + private snapshotManager: ISnapshotManager; logger: DebugLogger; wallet!: AccountWallet; delegatorContract!: DelegatorContract; @@ -15,7 +20,7 @@ export class DelegateCallsTest { constructor(testName: string) { this.logger = createDebugLogger(`aztec:e2e_delegate_calls:${testName}`); - this.snapshotManager = new SnapshotManager(`e2e_delegate_calls/${testName}`, dataPath); + this.snapshotManager = createSnapshotManager(`e2e_delegate_calls/${testName}`, dataPath); } /** @@ -27,7 +32,7 @@ export class DelegateCallsTest { await this.snapshotManager.snapshot('accounts', addAccounts(1, this.logger), async ({ accountKeys }, { pxe }) => { const accountManager = getSchnorrAccount(pxe, accountKeys[0][0], accountKeys[0][1], 1); this.wallet = await accountManager.getWallet(); - this.logger.verbose(`Wallet address: ${this.wallet.getAddress()}`); + this.logger.verbose(`Wallet address: ${this.wallet.getAddress()}`); }); await this.snapshotManager.snapshot( diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts index cc44a09b51f4..05b314228286 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_test.ts @@ -15,12 +15,12 @@ import { } from '@aztec/aztec.js'; import { type StatefulTestContract } from '@aztec/noir-contracts.js'; -import { SnapshotManager, addAccounts } from '../fixtures/snapshot_manager.js'; +import { type ISnapshotManager, addAccounts, createSnapshotManager } from '../fixtures/snapshot_manager.js'; const { E2E_DATA_PATH: dataPath } = process.env; export class DeployTest { - private snapshotManager: SnapshotManager; + private snapshotManager: ISnapshotManager; private wallets: AccountWallet[] = []; public logger: DebugLogger; @@ -30,7 +30,7 @@ export class DeployTest { constructor(testName: string) { this.logger = createDebugLogger(`aztec:e2e_deploy_contract:${testName}`); - this.snapshotManager = new SnapshotManager(`e2e_deploy_contract/${testName}`, dataPath); + this.snapshotManager = createSnapshotManager(`e2e_deploy_contract/${testName}`, dataPath); } async setup() { diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts index 6567c8e5cc06..84dab9e5065e 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts @@ -29,7 +29,6 @@ describe('e2e_deploy_contract private initialization', () => { const expected = siloNullifier(contract.address, new Fr(10)); expect(receipt.debugInfo?.nullifiers[1]).toEqual(expected); }, - 30_000, ); // Tests privately initializing an undeployed contract. Also requires pxe registration in advance. @@ -51,7 +50,6 @@ describe('e2e_deploy_contract private initialization', () => { await contract.methods.create_note(owner, 10).send().wait(); expect(await contract.methods.summed_values(owner).simulate()).toEqual(52n); }, - 30_000, ); // Tests privately initializing multiple undeployed contracts on the same tx through an account contract. diff --git a/yarn-project/end-to-end/src/e2e_fees.test.ts b/yarn-project/end-to-end/src/e2e_fees.test.ts deleted file mode 100644 index b521e7c8ede5..000000000000 --- a/yarn-project/end-to-end/src/e2e_fees.test.ts +++ /dev/null @@ -1,776 +0,0 @@ -import { - type AccountWallet, - type AztecAddress, - BatchCall, - type DebugLogger, - ExtendedNote, - Fr, - type FunctionCall, - FunctionSelector, - Note, - PrivateFeePaymentMethod, - PublicFeePaymentMethod, - type TxHash, - TxStatus, - type Wallet, - computeAuthWitMessageHash, - computeSecretHash, -} from '@aztec/aztec.js'; -import { FunctionData, GasSettings } from '@aztec/circuits.js'; -import { type ContractArtifact, decodeFunctionSignature } from '@aztec/foundation/abi'; -import { - TokenContract as BananaCoin, - FPCContract, - GasTokenContract, - SchnorrAccountContract, -} from '@aztec/noir-contracts.js'; - -import { jest } from '@jest/globals'; - -import { type BalancesFn, expectMapping, getBalancesFn, publicDeployAccounts, setup } from './fixtures/utils.js'; -import { GasPortalTestingHarnessFactory, type IGasBridgingTestHarness } from './shared/gas_portal_test_harness.js'; - -const TOKEN_NAME = 'BananaCoin'; -const TOKEN_SYMBOL = 'BC'; -const TOKEN_DECIMALS = 18n; -const BRIDGED_FPC_GAS = 500n; - -jest.setTimeout(1_000_000_000); - -describe('e2e_fees', () => { - let wallets: AccountWallet[]; - let aliceWallet: Wallet; - let aliceAddress: AztecAddress; - let bobAddress: AztecAddress; - let sequencerAddress: AztecAddress; - let gasTokenContract: GasTokenContract; - let bananaCoin: BananaCoin; - let bananaFPC: FPCContract; - let logger: DebugLogger; - - let gasBridgeTestHarness: IGasBridgingTestHarness; - - let gasBalances: BalancesFn; - let bananaPublicBalances: BalancesFn; - let bananaPrivateBalances: BalancesFn; - - const gasSettings = GasSettings.default(); - - beforeAll(async () => { - const ctx = await setup(3, {}, {}, true); - const { aztecNode, deployL1ContractsValues, pxe } = ctx; - ({ wallets, logger } = ctx); - - logFunctionSignatures(BananaCoin.artifact, logger); - logFunctionSignatures(FPCContract.artifact, logger); - logFunctionSignatures(GasTokenContract.artifact, logger); - logFunctionSignatures(SchnorrAccountContract.artifact, logger); - - await aztecNode.setConfig({ - feeRecipient: wallets.at(-1)!.getAddress(), - }); - - aliceWallet = wallets[0]; - aliceAddress = wallets[0].getAddress(); - bobAddress = wallets[1].getAddress(); - sequencerAddress = wallets[2].getAddress(); - - gasBridgeTestHarness = await GasPortalTestingHarnessFactory.create({ - aztecNode: aztecNode, - pxeService: pxe, - publicClient: deployL1ContractsValues.publicClient, - walletClient: deployL1ContractsValues.walletClient, - wallet: wallets[0], - logger, - mockL1: false, - }); - - gasTokenContract = gasBridgeTestHarness.l2Token; - - bananaCoin = await BananaCoin.deploy(wallets[0], wallets[0].getAddress(), TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS) - .send() - .deployed(); - - logger.info(`BananaCoin deployed at ${bananaCoin.address}`); - - bananaFPC = await FPCContract.deploy(wallets[0], bananaCoin.address, gasTokenContract.address).send().deployed(); - logger.info(`BananaPay deployed at ${bananaFPC.address}`); - await publicDeployAccounts(wallets[0], wallets); - - await gasBridgeTestHarness.bridgeFromL1ToL2(BRIDGED_FPC_GAS, BRIDGED_FPC_GAS, bananaFPC.address); - - bananaPublicBalances = getBalancesFn('🍌.public', bananaCoin.methods.balance_of_public, logger); - bananaPrivateBalances = getBalancesFn('🍌.private', bananaCoin.methods.balance_of_private, logger); - gasBalances = getBalancesFn('⛽', gasTokenContract.methods.balance_of_public, logger); - await expectMapping(bananaPrivateBalances, [aliceAddress, bananaFPC.address, sequencerAddress], [0n, 0n, 0n]); - await expectMapping(bananaPublicBalances, [aliceAddress, bananaFPC.address, sequencerAddress], [0n, 0n, 0n]); - await expectMapping(gasBalances, [aliceAddress, bananaFPC.address, sequencerAddress], [0n, BRIDGED_FPC_GAS, 0n]); - }); - - it('reverts transactions but still pays fees using PublicFeePaymentMethod', async () => { - const OutrageousPublicAmountAliceDoesNotHave = BigInt(1e15); - const PublicMintedAlicePublicBananas = BigInt(1e12); - const FeeAmount = 1n; - - const [initialAlicePrivateBananas, initialFPCPrivateBananas] = await bananaPrivateBalances( - aliceAddress, - bananaFPC.address, - ); - const [initialAlicePublicBananas, initialFPCPublicBananas] = await bananaPublicBalances( - aliceAddress, - bananaFPC.address, - ); - const [initialAliceGas, initialFPCGas, initialSequencerGas] = await gasBalances( - aliceAddress, - bananaFPC.address, - sequencerAddress, - ); - - await bananaCoin.methods.mint_public(aliceAddress, PublicMintedAlicePublicBananas).send().wait(); - // if we simulate locally, it throws an error - await expect( - bananaCoin.methods - .transfer_public(aliceAddress, sequencerAddress, OutrageousPublicAmountAliceDoesNotHave, 0) - .send({ - fee: { - gasSettings, - paymentMethod: new PublicFeePaymentMethod(bananaCoin.address, bananaFPC.address, wallets[0]), - }, - }) - .wait(), - ).rejects.toThrow(/attempt to subtract with underflow 'hi == high'/); - - // we did not pay the fee, because we did not submit the TX - await expectMapping( - bananaPrivateBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [initialAlicePrivateBananas, initialFPCPrivateBananas, 0n], - ); - await expectMapping( - bananaPublicBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [initialAlicePublicBananas + PublicMintedAlicePublicBananas, initialFPCPublicBananas, 0n], - ); - await expectMapping( - gasBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [initialAliceGas, initialFPCGas, initialSequencerGas], - ); - - // if we skip simulation, it includes the failed TX - const txReceipt = await bananaCoin.methods - .transfer_public(aliceAddress, sequencerAddress, OutrageousPublicAmountAliceDoesNotHave, 0) - .send({ - skipPublicSimulation: true, - fee: { - gasSettings, - paymentMethod: new PublicFeePaymentMethod(bananaCoin.address, bananaFPC.address, wallets[0]), - }, - }) - .wait({ dontThrowOnRevert: true }); - expect(txReceipt.status).toBe(TxStatus.REVERTED); - - // and thus we paid the fee - await expectMapping( - bananaPrivateBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [initialAlicePrivateBananas, initialFPCPrivateBananas, 0n], - ); - await expectMapping( - bananaPublicBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [initialAlicePublicBananas + PublicMintedAlicePublicBananas - FeeAmount, initialFPCPublicBananas + FeeAmount, 0n], - ); - await expectMapping( - gasBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [initialAliceGas, initialFPCGas - FeeAmount, initialSequencerGas + FeeAmount], - ); - - // TODO(#4712) - demonstrate reverts with the PrivateFeePaymentMethod. - // Can't do presently because all logs are "revertible" so we lose notes that get broadcasted during unshielding. - }); - - describe('private fees payments', () => { - let InitialAlicePrivateBananas: bigint; - let InitialAlicePublicBananas: bigint; - let InitialAliceGas: bigint; - - let InitialBobPrivateBananas: bigint; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let InitialBobPublicBananas: bigint; - - let InitialFPCPrivateBananas: bigint; - let InitialFPCPublicBananas: bigint; - let InitialFPCGas: bigint; - - let InitialSequencerGas: bigint; - - let MaxFee: bigint; - let FeeAmount: bigint; - let RefundAmount: bigint; - let RefundSecret: Fr; - - beforeAll(async () => { - // Fund Alice private and publicly - await mintPrivate(BigInt(1e12), aliceAddress); - await bananaCoin.methods.mint_public(aliceAddress, 1e12).send().wait(); - }); - - beforeEach(async () => { - FeeAmount = 1n; - MaxFee = BigInt(20e9); - RefundAmount = MaxFee - FeeAmount; - RefundSecret = Fr.random(); - - expect(gasSettings.getFeeLimit().toBigInt()).toEqual(MaxFee); - - [ - [InitialAlicePrivateBananas, InitialBobPrivateBananas, InitialFPCPrivateBananas], - [InitialAlicePublicBananas, InitialBobPublicBananas, InitialFPCPublicBananas], - [InitialAliceGas, InitialFPCGas, InitialSequencerGas], - ] = await Promise.all([ - bananaPrivateBalances(aliceAddress, bobAddress, bananaFPC.address), - bananaPublicBalances(aliceAddress, bobAddress, bananaFPC.address), - gasBalances(aliceAddress, bananaFPC.address, sequencerAddress), - ]); - }); - - it('pays fees for tx that dont run public app logic', async () => { - /** - * PRIVATE SETUP (1 nullifier for tx) - * check authwit (1 nullifier) - * reduce alice BC.private by MaxFee (1 nullifier) - * enqueue public call to increase FPC BC.public by MaxFee - * enqueue public call for fpc.pay_fee_with_shielded_rebate - * - * PRIVATE APP LOGIC - * reduce Alice's BC.private by transferAmount (1 note) - * create note for Bob of transferAmount (1 note) - * encrypted logs of 944 bytes - * unencrypted logs of 20 bytes - * - * PUBLIC SETUP - * increase FPC BC.public by MaxFee - * - * PUBLIC APP LOGIC - * N/A - * - * PUBLIC TEARDOWN - * call gas.pay_fee - * decrease FPC AZT by FeeAmount - * increase sequencer AZT by FeeAmount - * call banana.shield - * decrease FPC BC.public by RefundAmount - * create transparent note with RefundAmount - * - * this is expected to squash notes and nullifiers - */ - const transferAmount = 5n; - const tx = await bananaCoin.methods - .transfer(aliceAddress, bobAddress, transferAmount, 0n) - .send({ - fee: { - gasSettings, - paymentMethod: new PrivateFeePaymentMethod( - bananaCoin.address, - bananaFPC.address, - aliceWallet, - RefundSecret, - ), - }, - }) - .wait(); - - /** - * at present the user is paying DA gas for: - * 3 nullifiers = 3 * DA_BYTES_PER_FIELD * DA_GAS_PER_BYTE = 3 * 32 * 16 = 1536 DA gas - * 2 note hashes = 2 * DA_BYTES_PER_FIELD * DA_GAS_PER_BYTE = 2 * 32 * 16 = 1024 DA gas - * 964 bytes of logs = 964 * DA_GAS_PER_BYTE = 964 * 16 = 15424 DA gas - * tx overhead of 512 DA gas - * for a total of 18496 DA gas. - * - * The default teardown gas allocation at present is - * 100_000_000 for both DA and L2 gas. - * - * That produces a grand total of 200018496n. - * - * This will change because: - * 1. Gas use during public execution is not currently incorporated - * 2. We are presently squashing notes/nullifiers across non/revertible during private exeuction, - * but we shouldn't. - */ - - expect(tx.transactionFee).toEqual(200018496n); - - await expectMapping( - bananaPrivateBalances, - [aliceAddress, bobAddress, bananaFPC.address, sequencerAddress], - [InitialAlicePrivateBananas - MaxFee - transferAmount, transferAmount, InitialFPCPrivateBananas, 0n], - ); - await expectMapping( - bananaPublicBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [InitialAlicePublicBananas, InitialFPCPublicBananas + MaxFee - RefundAmount, 0n], - ); - await expectMapping( - gasBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [InitialAliceGas, InitialFPCGas - FeeAmount, InitialSequencerGas + FeeAmount], - ); - - await expect( - // this rejects if note can't be added - addPendingShieldNoteToPXE(0, RefundAmount, computeSecretHash(RefundSecret), tx.txHash), - ).resolves.toBeUndefined(); - }); - - it('pays fees for tx that creates notes in private', async () => { - /** - * PRIVATE SETUP - * check authwit - * reduce alice BC.private by MaxFee - * enqueue public call to increase FPC BC.public by MaxFee - * enqueue public call for fpc.pay_fee_with_shielded_rebate - * - * PRIVATE APP LOGIC - * increase alice BC.private by newlyMintedBananas - * - * PUBLIC SETUP - * increase FPC BC.public by MaxFee - * - * PUBLIC APP LOGIC - * BC increase total supply - * - * PUBLIC TEARDOWN - * call gas.pay_fee - * decrease FPC AZT by FeeAmount - * increase sequencer AZT by FeeAmount - * call banana.shield - * decrease FPC BC.public by RefundAmount - * create transparent note with RefundAmount - */ - const newlyMintedBananas = 10n; - const tx = await bananaCoin.methods - .privately_mint_private_note(newlyMintedBananas) - .send({ - fee: { - gasSettings, - paymentMethod: new PrivateFeePaymentMethod( - bananaCoin.address, - bananaFPC.address, - aliceWallet, - RefundSecret, - ), - }, - }) - .wait(); - - await expectMapping( - bananaPrivateBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [InitialAlicePrivateBananas - MaxFee + newlyMintedBananas, InitialFPCPrivateBananas, 0n], - ); - await expectMapping( - bananaPublicBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [InitialAlicePublicBananas, InitialFPCPublicBananas + MaxFee - RefundAmount, 0n], - ); - await expectMapping( - gasBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [InitialAliceGas, InitialFPCGas - FeeAmount, InitialSequencerGas + FeeAmount], - ); - - await expect( - // this rejects if note can't be added - addPendingShieldNoteToPXE(0, RefundAmount, computeSecretHash(RefundSecret), tx.txHash), - ).resolves.toBeUndefined(); - }); - - it('pays fees for tx that creates notes in public', async () => { - /** - * PRIVATE SETUP - * check authwit - * reduce alice BC.private by MaxFee - * enqueue public call to increase FPC BC.public by MaxFee - * enqueue public call for fpc.pay_fee_with_shielded_rebate - * - * PRIVATE APP LOGIC - * N/A - * - * PUBLIC SETUP - * increase FPC BC.public by MaxFee - * - * PUBLIC APP LOGIC - * BC decrease Alice public balance by shieldedBananas - * BC create transparent note of shieldedBananas - * - * PUBLIC TEARDOWN - * call gas.pay_fee - * decrease FPC AZT by FeeAmount - * increase sequencer AZT by FeeAmount - * call banana.shield - * decrease FPC BC.public by RefundAmount - * create transparent note with RefundAmount - */ - const shieldedBananas = 1n; - const shieldSecret = Fr.random(); - const shieldSecretHash = computeSecretHash(shieldSecret); - const tx = await bananaCoin.methods - .shield(aliceAddress, shieldedBananas, shieldSecretHash, 0n) - .send({ - fee: { - gasSettings, - paymentMethod: new PrivateFeePaymentMethod( - bananaCoin.address, - bananaFPC.address, - aliceWallet, - RefundSecret, - ), - }, - }) - .wait(); - - await expectMapping( - bananaPrivateBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [InitialAlicePrivateBananas - MaxFee, InitialFPCPrivateBananas, 0n], - ); - await expectMapping( - bananaPublicBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [InitialAlicePublicBananas - shieldedBananas, InitialFPCPublicBananas + MaxFee - RefundAmount, 0n], - ); - await expectMapping( - gasBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [InitialAliceGas, InitialFPCGas - FeeAmount, InitialSequencerGas + FeeAmount], - ); - - await expect(addPendingShieldNoteToPXE(0, shieldedBananas, shieldSecretHash, tx.txHash)).resolves.toBeUndefined(); - - await expect( - addPendingShieldNoteToPXE(0, RefundAmount, computeSecretHash(RefundSecret), tx.txHash), - ).resolves.toBeUndefined(); - }); - - it('pays fees for tx that creates notes in both private and public', async () => { - const privateTransfer = 1n; - const shieldedBananas = 1n; - const shieldSecret = Fr.random(); - const shieldSecretHash = computeSecretHash(shieldSecret); - - /** - * PRIVATE SETUP - * check authwit - * reduce alice BC.private by MaxFee - * enqueue public call to increase FPC BC.public by MaxFee - * enqueue public call for fpc.pay_fee_with_shielded_rebate - * - * PRIVATE APP LOGIC - * reduce Alice's private balance by privateTransfer - * create note for Bob with privateTransfer amount of private BC - * - * PUBLIC SETUP - * increase FPC BC.public by MaxFee - * - * PUBLIC APP LOGIC - * BC decrease Alice public balance by shieldedBananas - * BC create transparent note of shieldedBananas - * - * PUBLIC TEARDOWN - * call gas.pay_fee - * decrease FPC AZT by FeeAmount - * increase sequencer AZT by FeeAmount - * call banana.shield - * decrease FPC BC.public by RefundAmount - * create transparent note with RefundAmount - */ - const tx = await new BatchCall(aliceWallet, [ - bananaCoin.methods.transfer(aliceAddress, bobAddress, privateTransfer, 0n).request(), - bananaCoin.methods.shield(aliceAddress, shieldedBananas, shieldSecretHash, 0n).request(), - ]) - .send({ - fee: { - gasSettings, - paymentMethod: new PrivateFeePaymentMethod( - bananaCoin.address, - bananaFPC.address, - aliceWallet, - RefundSecret, - ), - }, - }) - .wait(); - - await expectMapping( - bananaPrivateBalances, - [aliceAddress, bobAddress, bananaFPC.address, sequencerAddress], - [ - InitialAlicePrivateBananas - MaxFee - privateTransfer, - InitialBobPrivateBananas + privateTransfer, - InitialFPCPrivateBananas, - 0n, - ], - ); - await expectMapping( - bananaPublicBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [InitialAlicePublicBananas - shieldedBananas, InitialFPCPublicBananas + MaxFee - RefundAmount, 0n], - ); - await expectMapping( - gasBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [InitialAliceGas, InitialFPCGas - FeeAmount, InitialSequencerGas + FeeAmount], - ); - - await expect(addPendingShieldNoteToPXE(0, shieldedBananas, shieldSecretHash, tx.txHash)).resolves.toBeUndefined(); - - await expect( - addPendingShieldNoteToPXE(0, RefundAmount, computeSecretHash(RefundSecret), tx.txHash), - ).resolves.toBeUndefined(); - }); - - it('rejects txs that dont have enough balance to cover gas costs', async () => { - // deploy a copy of bananaFPC but don't fund it! - const bankruptFPC = await FPCContract.deploy(aliceWallet, bananaCoin.address, gasTokenContract.address) - .send() - .deployed(); - - await expectMapping(gasBalances, [bankruptFPC.address], [0n]); - - await expect( - bananaCoin.methods - .privately_mint_private_note(10) - .send({ - // we need to skip public simulation otherwise the PXE refuses to accept the TX - skipPublicSimulation: true, - fee: { - gasSettings, - paymentMethod: new PrivateFeePaymentMethod( - bananaCoin.address, - bankruptFPC.address, - aliceWallet, - RefundSecret, - ), - }, - }) - .wait(), - ).rejects.toThrow('Tx dropped by P2P node.'); - }); - }); - - it('fails transaction that error in setup', async () => { - const OutrageousPublicAmountAliceDoesNotHave = BigInt(100e12); - - // simulation throws an error when setup fails - await expect( - bananaCoin.methods - .transfer_public(aliceAddress, sequencerAddress, OutrageousPublicAmountAliceDoesNotHave, 0) - .send({ - fee: { - gasSettings, - paymentMethod: new BuggedSetupFeePaymentMethod(bananaCoin.address, bananaFPC.address, wallets[0]), - }, - }) - .wait(), - ).rejects.toThrow(/Message not authorized by account 'is_valid == true'/); - - // so does the sequencer - await expect( - bananaCoin.methods - .transfer_public(aliceAddress, sequencerAddress, OutrageousPublicAmountAliceDoesNotHave, 0) - .send({ - skipPublicSimulation: true, - fee: { - gasSettings, - paymentMethod: new BuggedSetupFeePaymentMethod(bananaCoin.address, bananaFPC.address, wallets[0]), - }, - }) - .wait(), - ).rejects.toThrow(/Transaction [0-9a-f]{64} was dropped\. Reason: Tx dropped by P2P node\./); - }); - - it('fails transaction that error in teardown', async () => { - /** - * We trigger an error in teardown by having the FPC authorize a transfer of its entire balance to Alice - * as part of app logic. This will cause the FPC to not have enough funds to pay the refund back to Alice. - */ - const PublicMintedAlicePublicBananas = 100_000_000_000n; - - const [initialAlicePrivateBananas, initialFPCPrivateBananas] = await bananaPrivateBalances( - aliceAddress, - bananaFPC.address, - ); - const [initialAlicePublicBananas, initialFPCPublicBananas] = await bananaPublicBalances( - aliceAddress, - bananaFPC.address, - ); - const [initialAliceGas, initialFPCGas, initialSequencerGas] = await gasBalances( - aliceAddress, - bananaFPC.address, - sequencerAddress, - ); - - await bananaCoin.methods.mint_public(aliceAddress, PublicMintedAlicePublicBananas).send().wait(); - - await expect( - bananaCoin.methods - .mint_public(aliceAddress, 1n) // random operation - .send({ - fee: { - gasSettings, - paymentMethod: new BuggedTeardownFeePaymentMethod(bananaCoin.address, bananaFPC.address, wallets[0]), - }, - }) - .wait(), - ).rejects.toThrow(/invalid nonce/); - - // node also drops - await expect( - bananaCoin.methods - .mint_public(aliceAddress, 1n) // random operation - .send({ - skipPublicSimulation: true, - fee: { - gasSettings, - paymentMethod: new BuggedTeardownFeePaymentMethod(bananaCoin.address, bananaFPC.address, wallets[0]), - }, - }) - .wait(), - ).rejects.toThrow(/Transaction [0-9a-f]{64} was dropped\. Reason: Tx dropped by P2P node\./); - - // nothing happened - await expectMapping( - bananaPrivateBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [initialAlicePrivateBananas, initialFPCPrivateBananas, 0n], - ); - await expectMapping( - bananaPublicBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [initialAlicePublicBananas + PublicMintedAlicePublicBananas, initialFPCPublicBananas, 0n], - ); - await expectMapping( - gasBalances, - [aliceAddress, bananaFPC.address, sequencerAddress], - [initialAliceGas, initialFPCGas, initialSequencerGas], - ); - }); - - function logFunctionSignatures(artifact: ContractArtifact, logger: DebugLogger) { - artifact.functions.forEach(fn => { - const sig = decodeFunctionSignature(fn.name, fn.parameters); - logger.verbose(`${FunctionSelector.fromNameAndParameters(fn.name, fn.parameters)} => ${artifact.name}.${sig} `); - }); - } - - const mintPrivate = async (amount: bigint, address: AztecAddress) => { - // Mint bananas privately - const secret = Fr.random(); - const secretHash = computeSecretHash(secret); - logger.debug(`Minting ${amount} bananas privately for ${address} with secret ${secretHash.toString()}`); - const receipt = await bananaCoin.methods.mint_private(amount, secretHash).send().wait(); - - // Setup auth wit - await addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); - const txClaim = bananaCoin.methods.redeem_shield(address, amount, secret).send(); - const receiptClaim = await txClaim.wait({ debug: true }); - const { visibleNotes } = receiptClaim.debugInfo!; - expect(visibleNotes[0].note.items[0].toBigInt()).toBe(amount); - }; - - const addPendingShieldNoteToPXE = async (accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) => { - const note = new Note([new Fr(amount), secretHash]); - const extendedNote = new ExtendedNote( - note, - wallets[accountIndex].getAddress(), - bananaCoin.address, - BananaCoin.storage.pending_shields.slot, - BananaCoin.notes.TransparentNote.id, - txHash, - ); - await wallets[accountIndex].addNote(extendedNote); - }; -}); - -class BuggedSetupFeePaymentMethod extends PublicFeePaymentMethod { - override getFunctionCalls(gasSettings: GasSettings): Promise { - const maxFee = gasSettings.getFeeLimit(); - const nonce = Fr.random(); - const messageHash = computeAuthWitMessageHash( - this.paymentContract, - this.wallet.getChainId(), - this.wallet.getVersion(), - { - args: [this.wallet.getAddress(), this.paymentContract, maxFee, nonce], - functionData: new FunctionData( - FunctionSelector.fromSignature('transfer_public((Field),(Field),Field,Field)'), - false, - ), - to: this.asset, - }, - ); - - const tooMuchFee = new Fr(maxFee.toBigInt() * 2n); - - return Promise.resolve([ - this.wallet.setPublicAuthWit(messageHash, true).request(), - { - to: this.getPaymentContract(), - functionData: new FunctionData( - FunctionSelector.fromSignature('fee_entrypoint_public(Field,(Field),Field)'), - true, - ), - args: [tooMuchFee, this.asset, nonce], - }, - ]); - } -} - -class BuggedTeardownFeePaymentMethod extends PublicFeePaymentMethod { - override async getFunctionCalls(gasSettings: GasSettings): Promise { - // authorize the FPC to take the max fee from Alice - const nonce = Fr.random(); - const maxFee = gasSettings.getFeeLimit(); - const messageHash1 = computeAuthWitMessageHash( - this.paymentContract, - this.wallet.getChainId(), - this.wallet.getVersion(), - { - args: [this.wallet.getAddress(), this.paymentContract, maxFee, nonce], - functionData: new FunctionData( - FunctionSelector.fromSignature('transfer_public((Field),(Field),Field,Field)'), - false, - ), - to: this.asset, - }, - ); - - // authorize the FPC to take the maxFee - // do this first because we only get 2 feepayload calls - await this.wallet.setPublicAuthWit(messageHash1, true).send().wait(); - - return Promise.resolve([ - // in this, we're actually paying the fee in setup - { - to: this.getPaymentContract(), - functionData: new FunctionData( - FunctionSelector.fromSignature('fee_entrypoint_public(Field,(Field),Field)'), - true, - ), - args: [maxFee, this.asset, nonce], - }, - // and trying to take a little extra in teardown, but specify a bad nonce - { - to: this.asset, - functionData: new FunctionData( - FunctionSelector.fromSignature('transfer_public((Field),(Field),Field,Field)'), - false, - ), - args: [this.wallet.getAddress(), this.paymentContract, new Fr(1), Fr.random()], - }, - ]); - } -} 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 new file mode 100644 index 000000000000..516ac6d02b6b --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_fees/dapp_subscription.test.ts @@ -0,0 +1,235 @@ +import { + type AccountWallet, + type AztecAddress, + type FeePaymentMethod, + Fr, + type PXE, + PrivateFeePaymentMethod, + PublicFeePaymentMethod, + SentTx, +} from '@aztec/aztec.js'; +import { DefaultDappEntrypoint } from '@aztec/entrypoints/dapp'; +import { + type AppSubscriptionContract, + type TokenContract as BananaCoin, + type CounterContract, + type FPCContract, +} from '@aztec/noir-contracts.js'; + +import { expectMapping, expectMappingDelta } from '../fixtures/utils.js'; +import { FeesTest } from './fees_test.js'; + +type Balances = [bigint, bigint, bigint]; + +describe('e2e_fees dapp_subscription', () => { + let pxe: PXE; + + let aliceWallet: AccountWallet; + let aliceAddress: AztecAddress; // Dapp subscriber. + let bobAddress: AztecAddress; // Dapp owner. + let sequencerAddress: AztecAddress; + + let bananaCoin: BananaCoin; + let counterContract: CounterContract; + let subscriptionContract: AppSubscriptionContract; + let bananaFPC: FPCContract; + + let initialSubscriptionContractGasBalance: bigint; + let initialSequencerGasBalance: bigint; + let initialFPCGasBalance: bigint; + let initialBananasPublicBalances: Balances; // alice, bob, fpc + let initialBananasPrivateBalances: Balances; // alice, bob, fpc + + const t = new FeesTest('dapp_subscription'); + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.applyFundAlice(); + await t.applySetupSubscription(); + + ({ + aliceWallet, + aliceAddress, + bobAddress, + sequencerAddress, + bananaCoin, + bananaFPC, + subscriptionContract, + counterContract, + pxe, + } = await t.setup()); + }); + + afterAll(async () => { + await t.teardown(); + }); + + beforeAll(async () => { + await expectMapping( + t.gasBalances, + [aliceAddress, sequencerAddress, subscriptionContract.address, bananaFPC.address], + [0n, 0n, t.INITIAL_GAS_BALANCE, t.INITIAL_GAS_BALANCE], + ); + + await expectMapping( + t.bananaPrivateBalances, + [aliceAddress, bobAddress, bananaFPC.address], + [t.ALICE_INITIAL_BANANAS, 0n, 0n], + ); + + await expectMapping( + t.bananaPublicBalances, + [aliceAddress, bobAddress, bananaFPC.address], + [t.ALICE_INITIAL_BANANAS, 0n, 0n], + ); + }); + + beforeEach(async () => { + [initialSubscriptionContractGasBalance, initialSequencerGasBalance, initialFPCGasBalance] = (await t.gasBalances( + subscriptionContract, + sequencerAddress, + bananaFPC, + )) as Balances; + initialBananasPublicBalances = (await t.bananaPublicBalances(aliceAddress, bobAddress, bananaFPC)) as Balances; + initialBananasPrivateBalances = (await t.bananaPrivateBalances(aliceAddress, bobAddress, bananaFPC)) as Balances; + }); + + it('should allow Alice to subscribe by paying privately with bananas', async () => { + /** + PRIVATE SETUP + we first unshield `MAX_FEE` BC from alice's private balance to the FPC's public balance + + PUBLIC APP LOGIC + we then privately transfer `SUBSCRIPTION_AMOUNT` BC from alice to bob's subscription contract + + PUBLIC TEARDOWN + then the FPC calls `pay_fee`, reducing its gas balance by `FEE_AMOUNT`, and increasing the sequencer's gas balance by `FEE_AMOUNT` + the FPC also publicly sends `REFUND` BC to alice + */ + + const { transactionFee } = await subscribe( + new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), + ); + + await expectMapping( + t.gasBalances, + [sequencerAddress, bananaFPC.address], + [initialSequencerGasBalance + transactionFee!, initialFPCGasBalance - transactionFee!], + ); + + // alice, bob, fpc + await expectBananasPrivateDelta(-t.SUBSCRIPTION_AMOUNT - t.maxFee, t.SUBSCRIPTION_AMOUNT, 0n); + await expectBananasPublicDelta(0n, 0n, transactionFee!); + + // REFUND_AMOUNT is a transparent note note + }); + + it('should allow Alice to subscribe by paying with bananas in public', async () => { + /** + PRIVATE SETUP + we publicly transfer `MAX_FEE` BC from alice's public balance to the FPC's public balance + + PUBLIC APP LOGIC + we then privately transfer `SUBSCRIPTION_AMOUNT` BC from alice to bob's subscription contract + + PUBLIC TEARDOWN + then the FPC calls `pay_fee`, reducing its gas balance by `FEE_AMOUNT`, and increasing the sequencer's gas balance by `FEE_AMOUNT` + the FPC also publicly sends `REFUND` BC to alice + */ + const { transactionFee } = await subscribe( + new PublicFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), + ); + + await expectMapping( + t.gasBalances, + [sequencerAddress, bananaFPC.address], + [initialSequencerGasBalance + transactionFee!, initialFPCGasBalance - transactionFee!], + ); + + // alice, bob, fpc + // we pay the fee publicly, but the subscription payment is still private. + await expectBananasPrivateDelta(-t.SUBSCRIPTION_AMOUNT, t.SUBSCRIPTION_AMOUNT, 0n); + // we have the refund from the previous test, + // but since we paid publicly this time, the refund should have been "squashed" + await expectBananasPublicDelta(-transactionFee!, 0n, transactionFee!); + }); + + it('should call dapp subscription entrypoint', async () => { + // Subscribe again, so this test does not depend on the previous ones being run. + const { transactionFee: subscriptionTxFee } = await subscribe( + new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), + ); + + expect(await subscriptionContract.methods.is_initialized(aliceAddress).simulate()).toBe(true); + + const dappPayload = new DefaultDappEntrypoint(aliceAddress, aliceWallet, subscriptionContract.address); + const action = counterContract.methods.increment(bobAddress).request(); + const txExReq = await dappPayload.createTxExecutionRequest({ calls: [action] }); + const tx = await pxe.proveTx(txExReq, true); + const sentTx = new SentTx(pxe, pxe.sendTx(tx)); + const { transactionFee } = await sentTx.wait(); + + expect(await counterContract.methods.get_counter(bobAddress).simulate()).toBe(1n); + + await expectMapping( + t.gasBalances, + [sequencerAddress, subscriptionContract.address], + [ + initialSequencerGasBalance + transactionFee! + subscriptionTxFee!, + initialSubscriptionContractGasBalance - transactionFee!, + ], + ); + }); + + it('should reject after the sub runs out', async () => { + // Subscribe again. This will overwrite the previous subscription. + await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), 0); + await expect(dappIncrement()).rejects.toThrow( + "Failed to solve brillig function '(context.block_number()) as u64 < expiry_block_number as u64'", + ); + }); + + it('should reject after the txs run out', async () => { + // Subscribe again. This will overwrite the previous subscription. + await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), 5, 1); + await expect(dappIncrement()).resolves.toBeDefined(); + await expect(dappIncrement()).rejects.toThrow(/note.remaining_txs as u64 > 0/); + }); + + async function subscribe(paymentMethod: FeePaymentMethod, blockDelta: number = 5, txCount: number = 4) { + const nonce = Fr.random(); + const action = bananaCoin.methods.transfer(aliceAddress, bobAddress, t.SUBSCRIPTION_AMOUNT, nonce); + await aliceWallet.createAuthWit({ caller: subscriptionContract.address, action }); + + return subscriptionContract + .withWallet(aliceWallet) + .methods.subscribe(aliceAddress, nonce, (await pxe.getBlockNumber()) + blockDelta, txCount) + .send({ fee: { gasSettings: t.gasSettings, paymentMethod } }) + .wait(); + } + + async function dappIncrement() { + const dappEntrypoint = new DefaultDappEntrypoint(aliceAddress, aliceWallet, subscriptionContract.address); + const action = counterContract.methods.increment(bobAddress).request(); + const txExReq = await dappEntrypoint.createTxExecutionRequest({ calls: [action] }); + const tx = await pxe.proveTx(txExReq, true); + const sentTx = new SentTx(pxe, pxe.sendTx(tx)); + return sentTx.wait(); + } + + const expectBananasPrivateDelta = (aliceAmount: bigint, bobAmount: bigint, fpcAmount: bigint) => + expectMappingDelta( + initialBananasPrivateBalances, + t.bananaPrivateBalances, + [aliceAddress, bobAddress, bananaFPC.address], + [aliceAmount, bobAmount, fpcAmount], + ); + + const expectBananasPublicDelta = (aliceAmount: bigint, bobAmount: bigint, fpcAmount: bigint) => + expectMappingDelta( + initialBananasPublicBalances, + t.bananaPublicBalances, + [aliceAddress, bobAddress, bananaFPC.address], + [aliceAmount, bobAmount, fpcAmount], + ); +}); diff --git a/yarn-project/end-to-end/src/e2e_fees/failures.test.ts b/yarn-project/end-to-end/src/e2e_fees/failures.test.ts new file mode 100644 index 000000000000..dfde4f8662bf --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_fees/failures.test.ts @@ -0,0 +1,298 @@ +import { + type AccountWallet, + type AztecAddress, + Fr, + type FunctionCall, + FunctionSelector, + PublicFeePaymentMethod, + TxStatus, + computeAuthWitMessageHash, +} from '@aztec/aztec.js'; +import { FunctionData, type GasSettings } from '@aztec/circuits.js'; +import { type TokenContract as BananaCoin, type FPCContract } from '@aztec/noir-contracts.js'; + +import { expectMapping } from '../fixtures/utils.js'; +import { FeesTest } from './fees_test.js'; + +describe('e2e_fees failures', () => { + let aliceWallet: AccountWallet; + let aliceAddress: AztecAddress; + let sequencerAddress: AztecAddress; + let bananaCoin: BananaCoin; + let bananaFPC: FPCContract; + let gasSettings: GasSettings; + + const t = new FeesTest('failures'); + + beforeAll(async () => { + await t.applyBaseSnapshots(); + ({ aliceWallet, aliceAddress, sequencerAddress, bananaCoin, bananaFPC, gasSettings } = await t.setup()); + }); + + afterAll(async () => { + await t.teardown(); + }); + + it('reverts transactions but still pays fees using PublicFeePaymentMethod', async () => { + const OutrageousPublicAmountAliceDoesNotHave = BigInt(1e15); + const PublicMintedAlicePublicBananas = BigInt(1e12); + + const [initialAlicePrivateBananas, initialFPCPrivateBananas] = await t.bananaPrivateBalances( + aliceAddress, + bananaFPC.address, + ); + const [initialAlicePublicBananas, initialFPCPublicBananas] = await t.bananaPublicBalances( + aliceAddress, + bananaFPC.address, + ); + const [initialAliceGas, initialFPCGas, initialSequencerGas] = await t.gasBalances( + aliceAddress, + bananaFPC.address, + sequencerAddress, + ); + + await bananaCoin.methods.mint_public(aliceAddress, PublicMintedAlicePublicBananas).send().wait(); + // if we simulate locally, it throws an error + await expect( + bananaCoin.methods + .transfer_public(aliceAddress, sequencerAddress, OutrageousPublicAmountAliceDoesNotHave, 0) + .send({ + fee: { + gasSettings, + paymentMethod: new PublicFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), + }, + }) + .wait(), + ).rejects.toThrow(/attempt to subtract with underflow 'hi == high'/); + + // we did not pay the fee, because we did not submit the TX + await expectMapping( + t.bananaPrivateBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [initialAlicePrivateBananas, initialFPCPrivateBananas, 0n], + ); + await expectMapping( + t.bananaPublicBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [initialAlicePublicBananas + PublicMintedAlicePublicBananas, initialFPCPublicBananas, 0n], + ); + await expectMapping( + t.gasBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [initialAliceGas, initialFPCGas, initialSequencerGas], + ); + + // if we skip simulation, it includes the failed TX + const txReceipt = await bananaCoin.methods + .transfer_public(aliceAddress, sequencerAddress, OutrageousPublicAmountAliceDoesNotHave, 0) + .send({ + skipPublicSimulation: true, + fee: { + gasSettings, + paymentMethod: new PublicFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), + }, + }) + .wait({ dontThrowOnRevert: true }); + + expect(txReceipt.status).toBe(TxStatus.REVERTED); + const feeAmount = txReceipt.transactionFee!; + + // and thus we paid the fee + await expectMapping( + t.bananaPrivateBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [initialAlicePrivateBananas, initialFPCPrivateBananas, 0n], + ); + await expectMapping( + t.bananaPublicBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [initialAlicePublicBananas + PublicMintedAlicePublicBananas - feeAmount, initialFPCPublicBananas + feeAmount, 0n], + ); + await expectMapping( + t.gasBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [initialAliceGas, initialFPCGas - feeAmount, initialSequencerGas + feeAmount], + ); + + // TODO(#4712) - demonstrate reverts with the PrivateFeePaymentMethod. + // Can't do presently because all logs are "revertible" so we lose notes that get broadcasted during unshielding. + }); + + it('fails transaction that error in setup', async () => { + const OutrageousPublicAmountAliceDoesNotHave = BigInt(100e12); + + // simulation throws an error when setup fails + await expect( + bananaCoin.methods + .transfer_public(aliceAddress, sequencerAddress, OutrageousPublicAmountAliceDoesNotHave, 0) + .send({ + fee: { + gasSettings, + paymentMethod: new BuggedSetupFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), + }, + }) + .wait(), + ).rejects.toThrow(/Message not authorized by account 'is_valid == true'/); + + // so does the sequencer + await expect( + bananaCoin.methods + .transfer_public(aliceAddress, sequencerAddress, OutrageousPublicAmountAliceDoesNotHave, 0) + .send({ + skipPublicSimulation: true, + fee: { + gasSettings, + paymentMethod: new BuggedSetupFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), + }, + }) + .wait(), + ).rejects.toThrow(/Transaction [0-9a-f]{64} was dropped\. Reason: Tx dropped by P2P node\./); + }); + + it('fails transaction that error in teardown', async () => { + /** + * We trigger an error in teardown by having the FPC authorize a transfer of its entire balance to Alice + * as part of app logic. This will cause the FPC to not have enough funds to pay the refund back to Alice. + */ + const PublicMintedAlicePublicBananas = 100_000_000_000n; + + const [initialAlicePrivateBananas, initialFPCPrivateBananas] = await t.bananaPrivateBalances( + aliceAddress, + bananaFPC.address, + ); + const [initialAlicePublicBananas, initialFPCPublicBananas] = await t.bananaPublicBalances( + aliceAddress, + bananaFPC.address, + ); + const [initialAliceGas, initialFPCGas, initialSequencerGas] = await t.gasBalances( + aliceAddress, + bananaFPC.address, + sequencerAddress, + ); + + await bananaCoin.methods.mint_public(aliceAddress, PublicMintedAlicePublicBananas).send().wait(); + + await expect( + bananaCoin.methods + .mint_public(aliceAddress, 1n) // random operation + .send({ + fee: { + gasSettings, + paymentMethod: new BuggedTeardownFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), + }, + }) + .wait(), + ).rejects.toThrow(/invalid nonce/); + + // node also drops + await expect( + bananaCoin.methods + .mint_public(aliceAddress, 1n) // random operation + .send({ + skipPublicSimulation: true, + fee: { + gasSettings, + paymentMethod: new BuggedTeardownFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), + }, + }) + .wait(), + ).rejects.toThrow(/Transaction [0-9a-f]{64} was dropped\. Reason: Tx dropped by P2P node\./); + + // nothing happened + await expectMapping( + t.bananaPrivateBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [initialAlicePrivateBananas, initialFPCPrivateBananas, 0n], + ); + await expectMapping( + t.bananaPublicBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [initialAlicePublicBananas + PublicMintedAlicePublicBananas, initialFPCPublicBananas, 0n], + ); + await expectMapping( + t.gasBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [initialAliceGas, initialFPCGas, initialSequencerGas], + ); + }); +}); + +class BuggedSetupFeePaymentMethod extends PublicFeePaymentMethod { + override getFunctionCalls(gasSettings: GasSettings): Promise { + const maxFee = gasSettings.getFeeLimit(); + const nonce = Fr.random(); + const messageHash = computeAuthWitMessageHash( + this.paymentContract, + this.wallet.getChainId(), + this.wallet.getVersion(), + { + args: [this.wallet.getAddress(), this.paymentContract, maxFee, nonce], + functionData: new FunctionData( + FunctionSelector.fromSignature('transfer_public((Field),(Field),Field,Field)'), + false, + ), + to: this.asset, + }, + ); + + const tooMuchFee = new Fr(maxFee.toBigInt() * 2n); + + return Promise.resolve([ + this.wallet.setPublicAuthWit(messageHash, true).request(), + { + to: this.getPaymentContract(), + functionData: new FunctionData( + FunctionSelector.fromSignature('fee_entrypoint_public(Field,(Field),Field)'), + true, + ), + args: [tooMuchFee, this.asset, nonce], + }, + ]); + } +} + +class BuggedTeardownFeePaymentMethod extends PublicFeePaymentMethod { + override async getFunctionCalls(gasSettings: GasSettings): Promise { + // authorize the FPC to take the max fee from Alice + const nonce = Fr.random(); + const maxFee = gasSettings.getFeeLimit(); + const messageHash1 = computeAuthWitMessageHash( + this.paymentContract, + this.wallet.getChainId(), + this.wallet.getVersion(), + { + args: [this.wallet.getAddress(), this.paymentContract, maxFee, nonce], + functionData: new FunctionData( + FunctionSelector.fromSignature('transfer_public((Field),(Field),Field,Field)'), + false, + ), + to: this.asset, + }, + ); + + // authorize the FPC to take the maxFee + // do this first because we only get 2 feepayload calls + await this.wallet.setPublicAuthWit(messageHash1, true).send().wait(); + + return Promise.resolve([ + // in this, we're actually paying the fee in setup + { + to: this.getPaymentContract(), + functionData: new FunctionData( + FunctionSelector.fromSignature('fee_entrypoint_public(Field,(Field),Field)'), + true, + ), + args: [maxFee, this.asset, nonce], + }, + // and trying to take a little extra in teardown, but specify a bad nonce + { + to: this.asset, + functionData: new FunctionData( + FunctionSelector.fromSignature('transfer_public((Field),(Field),Field,Field)'), + false, + ), + args: [this.wallet.getAddress(), this.paymentContract, new Fr(1), Fr.random()], + }, + ]); + } +} 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 new file mode 100644 index 000000000000..39cd2308db8f --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -0,0 +1,269 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { + type AccountWallet, + type AztecAddress, + type AztecNode, + type DebugLogger, + ExtendedNote, + Fr, + Note, + type PXE, + SignerlessWallet, + type TxHash, + computeSecretHash, + createDebugLogger, +} from '@aztec/aztec.js'; +import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint'; +import { GasSettings } from '@aztec/circuits.js'; +import { createL1Clients } from '@aztec/ethereum'; +import { + AppSubscriptionContract, + TokenContract as BananaCoin, + CounterContract, + FPCContract, + GasTokenContract, +} from '@aztec/noir-contracts.js'; + +import { MNEMONIC } from '../fixtures/fixtures.js'; +import { + type ISnapshotManager, + type SubsystemsContext, + addAccounts, + createSnapshotManager, +} from '../fixtures/snapshot_manager.js'; +import { type BalancesFn, deployCanonicalGasToken, getBalancesFn, publicDeployAccounts } from '../fixtures/utils.js'; +import { GasPortalTestingHarnessFactory } from '../shared/gas_portal_test_harness.js'; + +const { E2E_DATA_PATH: dataPath } = process.env; + +/** + * Test fixture for testing fees. Provides the following snapshots: + * InitialAccounts: Initializes 3 Schnorr account contracts. + * PublicDeployAccounts: Deploys the accounts publicly. + * DeployGasToken: Deploys the gas token contract. + * FPCSetup: Deploys BananaCoin and FPC contracts, and bridges gas from L1. + * FundAlice: Mints private and public bananas to Alice. + * SetupSubscription: Deploys a counter contract and a subscription contract, and mints gas token to the subscription contract. + */ +export class FeesTest { + private snapshotManager: ISnapshotManager; + private wallets: AccountWallet[] = []; + + public logger: DebugLogger; + public pxe!: PXE; + public aztecNode!: AztecNode; + + public aliceWallet!: AccountWallet; + public aliceAddress!: AztecAddress; + public bobWallet!: AccountWallet; + public bobAddress!: AztecAddress; + public sequencerAddress!: AztecAddress; + + public gasSettings = GasSettings.default(); + public maxFee = this.gasSettings.getFeeLimit().toBigInt(); + + public gasTokenContract!: GasTokenContract; + public bananaCoin!: BananaCoin; + public bananaFPC!: FPCContract; + public counterContract!: CounterContract; + public subscriptionContract!: AppSubscriptionContract; + + public gasBalances!: BalancesFn; + public bananaPublicBalances!: BalancesFn; + public bananaPrivateBalances!: BalancesFn; + + public readonly INITIAL_GAS_BALANCE = BigInt(1e15); + public readonly ALICE_INITIAL_BANANAS = BigInt(1e12); + public readonly SUBSCRIPTION_AMOUNT = 10_000n; + public readonly APP_SPONSORED_TX_GAS_LIMIT = BigInt(10e9); + + constructor(testName: string) { + this.logger = createDebugLogger(`aztec:e2e_fees:${testName}`); + this.snapshotManager = createSnapshotManager(`e2e_fees/${testName}`, dataPath); + } + + async setup() { + const context = await this.snapshotManager.setup(); + await context.aztecNode.setConfig({ feeRecipient: this.sequencerAddress }); + ({ pxe: this.pxe, aztecNode: this.aztecNode } = context); + return this; + } + + async teardown() { + await this.snapshotManager.teardown(); + } + + /** Alice mints bananaCoin tokens privately to the target address. */ + async mintPrivate(amount: bigint, address: AztecAddress) { + const secret = Fr.random(); + const secretHash = computeSecretHash(secret); + const balanceBefore = await this.bananaCoin.methods.balance_of_private(this.aliceAddress).simulate(); + this.logger.debug(`Minting ${amount} bananas privately for ${address} with secret ${secretHash.toString()}`); + const receipt = await this.bananaCoin.methods.mint_private(amount, secretHash).send().wait(); + + await this.addPendingShieldNoteToPXE(this.aliceWallet, amount, secretHash, receipt.txHash); + await this.bananaCoin.methods.redeem_shield(address, amount, secret).send().wait(); + const balanceAfter = await this.bananaCoin.methods.balance_of_private(this.aliceAddress).simulate(); + expect(balanceAfter).toEqual(balanceBefore + amount); + } + + async addPendingShieldNoteToPXE(wallet: AccountWallet, amount: bigint, secretHash: Fr, txHash: TxHash) { + const note = new Note([new Fr(amount), secretHash]); + const extendedNote = new ExtendedNote( + note, + wallet.getAddress(), + this.bananaCoin.address, + BananaCoin.storage.pending_shields.slot, + BananaCoin.notes.TransparentNote.id, + txHash, + ); + await wallet.addNote(extendedNote); + } + + public async applyBaseSnapshots() { + await this.applyInitialAccountsSnapshot(); + await this.applyPublicDeployAccountsSnapshot(); + await this.applyDeployGasTokenSnapshot(); + await this.applyFPCSetupSnapshot(); + } + + private async applyInitialAccountsSnapshot() { + await this.snapshotManager.snapshot( + 'initial_accounts', + addAccounts(3, this.logger), + async ({ accountKeys }, { pxe }) => { + const accountManagers = accountKeys.map(ak => getSchnorrAccount(pxe, ak[0], ak[1], 1)); + await Promise.all(accountManagers.map(a => a.register())); + this.wallets = await Promise.all(accountManagers.map(a => a.getWallet())); + this.wallets.forEach((w, i) => this.logger.verbose(`Wallet ${i} address: ${w.getAddress()}`)); + [this.aliceWallet, this.bobWallet] = this.wallets.slice(0, 2); + [this.aliceAddress, this.bobAddress, this.sequencerAddress] = this.wallets.map(w => w.getAddress()); + }, + ); + } + + private async applyPublicDeployAccountsSnapshot() { + await this.snapshotManager.snapshot('public_deploy_accounts', () => + publicDeployAccounts(this.aliceWallet, this.wallets), + ); + } + + private async applyDeployGasTokenSnapshot() { + await this.snapshotManager.snapshot('deploy_gas_token', async context => { + await deployCanonicalGasToken( + new SignerlessWallet( + context.pxe, + new DefaultMultiCallEntrypoint(context.aztecNodeConfig.chainId, context.aztecNodeConfig.version), + ), + ); + }); + } + + private async applyFPCSetupSnapshot() { + await this.snapshotManager.snapshot( + 'fpc_setup', + async context => { + const harness = await this.createGasBridgeTestHarness(context); + const gasTokenContract = harness.l2Token; + expect(await context.pxe.isContractPubliclyDeployed(gasTokenContract.address)).toBe(true); + + const bananaCoin = await BananaCoin.deploy(this.aliceWallet, this.aliceAddress, 'BC', 'BC', 18n) + .send() + .deployed(); + + this.logger.info(`BananaCoin deployed at ${bananaCoin.address}`); + + const bananaFPC = await FPCContract.deploy(this.aliceWallet, bananaCoin.address, gasTokenContract.address) + .send() + .deployed(); + + this.logger.info(`BananaPay deployed at ${bananaFPC.address}`); + + await harness.bridgeFromL1ToL2(this.INITIAL_GAS_BALANCE, this.INITIAL_GAS_BALANCE, bananaFPC.address); + + return { + bananaCoinAddress: bananaCoin.address, + bananaFPCAddress: bananaFPC.address, + gasTokenAddress: gasTokenContract.address, + }; + }, + async data => { + const bananaFPC = await FPCContract.at(data.bananaFPCAddress, this.aliceWallet); + const bananaCoin = await BananaCoin.at(data.bananaCoinAddress, this.aliceWallet); + const gasTokenContract = await GasTokenContract.at(data.gasTokenAddress, this.aliceWallet); + + this.bananaCoin = bananaCoin; + this.bananaFPC = bananaFPC; + this.gasTokenContract = gasTokenContract; + + this.bananaPublicBalances = getBalancesFn('🍌.public', bananaCoin.methods.balance_of_public, this.logger); + this.bananaPrivateBalances = getBalancesFn('🍌.private', bananaCoin.methods.balance_of_private, this.logger); + this.gasBalances = getBalancesFn('⛽', gasTokenContract.methods.balance_of_public, this.logger); + }, + ); + } + + public async applyFundAlice() { + await this.snapshotManager.snapshot( + 'fund_alice', + async () => { + await this.mintPrivate(BigInt(this.ALICE_INITIAL_BANANAS), this.aliceAddress); + await this.bananaCoin.methods.mint_public(this.aliceAddress, this.ALICE_INITIAL_BANANAS).send().wait(); + }, + () => Promise.resolve(), + ); + } + + public async applySetupSubscription() { + await this.snapshotManager.snapshot( + 'setup_subscription', + async () => { + // Deploy counter contract for testing with Bob as owner + const counterContract = await CounterContract.deploy(this.bobWallet, 0, this.bobAddress).send().deployed(); + + // Deploy subscription contract, that allows subscriptions for SUBSCRIPTION_AMOUNT of bananas + const subscriptionContract = await AppSubscriptionContract.deploy( + this.bobWallet, + counterContract.address, + this.bobAddress, + this.bananaCoin.address, + this.SUBSCRIPTION_AMOUNT, + this.gasTokenContract.address, + this.APP_SPONSORED_TX_GAS_LIMIT, + ) + .send() + .deployed(); + + // Mint some gas tokens to the subscription contract + // Could also use bridgeFromL1ToL2 from the harness, but this is more direct + await this.gasTokenContract.methods + .mint_public(subscriptionContract.address, this.INITIAL_GAS_BALANCE) + .send() + .wait(); + + return { + counterContractAddress: counterContract.address, + subscriptionContractAddress: subscriptionContract.address, + }; + }, + async ({ counterContractAddress, subscriptionContractAddress }) => { + this.counterContract = await CounterContract.at(counterContractAddress, this.bobWallet); + this.subscriptionContract = await AppSubscriptionContract.at(subscriptionContractAddress, this.bobWallet); + }, + ); + } + + private createGasBridgeTestHarness(context: SubsystemsContext) { + const { publicClient, walletClient } = createL1Clients(context.aztecNodeConfig.rpcUrl, MNEMONIC); + + return GasPortalTestingHarnessFactory.create({ + aztecNode: context.aztecNode, + pxeService: context.pxe, + publicClient: publicClient, + walletClient: walletClient, + wallet: this.aliceWallet, + logger: this.logger, + mockL1: false, + }); + } +} 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 new file mode 100644 index 000000000000..76a0a712d18b --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts @@ -0,0 +1,388 @@ +import { + type AztecAddress, + BatchCall, + Fr, + PrivateFeePaymentMethod, + type TxReceipt, + type Wallet, + computeSecretHash, +} from '@aztec/aztec.js'; +import { type GasSettings } from '@aztec/circuits.js'; +import { type TokenContract as BananaCoin, FPCContract, type GasTokenContract } from '@aztec/noir-contracts.js'; + +import { expectMapping } from '../fixtures/utils.js'; +import { FeesTest } from './fees_test.js'; + +describe('e2e_fees private_payment', () => { + let aliceWallet: Wallet; + let aliceAddress: AztecAddress; + let bobAddress: AztecAddress; + let sequencerAddress: AztecAddress; + let gasTokenContract: GasTokenContract; + let bananaCoin: BananaCoin; + let bananaFPC: FPCContract; + let gasSettings: GasSettings; + + const t = new FeesTest('private_payment'); + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.applyFundAlice(); + ({ aliceWallet, aliceAddress, bobAddress, sequencerAddress, gasTokenContract, bananaCoin, bananaFPC, gasSettings } = + await t.setup()); + }); + + afterAll(async () => { + await t.teardown(); + }); + + let InitialAlicePublicBananas: bigint; + let InitialAlicePrivateBananas: bigint; + let InitialAliceGas: bigint; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let InitialBobPublicBananas: bigint; + let InitialBobPrivateBananas: bigint; + + let InitialFPCPublicBananas: bigint; + let InitialFPCPrivateBananas: bigint; + let InitialFPCGas: bigint; + + let InitialSequencerGas: bigint; + + let maxFee: bigint; + let refundSecret: Fr; + + beforeEach(async () => { + maxFee = BigInt(20e9); + refundSecret = Fr.random(); + + expect(gasSettings.getFeeLimit().toBigInt()).toEqual(maxFee); + + [ + [InitialAlicePrivateBananas, InitialBobPrivateBananas, InitialFPCPrivateBananas], + [InitialAlicePublicBananas, InitialBobPublicBananas, InitialFPCPublicBananas], + [InitialAliceGas, InitialFPCGas, InitialSequencerGas], + ] = await Promise.all([ + t.bananaPrivateBalances(aliceAddress, bobAddress, bananaFPC.address), + t.bananaPublicBalances(aliceAddress, bobAddress, bananaFPC.address), + t.gasBalances(aliceAddress, bananaFPC.address, sequencerAddress), + ]); + }); + + const getFeeAndRefund = (tx: Pick) => [tx.transactionFee!, maxFee - tx.transactionFee!]; + + it('pays fees for tx that dont run public app logic', async () => { + /** + * PRIVATE SETUP (1 nullifier for tx) + * check authwit (1 nullifier) + * reduce alice BC.private by MaxFee (1 nullifier) + * enqueue public call to increase FPC BC.public by MaxFee + * enqueue public call for fpc.pay_fee_with_shielded_rebate + * + * PRIVATE APP LOGIC + * reduce Alice's BC.private by transferAmount (1 note) + * create note for Bob of transferAmount (1 note) + * encrypted logs of 944 bytes + * unencrypted logs of 20 bytes + * + * PUBLIC SETUP + * increase FPC BC.public by MaxFee + * + * PUBLIC APP LOGIC + * N/A + * + * PUBLIC TEARDOWN + * call gas.pay_fee + * decrease FPC AZT by FeeAmount + * increase sequencer AZT by FeeAmount + * call banana.shield + * decrease FPC BC.public by RefundAmount + * create transparent note with RefundAmount + * + * this is expected to squash notes and nullifiers + */ + const transferAmount = 5n; + const tx = await bananaCoin.methods + .transfer(aliceAddress, bobAddress, transferAmount, 0n) + .send({ + fee: { + gasSettings, + paymentMethod: new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet, refundSecret), + }, + }) + .wait(); + + /** + * at present the user is paying DA gas for: + * 3 nullifiers = 3 * DA_BYTES_PER_FIELD * DA_GAS_PER_BYTE = 3 * 32 * 16 = 1536 DA gas + * 2 note hashes = 2 * DA_BYTES_PER_FIELD * DA_GAS_PER_BYTE = 2 * 32 * 16 = 1024 DA gas + * 964 bytes of logs = 964 * DA_GAS_PER_BYTE = 964 * 16 = 15424 DA gas + * tx overhead of 512 DA gas + * for a total of 18496 DA gas. + * + * The default teardown gas allocation at present is + * 100_000_000 for both DA and L2 gas. + * + * That produces a grand total of 200018496n. + * + * This will change because: + * 1. Gas use during public execution is not currently incorporated + * 2. We are presently squashing notes/nullifiers across non/revertible during private exeuction, + * but we shouldn't. + */ + expect(tx.transactionFee).toEqual(200018496n); + const [feeAmount, refundAmount] = getFeeAndRefund(tx); + + await expectMapping( + t.bananaPrivateBalances, + [aliceAddress, bobAddress, bananaFPC.address, sequencerAddress], + [InitialAlicePrivateBananas - maxFee - transferAmount, transferAmount, InitialFPCPrivateBananas, 0n], + ); + await expectMapping( + t.bananaPublicBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [InitialAlicePublicBananas, InitialFPCPublicBananas + maxFee - refundAmount, 0n], + ); + await expectMapping( + t.gasBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [InitialAliceGas, InitialFPCGas - feeAmount, InitialSequencerGas + feeAmount], + ); + + await expect( + // this rejects if note can't be added + t.addPendingShieldNoteToPXE(t.aliceWallet, refundAmount, computeSecretHash(refundSecret), tx.txHash), + ).resolves.toBeUndefined(); + }); + + it('pays fees for tx that creates notes in private', async () => { + /** + * PRIVATE SETUP + * check authwit + * reduce alice BC.private by MaxFee + * enqueue public call to increase FPC BC.public by MaxFee + * enqueue public call for fpc.pay_fee_with_shielded_rebate + * + * PRIVATE APP LOGIC + * increase alice BC.private by newlyMintedBananas + * + * PUBLIC SETUP + * increase FPC BC.public by MaxFee + * + * PUBLIC APP LOGIC + * BC increase total supply + * + * PUBLIC TEARDOWN + * call gas.pay_fee + * decrease FPC AZT by FeeAmount + * increase sequencer AZT by FeeAmount + * call banana.shield + * decrease FPC BC.public by RefundAmount + * create transparent note with RefundAmount + */ + const newlyMintedBananas = 10n; + const tx = await bananaCoin.methods + .privately_mint_private_note(newlyMintedBananas) + .send({ + fee: { + gasSettings, + paymentMethod: new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet, refundSecret), + }, + }) + .wait(); + + const [feeAmount, refundAmount] = getFeeAndRefund(tx); + + await expectMapping( + t.bananaPrivateBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [InitialAlicePrivateBananas - maxFee + newlyMintedBananas, InitialFPCPrivateBananas, 0n], + ); + await expectMapping( + t.bananaPublicBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [InitialAlicePublicBananas, InitialFPCPublicBananas + maxFee - refundAmount, 0n], + ); + await expectMapping( + t.gasBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [InitialAliceGas, InitialFPCGas - feeAmount, InitialSequencerGas + feeAmount], + ); + + await expect( + // this rejects if note can't be added + t.addPendingShieldNoteToPXE(t.aliceWallet, refundAmount, computeSecretHash(refundSecret), tx.txHash), + ).resolves.toBeUndefined(); + }); + + it('pays fees for tx that creates notes in public', async () => { + /** + * PRIVATE SETUP + * check authwit + * reduce alice BC.private by MaxFee + * enqueue public call to increase FPC BC.public by MaxFee + * enqueue public call for fpc.pay_fee_with_shielded_rebate + * + * PRIVATE APP LOGIC + * N/A + * + * PUBLIC SETUP + * increase FPC BC.public by MaxFee + * + * PUBLIC APP LOGIC + * BC decrease Alice public balance by shieldedBananas + * BC create transparent note of shieldedBananas + * + * PUBLIC TEARDOWN + * call gas.pay_fee + * decrease FPC AZT by FeeAmount + * increase sequencer AZT by FeeAmount + * call banana.shield + * decrease FPC BC.public by RefundAmount + * create transparent note with RefundAmount + */ + const shieldedBananas = 1n; + const shieldSecret = Fr.random(); + const shieldSecretHash = computeSecretHash(shieldSecret); + const tx = await bananaCoin.methods + .shield(aliceAddress, shieldedBananas, shieldSecretHash, 0n) + .send({ + fee: { + gasSettings, + paymentMethod: new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet, refundSecret), + }, + }) + .wait(); + + const [feeAmount, refundAmount] = getFeeAndRefund(tx); + + await expectMapping( + t.bananaPrivateBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [InitialAlicePrivateBananas - maxFee, InitialFPCPrivateBananas, 0n], + ); + await expectMapping( + t.bananaPublicBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [InitialAlicePublicBananas - shieldedBananas, InitialFPCPublicBananas + maxFee - refundAmount, 0n], + ); + await expectMapping( + t.gasBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [InitialAliceGas, InitialFPCGas - feeAmount, InitialSequencerGas + feeAmount], + ); + + await expect( + t.addPendingShieldNoteToPXE(t.aliceWallet, shieldedBananas, shieldSecretHash, tx.txHash), + ).resolves.toBeUndefined(); + + await expect( + t.addPendingShieldNoteToPXE(t.aliceWallet, refundAmount, computeSecretHash(refundSecret), tx.txHash), + ).resolves.toBeUndefined(); + }); + + it('pays fees for tx that creates notes in both private and public', async () => { + const privateTransfer = 1n; + const shieldedBananas = 1n; + const shieldSecret = Fr.random(); + const shieldSecretHash = computeSecretHash(shieldSecret); + + /** + * PRIVATE SETUP + * check authwit + * reduce alice BC.private by MaxFee + * enqueue public call to increase FPC BC.public by MaxFee + * enqueue public call for fpc.pay_fee_with_shielded_rebate + * + * PRIVATE APP LOGIC + * reduce Alice's private balance by privateTransfer + * create note for Bob with privateTransfer amount of private BC + * + * PUBLIC SETUP + * increase FPC BC.public by MaxFee + * + * PUBLIC APP LOGIC + * BC decrease Alice public balance by shieldedBananas + * BC create transparent note of shieldedBananas + * + * PUBLIC TEARDOWN + * call gas.pay_fee + * decrease FPC AZT by FeeAmount + * increase sequencer AZT by FeeAmount + * call banana.shield + * decrease FPC BC.public by RefundAmount + * create transparent note with RefundAmount + */ + const tx = await new BatchCall(aliceWallet, [ + bananaCoin.methods.transfer(aliceAddress, bobAddress, privateTransfer, 0n).request(), + bananaCoin.methods.shield(aliceAddress, shieldedBananas, shieldSecretHash, 0n).request(), + ]) + .send({ + fee: { + gasSettings, + paymentMethod: new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet, refundSecret), + }, + }) + .wait(); + + const [feeAmount, refundAmount] = getFeeAndRefund(tx); + + await expectMapping( + t.bananaPrivateBalances, + [aliceAddress, bobAddress, bananaFPC.address, sequencerAddress], + [ + InitialAlicePrivateBananas - maxFee - privateTransfer, + InitialBobPrivateBananas + privateTransfer, + InitialFPCPrivateBananas, + 0n, + ], + ); + await expectMapping( + t.bananaPublicBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [InitialAlicePublicBananas - shieldedBananas, InitialFPCPublicBananas + maxFee - refundAmount, 0n], + ); + await expectMapping( + t.gasBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [InitialAliceGas, InitialFPCGas - feeAmount, InitialSequencerGas + feeAmount], + ); + + await expect( + t.addPendingShieldNoteToPXE(t.aliceWallet, shieldedBananas, shieldSecretHash, tx.txHash), + ).resolves.toBeUndefined(); + + await expect( + t.addPendingShieldNoteToPXE(t.aliceWallet, refundAmount, computeSecretHash(refundSecret), tx.txHash), + ).resolves.toBeUndefined(); + }); + + it('rejects txs that dont have enough balance to cover gas costs', async () => { + // deploy a copy of bananaFPC but don't fund it! + const bankruptFPC = await FPCContract.deploy(aliceWallet, bananaCoin.address, gasTokenContract.address) + .send() + .deployed(); + + await expectMapping(t.gasBalances, [bankruptFPC.address], [0n]); + + await expect( + bananaCoin.methods + .privately_mint_private_note(10) + .send({ + // we need to skip public simulation otherwise the PXE refuses to accept the TX + skipPublicSimulation: true, + fee: { + gasSettings, + paymentMethod: new PrivateFeePaymentMethod( + bananaCoin.address, + bankruptFPC.address, + aliceWallet, + refundSecret, + ), + }, + }) + .wait(), + ).rejects.toThrow('Tx dropped by P2P node.'); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index 2dd4614f80ed..8ffb4dac4344 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -77,7 +77,7 @@ describe('e2e_lending_contract', () => { new TokenSimulator(collateralAsset, logger, [lendingContract.address, wallet.getAddress()]), new TokenSimulator(stableCoin, logger, [lendingContract.address, wallet.getAddress()]), ); - }, 200_000); + }, 300_000); afterAll(() => teardown()); diff --git a/yarn-project/end-to-end/src/e2e_nested_contract/nested_contract_test.ts b/yarn-project/end-to-end/src/e2e_nested_contract/nested_contract_test.ts index 512320b7fbdd..561b79c7fd3b 100644 --- a/yarn-project/end-to-end/src/e2e_nested_contract/nested_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_nested_contract/nested_contract_test.ts @@ -9,16 +9,17 @@ import { import { ChildContract, ParentContract } from '@aztec/noir-contracts.js'; import { - SnapshotManager, + type ISnapshotManager, type SubsystemsContext, addAccounts, + createSnapshotManager, publicDeployAccounts, } from '../fixtures/snapshot_manager.js'; const { E2E_DATA_PATH: dataPath } = process.env; export class NestedContractTest { - private snapshotManager: SnapshotManager; + private snapshotManager: ISnapshotManager; logger: DebugLogger; wallets: AccountWallet[] = []; accounts: CompleteAddress[] = []; @@ -29,7 +30,7 @@ export class NestedContractTest { constructor(testName: string) { this.logger = createDebugLogger(`aztec:e2e_nested_contract:${testName}`); - this.snapshotManager = new SnapshotManager(`e2e_nested_contract/${testName}`, dataPath); + this.snapshotManager = createSnapshotManager(`e2e_nested_contract/${testName}`, dataPath); } /** diff --git a/yarn-project/end-to-end/src/e2e_ordering.test.ts b/yarn-project/end-to-end/src/e2e_ordering.test.ts index e5ad26a38684..99aaa3d190b2 100644 --- a/yarn-project/end-to-end/src/e2e_ordering.test.ts +++ b/yarn-project/end-to-end/src/e2e_ordering.test.ts @@ -7,10 +7,12 @@ import { jest } from '@jest/globals'; import { setup } from './fixtures/utils.js'; -jest.setTimeout(30_000); +const TIMEOUT = 300_000; // See https://github.com/AztecProtocol/aztec-packages/issues/1601 describe('e2e_ordering', () => { + jest.setTimeout(TIMEOUT); + let pxe: PXE; let wallet: Wallet; let teardown: () => Promise; @@ -29,7 +31,7 @@ describe('e2e_ordering', () => { beforeEach(async () => { ({ teardown, pxe, wallet } = await setup()); - }, 200_000); + }, TIMEOUT); afterEach(() => teardown()); @@ -42,7 +44,7 @@ describe('e2e_ordering', () => { parent = await ParentContract.deploy(wallet).send().deployed(); child = await ChildContract.deploy(wallet).send().deployed(); pubSetValueSelector = child.methods.pub_set_value.selector; - }); + }, TIMEOUT); describe('enqueued public calls ordering', () => { const nestedValue = 10n; diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts index 566118a99cc1..72c5c4c0aad0 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/deposits.test.ts @@ -33,7 +33,7 @@ describe('e2e_public_cross_chain_messaging deposits', () => { ownerAddress = crossChainTestHarness.ownerAddress; l2Bridge = crossChainTestHarness.l2Bridge; l2Token = crossChainTestHarness.l2Token; - }, 200_000); + }, 300_000); afterEach(async () => { await t.teardown(); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/failure_cases.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/failure_cases.test.ts index 8e8bb4f1bb98..a6b1b6e6ca51 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/failure_cases.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/failure_cases.test.ts @@ -17,7 +17,7 @@ describe('e2e_public_cross_chain_messaging failures', () => { ({ crossChainTestHarness, user1Wallet, user2Wallet } = t); ethAccount = crossChainTestHarness.ethAccount; l2Bridge = crossChainTestHarness.l2Bridge; - }, 200_000); + }, 300_000); afterAll(async () => { await t.teardown(); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l1_to_l2.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l1_to_l2.test.ts index 9285a56a36d6..bc31966bfe68 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l1_to_l2.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l1_to_l2.test.ts @@ -27,7 +27,7 @@ describe('e2e_public_cross_chain_messaging l1_to_l2', () => { aztecNode = crossChainTestHarness.aztecNode; inbox = crossChainTestHarness.inbox; - }, 200_000); + }, 300_000); afterAll(async () => { await t.teardown(); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts index b43333c5edf7..0be2acfddbff 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/l2_to_l1.test.ts @@ -21,7 +21,7 @@ describe('e2e_public_cross_chain_messaging l2_to_l1', () => { aztecNode = crossChainTestHarness.aztecNode; outbox = crossChainTestHarness.outbox; - }, 200_000); + }, 300_000); afterAll(async () => { await t.teardown(); diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts index 38add3ebc8a4..b747d543e91f 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging/public_cross_chain_messaging_contract_test.ts @@ -10,26 +10,18 @@ import { type PXE, createDebugLogger, } from '@aztec/aztec.js'; +import { createL1Clients } from '@aztec/ethereum'; import { InboxAbi, OutboxAbi, PortalERC20Abi, TokenPortalAbi } from '@aztec/l1-artifacts'; import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts.js'; -import { - type Chain, - type HttpTransport, - type PublicClient, - createPublicClient, - createWalletClient, - getContract, - http, -} from 'viem'; -import { mnemonicToAccount } from 'viem/accounts'; -import { foundry } from 'viem/chains'; +import { type Chain, type HttpTransport, type PublicClient, getContract } from 'viem'; import { MNEMONIC } from '../fixtures/fixtures.js'; import { - SnapshotManager, + type ISnapshotManager, type SubsystemsContext, addAccounts, + createSnapshotManager, publicDeployAccounts, } from '../fixtures/snapshot_manager.js'; import { CrossChainTestHarness } from '../shared/cross_chain_test_harness.js'; @@ -37,7 +29,7 @@ import { CrossChainTestHarness } from '../shared/cross_chain_test_harness.js'; const { E2E_DATA_PATH: dataPath } = process.env; export class PublicCrossChainMessagingContractTest { - private snapshotManager: SnapshotManager; + private snapshotManager: ISnapshotManager; logger: DebugLogger; wallets: AccountWallet[] = []; accounts: CompleteAddress[] = []; @@ -60,7 +52,7 @@ export class PublicCrossChainMessagingContractTest { constructor(testName: string) { this.logger = createDebugLogger(`aztec:e2e_public_cross_chain_messaging:${testName}`); - this.snapshotManager = new SnapshotManager(`e2e_public_cross_chain_messaging/${testName}`, dataPath); + this.snapshotManager = createSnapshotManager(`e2e_public_cross_chain_messaging/${testName}`, dataPath); } async setup() { @@ -80,22 +72,6 @@ export class PublicCrossChainMessagingContractTest { await this.snapshotManager.teardown(); } - viemStuff(rpcUrl: string) { - const hdAccount = mnemonicToAccount(MNEMONIC); - - const walletClient = createWalletClient({ - account: hdAccount, - chain: foundry, - transport: http(rpcUrl), - }); - const publicClient = createPublicClient({ - chain: foundry, - transport: http(rpcUrl), - }); - - return { walletClient, publicClient }; - } - async applyBaseSnapshots() { // Note that we are using the same `pxe`, `aztecNodeConfig` and `aztecNode` across all snapshots. // This is to not have issues with different networks. @@ -126,7 +102,7 @@ export class PublicCrossChainMessagingContractTest { this.logger.verbose(`Public deploy accounts...`); await publicDeployAccounts(this.wallets[0], this.accounts.slice(0, 3)); - const { publicClient, walletClient } = this.viemStuff(this.aztecNodeConfig.rpcUrl); + const { publicClient, walletClient } = createL1Clients(this.aztecNodeConfig.rpcUrl, MNEMONIC); this.logger.verbose(`Setting up cross chain harness...`); this.crossChainTestHarness = await CrossChainTestHarness.new( @@ -151,7 +127,7 @@ export class PublicCrossChainMessagingContractTest { this.ownerAddress = AztecAddress.fromString(crossChainContext.ownerAddress.toString()); const tokenPortalAddress = EthAddress.fromString(crossChainContext.tokenPortal.toString()); - const { publicClient, walletClient } = this.viemStuff(this.aztecNodeConfig.rpcUrl); + const { publicClient, walletClient } = createL1Clients(this.aztecNodeConfig.rpcUrl, MNEMONIC); const inbox = getContract({ address: this.aztecNodeConfig.l1Contracts.inboxAddress.toString(), diff --git a/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts index 6325ead8df74..5e7c977c8458 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/token_contract_test.ts @@ -13,9 +13,10 @@ import { import { DocsExampleContract, TokenContract } from '@aztec/noir-contracts.js'; import { - SnapshotManager, + type ISnapshotManager, type SubsystemsContext, addAccounts, + createSnapshotManager, publicDeployAccounts, } from '../fixtures/snapshot_manager.js'; import { TokenSimulator } from '../simulators/token_simulator.js'; @@ -26,7 +27,7 @@ export class TokenContractTest { static TOKEN_NAME = 'Aztec Token'; static TOKEN_SYMBOL = 'AZT'; static TOKEN_DECIMALS = 18n; - private snapshotManager: SnapshotManager; + private snapshotManager: ISnapshotManager; logger: DebugLogger; wallets: AccountWallet[] = []; accounts: CompleteAddress[] = []; @@ -36,7 +37,7 @@ export class TokenContractTest { constructor(testName: string) { this.logger = createDebugLogger(`aztec:e2e_token_contract:${testName}`); - this.snapshotManager = new SnapshotManager(`e2e_token_contract/${testName}`, dataPath); + this.snapshotManager = createSnapshotManager(`e2e_token_contract/${testName}`, dataPath); } /** diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index 9fe93aafd776..2f1e67405272 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -12,7 +12,7 @@ import { } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; import { asyncMap } from '@aztec/foundation/async-map'; -import { createDebugLogger } from '@aztec/foundation/log'; +import { type Logger, createDebugLogger } from '@aztec/foundation/log'; import { makeBackoff, retry } from '@aztec/foundation/retry'; import { resolver, reviver } from '@aztec/foundation/serialize'; import { type PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; @@ -43,15 +43,30 @@ type SnapshotEntry = { snapshotPath: string; }; -export class SnapshotManager { - private snapshotStack: SnapshotEntry[] = []; +export function createSnapshotManager(testName: string, dataPath?: string) { + return dataPath ? new SnapshotManager(testName, dataPath) : new MockSnapshotManager(testName); +} + +export interface ISnapshotManager { + snapshot( + name: string, + apply: (context: SubsystemsContext) => Promise, + restore?: (snapshotData: T, context: SubsystemsContext) => Promise, + ): Promise; + + setup(): Promise; + + teardown(): Promise; +} + +/** Snapshot manager that does not perform snapshotting, it just applies transition and restoration functions as it receives them. */ +class MockSnapshotManager implements ISnapshotManager { private context?: SubsystemsContext; - private livePath: string; private logger: DebugLogger; - constructor(testName: string, private dataPath?: string) { - this.livePath = this.dataPath ? join(this.dataPath, 'live', testName) : ''; + constructor(testName: string) { this.logger = createDebugLogger(`aztec:snapshot_manager:${testName}`); + this.logger.warn(`No data path given, will not persist any snapshots.`); } public async snapshot( @@ -59,18 +74,49 @@ export class SnapshotManager { apply: (context: SubsystemsContext) => Promise, restore: (snapshotData: T, context: SubsystemsContext) => Promise = () => Promise.resolve(), ) { - if (!this.dataPath) { - // We are running in disabled mode. Just apply the state. - this.logger.verbose(`No data path given, will not persist any snapshots.`); - this.context = await this.setupFromFresh(); - this.logger.verbose(`Applying state transition for ${name}...`); - const snapshotData = await apply(this.context); - this.logger.verbose(`State transition for ${name} complete.`); - // Execute the restoration function. - await restore(snapshotData, this.context); - return; + // We are running in disabled mode. Just apply the state. + const context = await this.setup(); + this.logger.verbose(`Applying state transition for ${name}...`); + const snapshotData = await apply(context); + this.logger.verbose(`State transition for ${name} complete.`); + // Execute the restoration function. + await restore(snapshotData, context); + return; + } + + public async setup() { + if (!this.context) { + this.context = await setupFromFresh(undefined, this.logger); } + return this.context; + } + + public async teardown() { + await teardown(this.context); + this.context = undefined; + } +} + +/** + * Snapshot engine for local e2e tests. Read more: + * https://github.com/AztecProtocol/aztec-packages/pull/5526 + */ +class SnapshotManager implements ISnapshotManager { + private snapshotStack: SnapshotEntry[] = []; + private context?: SubsystemsContext; + private livePath: string; + private logger: DebugLogger; + + constructor(testName: string, private dataPath: string) { + this.livePath = join(this.dataPath, 'live', testName); + this.logger = createDebugLogger(`aztec:snapshot_manager:${testName}`); + } + public async snapshot( + name: string, + apply: (context: SubsystemsContext) => Promise, + restore: (snapshotData: T, context: SubsystemsContext) => Promise = () => Promise.resolve(), + ) { const snapshotPath = join(this.dataPath, 'snapshots', ...this.snapshotStack.map(e => e.name), name, 'snapshot'); if (existsSync(snapshotPath)) { @@ -82,24 +128,21 @@ export class SnapshotManager { } // Snapshot didn't exist at snapshotPath, and by definition none of the child snapshots can exist. - - if (!this.context) { - // We have no subsystem context yet, create it from the top of the snapshot stack (if it exists). - this.context = await this.setup(); - } + // If we have no subsystem context yet, create it from the top of the snapshot stack (if it exists). + const context = await this.setup(); this.snapshotStack.push({ name, apply, restore, snapshotPath }); // Apply current state transition. this.logger.verbose(`Applying state transition for ${name}...`); - const snapshotData = await apply(this.context); + const snapshotData = await apply(context); this.logger.verbose(`State transition for ${name} complete.`); // Execute the restoration function. - await restore(snapshotData, this.context); + await restore(snapshotData, context); // Save the snapshot data. - const ethCheatCodes = new EthCheatCodes(this.context.aztecNodeConfig.rpcUrl); + const ethCheatCodes = new EthCheatCodes(context.aztecNodeConfig.rpcUrl); const anvilStateFile = `${this.livePath}/anvil.dat`; await ethCheatCodes.dumpChainState(anvilStateFile); writeFileSync(`${this.livePath}/${name}.json`, JSON.stringify(snapshotData || {}, resolver)); @@ -132,7 +175,7 @@ export class SnapshotManager { if (previousSnapshotPath) { this.logger.verbose(`Copying snapshot from ${previousSnapshotPath} to ${this.livePath}...`); copySync(previousSnapshotPath, this.livePath); - this.context = await this.setupFromState(this.livePath); + this.context = await setupFromState(this.livePath, this.logger); // Execute each of the previous snapshots restoration functions in turn. await asyncMap(this.snapshotStack, async e => { const snapshotData = JSON.parse(readFileSync(`${e.snapshotPath}/${e.name}.json`, 'utf-8'), reviver); @@ -141,7 +184,7 @@ export class SnapshotManager { this.logger.verbose(`Restoration of ${e.name} complete.`); }); } else { - this.context = await this.setupFromFresh(this.livePath); + this.context = await setupFromFresh(this.livePath, this.logger); } } return this.context; @@ -151,128 +194,135 @@ export class SnapshotManager { * Destroys the current subsystem context. */ public async teardown() { - if (!this.context) { - return; - } - await this.context.aztecNode.stop(); - await this.context.pxe.stop(); - await this.context.acvmConfig?.cleanup(); - await this.context.anvil.stop(); + await teardown(this.context); this.context = undefined; removeSync(this.livePath); } +} - /** - * Initializes a fresh set of subsystems. - * If given a statePath, the state will be written to the path. - * If there is no statePath, in-memory and temporary state locations will be used. - */ - private async setupFromFresh(statePath?: string): Promise { - this.logger.verbose(`Initializing state...`); - - // Fetch the AztecNode config. - // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. - const aztecNodeConfig: AztecNodeConfig = getConfigEnvVars(); - aztecNodeConfig.dataDirectory = statePath; - - // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. - this.logger.verbose('Starting anvil...'); - const anvil = await retry( - async () => { - const ethereumHostPort = await getPort(); - aztecNodeConfig.rpcUrl = `http://127.0.0.1:${ethereumHostPort}`; - const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); - await anvil.start(); - return anvil; - }, - 'Start anvil', - makeBackoff([5, 5, 5]), - ); - - // Deploy our L1 contracts. - this.logger.verbose('Deploying L1 contracts...'); - const hdAccount = mnemonicToAccount(MNEMONIC); - const privKeyRaw = hdAccount.getHdKey().privateKey; - const publisherPrivKey = privKeyRaw === null ? null : Buffer.from(privKeyRaw); - const deployL1ContractsValues = await setupL1Contracts(aztecNodeConfig.rpcUrl, hdAccount, this.logger); - aztecNodeConfig.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; - aztecNodeConfig.l1Contracts = deployL1ContractsValues.l1ContractAddresses; - aztecNodeConfig.l1BlockPublishRetryIntervalMS = 100; - - const acvmConfig = await getACVMConfig(this.logger); - if (acvmConfig) { - aztecNodeConfig.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; - aztecNodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; - } +/** + * Destroys the current subsystem context. + */ +async function teardown(context: SubsystemsContext | undefined) { + if (!context) { + return; + } + await context.aztecNode.stop(); + await context.pxe.stop(); + await context.acvmConfig?.cleanup(); + await context.anvil.stop(); +} - this.logger.verbose('Creating and synching an aztec node...'); - const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); +/** + * Initializes a fresh set of subsystems. + * If given a statePath, the state will be written to the path. + * If there is no statePath, in-memory and temporary state locations will be used. + */ +async function setupFromFresh(statePath: string | undefined, logger: Logger): Promise { + logger.verbose(`Initializing state...`); + + // Fetch the AztecNode config. + // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. + const aztecNodeConfig: AztecNodeConfig = getConfigEnvVars(); + aztecNodeConfig.dataDirectory = statePath; + + // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. + logger.verbose('Starting anvil...'); + const anvil = await retry( + async () => { + const ethereumHostPort = await getPort(); + aztecNodeConfig.rpcUrl = `http://127.0.0.1:${ethereumHostPort}`; + const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); + await anvil.start(); + return anvil; + }, + 'Start anvil', + makeBackoff([5, 5, 5]), + ); + + // Deploy our L1 contracts. + logger.verbose('Deploying L1 contracts...'); + const hdAccount = mnemonicToAccount(MNEMONIC); + const privKeyRaw = hdAccount.getHdKey().privateKey; + const publisherPrivKey = privKeyRaw === null ? null : Buffer.from(privKeyRaw); + const deployL1ContractsValues = await setupL1Contracts(aztecNodeConfig.rpcUrl, hdAccount, logger); + aztecNodeConfig.publisherPrivateKey = `0x${publisherPrivKey!.toString('hex')}`; + aztecNodeConfig.l1Contracts = deployL1ContractsValues.l1ContractAddresses; + aztecNodeConfig.l1BlockPublishRetryIntervalMS = 100; + + const acvmConfig = await getACVMConfig(logger); + if (acvmConfig) { + aztecNodeConfig.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; + aztecNodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; + } - this.logger.verbose('Creating pxe...'); - const pxeConfig = getPXEServiceConfig(); - pxeConfig.dataDirectory = statePath; - const pxe = await createPXEService(aztecNode, pxeConfig); + logger.verbose('Creating and synching an aztec node...'); + const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); - if (statePath) { - writeFileSync(`${statePath}/aztec_node_config.json`, JSON.stringify(aztecNodeConfig)); - } + logger.verbose('Creating pxe...'); + const pxeConfig = getPXEServiceConfig(); + pxeConfig.dataDirectory = statePath; + const pxe = await createPXEService(aztecNode, pxeConfig); - return { - aztecNodeConfig, - anvil, - aztecNode, - pxe, - acvmConfig, - }; + if (statePath) { + writeFileSync(`${statePath}/aztec_node_config.json`, JSON.stringify(aztecNodeConfig)); } - /** - * Given a statePath, setup the system starting from that state. - */ - private async setupFromState(statePath: string): Promise { - this.logger.verbose(`Initializing with saved state at ${statePath}...`); - - // Load config. - // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. - const aztecNodeConfig: AztecNodeConfig = JSON.parse( - readFileSync(`${statePath}/aztec_node_config.json`, 'utf-8'), - reviver, - ); - aztecNodeConfig.dataDirectory = statePath; - - // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. - const ethereumHostPort = await getPort(); - aztecNodeConfig.rpcUrl = `http://localhost:${ethereumHostPort}`; - const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); - await anvil.start(); - // Load anvil state. - const anvilStateFile = `${statePath}/anvil.dat`; - const ethCheatCodes = new EthCheatCodes(aztecNodeConfig.rpcUrl); - await ethCheatCodes.loadChainState(anvilStateFile); - - // TODO: Encapsulate this in a NativeAcvm impl. - const acvmConfig = await getACVMConfig(this.logger); - if (acvmConfig) { - aztecNodeConfig.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; - aztecNodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; - } + return { + aztecNodeConfig, + anvil, + aztecNode, + pxe, + acvmConfig, + }; +} - this.logger.verbose('Creating aztec node...'); - const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); - - this.logger.verbose('Creating pxe...'); - const pxeConfig = getPXEServiceConfig(); - pxeConfig.dataDirectory = statePath; - const pxe = await createPXEService(aztecNode, pxeConfig); - - return { - aztecNodeConfig, - anvil, - aztecNode, - pxe, - acvmConfig, - }; +/** + * Given a statePath, setup the system starting from that state. + */ +async function setupFromState(statePath: string, logger: Logger): Promise { + logger.verbose(`Initializing with saved state at ${statePath}...`); + + // Load config. + // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. + const aztecNodeConfig: AztecNodeConfig = JSON.parse( + readFileSync(`${statePath}/aztec_node_config.json`, 'utf-8'), + reviver, + ); + aztecNodeConfig.dataDirectory = statePath; + + // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. + const ethereumHostPort = await getPort(); + aztecNodeConfig.rpcUrl = `http://localhost:${ethereumHostPort}`; + const anvil = createAnvil({ anvilBinary: './scripts/anvil_kill_wrapper.sh', port: ethereumHostPort }); + await anvil.start(); + // Load anvil state. + const anvilStateFile = `${statePath}/anvil.dat`; + const ethCheatCodes = new EthCheatCodes(aztecNodeConfig.rpcUrl); + await ethCheatCodes.loadChainState(anvilStateFile); + + // TODO: Encapsulate this in a NativeAcvm impl. + const acvmConfig = await getACVMConfig(logger); + if (acvmConfig) { + aztecNodeConfig.acvmWorkingDirectory = acvmConfig.acvmWorkingDirectory; + aztecNodeConfig.acvmBinaryPath = acvmConfig.expectedAcvmPath; } + + logger.verbose('Creating aztec node...'); + const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); + + logger.verbose('Creating pxe...'); + const pxeConfig = getPXEServiceConfig(); + pxeConfig.dataDirectory = statePath; + const pxe = await createPXEService(aztecNode, pxeConfig); + + return { + aztecNodeConfig, + anvil, + aztecNode, + pxe, + acvmConfig, + }; } /** diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 9dca4d82eb1f..e1d257d8204b 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -601,8 +601,9 @@ export function getBalancesFn( symbol: string, method: ContractMethod, logger: any, -): (...addresses: AztecAddress[]) => Promise { - const balances = async (...addresses: AztecAddress[]) => { +): (...addresses: (AztecAddress | { address: AztecAddress })[]) => Promise { + const balances = async (...addressLikes: (AztecAddress | { address: AztecAddress })[]) => { + const addresses = addressLikes.map(addressLike => ('address' in addressLike ? addressLike.address : addressLike)); const b = await Promise.all(addresses.map(address => method(address).simulate())); const debugString = `${symbol} balances: ${addresses.map((address, i) => `${address}: ${b[i]}`).join(', ')}`; logger.verbose(debugString); @@ -624,6 +625,20 @@ export async function expectMapping( expect(outputs).toEqual(expectedOutputs); } +export async function expectMappingDelta( + initialValues: V[], + fn: (...k: K[]) => Promise, + inputs: K[], + expectedDiffs: V[], +): Promise { + expect(inputs.length).toBe(expectedDiffs.length); + + const outputs = await fn(...inputs); + const diffs = outputs.map((output, i) => output - initialValues[i]); + + expect(diffs).toEqual(expectedDiffs); +} + /** * Deploy the protocol contracts to a running instance. */ @@ -633,6 +648,8 @@ export async function deployCanonicalGasToken(deployer: Wallet) { const canonicalGasToken = getCanonicalGasToken(gasPortalAddress); if (await deployer.isContractClassPubliclyRegistered(canonicalGasToken.contractClass.id)) { + getLogger().debug('Gas token already deployed'); + await expect(deployer.isContractPubliclyDeployed(canonicalGasToken.address)).resolves.toBe(true); return; } @@ -640,6 +657,8 @@ export async function deployCanonicalGasToken(deployer: Wallet) { .send({ contractAddressSalt: canonicalGasToken.instance.salt, universalDeploy: true }) .deployed(); + getLogger().info(`Gas token publicly deployed at ${gasToken.address}`); + await expect(deployer.isContractClassPubliclyRegistered(gasToken.instance.contractClassId)).resolves.toBe(true); await expect(deployer.getContractInstance(gasToken.address)).resolves.toBeDefined(); await expect(deployer.isContractPubliclyDeployed(gasToken.address)).resolves.toBe(true); diff --git a/yarn-project/end-to-end/src/flakey_e2e_account_init_fees.test.ts b/yarn-project/end-to-end/src/flakey_e2e_account_init_fees.test.ts index 2f488cca319d..d17f93b4aea3 100644 --- a/yarn-project/end-to-end/src/flakey_e2e_account_init_fees.test.ts +++ b/yarn-project/end-to-end/src/flakey_e2e_account_init_fees.test.ts @@ -40,7 +40,7 @@ const TOKEN_SYMBOL = 'BC'; const TOKEN_DECIMALS = 18n; const BRIDGED_FPC_GAS = BigInt(10e12); -jest.setTimeout(1000_000); +jest.setTimeout(1_000_000); describe('e2e_fees_account_init', () => { let ctx: EndToEndContext; diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 8856aa0fc94b..6cec140a9783 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -15,7 +15,8 @@ import { getContract, http, } from 'viem'; -import { type HDAccount, type PrivateKeyAccount } from 'viem/accounts'; +import { type HDAccount, type PrivateKeyAccount, mnemonicToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; import { type L1ContractAddresses } from './l1_contract_addresses.js'; @@ -85,6 +86,34 @@ export interface L1ContractArtifactsForDeployment { gasPortal: ContractArtifacts; } +/** + * Creates a wallet and a public viem client for interacting with L1. + * @param rpcUrl - RPC URL to connect to L1. + * @param mnemonicOrHdAccount - Mnemonic or account for the wallet client. + * @param chain - Optional chain spec (defaults to local foundry). + * @returns - A wallet and a public client. + */ +export function createL1Clients( + rpcUrl: string, + mnemonicOrHdAccount: string | HDAccount, + chain: Chain = foundry, +): { publicClient: PublicClient; walletClient: WalletClient } { + const hdAccount = + typeof mnemonicOrHdAccount === 'string' ? mnemonicToAccount(mnemonicOrHdAccount) : mnemonicOrHdAccount; + + const walletClient = createWalletClient({ + account: hdAccount, + chain, + transport: http(rpcUrl), + }); + const publicClient = createPublicClient({ + chain, + transport: http(rpcUrl), + }); + + return { walletClient, publicClient }; +} + /** * Deploys the aztec L1 contracts; Rollup, Contract Deployment Emitter & (optionally) Decoder Helper. * @param rpcUrl - URL of the ETH RPC to use for deployment. diff --git a/yarn-project/foundation/src/abi/function_selector.ts b/yarn-project/foundation/src/abi/function_selector.ts index d09669d7ce30..c37df24d8395 100644 --- a/yarn-project/foundation/src/abi/function_selector.ts +++ b/yarn-project/foundation/src/abi/function_selector.ts @@ -3,6 +3,7 @@ import { keccak256, randomBytes } from '../crypto/index.js'; import { type Fr } from '../fields/fields.js'; import { BufferReader } from '../serialize/buffer_reader.js'; import { FieldReader } from '../serialize/field_reader.js'; +import { TypeRegistry } from '../serialize/type_registry.js'; import { type ABIParameter } from './abi.js'; import { decodeFunctionSignature } from './decoder.js'; import { Selector } from './selector.js'; @@ -126,4 +127,14 @@ export class FunctionSelector extends Selector { static random() { return FunctionSelector.fromBuffer(randomBytes(Selector.SIZE)); } + + toJSON() { + return { + type: 'FunctionSelector', + value: this.toString(), + }; + } } + +// For deserializing JSON. +TypeRegistry.register('FunctionSelector', FunctionSelector); diff --git a/yarn-project/prover-client/src/orchestrator/orchestrator.ts b/yarn-project/prover-client/src/orchestrator/orchestrator.ts index bcbcf85fe862..9e03c88b08e9 100644 --- a/yarn-project/prover-client/src/orchestrator/orchestrator.ts +++ b/yarn-project/prover-client/src/orchestrator/orchestrator.ts @@ -197,7 +197,7 @@ export class ProvingOrchestrator { } // we need to pad the rollup with empty transactions - logger.info( + logger.debug( `Padding rollup with ${ this.provingState.totalNumTxs - this.provingState.transactionsReceived } empty transactions`, diff --git a/yarn-project/prover-client/src/prover-pool/memory-proving-queue.ts b/yarn-project/prover-client/src/prover-pool/memory-proving-queue.ts index c3e5194f0276..1f93a17f3e83 100644 --- a/yarn-project/prover-client/src/prover-pool/memory-proving-queue.ts +++ b/yarn-project/prover-client/src/prover-pool/memory-proving-queue.ts @@ -132,7 +132,7 @@ export class MemoryProvingQueue implements CircuitProver, ProvingJobSource { signal.addEventListener('abort', () => reject(new AbortedError('Operation has been aborted'))); } - this.log.info( + this.log.debug( `Adding id=${item.id} type=${ProvingRequestType[request.type]} proving job to queue depth=${this.queue.length()}`, ); // TODO (alexg) remove the `any` diff --git a/yarn-project/prover-client/src/prover-pool/prover-agent.ts b/yarn-project/prover-client/src/prover-pool/prover-agent.ts index e5ae9f156f95..401795969375 100644 --- a/yarn-project/prover-client/src/prover-pool/prover-agent.ts +++ b/yarn-project/prover-client/src/prover-pool/prover-agent.ts @@ -40,7 +40,7 @@ export class ProverAgent { try { const [time, result] = await elapsed(() => this.work(job.request)); await queue.resolveProvingJob(job.id, result); - this.log.info( + this.log.debug( `Processed proving job id=${job.id} type=${ProvingRequestType[job.request.type]} duration=${time}ms`, ); } catch (err) { diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index d365f94bfbe4..c6e64085fcb8 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -160,7 +160,7 @@ export class Sequencer { if (pendingTxs.length < this.minTxsPerBLock) { return; } - this.log.info(`Retrieved ${pendingTxs.length} txs from P2P pool`); + this.log.debug(`Retrieved ${pendingTxs.length} txs from P2P pool`); const historicalHeader = (await this.l2BlockSource.getBlock(-1))?.header; const newBlockNumber =