Skip to content

Commit

Permalink
feat: initial external calls
Browse files Browse the repository at this point in the history
  • Loading branch information
Maddiaa0 committed Jan 24, 2024
1 parent 4e51e49 commit c7b392d
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 6 deletions.
35 changes: 32 additions & 3 deletions yarn-project/acir-simulator/src/avm/avm_context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FunctionSelector } from '@aztec/circuits.js';
import { Fr } from '@aztec/foundation/fields';

import { AvmExecutionEnvironment } from './avm_execution_environment.js';
Expand Down Expand Up @@ -33,10 +34,18 @@ export class AvmContext {
*
* @param calldata -
*/
public call(calldata: Fr[]): AvmMessageCallResult {
public async call(calldata: Fr[]): Promise<AvmMessageCallResult> {
// 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,
);

// TODO: handle this gracefully ! with account abstraction can we allow this?
if (!bytecode) {
throw new Error('No bytecode found');
}

const instructions: Instruction[] = decodeBytecode(bytecode);

Expand All @@ -45,4 +54,24 @@ 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);
}

// TODO: more documentation / thinking
/**
* 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<string, any>): 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]);
}
}
}
56 changes: 56 additions & 0 deletions yarn-project/acir-simulator/src/avm/avm_execution_environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
79 changes: 79 additions & 0 deletions yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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 { 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<PublicContractsDB>;

beforeEach(() => {
machineState = new AvmMachineState([], initExecutionEnvironment());

contractsDb = mock<PublicContractsDB>();

const commitmentsDb = mock<CommitmentsDB>();
const publicStateDb = mock<PublicStateDB>();
const hostStorage = new HostStorage(publicStateDb, contractsDb, commitmentsDb);
journal = new AvmJournal(hostStorage);
});

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(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.readMemory(successOffset);
expect(successValue).toEqual(new Fr(1n));

const retValue = machineState.readMemoryChunk(retOffset, retSize);
expect(retValue).toEqual([new Fr(1n), new Fr(2n)]);
});
});
});
52 changes: 52 additions & 0 deletions yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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';

/** - */
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, journal: AvmJournal): Promise<void> {
// 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, 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);

this.incrementPc(machineState);
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -87,7 +88,7 @@ export const INSTRUCTION_SET: Map<Opcode, InstructionConstructorAndMembers> = ne
//[Opcode.EMITUNENCRYPTEDLOG, Emitunencryptedlog],

//// Control Flow - Contract Calls
//[Opcode.CALL, Call],
// [Opcode.CALL, Call],
//[Opcode.STATICCALL, Staticcall],
[Opcode.RETURN, Return],
//[Opcode.REVERT, Revert],
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/acir-simulator/src/avm/opcodes/memory.test.ts
Original file line number Diff line number Diff line change
@@ -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 { initExecutionEnvironment } from '../fixtures/index.js';
Expand All @@ -9,7 +9,7 @@ import { CMov, CalldataCopy, Cast, Mov, Set } from './memory.js';

describe('Memory instructions', () => {
let machineState: AvmMachineState;
let journal = mock<AvmJournal>();
let journal: MockProxy<AvmJournal>;

beforeEach(() => {
machineState = new AvmMachineState([], initExecutionEnvironment());
Expand Down

0 comments on commit c7b392d

Please sign in to comment.