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 265e90d commit 9ec7ef2
Show file tree
Hide file tree
Showing 9 changed files with 441 additions and 8 deletions.
93 changes: 89 additions & 4 deletions yarn-project/acir-simulator/src/avm/avm_context.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -30,10 +33,19 @@ export class AvmContext {
* - We run the interpreter
*
*/
public call(): AvmMessageCallResult {
public async call(): 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,
);

// 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);

Expand All @@ -42,4 +54,77 @@ 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 prepExternalCall(
address: AztecAddress,
executionEnvironment: AvmExecutionEnvironment,
journal: AvmJournal,
): AvmContext {
const newExecutionEnvironment = executionEnvironment.newCall(address);
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 prepExternalStaticCall(
address: AztecAddress,
executionEnvironment: AvmExecutionEnvironment,
journal: AvmJournal,
): AvmContext {
const newExecutionEnvironment = executionEnvironment.newStaticCall(address);
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';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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,
storageAddress: 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, {
address: 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,
storageAddress: newAddress,
isStaticCall: true,
});
});
});

/**
* Check all properties of one object are the same, except for the specified differentProperties
*/
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]);
}
}
}
55 changes: 55 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 @@ -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(
/** - */
Expand Down Expand Up @@ -36,4 +37,58 @@ export class AvmExecutionEnvironment {
/** - */
public readonly calldata: Fr[],
) {}

public newCall(address: AztecAddress): AvmExecutionEnvironment {
return new AvmExecutionEnvironment(
address,
address,
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,
address,
this.origin,
this.sender,
this.portal,
this.feePerL1Gas,
this.feePerL2Gas,
this.feePerDaGas,
this.contractCallDepth,
this.globals,
true,
this.isDelegateCall,
this.calldata,
);
}

public newDelegateCall(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,
true,
this.calldata,
);
}
}
126 changes: 126 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,126 @@
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', () => {
// 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.writeMemory(0, gas);
machineState.writeMemory(1, addr);
machineState.writeMemoryChunk(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.readMemory(successOffset);
expect(successValue).toEqual(new Fr(1n));

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));
});
});
});
Loading

0 comments on commit 9ec7ef2

Please sign in to comment.