From 7c55079fbdd3ef5c27a1863b8e27fca8017caed8 Mon Sep 17 00:00:00 2001 From: Maddiaa0 <47148561+Maddiaa0@users.noreply.github.com> Date: Tue, 23 Jan 2024 23:43:58 +0000 Subject: [PATCH] feat: initial external calls --- .../acir-simulator/src/avm/avm_context.ts | 35 +++++++- .../src/avm/avm_execution_environment.test.ts | 49 +++++++++++ .../src/avm/avm_execution_environment.ts | 56 +++++++++++++ .../src/avm/opcodes/external_calls.test.ts | 83 +++++++++++++++++++ .../src/avm/opcodes/external_calls.ts | 52 ++++++++++++ .../src/avm/opcodes/instruction_set.ts | 3 +- .../src/avm/opcodes/memory.test.ts | 4 +- 7 files changed, 276 insertions(+), 6 deletions(-) create mode 100644 yarn-project/acir-simulator/src/avm/avm_execution_environment.test.ts create mode 100644 yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts create mode 100644 yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts diff --git a/yarn-project/acir-simulator/src/avm/avm_context.ts b/yarn-project/acir-simulator/src/avm/avm_context.ts index 63cf86305039..f385b0812ee2 100644 --- a/yarn-project/acir-simulator/src/avm/avm_context.ts +++ b/yarn-project/acir-simulator/src/avm/avm_context.ts @@ -1,3 +1,4 @@ +import { FunctionSelector } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { AvmExecutionEnvironment } from './avm_execution_environment.js'; @@ -33,10 +34,18 @@ export class AvmContext { * * @param calldata - */ - public call(calldata: Fr[]): AvmMessageCallResult { + public async call(calldata: Fr[]): Promise { // NOTE: the following is mocked as getPublicBytecode does not exist yet - // const bytecode = stateManager.journal.hostStorage.contractsDb.getBytecode(this.executionEnvironment.address); - const bytecode = Buffer.from('0x01000100020003'); + const selector = new FunctionSelector(0); + const bytecode = await this.stateManager.journal.hostStorage.contractsDb.getBytecode( + this.executionEnvironment.address, + selector, + ); + + // TODO: handle this gracefully ! with account abstraction can we allow this? + if (!bytecode) { + throw new Error('No bytecode found'); + } const instructions: Instruction[] = decodeBytecode(bytecode); @@ -45,4 +54,24 @@ export class AvmContext { return interpreter.run(); } + + /** + * Create a new forked avm context - for internal calls + */ + public newWithForkedState(): AvmContext { + const forkedState = AvmStateManager.forkStateManager(this.stateManager); + return new AvmContext(this.executionEnvironment, forkedState); + } + + // TODO: more documentation / thinking + /** + * Create a new forked avm context - for external calls + */ + public static newWithForkedState( + executionEnvironment: AvmExecutionEnvironment, + stateManager: AvmStateManager, + ): AvmContext { + const forkedState = AvmStateManager.forkStateManager(stateManager); + return new AvmContext(executionEnvironment, forkedState); + } } 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 000000000000..24b14be2fd41 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/avm_execution_environment.test.ts @@ -0,0 +1,49 @@ +import { Fr } from '@aztec/foundation/fields'; + +import { initExecutionEnvironment } from './fixtures/index.js'; + +describe('Execution Environment', () => { + it('New call should fork execution environment correctly', () => { + const newAddress = new Fr(123456n); + + const executionEnvironment = initExecutionEnvironment(); + const newExecutionEnvironment = executionEnvironment.newCall(newAddress); + + allTheSameExcept(executionEnvironment, newExecutionEnvironment, { address: newAddress }); + }); + + it('New delegate call should fork execution environment correctly', () => { + const newAddress = new Fr(123456n); + + const executionEnvironment = initExecutionEnvironment(); + const newExecutionEnvironment = executionEnvironment.newDelegateCall(newAddress); + + allTheSameExcept(executionEnvironment, newExecutionEnvironment, { + storageAddress: newAddress, + isDelegateCall: true, + }); + }); + + it('New static call call should fork execution environment correctly', () => { + const newAddress = new Fr(123456n); + + const executionEnvironment = initExecutionEnvironment(); + const newExecutionEnvironment = executionEnvironment.newStaticCall(newAddress); + + allTheSameExcept(executionEnvironment, newExecutionEnvironment, { address: newAddress, isStaticCall: true }); + }); +}); + +/** + * Check all properties of one object are the same, except for the specified differentProperties + * TODO: maybe move this into some foundation test utilities file? + */ +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 495a579feab2..7600fd156ece 100644 --- a/yarn-project/acir-simulator/src/avm/avm_execution_environment.ts +++ b/yarn-project/acir-simulator/src/avm/avm_execution_environment.ts @@ -36,4 +36,60 @@ export class AvmExecutionEnvironment { /** - */ public readonly calldata: Fr[], ) {} + + // TODO: gas not implemented + public newCall(address: AztecAddress): AvmExecutionEnvironment { + return new AvmExecutionEnvironment( + address, + this.storageAddress, + this.origin, + this.sender, + this.portal, + this.feePerL1Gas, + this.feePerL2Gas, + this.feePerDaGas, + this.contractCallDepth, + this.globals, + this.isStaticCall, + this.isDelegateCall, + this.calldata, + ); + } + + public newStaticCall(address: AztecAddress): AvmExecutionEnvironment { + return new AvmExecutionEnvironment( + address, + this.storageAddress, + this.origin, + this.sender, + this.portal, + this.feePerL1Gas, + this.feePerL2Gas, + this.feePerDaGas, + this.contractCallDepth, + this.globals, + true, + this.isDelegateCall, + this.calldata, + ); + } + + // TODO: gas not implemented + public newDelegateCall(storageAddress: AztecAddress): AvmExecutionEnvironment { + return new AvmExecutionEnvironment( + this.address, + storageAddress, + this.origin, + this.sender, + this.portal, + this.feePerL1Gas, + this.feePerL2Gas, + this.feePerDaGas, + this.contractCallDepth, + this.globals, + this.isStaticCall, + true, + this.calldata, + ); + } } 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 000000000000..49dc7b315e4c --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts @@ -0,0 +1,83 @@ +import { BlockHeader } from '@aztec/circuits.js'; +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 { AvmStateManager } from '../avm_state_manager.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 stateManager: AvmStateManager; + + let contractsDb: MockProxy; + + beforeEach(() => { + machineState = new AvmMachineState([], initExecutionEnvironment()); + + contractsDb = mock(); + + const commitmentsDb = mock(); + const publicStateDb = mock(); + const hostStorage = new HostStorage(publicStateDb, contractsDb, commitmentsDb); + const journal = new AvmJournal(hostStorage); + const blockHeader = BlockHeader.empty(); + + stateManager = new AvmStateManager(blockHeader, journal); + }); + + describe('Call', () => { + it('Should create a new call context correctly', async () => { + // TODO: gas not implemented + // prettier-ignore-start + // mem index | value + 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); + machineState.writeMemory(1, addr); + machineState.writeMemoryChunk(2, args); + + // 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]], + [Opcode.RETURN, [/* retOffset */ 0, /* size */ 2]], + ]; + + const otherContextInstructionsBytecode = Buffer.concat( + otherContextInstructions.map(([opcode, args]) => encodeToBytecode(opcode, args)), + ); + jest + .spyOn(stateManager.journal.hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(otherContextInstructionsBytecode)); + + const instruction = new Call(gasOffset, addrOffset, argsOffset, argsSize, retOffset, retSize, successOffset); + await instruction.execute(machineState, stateManager); + + const successValue = machineState.readMemory(successOffset); + expect(successValue).toEqual(new Fr(1n)); + + const retValue = machineState.readMemoryChunk(retOffset, retSize); + expect(retValue).toEqual([new Fr(1n), new Fr(2n)]); + }); + }); +}); 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 000000000000..2d9018f7a6e6 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts @@ -0,0 +1,52 @@ +import { Fr } from '@aztec/foundation/fields'; + +import { AvmContext } from '../avm_context.js'; +import { AvmMachineState } from '../avm_machine_state.js'; +import { AvmStateManager } from '../avm_state_manager.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: there is no concept of remaining / available gas at this moment + async execute(machineState: AvmMachineState, stateManager: AvmStateManager): 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); + // 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, stateManager); + + 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); + + this.incrementPc(machineState); + } +} 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 3413673f7e62..9ea66060ae3d 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 6cad7472548e..e57e39837126 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 { AvmStateManager } from '../avm_state_manager.js'; @@ -9,7 +9,7 @@ import { CMov, CalldataCopy, Cast, Mov, Set } from './memory.js'; describe('Memory instructions', () => { let machineState: AvmMachineState; - let stateManager = mock(); + let stateManager: MockProxy; beforeEach(() => { machineState = new AvmMachineState([], initExecutionEnvironment());