diff --git a/yarn-project/acir-simulator/src/avm/avm_context.ts b/yarn-project/acir-simulator/src/avm/avm_context.ts index 699f9365b15..aed883fef21 100644 --- a/yarn-project/acir-simulator/src/avm/avm_context.ts +++ b/yarn-project/acir-simulator/src/avm/avm_context.ts @@ -1,7 +1,10 @@ +import { AztecAddress, FunctionSelector } from '@aztec/circuits.js'; +import { Fr } from '@aztec/foundation/fields'; + import { AvmExecutionEnvironment } from './avm_execution_environment.js'; import { AvmMachineState } from './avm_machine_state.js'; import { AvmMessageCallResult } from './avm_message_call_result.js'; -import { AvmInterpreter } from './interpreter/index.js'; +import { AvmInterpreter, AvmInterpreterError } from './interpreter/index.js'; import { AvmJournal } from './journal/journal.js'; import { decodeBytecode } from './opcodes/decode_bytecode.js'; import { Instruction } from './opcodes/index.js'; @@ -30,10 +33,19 @@ export class AvmContext { * - We run the interpreter * */ - public call(): AvmMessageCallResult { + async call(): Promise { // NOTE: the following is mocked as getPublicBytecode does not exist yet - // const bytecode = journal.journal.hostStorage.contractsDb.getBytecode(this.executionEnvironment.address); - const bytecode = Buffer.from('0x01000100020003'); + const selector = new FunctionSelector(0); + const bytecode = await this.journal.hostStorage.contractsDb.getBytecode( + this.executionEnvironment.address, + selector, + ); + + // This assumes that we will not be able to send messages to accounts without code + // Pending classes and instances impl details + if (!bytecode) { + throw new NoBytecodeFoundInterpreterError(this.executionEnvironment.address); + } const instructions: Instruction[] = decodeBytecode(bytecode); @@ -42,4 +54,79 @@ export class AvmContext { return interpreter.run(); } + + /** + * Create a new forked avm context - for internal calls + */ + public newWithForkedState(): AvmContext { + const forkedState = AvmJournal.branchParent(this.journal); + return new AvmContext(this.executionEnvironment, forkedState); + } + + /** + * Create a new forked avm context - for external calls + */ + public static newWithForkedState(executionEnvironment: AvmExecutionEnvironment, journal: AvmJournal): AvmContext { + const forkedState = AvmJournal.branchParent(journal); + return new AvmContext(executionEnvironment, forkedState); + } + + /** + * Prepare a new AVM context that will be ready for an external call + * - It will fork the journal + * - It will set the correct execution Environment Variables for a call + * - Alter both address and storageAddress + * + * @param address - The contract to call + * @param executionEnvironment - The current execution environment + * @param journal - The current journal + * @returns new AvmContext instance + */ + public static prepExternalCallContext( + address: AztecAddress, + calldata: Fr[], + executionEnvironment: AvmExecutionEnvironment, + journal: AvmJournal, + ): AvmContext { + const newExecutionEnvironment = executionEnvironment.newCall(address, calldata); + const forkedState = AvmJournal.branchParent(journal); + return new AvmContext(newExecutionEnvironment, forkedState); + } + + /** + * Prepare a new AVM context that will be ready for an external static call + * - It will fork the journal + * - It will set the correct execution Environment Variables for a call + * - Alter both address and storageAddress + * + * @param address - The contract to call + * @param executionEnvironment - The current execution environment + * @param journal - The current journal + * @returns new AvmContext instance + */ + public static prepExternalStaticCallContext( + address: AztecAddress, + calldata: Fr[], + executionEnvironment: AvmExecutionEnvironment, + journal: AvmJournal, + ): AvmContext { + const newExecutionEnvironment = executionEnvironment.newStaticCall(address, calldata); + const forkedState = AvmJournal.branchParent(journal); + return new AvmContext(newExecutionEnvironment, forkedState); + } + + /** + * Merge the journal of this call with it's parent + * NOTE: this should never be called on a root context - only from within a nested call + */ + public mergeJournal() { + this.journal.mergeWithParent(); + } +} + +class NoBytecodeFoundInterpreterError extends AvmInterpreterError { + constructor(contractAddress: AztecAddress) { + super(`No bytecode found at: ${contractAddress}`); + this.name = 'NoBytecodeFoundInterpreterError'; + } } diff --git a/yarn-project/acir-simulator/src/avm/avm_execution_environment.test.ts b/yarn-project/acir-simulator/src/avm/avm_execution_environment.test.ts new file mode 100644 index 00000000000..bf792862c8d --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/avm_execution_environment.test.ts @@ -0,0 +1,55 @@ +import { Fr } from '@aztec/foundation/fields'; + +import { initExecutionEnvironment } from './fixtures/index.js'; + +describe('Execution Environment', () => { + const newAddress = new Fr(123456n); + const calldata = [new Fr(1n), new Fr(2n), new Fr(3n)]; + + it('New call should fork execution environment correctly', () => { + const executionEnvironment = initExecutionEnvironment(); + const newExecutionEnvironment = executionEnvironment.newCall(newAddress, calldata); + + allTheSameExcept(executionEnvironment, newExecutionEnvironment, { + address: newAddress, + storageAddress: newAddress, + calldata, + }); + }); + + it('New delegate call should fork execution environment correctly', () => { + const executionEnvironment = initExecutionEnvironment(); + const newExecutionEnvironment = executionEnvironment.newDelegateCall(newAddress, calldata); + + allTheSameExcept(executionEnvironment, newExecutionEnvironment, { + address: newAddress, + isDelegateCall: true, + calldata, + }); + }); + + it('New static call call should fork execution environment correctly', () => { + const executionEnvironment = initExecutionEnvironment(); + const newExecutionEnvironment = executionEnvironment.newStaticCall(newAddress, calldata); + + allTheSameExcept(executionEnvironment, newExecutionEnvironment, { + address: newAddress, + storageAddress: newAddress, + isStaticCall: true, + calldata, + }); + }); +}); + +/** + * Check all properties of one object are the same, except for the specified differentProperties + */ +function allTheSameExcept(referenceObject: any, comparingObject: any, differentProperties: Record): void { + for (const key in referenceObject) { + if (Object.keys(differentProperties).includes(key)) { + expect(comparingObject[key]).toEqual(differentProperties[key]); + } else { + expect(comparingObject[key]).toEqual(referenceObject[key]); + } + } +} diff --git a/yarn-project/acir-simulator/src/avm/avm_execution_environment.ts b/yarn-project/acir-simulator/src/avm/avm_execution_environment.ts index 495a579feab..6c921bd08e9 100644 --- a/yarn-project/acir-simulator/src/avm/avm_execution_environment.ts +++ b/yarn-project/acir-simulator/src/avm/avm_execution_environment.ts @@ -7,6 +7,7 @@ import { Fr } from '@aztec/foundation/fields'; * Contains variables that remain constant during AVM execution * These variables are provided by the public kernel circuit */ +// TODO(https://github.com/AztecProtocol/aztec-packages/issues/3992): gas not implemented export class AvmExecutionEnvironment { constructor( /** - */ @@ -36,4 +37,58 @@ export class AvmExecutionEnvironment { /** - */ public readonly calldata: Fr[], ) {} + + public newCall(address: AztecAddress, calldata: Fr[]): AvmExecutionEnvironment { + return new AvmExecutionEnvironment( + /*address=*/ address, + /*storageAddress=*/ address, + this.origin, + this.sender, + this.portal, + this.feePerL1Gas, + this.feePerL2Gas, + this.feePerDaGas, + this.contractCallDepth, + this.globals, + this.isStaticCall, + this.isDelegateCall, + /*calldata=*/ calldata, + ); + } + + public newStaticCall(address: AztecAddress, calldata: Fr[]): AvmExecutionEnvironment { + return new AvmExecutionEnvironment( + /*address=*/ address, + /*storageAddress=*/ address, + this.origin, + this.sender, + this.portal, + this.feePerL1Gas, + this.feePerL2Gas, + this.feePerDaGas, + this.contractCallDepth, + this.globals, + /*isStaticCall=*/ true, + this.isDelegateCall, + /*calldata=*/ calldata, + ); + } + + public newDelegateCall(address: AztecAddress, calldata: Fr[]): AvmExecutionEnvironment { + return new AvmExecutionEnvironment( + /*address=*/ address, + this.storageAddress, + this.origin, + this.sender, + this.portal, + this.feePerL1Gas, + this.feePerL2Gas, + this.feePerDaGas, + this.contractCallDepth, + this.globals, + this.isStaticCall, + /*isDelegateCall=*/ true, + /*calldata=*/ calldata, + ); + } } diff --git a/yarn-project/acir-simulator/src/avm/index.test.ts b/yarn-project/acir-simulator/src/avm/index.test.ts index 282afcc901e..b43318d72ae 100644 --- a/yarn-project/acir-simulator/src/avm/index.test.ts +++ b/yarn-project/acir-simulator/src/avm/index.test.ts @@ -11,7 +11,7 @@ import { encodeToBytecode } from './opcodes/encode_to_bytecode.js'; import { Opcode } from './opcodes/opcodes.js'; describe('avm', () => { - it('Should execute bytecode', () => { + it('Should execute bytecode', async () => { const calldata: Fr[] = [new Fr(1), new Fr(2)]; const journal = mock(); @@ -31,7 +31,7 @@ describe('avm', () => { // Execute instructions const context = new AvmMachineState(initExecutionEnvironment({ calldata })); const interpreter = new AvmInterpreter(context, journal, instructions); - const avmReturnData = interpreter.run(); + const avmReturnData = await interpreter.run(); expect(avmReturnData.reverted).toBe(false); diff --git a/yarn-project/acir-simulator/src/avm/interpreter/interpreter.test.ts b/yarn-project/acir-simulator/src/avm/interpreter/interpreter.test.ts index 39b8092dabf..7ecaf130942 100644 --- a/yarn-project/acir-simulator/src/avm/interpreter/interpreter.test.ts +++ b/yarn-project/acir-simulator/src/avm/interpreter/interpreter.test.ts @@ -18,7 +18,7 @@ describe('interpreter', () => { journal = mock(); }); - it('Should execute a series of instructions', () => { + it('Should execute a series of instructions', async () => { const calldata: Fr[] = [new Fr(1), new Fr(2)]; const instructions: Instruction[] = [ @@ -27,26 +27,26 @@ describe('interpreter', () => { new Return(/*returnOffset=*/ 2, /*copySize=*/ 1), ]; - const context = new AvmMachineState(initExecutionEnvironment({ calldata })); - const interpreter = new AvmInterpreter(context, journal, instructions); - const avmReturnData = interpreter.run(); + const machineState = new AvmMachineState(initExecutionEnvironment({ calldata })); + const interpreter = new AvmInterpreter(machineState, journal, instructions); + const avmReturnData = await interpreter.run(); expect(avmReturnData.reverted).toBe(false); expect(avmReturnData.revertReason).toBeUndefined(); expect(avmReturnData.output).toEqual([new Fr(3)]); }); - it('Should revert with an invalid jump', () => { + it('Should revert with an invalid jump', async () => { const calldata: Fr[] = []; const invalidJumpDestination = 22; const instructions: Instruction[] = [new Jump(invalidJumpDestination)]; - const context = new AvmMachineState(initExecutionEnvironment({ calldata })); - const interpreter = new AvmInterpreter(context, journal, instructions); + const machineState = new AvmMachineState(initExecutionEnvironment({ calldata })); + const interpreter = new AvmInterpreter(machineState, journal, instructions); - const avmReturnData = interpreter.run(); + const avmReturnData = await interpreter.run(); expect(avmReturnData.reverted).toBe(true); expect(avmReturnData.revertReason).toBeInstanceOf(InvalidProgramCounterError); diff --git a/yarn-project/acir-simulator/src/avm/interpreter/interpreter.ts b/yarn-project/acir-simulator/src/avm/interpreter/interpreter.ts index f8aa30fed67..75b90eb5d0c 100644 --- a/yarn-project/acir-simulator/src/avm/interpreter/interpreter.ts +++ b/yarn-project/acir-simulator/src/avm/interpreter/interpreter.ts @@ -29,7 +29,7 @@ export class AvmInterpreter { * - reverted execution will return false * - any other panic will throw */ - run(): AvmMessageCallResult { + async run(): Promise { assert(this.instructions.length > 0); try { @@ -37,7 +37,7 @@ export class AvmInterpreter { const instruction = this.instructions[this.machineState.pc]; assert(!!instruction); // This should never happen - instruction.execute(this.machineState, this.journal); + await instruction.execute(this.machineState, this.journal); if (this.machineState.pc >= this.instructions.length) { throw new InvalidProgramCounterError(this.machineState.pc, /*max=*/ this.instructions.length); diff --git a/yarn-project/acir-simulator/src/avm/journal/journal.test.ts b/yarn-project/acir-simulator/src/avm/journal/journal.test.ts index a11a3b38743..1d5d0d08671 100644 --- a/yarn-project/acir-simulator/src/avm/journal/journal.test.ts +++ b/yarn-project/acir-simulator/src/avm/journal/journal.test.ts @@ -29,7 +29,7 @@ describe('journal', () => { journal.writeStorage(contractAddress, key, value); const journalUpdates: JournalData = journal.flush(); - expect(journalUpdates.storageWrites.get(contractAddress)?.get(key)).toEqual(value); + expect(journalUpdates.storageWrites.get(contractAddress.toBigInt())?.get(key.toBigInt())).toEqual(value); }); it('When reading from storage, should check the parent first', async () => { diff --git a/yarn-project/acir-simulator/src/avm/journal/journal.ts b/yarn-project/acir-simulator/src/avm/journal/journal.ts index cb69b3c33ef..62670b8f2f7 100644 --- a/yarn-project/acir-simulator/src/avm/journal/journal.ts +++ b/yarn-project/acir-simulator/src/avm/journal/journal.ts @@ -14,7 +14,7 @@ export type JournalData = { /** - */ newNullifiers: Fr[]; /** contract address -\> key -\> value */ - storageWrites: Map>; + storageWrites: Map>; }; /** @@ -32,7 +32,7 @@ export class AvmJournal { // Reading state - must be tracked for vm execution // contract address -> key -> value // TODO(https://github.com/AztecProtocol/aztec-packages/issues/3999) - private storageReads: Map> = new Map(); + private storageReads: Map> = new Map(); // New written state private newCommitments: Fr[] = []; @@ -43,7 +43,7 @@ export class AvmJournal { private newLogs: Fr[][] = []; // contract address -> key -> value - private storageWrites: Map> = new Map(); + private storageWrites: Map> = new Map(); private parentJournal: AvmJournal | undefined; @@ -76,12 +76,12 @@ export class AvmJournal { * @param value - */ public writeStorage(contractAddress: Fr, key: Fr, value: Fr) { - let contractMap = this.storageWrites.get(contractAddress); + let contractMap = this.storageWrites.get(contractAddress.toBigInt()); if (!contractMap) { contractMap = new Map(); - this.storageWrites.set(contractAddress, contractMap); + this.storageWrites.set(contractAddress.toBigInt(), contractMap); } - contractMap.set(key, value); + contractMap.set(key.toBigInt(), value); } /** @@ -93,7 +93,7 @@ export class AvmJournal { * @returns current value */ public readStorage(contractAddress: Fr, key: Fr): Promise { - const cachedValue = this.storageWrites.get(contractAddress)?.get(key); + const cachedValue = this.storageWrites.get(contractAddress.toBigInt())?.get(key.toBigInt()); if (cachedValue) { return Promise.resolve(cachedValue); } @@ -168,7 +168,7 @@ export class AvmJournal { * @param hostMap - The map to be merged into * @param childMap - The map to be merged from */ -function mergeContractMaps(hostMap: Map>, childMap: Map>) { +function mergeContractMaps(hostMap: Map>, childMap: Map>) { for (const [key, value] of childMap) { const map1Value = hostMap.get(key); if (!map1Value) { @@ -184,7 +184,7 @@ function mergeContractMaps(hostMap: Map>, childMap: Map, childMap: Map) { +function mergeStorageMaps(hostMap: Map, childMap: Map) { for (const [key, value] of childMap) { hostMap.set(key, value); } diff --git a/yarn-project/acir-simulator/src/avm/opcodes/.eslintrc.cjs b/yarn-project/acir-simulator/src/avm/opcodes/.eslintrc.cjs new file mode 100644 index 00000000000..b972e1a3de3 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/opcodes/.eslintrc.cjs @@ -0,0 +1,8 @@ +const baseConfig = require('@aztec/foundation/eslint'); +module.exports = { + ...baseConfig, + rules: { + ...baseConfig.rules, + 'require-await': 'off', + }, +}; diff --git a/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.test.ts index 6dddce70aac..a9911f8d82d 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.test.ts @@ -10,34 +10,34 @@ describe('Arithmetic Instructions', () => { let machineState: AvmMachineState; let journal: MockProxy; - beforeEach(() => { + beforeEach(async () => { machineState = new AvmMachineState(initExecutionEnvironment()); journal = mock(); }); describe('Add', () => { - it('Should add correctly over field elements', () => { + it('Should add correctly over field elements', async () => { const a = new Field(1n); const b = new Field(2n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Add(0, 1, 2).execute(machineState, journal); + await new Add(0, 1, 2).execute(machineState, journal); const expected = new Field(3n); const actual = machineState.memory.get(2); expect(actual).toEqual(expected); }); - it('Should wrap around on addition', () => { + it('Should wrap around on addition', async () => { const a = new Field(1n); const b = new Field(Field.MODULUS - 1n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Add(0, 1, 2).execute(machineState, journal); + await new Add(0, 1, 2).execute(machineState, journal); const expected = new Field(0n); const actual = machineState.memory.get(2); @@ -46,14 +46,14 @@ describe('Arithmetic Instructions', () => { }); describe('Sub', () => { - it('Should subtract correctly over field elements', () => { + it('Should subtract correctly over field elements', async () => { const a = new Field(1n); const b = new Field(2n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Sub(0, 1, 2).execute(machineState, journal); + await new Sub(0, 1, 2).execute(machineState, journal); const expected = new Field(Field.MODULUS - 1n); const actual = machineState.memory.get(2); @@ -62,28 +62,28 @@ describe('Arithmetic Instructions', () => { }); describe('Mul', () => { - it('Should multiply correctly over field elements', () => { + it('Should multiply correctly over field elements', async () => { const a = new Field(2n); const b = new Field(3n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Mul(0, 1, 2).execute(machineState, journal); + await new Mul(0, 1, 2).execute(machineState, journal); const expected = new Field(6n); const actual = machineState.memory.get(2); expect(actual).toEqual(expected); }); - it('Should wrap around on multiplication', () => { + it('Should wrap around on multiplication', async () => { const a = new Field(2n); const b = new Field(Field.MODULUS / 2n - 1n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Mul(0, 1, 2).execute(machineState, journal); + await new Mul(0, 1, 2).execute(machineState, journal); const expected = new Field(Field.MODULUS - 3n); const actual = machineState.memory.get(2); @@ -92,14 +92,14 @@ describe('Arithmetic Instructions', () => { }); describe('Div', () => { - it('Should perform field division', () => { + it('Should perform field division', async () => { const a = new Field(2n); const b = new Field(3n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Div(0, 1, 2).execute(machineState, journal); + await new Div(0, 1, 2).execute(machineState, journal); // Note const actual = machineState.memory.get(2); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.ts b/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.ts index d6b5bc2bccb..507430feae7 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.ts @@ -10,7 +10,7 @@ export class Add extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); const b = machineState.memory.get(this.bOffset); @@ -29,7 +29,7 @@ export class Sub extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); const b = machineState.memory.get(this.bOffset); @@ -48,7 +48,7 @@ export class Mul extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); const b = machineState.memory.get(this.bOffset); @@ -68,7 +68,7 @@ export class Div extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); const b = machineState.memory.get(this.bOffset); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/bitwise.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/bitwise.test.ts index 4c791379c7e..426542a2733 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/bitwise.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/bitwise.test.ts @@ -10,43 +10,43 @@ describe('Bitwise instructions', () => { let machineState: AvmMachineState; let journal: MockProxy; - beforeEach(() => { + beforeEach(async () => { machineState = new AvmMachineState(initExecutionEnvironment()); journal = mock(); }); - it('Should AND correctly over integral types', () => { + it('Should AND correctly over integral types', async () => { machineState.memory.set(0, new Uint32(0b11111110010011100100n)); machineState.memory.set(1, new Uint32(0b11100100111001001111n)); - new And(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); + await new And(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); const actual = machineState.memory.get(2); expect(actual).toEqual(new Uint32(0b11100100010001000100n)); }); - it('Should OR correctly over integral types', () => { + it('Should OR correctly over integral types', async () => { const a = new Uint32(0b11111110010011100100n); const b = new Uint32(0b11100100111001001111n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Or(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); + await new Or(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); const expected = new Uint32(0b11111110111011101111n); const actual = machineState.memory.get(2); expect(actual).toEqual(expected); }); - it('Should XOR correctly over integral types', () => { + it('Should XOR correctly over integral types', async () => { const a = new Uint32(0b11111110010011100100n); const b = new Uint32(0b11100100111001001111n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Xor(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); + await new Xor(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); const expected = new Uint32(0b00011010101010101011n); const actual = machineState.memory.get(2); @@ -54,42 +54,42 @@ describe('Bitwise instructions', () => { }); describe('SHR', () => { - it('Should shift correctly 0 positions over integral types', () => { + it('Should shift correctly 0 positions over integral types', async () => { const a = new Uint32(0b11111110010011100100n); const b = new Uint32(0n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Shr(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); + await new Shr(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); const expected = a; const actual = machineState.memory.get(2); expect(actual).toEqual(expected); }); - it('Should shift correctly 2 positions over integral types', () => { + it('Should shift correctly 2 positions over integral types', async () => { const a = new Uint32(0b11111110010011100100n); const b = new Uint32(2n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Shr(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); + await new Shr(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); const expected = new Uint32(0b00111111100100111001n); const actual = machineState.memory.get(2); expect(actual).toEqual(expected); }); - it('Should shift correctly 19 positions over integral types', () => { + it('Should shift correctly 19 positions over integral types', async () => { const a = new Uint32(0b11111110010011100100n); const b = new Uint32(19n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Shr(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); + await new Shr(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); const expected = new Uint32(0b01n); const actual = machineState.memory.get(2); @@ -98,56 +98,56 @@ describe('Bitwise instructions', () => { }); describe('SHL', () => { - it('Should shift correctly 0 positions over integral types', () => { + it('Should shift correctly 0 positions over integral types', async () => { const a = new Uint32(0b11111110010011100100n); const b = new Uint32(0n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Shl(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); + await new Shl(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); const expected = a; const actual = machineState.memory.get(2); expect(actual).toEqual(expected); }); - it('Should shift correctly 2 positions over integral types', () => { + it('Should shift correctly 2 positions over integral types', async () => { const a = new Uint32(0b11111110010011100100n); const b = new Uint32(2n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Shl(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); + await new Shl(0, 1, 2, TypeTag.UINT32).execute(machineState, journal); const expected = new Uint32(0b1111111001001110010000n); const actual = machineState.memory.get(2); expect(actual).toEqual(expected); }); - it('Should shift correctly over bit limit over integral types', () => { + it('Should shift correctly over bit limit over integral types', async () => { const a = new Uint16(0b1110010011100111n); const b = new Uint16(17n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Shl(0, 1, 2, TypeTag.UINT16).execute(machineState, journal); + await new Shl(0, 1, 2, TypeTag.UINT16).execute(machineState, journal); const expected = new Uint16(0n); const actual = machineState.memory.get(2); expect(actual).toEqual(expected); }); - it('Should truncate when shifting over bit size over integral types', () => { + it('Should truncate when shifting over bit size over integral types', async () => { const a = new Uint16(0b1110010011100111n); const b = new Uint16(2n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new Shl(0, 1, 2, TypeTag.UINT16).execute(machineState, journal); + await new Shl(0, 1, 2, TypeTag.UINT16).execute(machineState, journal); const expected = new Uint16(0b1001001110011100n); const actual = machineState.memory.get(2); @@ -155,12 +155,12 @@ describe('Bitwise instructions', () => { }); }); - it('Should NOT correctly over integral types', () => { + it('Should NOT correctly over integral types', async () => { const a = new Uint16(0b0110010011100100n); machineState.memory.set(0, a); - new Not(0, 1, TypeTag.UINT16).execute(machineState, journal); + await new Not(0, 1, TypeTag.UINT16).execute(machineState, journal); const expected = new Uint16(0b1001101100011011n); // high bits! const actual = machineState.memory.get(1); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/bitwise.ts b/yarn-project/acir-simulator/src/avm/opcodes/bitwise.ts index 7d7f881bbf9..31420d0b3bc 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/bitwise.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/bitwise.ts @@ -11,7 +11,7 @@ export class And extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { Instruction.checkTags(machineState, this.inTag, this.aOffset, this.bOffset); const a = machineState.memory.getAs(this.aOffset); @@ -32,7 +32,7 @@ export class Or extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { Instruction.checkTags(machineState, this.inTag, this.aOffset, this.bOffset); const a = machineState.memory.getAs(this.aOffset); @@ -53,7 +53,7 @@ export class Xor extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { Instruction.checkTags(machineState, this.inTag, this.aOffset, this.bOffset); const a = machineState.memory.getAs(this.aOffset); @@ -74,7 +74,7 @@ export class Not extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { Instruction.checkTags(machineState, this.inTag, this.aOffset); const a = machineState.memory.getAs(this.aOffset); @@ -94,7 +94,7 @@ export class Shl extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { Instruction.checkTags(machineState, this.inTag, this.aOffset, this.bOffset); const a = machineState.memory.getAs(this.aOffset); @@ -115,7 +115,7 @@ export class Shr extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { Instruction.checkTags(machineState, this.inTag, this.aOffset, this.bOffset); const a = machineState.memory.getAs(this.aOffset); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/call.ts b/yarn-project/acir-simulator/src/avm/opcodes/call.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/yarn-project/acir-simulator/src/avm/opcodes/comparators.ts b/yarn-project/acir-simulator/src/avm/opcodes/comparators.ts index d9a4c48d867..8bdb406f9c6 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/comparators.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/comparators.ts @@ -11,7 +11,7 @@ export class Eq extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); const b = machineState.memory.get(this.bOffset); @@ -30,7 +30,7 @@ export class Lt extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); const b = machineState.memory.get(this.bOffset); @@ -49,7 +49,7 @@ export class Lte extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); const b = machineState.memory.get(this.bOffset); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/control_flow.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/control_flow.test.ts index 75a46347f61..ca4cdd45a5c 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/control_flow.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/control_flow.test.ts @@ -20,17 +20,17 @@ describe('Control Flow Opcodes', () => { machineState = new AvmMachineState(initExecutionEnvironment()); }); - it('Should implement JUMP', () => { + it('Should implement JUMP', async () => { const jumpLocation = 22; expect(machineState.pc).toBe(0); const instruction = new Jump(jumpLocation); - instruction.execute(machineState, journal); + await instruction.execute(machineState, journal); expect(machineState.pc).toBe(jumpLocation); }); - it('Should implement JUMPI - truthy', () => { + it('Should implement JUMPI - truthy', async () => { const jumpLocation = 22; const jumpLocation1 = 69; @@ -40,16 +40,16 @@ describe('Control Flow Opcodes', () => { machineState.memory.set(1, new Uint16(2n)); const instruction = new JumpI(jumpLocation, 0); - instruction.execute(machineState, journal); + await instruction.execute(machineState, journal); expect(machineState.pc).toBe(jumpLocation); // Truthy can be greater than 1 const instruction1 = new JumpI(jumpLocation1, 1); - instruction1.execute(machineState, journal); + await instruction1.execute(machineState, journal); expect(machineState.pc).toBe(jumpLocation1); }); - it('Should implement JUMPI - falsy', () => { + it('Should implement JUMPI - falsy', async () => { const jumpLocation = 22; expect(machineState.pc).toBe(0); @@ -57,11 +57,11 @@ describe('Control Flow Opcodes', () => { machineState.memory.set(0, new Uint16(0n)); const instruction = new JumpI(jumpLocation, 0); - instruction.execute(machineState, journal); + await instruction.execute(machineState, journal); expect(machineState.pc).toBe(1); }); - it('Should implement Internal Call and Return', () => { + it('Should implement Internal Call and Return', async () => { const jumpLocation = 22; expect(machineState.pc).toBe(0); @@ -69,14 +69,14 @@ describe('Control Flow Opcodes', () => { const instruction = new InternalCall(jumpLocation); const returnInstruction = new InternalReturn(); - instruction.execute(machineState, journal); + await instruction.execute(machineState, journal); expect(machineState.pc).toBe(jumpLocation); - returnInstruction.execute(machineState, journal); + await returnInstruction.execute(machineState, journal); expect(machineState.pc).toBe(1); }); - it('Should chain series of control flow instructions', () => { + it('Should chain series of control flow instructions', async () => { const jumpLocation0 = 22; const jumpLocation1 = 69; const jumpLocation2 = 1337; @@ -106,17 +106,17 @@ describe('Control Flow Opcodes', () => { ]; for (let i = 0; i < instructions.length; i++) { - instructions[i].execute(machineState, journal); + await instructions[i].execute(machineState, journal); expect(machineState.pc).toBe(expectedPcs[i]); } }); - it('Should error if Internal Return is called without a corresponding Internal Call', () => { - const returnInstruction = new InternalReturn(); - expect(() => returnInstruction.execute(machineState, journal)).toThrow(InstructionExecutionError); + it('Should error if Internal Return is called without a corresponding Internal Call', async () => { + const returnInstruction = () => new InternalReturn().execute(machineState, journal); + await expect(returnInstruction()).rejects.toThrow(InstructionExecutionError); }); - it('Should increment PC on All other Instructions', () => { + it('Should increment PC on All other Instructions', async () => { const instructions = [ new Add(0, 1, 2), new Sub(0, 1, 2), @@ -144,7 +144,7 @@ describe('Control Flow Opcodes', () => { innerMachineState.memory.set(1, new Uint16(8n)); innerMachineState.memory.set(2, new Uint16(12n)); expect(machineState.pc).toBe(0); - instruction.execute(innerMachineState, journal); + await instruction.execute(innerMachineState, journal); expect(innerMachineState.pc).toBe(1); } }); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/control_flow.ts b/yarn-project/acir-simulator/src/avm/opcodes/control_flow.ts index 95c9e12dcdb..518b987a1de 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/control_flow.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/control_flow.ts @@ -13,7 +13,7 @@ export class Return extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const returnData = machineState.memory .getSlice(this.returnOffset, this.copySize) .map(fvt => new Fr(fvt.toBigInt())); @@ -32,7 +32,7 @@ export class Jump extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { machineState.pc = this.jumpOffset; } } @@ -45,7 +45,7 @@ export class JumpI extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const condition = machineState.memory.getAs(this.condOffset); // TODO: reconsider this casting @@ -65,7 +65,7 @@ export class InternalCall extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { machineState.internalCallStack.push(machineState.pc + 1); machineState.pc = this.jumpOffset; } @@ -79,7 +79,7 @@ export class InternalReturn extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const jumpOffset = machineState.internalCallStack.pop(); if (jumpOffset === undefined) { throw new InstructionExecutionError('Internal call empty!'); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts new file mode 100644 index 00000000000..7a2f74df0e4 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts @@ -0,0 +1,132 @@ +import { Fr } from '@aztec/foundation/fields'; + +import { jest } from '@jest/globals'; +import { MockProxy, mock } from 'jest-mock-extended'; + +import { CommitmentsDB, PublicContractsDB, PublicStateDB } from '../../index.js'; +import { AvmMachineState } from '../avm_machine_state.js'; +import { Field } from '../avm_memory_types.js'; +import { initExecutionEnvironment } from '../fixtures/index.js'; +import { HostStorage } from '../journal/host_storage.js'; +import { AvmJournal } from '../journal/journal.js'; +import { encodeToBytecode } from './encode_to_bytecode.js'; +import { Call } from './external_calls.js'; +import { Opcode } from './opcodes.js'; + +describe('External Calls', () => { + let machineState: AvmMachineState; + let journal: AvmJournal; + + let contractsDb: MockProxy; + + beforeEach(() => { + machineState = new AvmMachineState(initExecutionEnvironment()); + + contractsDb = mock(); + + const commitmentsDb = mock(); + const publicStateDb = mock(); + const hostStorage = new HostStorage(publicStateDb, contractsDb, commitmentsDb); + journal = new AvmJournal(hostStorage); + }); + + describe('Call', () => { + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/3992): gas not implemented + it('Should execute a call correctly', async () => { + const gasOffset = 0; + const gas = Fr.zero(); + + const addrOffset = 1; + const addr = new Fr(123456n); + + const argsOffset = 2; + const args = [new Fr(1n), new Fr(2n), new Fr(3n)]; + const argsSize = args.length; + + const retOffset = 8; + const retSize = 2; + + const successOffset = 7; + + machineState.memory.set(0, gas); + machineState.memory.set(1, addr); + machineState.memory.setSlice(2, args); + + const otherContextInstructions: [Opcode, any[]][] = [ + // Place [1,2,3] into memory + [Opcode.CALLDATACOPY, [/*value=*/ 0, /*copySize=*/ argsSize, /*destOffset=*/ 0]], + // Store 1 into slot 1 + [Opcode.SSTORE, [/*slotOffset=*/ 0, /*dataOffset=*/ 0]], + // Return [1,2] from memory + [Opcode.RETURN, [/*retOffset=*/ 0, /*size=*/ 2]], + ]; + + const otherContextInstructionsBytecode = Buffer.concat( + otherContextInstructions.map(([opcode, args]) => encodeToBytecode(opcode, args)), + ); + jest + .spyOn(journal.hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(otherContextInstructionsBytecode)); + + const instruction = new Call(gasOffset, addrOffset, argsOffset, argsSize, retOffset, retSize, successOffset); + await instruction.execute(machineState, journal); + + const successValue = machineState.memory.get(successOffset); + expect(successValue).toEqual(new Fr(1n)); + + const retValue = machineState.memory.getSlice(retOffset, retSize); + expect(retValue).toEqual([new Fr(1n), new Fr(2n)]); + + // Check that the storage call has been merged into the parent journal + const { storageWrites } = journal.flush(); + expect(storageWrites.size).toEqual(1); + + const nestedContractWrites = storageWrites.get(addr.toBigInt()); + expect(nestedContractWrites).toBeDefined(); + + const slotNumber = 1n; + const expectedStoredValue = new Fr(1n); + expect(nestedContractWrites!.get(slotNumber)).toEqual(expectedStoredValue); + }); + }); + + describe('Static Call', () => { + it('Should fail if a static call attempts to touch storage', async () => { + const gasOffset = 0; + const gas = new Field(0); + const addrOffset = 1; + const addr = new Field(123456n); + const argsOffset = 2; + const args = [new Field(1n), new Field(2n), new Field(3n)]; + + const argsSize = args.length; + const retOffset = 8; + const retSize = 2; + const successOffset = 7; + + machineState.memory.set(0, gas); + machineState.memory.set(1, addr); + machineState.memory.setSlice(2, args); + + const otherContextInstructions: [Opcode, any[]][] = [ + // Place [1,2,3] into memory + [Opcode.CALLDATACOPY, [/*value=*/ 0, /*copySize=*/ argsSize, /*destOffset=*/ 0]], + [Opcode.SSTORE, [/*slotOffset*/ 1, /*dataOffset=*/ 0]], + ]; + + const otherContextInstructionsBytecode = Buffer.concat( + otherContextInstructions.map(([opcode, args]) => encodeToBytecode(opcode, args)), + ); + jest + .spyOn(journal.hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(otherContextInstructionsBytecode)); + + const instruction = new Call(gasOffset, addrOffset, argsOffset, argsSize, retOffset, retSize, successOffset); + await instruction.execute(machineState, journal); + + // No revert has occurred, but the nested execution has failed + const successValue = machineState.memory.get(successOffset); + expect(successValue).toEqual(new Fr(0n)); + }); + }); +}); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts new file mode 100644 index 00000000000..fc98f6c780c --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts @@ -0,0 +1,98 @@ +import { Fr } from '@aztec/foundation/fields'; + +import { AvmContext } from '../avm_context.js'; +import { AvmMachineState } from '../avm_machine_state.js'; +import { Field } from '../avm_memory_types.js'; +import { AvmJournal } from '../journal/journal.js'; +import { Instruction } from './instruction.js'; + +export class Call extends Instruction { + static type: string = 'CALL'; + static numberOfOperands = 7; + + constructor( + private /* Unused due to no formal gas implementation at this moment */ _gasOffset: number, + private addrOffset: number, + private argsOffset: number, + private argSize: number, + private retOffset: number, + private retSize: number, + private successOffset: number, + ) { + super(); + } + + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/3992): there is no concept of remaining / available gas at this moment + async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { + const callAddress = machineState.memory.getAs(this.addrOffset); + const calldata = machineState.memory.getSlice(this.argsOffset, this.argSize).map(f => new Fr(f.toBigInt())); + + const avmContext = AvmContext.prepExternalCallContext( + new Fr(callAddress.toBigInt()), + calldata, + machineState.executionEnvironment, + journal, + ); + + const returnObject = await avmContext.call(); + const success = !returnObject.reverted; + + // We only take as much data as was specified in the return size -> TODO: should we be reverting here + const returnData = returnObject.output.slice(0, this.retSize); + + // Write our return data into memory + machineState.memory.set(this.successOffset, new Fr(success)); + machineState.memory.setSlice(this.retOffset, returnData); + + if (success) { + avmContext.mergeJournal(); + } + + this.incrementPc(machineState); + } +} + +export class StaticCall extends Instruction { + static type: string = 'STATICCALL'; + static numberOfOperands = 7; + + constructor( + private /* Unused due to no formal gas implementation at this moment */ _gasOffset: number, + private addrOffset: number, + private argsOffset: number, + private argSize: number, + private retOffset: number, + private retSize: number, + private successOffset: number, + ) { + super(); + } + + async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { + const callAddress = machineState.memory.get(this.addrOffset); + const calldata = machineState.memory.getSlice(this.argsOffset, this.argSize).map(f => new Fr(f.toBigInt())); + + const avmContext = AvmContext.prepExternalStaticCallContext( + new Fr(callAddress.toBigInt()), + calldata, + machineState.executionEnvironment, + journal, + ); + + const returnObject = await avmContext.call(); + const success = !returnObject.reverted; + + // We only take as much data as was specified in the return size -> TODO: should we be reverting here + const returnData = returnObject.output.slice(0, this.retSize); + + // Write our return data into memory + machineState.memory.set(this.successOffset, new Fr(success)); + machineState.memory.setSlice(this.retOffset, returnData); + + if (success) { + avmContext.mergeJournal(); + } + + this.incrementPc(machineState); + } +} diff --git a/yarn-project/acir-simulator/src/avm/opcodes/index.ts b/yarn-project/acir-simulator/src/avm/opcodes/index.ts index f1f479bab46..10ce6b4d0f7 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/index.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/index.ts @@ -1,6 +1,5 @@ export * from './arithmetic.js'; export * from './control_flow.js'; -export * from './call.js'; export * from './instruction.js'; export * from './comparators.js'; export * from './memory.js'; diff --git a/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts b/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts index 03e362018ec..4878019b5f0 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts @@ -9,7 +9,7 @@ export const AVM_OPCODE_BYTE_LENGTH = 1; * Opcode base class */ export abstract class Instruction { - abstract execute(machineState: AvmMachineState, journal: AvmJournal): void; + abstract execute(machineState: AvmMachineState, journal: AvmJournal): Promise; incrementPc(machineState: AvmMachineState): void { machineState.pc++; diff --git a/yarn-project/acir-simulator/src/avm/opcodes/instruction_set.ts b/yarn-project/acir-simulator/src/avm/opcodes/instruction_set.ts index 3413673f7e6..9ea66060ae3 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/instruction_set.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/instruction_set.ts @@ -1,6 +1,7 @@ import { Add, Div, Mul, Sub } from './arithmetic.js'; import { And, Not, Or, Shl, Shr, Xor } from './bitwise.js'; import { InternalCall, InternalReturn, Jump, JumpI, Return } from './control_flow.js'; +// import { Call } from './external_calls.js'; import { Instruction } from './instruction.js'; import { CMov, CalldataCopy, Cast, Mov, Set } from './memory.js'; import { Opcode } from './opcodes.js'; @@ -87,7 +88,7 @@ export const INSTRUCTION_SET: Map = ne //[Opcode.EMITUNENCRYPTEDLOG, Emitunencryptedlog], //// Control Flow - Contract Calls - //[Opcode.CALL, Call], + // [Opcode.CALL, Call], //[Opcode.STATICCALL, Staticcall], [Opcode.RETURN, Return], //[Opcode.REVERT, Revert], diff --git a/yarn-project/acir-simulator/src/avm/opcodes/memory.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/memory.test.ts index 8b1b7d101f8..de9af42972d 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/memory.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/memory.test.ts @@ -1,6 +1,6 @@ import { Fr } from '@aztec/foundation/fields'; -import { mock } from 'jest-mock-extended'; +import { MockProxy, mock } from 'jest-mock-extended'; import { AvmMachineState } from '../avm_machine_state.js'; import { Field, TypeTag, Uint8, Uint16, Uint32, Uint64, Uint128 } from '../avm_memory_types.js'; @@ -10,16 +10,16 @@ import { CMov, CalldataCopy, Cast, Mov, Set } from './memory.js'; describe('Memory instructions', () => { let machineState: AvmMachineState; - let journal = mock(); + let journal: MockProxy; - beforeEach(() => { + beforeEach(async () => { machineState = new AvmMachineState(initExecutionEnvironment()); journal = mock(); }); describe('SET', () => { - it('should correctly set value and tag (uninitialized)', () => { - new Set(/*value=*/ 1234n, /*offset=*/ 1, TypeTag.UINT16).execute(machineState, journal); + it('should correctly set value and tag (uninitialized)', async () => { + await new Set(/*value=*/ 1234n, /*offset=*/ 1, TypeTag.UINT16).execute(machineState, journal); const actual = machineState.memory.get(1); const tag = machineState.memory.getTag(1); @@ -28,10 +28,10 @@ describe('Memory instructions', () => { expect(tag).toEqual(TypeTag.UINT16); }); - it('should correctly set value and tag (overwriting)', () => { + it('should correctly set value and tag (overwriting)', async () => { machineState.memory.set(1, new Field(27)); - new Set(/*value=*/ 1234n, /*offset=*/ 1, TypeTag.UINT32).execute(machineState, journal); + await new Set(/*value=*/ 1234n, /*offset=*/ 1, TypeTag.UINT32).execute(machineState, journal); const actual = machineState.memory.get(1); const tag = machineState.memory.getTag(1); @@ -150,10 +150,10 @@ describe('Memory instructions', () => { expect(tags).toEqual([TypeTag.UINT8, TypeTag.UINT16, TypeTag.UINT32, TypeTag.UINT64, TypeTag.UINT128]); }); - it('Should cast between field elements', () => { + it('Should cast between field elements', async () => { machineState.memory.set(0, new Field(12345678n)); - new Cast(/*aOffset=*/ 0, /*dstOffset=*/ 1, TypeTag.FIELD).execute(machineState, journal); + await new Cast(/*aOffset=*/ 0, /*dstOffset=*/ 1, TypeTag.FIELD).execute(machineState, journal); const actual = machineState.memory.get(1); expect(actual).toEqual(new Field(12345678n)); @@ -163,9 +163,9 @@ describe('Memory instructions', () => { }); describe('MOV', () => { - it('Should move integrals on different memory cells', () => { + it('Should move integrals on different memory cells', async () => { machineState.memory.set(1, new Uint16(27)); - new Mov(/*offsetA=*/ 1, /*offsetA=*/ 2).execute(machineState, journal); + await new Mov(/*offsetA=*/ 1, /*offsetA=*/ 2).execute(machineState, journal); const actual = machineState.memory.get(2); const tag = machineState.memory.getTag(2); @@ -174,9 +174,9 @@ describe('Memory instructions', () => { expect(tag).toEqual(TypeTag.UINT16); }); - it('Should move field elements on different memory cells', () => { + it('Should move field elements on different memory cells', async () => { machineState.memory.set(1, new Field(27)); - new Mov(/*offsetA=*/ 1, /*offsetA=*/ 2).execute(machineState, journal); + await new Mov(/*offsetA=*/ 1, /*offsetA=*/ 2).execute(machineState, journal); const actual = machineState.memory.get(2); const tag = machineState.memory.getTag(2); @@ -187,12 +187,15 @@ describe('Memory instructions', () => { }); describe('CMOV', () => { - it('Should move A if COND is true, on different memory cells (integral condition)', () => { + it('Should move A if COND is true, on different memory cells (integral condition)', async () => { machineState.memory.set(0, new Uint32(123)); // A machineState.memory.set(1, new Uint16(456)); // B machineState.memory.set(2, new Uint8(2)); // Condition - new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute(machineState, journal); + await new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( + machineState, + journal, + ); const actual = machineState.memory.get(3); const tag = machineState.memory.getTag(3); @@ -200,12 +203,15 @@ describe('Memory instructions', () => { expect(tag).toEqual(TypeTag.UINT32); }); - it('Should move B if COND is false, on different memory cells (integral condition)', () => { + it('Should move B if COND is false, on different memory cells (integral condition)', async () => { machineState.memory.set(0, new Uint32(123)); // A machineState.memory.set(1, new Uint16(456)); // B machineState.memory.set(2, new Uint8(0)); // Condition - new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute(machineState, journal); + await new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( + machineState, + journal, + ); const actual = machineState.memory.get(3); const tag = machineState.memory.getTag(3); @@ -213,12 +219,15 @@ describe('Memory instructions', () => { expect(tag).toEqual(TypeTag.UINT16); }); - it('Should move A if COND is true, on different memory cells (field condition)', () => { + it('Should move A if COND is true, on different memory cells (field condition)', async () => { machineState.memory.set(0, new Uint32(123)); // A machineState.memory.set(1, new Uint16(456)); // B machineState.memory.set(2, new Field(1)); // Condition - new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute(machineState, journal); + await new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( + machineState, + journal, + ); const actual = machineState.memory.get(3); const tag = machineState.memory.getTag(3); @@ -226,12 +235,15 @@ describe('Memory instructions', () => { expect(tag).toEqual(TypeTag.UINT32); }); - it('Should move B if COND is false, on different memory cells (integral condition)', () => { + it('Should move B if COND is false, on different memory cells (integral condition)', async () => { machineState.memory.set(0, new Uint32(123)); // A machineState.memory.set(1, new Uint16(456)); // B machineState.memory.set(2, new Field(0)); // Condition - new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute(machineState, journal); + await new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( + machineState, + journal, + ); const actual = machineState.memory.get(3); const tag = machineState.memory.getTag(3); @@ -241,34 +253,34 @@ describe('Memory instructions', () => { }); describe('CALLDATACOPY', () => { - it('Writes nothing if size is 0', () => { + it('Writes nothing if size is 0', async () => { const calldata = [new Fr(1n), new Fr(2n), new Fr(3n)]; machineState = new AvmMachineState(initExecutionEnvironment({ calldata })); machineState.memory.set(0, new Uint16(12)); // Some previous data to be overwritten - new CalldataCopy(/*cdOffset=*/ 0, /*copySize=*/ 0, /*dstOffset=*/ 0).execute(machineState, journal); + await new CalldataCopy(/*cdOffset=*/ 0, /*copySize=*/ 0, /*dstOffset=*/ 0).execute(machineState, journal); const actual = machineState.memory.get(0); expect(actual).toEqual(new Uint16(12)); }); - it('Copies all calldata', () => { + it('Copies all calldata', async () => { const calldata = [new Fr(1n), new Fr(2n), new Fr(3n)]; machineState = new AvmMachineState(initExecutionEnvironment({ calldata })); machineState.memory.set(0, new Uint16(12)); // Some previous data to be overwritten - new CalldataCopy(/*cdOffset=*/ 0, /*copySize=*/ 3, /*dstOffset=*/ 0).execute(machineState, journal); + await new CalldataCopy(/*cdOffset=*/ 0, /*copySize=*/ 3, /*dstOffset=*/ 0).execute(machineState, journal); const actual = machineState.memory.getSlice(/*offset=*/ 0, /*size=*/ 3); expect(actual).toEqual([new Field(1), new Field(2), new Field(3)]); }); - it('Copies slice of calldata', () => { + it('Copies slice of calldata', async () => { const calldata = [new Fr(1n), new Fr(2n), new Fr(3n)]; machineState = new AvmMachineState(initExecutionEnvironment({ calldata })); machineState.memory.set(0, new Uint16(12)); // Some previous data to be overwritten - new CalldataCopy(/*cdOffset=*/ 1, /*copySize=*/ 2, /*dstOffset=*/ 0).execute(machineState, journal); + await new CalldataCopy(/*cdOffset=*/ 1, /*copySize=*/ 2, /*dstOffset=*/ 0).execute(machineState, journal); const actual = machineState.memory.getSlice(/*offset=*/ 0, /*size=*/ 2); expect(actual).toEqual([new Field(2), new Field(3)]); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/memory.ts b/yarn-project/acir-simulator/src/avm/opcodes/memory.ts index 16aa2327db4..0c8ada5b6cd 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/memory.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/memory.ts @@ -5,13 +5,13 @@ import { Instruction } from './instruction.js'; export class Set extends Instruction { static type: string = 'SET'; - static numberOfOperands = 2; + static numberOfOperands = 3; constructor(private value: bigint, private dstOffset: number, private dstTag: TypeTag) { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const res = TaggedMemory.integralFromTag(this.value, this.dstTag); machineState.memory.set(this.dstOffset, res); @@ -22,13 +22,13 @@ export class Set extends Instruction { export class Cast extends Instruction { static type: string = 'CAST'; - static numberOfOperands = 2; + static numberOfOperands = 3; constructor(private aOffset: number, private dstOffset: number, private dstTag: TypeTag) { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); // TODO: consider not using toBigInt() @@ -49,7 +49,7 @@ export class Mov extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); machineState.memory.set(this.dstOffset, a); @@ -66,7 +66,7 @@ export class CMov extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); const b = machineState.memory.get(this.bOffset); const cond = machineState.memory.get(this.condOffset); @@ -86,7 +86,7 @@ export class CalldataCopy extends Instruction { super(); } - execute(machineState: AvmMachineState, _journal: AvmJournal): void { + async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const transformedData = machineState.executionEnvironment.calldata .slice(this.cdOffset, this.cdOffset + this.copySize) .map(f => new Field(f)); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts index d5fcb0da8be..47bc4433862 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts @@ -4,9 +4,10 @@ import { Fr } from '@aztec/foundation/fields'; import { MockProxy, mock } from 'jest-mock-extended'; import { AvmMachineState } from '../avm_machine_state.js'; +import { Field } from '../avm_memory_types.js'; import { initExecutionEnvironment } from '../fixtures/index.js'; import { AvmJournal } from '../journal/journal.js'; -import { SLoad, SStore } from './storage.js'; +import { SLoad, SStore, StaticCallStorageAlterError } from './storage.js'; describe('Storage Instructions', () => { let journal: MockProxy; @@ -20,16 +21,30 @@ describe('Storage Instructions', () => { machineState = new AvmMachineState(executionEnvironment); }); - it('Sstore should Write into storage', () => { - const a = new Fr(1n); - const b = new Fr(2n); + it('Sstore should Write into storage', async () => { + const a = new Field(1n); + const b = new Field(2n); machineState.memory.set(0, a); machineState.memory.set(1, b); - new SStore(0, 1).execute(machineState, journal); + await new SStore(0, 1).execute(machineState, journal); - expect(journal.writeStorage).toBeCalledWith(address, a, b); + expect(journal.writeStorage).toBeCalledWith(address, new Fr(a.toBigInt()), new Fr(b.toBigInt())); + }); + + it('Should not be able to write to storage in a static call', async () => { + const executionEnvironment = initExecutionEnvironment({ isStaticCall: true }); + machineState = new AvmMachineState(executionEnvironment); + + const a = new Field(1n); + const b = new Field(2n); + + machineState.memory.set(0, a); + machineState.memory.set(1, b); + + const instruction = () => new SStore(0, 1).execute(machineState, journal); + await expect(instruction()).rejects.toThrowError(StaticCallStorageAlterError); }); it('Sload should Read into storage', async () => { @@ -37,15 +52,15 @@ describe('Storage Instructions', () => { const expectedResult = new Fr(1n); journal.readStorage.mockReturnValueOnce(Promise.resolve(expectedResult)); - const a = new Fr(1n); - const b = new Fr(2n); + const a = new Field(1n); + const b = new Field(2n); machineState.memory.set(0, a); machineState.memory.set(1, b); await new SLoad(0, 1).execute(machineState, journal); - expect(journal.readStorage).toBeCalledWith(address, a); + expect(journal.readStorage).toBeCalledWith(address, new Fr(a.toBigInt())); const actual = machineState.memory.get(1); expect(actual).toEqual(expectedResult); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/storage.ts b/yarn-project/acir-simulator/src/avm/opcodes/storage.ts index c8631e515d7..e8f078f7909 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/storage.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/storage.ts @@ -1,6 +1,7 @@ import { Fr } from '@aztec/foundation/fields'; import { AvmMachineState } from '../avm_machine_state.js'; +import { AvmInterpreterError } from '../interpreter/interpreter.js'; import { AvmJournal } from '../journal/journal.js'; import { Instruction } from './instruction.js'; @@ -13,7 +14,11 @@ export class SStore extends Instruction { super(); } - execute(machineState: AvmMachineState, journal: AvmJournal): void { + async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { + if (machineState.executionEnvironment.isStaticCall) { + throw new StaticCallStorageAlterError(); + } + const slot = machineState.memory.get(this.slotOffset); const data = machineState.memory.get(this.dataOffset); @@ -46,3 +51,13 @@ export class SLoad extends Instruction { this.incrementPc(machineState); } } + +/** + * Error is thrown when a static call attempts to alter storage + */ +export class StaticCallStorageAlterError extends AvmInterpreterError { + constructor() { + super('Static calls cannot alter storage'); + this.name = 'StaticCallStorageAlterError'; + } +}