diff --git a/yarn-project/acir-simulator/src/avm/avm_context.ts b/yarn-project/acir-simulator/src/avm/avm_context.ts index ea0f0c44a68..45491765805 100644 --- a/yarn-project/acir-simulator/src/avm/avm_context.ts +++ b/yarn-project/acir-simulator/src/avm/avm_context.ts @@ -1,4 +1,4 @@ -import { FunctionSelector } from '@aztec/circuits.js'; +import { AztecAddress, FunctionSelector } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { AvmExecutionEnvironment } from './avm_execution_environment.js'; @@ -67,11 +67,33 @@ export class AvmContext { /** * Create a new forked avm context - for external calls */ - public static newWithForkedState( + public static newWithForkedState(executionEnvironment: AvmExecutionEnvironment, journal: AvmJournal): AvmContext { + const forkedState = AvmJournal.branchParent(journal); + return new AvmContext(executionEnvironment, forkedState); + } + + public static prepExternalCall( + address: AztecAddress, executionEnvironment: AvmExecutionEnvironment, journal: AvmJournal, ): AvmContext { + const newExecutionEnvironment = executionEnvironment.newCall(address); const forkedState = AvmJournal.branchParent(journal); - return new AvmContext(executionEnvironment, forkedState); + return new AvmContext(newExecutionEnvironment, forkedState); + } + + public static prepExternalStaticCall( + address: AztecAddress, + executionEnvironment: AvmExecutionEnvironment, + journal: AvmJournal, + ): AvmContext { + const newExecutionEnvironment = executionEnvironment.newStaticCall(address); + const forkedState = AvmJournal.branchParent(journal); + return new AvmContext(newExecutionEnvironment, forkedState); + } + + // TODO: document + public mergeJournal() { + this.journal.mergeWithParent(); } } 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 index 24b14be2fd4..4110d97ce7f 100644 --- a/yarn-project/acir-simulator/src/avm/avm_execution_environment.test.ts +++ b/yarn-project/acir-simulator/src/avm/avm_execution_environment.test.ts @@ -9,7 +9,10 @@ describe('Execution Environment', () => { const executionEnvironment = initExecutionEnvironment(); const newExecutionEnvironment = executionEnvironment.newCall(newAddress); - allTheSameExcept(executionEnvironment, newExecutionEnvironment, { address: newAddress }); + allTheSameExcept(executionEnvironment, newExecutionEnvironment, { + address: newAddress, + storageAddress: newAddress, + }); }); it('New delegate call should fork execution environment correctly', () => { 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 7600fd156ec..c93b8a02f69 100644 --- a/yarn-project/acir-simulator/src/avm/avm_execution_environment.ts +++ b/yarn-project/acir-simulator/src/avm/avm_execution_environment.ts @@ -41,7 +41,7 @@ export class AvmExecutionEnvironment { public newCall(address: AztecAddress): AvmExecutionEnvironment { return new AvmExecutionEnvironment( address, - this.storageAddress, + address, this.origin, this.sender, this.portal, 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 index 712d01d564c..5656e4d0758 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts @@ -1,4 +1,3 @@ -import { BlockHeader } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { jest } from '@jest/globals'; @@ -31,21 +30,21 @@ describe('External Calls', () => { }); describe('Call', () => { - it('Should create a new call context correctly', async () => { - // TODO: gas not implemented - // prettier-ignore-start - // mem index | value + // TODO: 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)]; - // prettier-ignore-end - const argsSize = args.length; + const retOffset = 8; const retSize = 2; + const successOffset = 7; machineState.writeMemory(0, gas); @@ -54,8 +53,11 @@ describe('External Calls', () => { // TODO: mock the call that is made -> set the bytecode to be a return of two values const otherContextInstructions: [Opcode, any[]][] = [ - [Opcode.SET, [/* value */ 1, /* destOffset */ 0]], - [Opcode.SET, [/* value */ 2, /* destOffset */ 1]], + // 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]], ]; @@ -74,6 +76,52 @@ describe('External Calls', () => { const retValue = machineState.readMemoryChunk(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); + expect(nestedContractWrites).toBeDefined(); + expect(nestedContractWrites!.get(args[0])).toEqual(args[0]); + }); + }); + + describe('Static Call', () => { + it('Should fail if a static call attempts to touch storage', 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.writeMemory(0, gas); + machineState.writeMemory(1, addr); + machineState.writeMemoryChunk(2, args); + + const otherContextInstructions: [Opcode, any[]][] = [ + [Opcode.SET, [/* value */ 1, /* destOffset */ 1]], + [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.readMemory(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 index 728e4daae39..41b1a98b175 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts @@ -2,10 +2,9 @@ import { Fr } from '@aztec/foundation/fields'; import { AvmContext } from '../avm_context.js'; import { AvmMachineState } from '../avm_machine_state.js'; -import { Instruction } from './instruction.js'; import { AvmJournal } from '../journal/journal.js'; +import { Instruction } from './instruction.js'; -/** - */ export class Call extends Instruction { static type: string = 'CALL'; static numberOfOperands = 7; @@ -24,18 +23,48 @@ export class Call extends Instruction { // TODO: there is no concept of remaining / available gas at this moment async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { - // This instruction will need to create another instance of the AVM with: - // - a forked State Manager - // - the same execution environment variables - // - a fresh memory instance + const callAddress = machineState.readMemory(this.addrOffset); + const calldata = machineState.readMemoryChunk(this.argsOffset, this.argSize); + + const avmContext = AvmContext.prepExternalCall(callAddress, machineState.executionEnvironment, journal); + + const returnObject = await avmContext.call(calldata); + 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.writeMemory(this.successOffset, new Fr(success)); + machineState.writeMemoryChunk(this.retOffset, returnData); + + 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.readMemory(this.addrOffset); - // TODO: check that we can assume that this memory chunk will be field elements const calldata = machineState.readMemoryChunk(this.argsOffset, this.argSize); - // TODO: could this be consolidated within an AVMContext static member? - const newExecutionEnvironment = machineState.executionEnvironment.newCall(callAddress); - const avmContext = AvmContext.newWithForkedState(newExecutionEnvironment, journal); + const avmContext = AvmContext.prepExternalStaticCall(callAddress, machineState.executionEnvironment, journal); const returnObject = await avmContext.call(calldata); const success = !returnObject.reverted; @@ -47,6 +76,8 @@ export class Call extends Instruction { machineState.writeMemory(this.successOffset, new Fr(success)); machineState.writeMemoryChunk(this.retOffset, returnData); + avmContext.mergeJournal(); + this.incrementPc(machineState); } } 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 68ddd96855d..76aa4e06eb4 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts @@ -6,7 +6,7 @@ import { MockProxy, mock } from 'jest-mock-extended'; import { AvmMachineState } from '../avm_machine_state.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; @@ -32,6 +32,19 @@ describe('Storage Instructions', () => { expect(journal.writeStorage).toBeCalledWith(address, a, b); }); + it('Should not be able to write to storage in a static call', () => { + const executionEnvironment = initExecutionEnvironment({ isStaticCall: true }); + machineState = new AvmMachineState([], executionEnvironment); + + const a = new Fr(1n); + const b = new Fr(2n); + + machineState.writeMemory(0, a); + machineState.writeMemory(1, b); + + expect(() => new SStore(0, 1).execute(machineState, journal)).toThrowError(StaticCallStorageAlterError); + }); + it('Sload should Read into storage', async () => { // Mock response const expectedResult = new Fr(1n); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/storage.ts b/yarn-project/acir-simulator/src/avm/opcodes/storage.ts index e03ae48a851..262716c087d 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/storage.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/storage.ts @@ -1,4 +1,5 @@ import { AvmMachineState } from '../avm_machine_state.js'; +import { AvmInterpreterError } from '../interpreter/interpreter.js'; import { AvmJournal } from '../journal/journal.js'; import { Instruction } from './instruction.js'; @@ -12,6 +13,10 @@ export class SStore extends Instruction { } execute(machineState: AvmMachineState, journal: AvmJournal): void { + if (machineState.executionEnvironment.isStaticCall) { + throw new StaticCallStorageAlterError(); + } + const slot = machineState.readMemory(this.slotOffset); const data = machineState.readMemory(this.dataOffset); @@ -40,3 +45,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'; + } +}