Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class PhasesTxValidator implements TxValidator<Tx> {

return { result: 'valid' };
} finally {
await this.contractDataSource.removeNewContracts(tx);
this.contractDataSource.clearContractsForTx();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Fr } from '@aztec/foundation/fields';
import { createLogger } from '@aztec/foundation/log';
import { TestDateProvider } from '@aztec/foundation/timer';
import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token';
import { RevertCode } from '@aztec/stdlib/avm';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract';
import { GasFees } from '@aztec/stdlib/gas';
Expand All @@ -10,6 +11,8 @@ import { getTelemetryClient } from '@aztec/telemetry-client';
import { NativeWorldStateService } from '@aztec/world-state';

import { PublicTxSimulationTester, SimpleContractDataSource } from '../../server.js';
import { createContractClassAndInstance } from '../avm/fixtures/index.js';
import { addNewContractClassToTx, addNewContractInstanceToTx } from '../fixtures/utils.js';
import { WorldStateDB } from '../public_db_sources.js';
import { PublicProcessor } from '../public_processor.js';
import { PublicTxSimulator } from '../public_tx_simulator.js';
Expand All @@ -22,6 +25,7 @@ describe('Public Processor app tests: TokenContract', () => {
const sender = AztecAddress.fromNumber(111);

let token: ContractInstanceWithAddress;
let worldStateDB: WorldStateDB;
let tester: PublicTxSimulationTester;
let processor: PublicProcessor;

Expand All @@ -32,7 +36,7 @@ describe('Public Processor app tests: TokenContract', () => {

const contractDataSource = new SimpleContractDataSource();
const merkleTrees = await (await NativeWorldStateService.tmp()).fork();
const worldStateDB = new WorldStateDB(merkleTrees, contractDataSource);
worldStateDB = new WorldStateDB(merkleTrees, contractDataSource);
const simulator = new PublicTxSimulator(merkleTrees, worldStateDB, globals, /*doMerkleOperations=*/ true);

processor = new PublicProcessor(
Expand Down Expand Up @@ -112,4 +116,103 @@ describe('Public Processor app tests: TokenContract', () => {
const endTime = performance.now();
logger.verbose(`TokenContract public processor test took ${endTime - startTime}ms\n`);
});

it('new contract cannot get removed from ContractDataSource by a later failing transaction', async () => {
const mintAmount = 1_000_000n;
const constructorArgs = [admin, /*name=*/ 'Token', /*symbol=*/ 'TOK', /*decimals=*/ new Fr(18)];

const { contractClass, contractInstance } = await createContractClassAndInstance(
constructorArgs,
admin,
TokenContractArtifact,
);
const token = contractInstance;

// another token instance, same contract class
const otherAdmin = AztecAddress.fromNumber(43);
const anotherToken = await tester.registerAndDeployContract(
constructorArgs,
/*deployer=*/ otherAdmin,
TokenContractArtifact,
);

// First transaction - deploys and initializes first token contract
const passingConstructorTx = await tester.createTx(
/*sender=*/ admin,
/*setupCalls=*/ [],
/*appCalls=*/ [
{
address: token.address,
fnName: 'constructor',
args: constructorArgs,
contractArtifact: TokenContractArtifact,
},
],
);
await addNewContractClassToTx(passingConstructorTx, contractClass);
await addNewContractInstanceToTx(passingConstructorTx, contractInstance);

// NOTE: we need to include the contract artifact for each enqueued call, otherwise the tester
// will not know how to construct the TX since we are intentionally not adding the contract to
// the contract data source.

// Second transaction - deploys second token but fails during transfer
const receiver = AztecAddress.fromNumber(222);
const transferAmount = 10n;
const nonce = new Fr(0);
const failingConstructorTx = await tester.createTx(
/*sender=*/ admin,
/*setupCalls=*/ [],
/*appCalls=*/ [
{
address: anotherToken.address,
fnName: 'constructor',
args: constructorArgs,
contractArtifact: TokenContractArtifact,
},
// The next enqueued call will fail because sender has no tokens to transfer
{
address: anotherToken.address,
fnName: 'transfer_in_public',
args: [/*from=*/ sender, /*to=*/ receiver, transferAmount, nonce],
contractArtifact: TokenContractArtifact,
},
],
);
// FIXME(#12375): should be able to include the nullifier insertions, but at the moment
// tx simulator cannot recover from errors during revertible private insertions.
// Once fixed, this skipNullifierInsertion flag can be removed.
await addNewContractClassToTx(failingConstructorTx, contractClass, /*skipNullifierInsertion=*/ true);
await addNewContractInstanceToTx(failingConstructorTx, contractInstance, /*skipNullifierInsertion=*/ true);

// Third transaction - verifies first token is still accessible by minting
const mintTx = await tester.createTx(
/*sender=*/ admin,
/*setupCalls=*/ [],
/*appCalls=*/ [
{
address: token.address,
fnName: 'mint_to_public',
args: [/*to=*/ sender, mintAmount],
contractArtifact: TokenContractArtifact,
},
],
);

const results = await processor.process([passingConstructorTx, failingConstructorTx, mintTx]);
//const results = await processor.process([passingConstructorTx]);
const processedTxs = results[0];
const failedTxs = results[1];
expect(processedTxs.length).toBe(3);
expect(failedTxs.length).toBe(0);

// First tx should succeed (constructor)
expect(processedTxs[0].revertCode).toEqual(RevertCode.OK);

// Second tx should revert in app logic (failed transfer)
expect(processedTxs[1].revertCode).toEqual(RevertCode.APP_LOGIC_REVERTED);

// Third tx should succeed (mint), proving first contract is still accessible
expect(processedTxs[2].revertCode).toEqual(RevertCode.OK);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DEPLOYER_CONTRACT_ADDRESS } from '@aztec/constants';
import { Fr } from '@aztec/foundation/fields';
import { createLogger } from '@aztec/foundation/log';
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
import { computeFeePayerBalanceStorageSlot } from '@aztec/protocol-contracts/fee-juice';
import { computeFeePayerBalanceStorageSlot, getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice';
import type { ContractArtifact } from '@aztec/stdlib/abi';
import { PublicDataWrite } from '@aztec/stdlib/avm';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
Expand All @@ -11,6 +11,7 @@ import { computePublicDataTreeLeafSlot, siloNullifier } from '@aztec/stdlib/hash
import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
import { MerkleTreeId } from '@aztec/stdlib/trees';

import { createContractClassAndInstance } from './index.js';
import type { SimpleContractDataSource } from './simple_contract_data_source.js';

/**
Expand Down Expand Up @@ -58,25 +59,31 @@ export abstract class BaseAvmSimulationTester {
seed = 0,
originalContractClassId?: Fr, // if previously upgraded
): Promise<ContractInstanceWithAddress> {
const contractInstance = await this.contractDataSource.registerAndDeployContract(
const { contractClass, contractInstance } = await createContractClassAndInstance(
constructorArgs,
deployer,
contractArtifact,
seed,
originalContractClassId,
);

await this.contractDataSource.addNewContract(contractArtifact, contractClass, contractInstance);

if (!skipNullifierInsertion) {
await this.insertContractAddressNullifier(contractInstance.address);
}
return contractInstance;
}

async registerFeeJuiceContract(): Promise<ContractInstanceWithAddress> {
return await this.contractDataSource.registerFeeJuiceContract();
}

getFirstContractInstance(): ContractInstanceWithAddress {
return this.contractDataSource.getFirstContractInstance();
const feeJuice = await getCanonicalFeeJuice();
const feeJuiceContractClassPublic = {
...feeJuice.contractClass,
privateFunctions: [],
unconstrainedFunctions: [],
};
await this.contractDataSource.addNewContract(feeJuice.artifact, feeJuiceContractClassPublic, feeJuice.instance);
return feeJuice.instance;
}

addContractClass(contractClass: ContractClassPublic, contractArtifact: ContractArtifact): Promise<void> {
Expand Down
62 changes: 61 additions & 1 deletion yarn-project/simulator/src/public/avm/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import { MAX_L2_GAS_PER_TX_PUBLIC_PORTION } from '@aztec/constants';
import {
DEPLOYER_CONTRACT_ADDRESS,
MAX_L2_GAS_PER_TX_PUBLIC_PORTION,
PUBLIC_DISPATCH_SELECTOR,
} from '@aztec/constants';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { AvmGadgetsTestContractArtifact } from '@aztec/noir-contracts.js/AvmGadgetsTest';
import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest';
import { type ContractArtifact, type FunctionArtifact, FunctionSelector } from '@aztec/stdlib/abi';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import {
type ContractClassPublic,
type ContractInstanceWithAddress,
computeInitializationHash,
} from '@aztec/stdlib/contract';
import { isNoirCallStackUnresolved } from '@aztec/stdlib/errors';
import { GasFees } from '@aztec/stdlib/gas';
import { siloNullifier } from '@aztec/stdlib/hash';
import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
import { makeContractClassPublic, makeContractInstanceFromClassId } from '@aztec/stdlib/testing';
import { GlobalVariables } from '@aztec/stdlib/tx';

import { strict as assert } from 'assert';
Expand Down Expand Up @@ -234,3 +245,52 @@ export function resolveAvmGadgetsTestContractAssertionMessage(

return resolveAssertionMessageFromRevertData(output, functionArtifact);
}

/**
* Create a contract class and instance given constructor args, artifact, etc.
* NOTE: This is useful for testing real-ish contract class registration and instance deployment TXs (via logs)
* @param constructorArgs - The constructor arguments for the contract.
* @param deployer - The deployer of the contract.
* @param contractArtifact - The contract artifact for the contract.
* @param seed - The seed for the contract.
* @param originalContractClassId - The original contract class ID (if upgraded)
* @returns The contract class, instance, and contract address nullifier.
*/
export async function createContractClassAndInstance(
constructorArgs: any[],
deployer: AztecAddress,
contractArtifact: ContractArtifact,
seed = 0,
originalContractClassId?: Fr, // if previously upgraded
): Promise<{
contractClass: ContractClassPublic;
contractInstance: ContractInstanceWithAddress;
contractAddressNullifier: Fr;
}> {
const bytecode = getContractFunctionArtifact(PUBLIC_DISPATCH_FN_NAME, contractArtifact)!.bytecode;
const contractClass = await makeContractClassPublic(
seed,
/*publicDispatchFunction=*/ { bytecode, selector: new FunctionSelector(PUBLIC_DISPATCH_SELECTOR) },
);

const constructorAbi = getContractFunctionArtifact('constructor', contractArtifact);
const initializationHash = await computeInitializationHash(constructorAbi, constructorArgs);
const contractInstance =
originalContractClassId === undefined
? await makeContractInstanceFromClassId(contractClass.id, seed, {
deployer,
initializationHash,
})
: await makeContractInstanceFromClassId(originalContractClassId, seed, {
deployer,
initializationHash,
currentClassId: contractClass.id,
});

const contractAddressNullifier = await siloNullifier(
AztecAddress.fromNumber(DEPLOYER_CONTRACT_ADDRESS),
contractInstance.address.toField(),
);

return { contractClass, contractInstance, contractAddressNullifier };
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { PUBLIC_DISPATCH_SELECTOR } from '@aztec/constants';
import type { Fr } from '@aztec/foundation/fields';
import { createLogger } from '@aztec/foundation/log';
import { getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice';
import { type ContractArtifact, FunctionSelector } from '@aztec/stdlib/abi';
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
import {
type ContractClassPublic,
type ContractDataSource,
type ContractInstanceWithAddress,
type PublicFunction,
computeInitializationHash,
computePublicBytecodeCommitment,
} from '@aztec/stdlib/contract';
import { makeContractClassPublic, makeContractInstanceFromClassId } from '@aztec/stdlib/testing';

import { PUBLIC_DISPATCH_FN_NAME, getContractFunctionArtifact } from './index.js';
import { PUBLIC_DISPATCH_FN_NAME } from './index.js';

/**
* This class is used during public/avm testing to function as a database of
Expand All @@ -39,64 +35,22 @@ export class SimpleContractDataSource implements ContractDataSource {
* Derive the contract class and instance with some seed.
* Add both to the contract data source along with the contract artifact.
*/
async registerAndDeployContract(
constructorArgs: any[],
deployer: AztecAddress,
async addNewContract(
contractArtifact: ContractArtifact,
seed = 0,
originalContractClassId?: Fr, // if previously upgraded
): Promise<ContractInstanceWithAddress> {
const bytecode = getContractFunctionArtifact(PUBLIC_DISPATCH_FN_NAME, contractArtifact)!.bytecode;
const contractClass = await makeContractClassPublic(
seed,
/*publicDispatchFunction=*/ { bytecode, selector: new FunctionSelector(PUBLIC_DISPATCH_SELECTOR) },
);

const constructorAbi = getContractFunctionArtifact('constructor', contractArtifact);
const initializationHash = await computeInitializationHash(constructorAbi, constructorArgs);
this.logger.trace(`Initialization hash for contract class ${contractClass.id}: ${initializationHash.toString()}`);
const contractInstance =
originalContractClassId === undefined
? await makeContractInstanceFromClassId(contractClass.id, seed, {
deployer,
initializationHash,
})
: await makeContractInstanceFromClassId(originalContractClassId, seed, {
deployer,
initializationHash,
currentClassId: contractClass.id,
});

contractClass: ContractClassPublic,
contractInstance: ContractInstanceWithAddress,
) {
this.addContractArtifact(contractClass.id, contractArtifact);
await this.addContractClass(contractClass);
await this.addContractInstance(contractInstance);
return contractInstance;
}

async registerFeeJuiceContract(): Promise<ContractInstanceWithAddress> {
const feeJuice = await getCanonicalFeeJuice();
const feeJuiceContractClassPublic = {
...feeJuice.contractClass,
privateFunctions: [],
unconstrainedFunctions: [],
};

this.addContractArtifact(feeJuiceContractClassPublic.id, feeJuice.artifact);
await this.addContractClass(feeJuiceContractClassPublic);
await this.addContractInstance(feeJuice.instance);
return feeJuice.instance;
}

getFirstContractInstance(): ContractInstanceWithAddress {
return this.contractInstances.values().next().value;
}

addContractArtifact(classId: Fr, artifact: ContractArtifact): void {
this.contractArtifacts.set(classId.toString(), artifact);
}

/////////////////////////////////////////////////////////////
// ContractDataSource function impelementations
// ContractDataSource function implementations
getPublicFunction(_address: AztecAddress, _selector: FunctionSelector): Promise<PublicFunction> {
throw new Error('Method not implemented.');
}
Expand Down
Loading