Skip to content

Commit

Permalink
feat: implement static call
Browse files Browse the repository at this point in the history
  • Loading branch information
Maddiaa0 committed Jan 24, 2024
1 parent c7b392d commit cf2c2f7
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 31 deletions.
28 changes: 25 additions & 3 deletions yarn-project/acir-simulator/src/avm/avm_context.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -19,7 +22,7 @@ describe('Execution Environment', () => {
const newExecutionEnvironment = executionEnvironment.newDelegateCall(newAddress);

allTheSameExcept(executionEnvironment, newExecutionEnvironment, {
storageAddress: newAddress,
address: newAddress,
isDelegateCall: true,
});
});
Expand All @@ -30,7 +33,7 @@ describe('Execution Environment', () => {
const executionEnvironment = initExecutionEnvironment();
const newExecutionEnvironment = executionEnvironment.newStaticCall(newAddress);

allTheSameExcept(executionEnvironment, newExecutionEnvironment, { address: newAddress, isStaticCall: true });
allTheSameExcept(executionEnvironment, newExecutionEnvironment, { address: newAddress, storageAddress: newAddress, isStaticCall: true });
});
});

Expand Down
10 changes: 5 additions & 5 deletions yarn-project/acir-simulator/src/avm/avm_execution_environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -59,7 +59,7 @@ export class AvmExecutionEnvironment {
public newStaticCall(address: AztecAddress): AvmExecutionEnvironment {
return new AvmExecutionEnvironment(
address,
this.storageAddress,
address,
this.origin,
this.sender,
this.portal,
Expand All @@ -75,10 +75,10 @@ export class AvmExecutionEnvironment {
}

// TODO: gas not implemented
public newDelegateCall(storageAddress: AztecAddress): AvmExecutionEnvironment {
public newDelegateCall(address: AztecAddress): AvmExecutionEnvironment {
return new AvmExecutionEnvironment(
this.address,
storageAddress,
address,
this.storageAddress,
this.origin,
this.sender,
this.portal,
Expand Down
66 changes: 57 additions & 9 deletions yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { BlockHeader } from '@aztec/circuits.js';
import { Fr } from '@aztec/foundation/fields';

import { jest } from '@jest/globals';
Expand Down Expand Up @@ -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);
Expand All @@ -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]],
];

Expand All @@ -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));
});
});
});
51 changes: 41 additions & 10 deletions yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<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);
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<void> {
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;
Expand All @@ -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);
}
}
15 changes: 14 additions & 1 deletion yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AvmJournal>;
Expand All @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions yarn-project/acir-simulator/src/avm/opcodes/storage.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);

Expand Down Expand Up @@ -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';
}
}

0 comments on commit cf2c2f7

Please sign in to comment.