Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(avm): refactor AVM Simulator and fix issues #4424

Merged
merged 17 commits into from
Feb 5, 2024
19 changes: 17 additions & 2 deletions yarn-project/acir-simulator/src/avm/avm_context.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
describe('Avm', () => {
it('Executes a simple call', () => {});
// import { AztecAddress, Fr } from '@aztec/circuits.js';
// import { initContext } from './fixtures/index.js';

describe('Avm Context', () => {
it('New call should fork context correctly', () => {
// const context = initContext();
// const newAddress = AztecAddress.random();
// const newCalldata = [new Fr(1), new Fr(2)];
// const newContext = context.createNestedContractCallContext(newAddress, newCalldata);
});

it('New static call should fork context correctly', () => {
// const context = initContext();
// const newAddress = AztecAddress.random();
// const newCalldata = [new Fr(1), new Fr(2)];
// const newContext = context.createNestedContractStaticCallContext(newAddress, newCalldata);
});
});
157 changes: 40 additions & 117 deletions yarn-project/acir-simulator/src/avm/avm_context.ts
Original file line number Diff line number Diff line change
@@ -1,140 +1,63 @@
import { AztecAddress, FunctionSelector } from '@aztec/circuits.js';
import { AztecAddress } 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 { AvmInterpreterError, executeAvm } from './interpreter/index.js';
import { AvmJournal } from './journal/journal.js';
import { Instruction } from './opcodes/instruction.js';
import { decodeFromBytecode } from './serialization/bytecode_serialization.js';

// FIXME: dependency cycle.
import { AvmWorldStateJournal } from './journal/journal.js';

/**
* Avm Executor manages the execution of the AVM
*
* It stores a state manager
* An execution context includes the information necessary to initiate AVM
* execution along with all state maintained by the AVM throughout execution.
*/
export class AvmContext {
/** Contains constant variables provided by the kernel */
private executionEnvironment: AvmExecutionEnvironment;
/** Manages mutable state during execution - (caching, fetching) */
private journal: AvmJournal;

constructor(executionEnvironment: AvmExecutionEnvironment, journal: AvmJournal) {
this.executionEnvironment = executionEnvironment;
this.journal = journal;
}

/**
* Call a contract with the given calldata
*
* - We get the contract from storage
* - We interpret the bytecode
* - We run the interpreter
*
*/
async call(): Promise<AvmMessageCallResult> {
// NOTE: the following is mocked as getPublicBytecode does not exist yet
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[] = decodeFromBytecode(bytecode);

const machineState = new AvmMachineState(this.executionEnvironment);
return executeAvm(machineState, this.journal, instructions);
}

/**
* 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
* Create a new AVM context
* @param worldState - Manages mutable state during execution - (caching, fetching)
* @param environment - Contains constant variables provided by the kernel
* @param machineState - VM state that is modified on an instruction-by-instruction basis
* @returns new AvmContext instance
*/
public static newWithForkedState(executionEnvironment: AvmExecutionEnvironment, journal: AvmJournal): AvmContext {
const forkedState = AvmJournal.branchParent(journal);
return new AvmContext(executionEnvironment, forkedState);
}
constructor(
public worldState: AvmWorldStateJournal,
public environment: AvmExecutionEnvironment,
public machineState: AvmMachineState,
) {}

/**
* 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
* Prepare a new AVM context that will be ready for an external/nested call
* - Fork the world state journal
* - Derive a machine state from the current state
* - E.g., gas metering is preserved but pc is reset
* - Derive an execution environment from the caller/parent
* - Alter both address and storageAddress
*
* @param address - The contract to call
* @param executionEnvironment - The current execution environment
* @param journal - The current journal
* @param address - The contract instance to initialize a context for
* @param calldata - Data/arguments for nested call
* @returns new AvmContext instance
*/
public static prepExternalCallContext(
address: AztecAddress,
calldata: Fr[],
executionEnvironment: AvmExecutionEnvironment,
journal: AvmJournal,
): AvmContext {
const newExecutionEnvironment = executionEnvironment.newCall(address, calldata);
const forkedState = AvmJournal.branchParent(journal);
return new AvmContext(newExecutionEnvironment, forkedState);
public createNestedContractCallContext(address: AztecAddress, calldata: Fr[]): AvmContext {
const newExecutionEnvironment = this.environment.deriveEnvironmentForNestedCall(address, calldata);
const forkedWorldState = this.worldState.fork();
const machineState = AvmMachineState.fromState(this.machineState);
return new AvmContext(forkedWorldState, newExecutionEnvironment, machineState);
}

/**
* 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
* Prepare a new AVM context that will be ready for an external/nested static call
* - Fork the world state journal
* - Derive a machine state from the current state
* - E.g., gas metering is preserved but pc is reset
* - Derive an execution environment from the caller/parent
Comment on lines +48 to +49
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gas metering actually is not preserved. You allocate gas to a nested context via the call instruction's gas argument.

* - Alter both address and storageAddress
*
* @param address - The contract to call
* @param executionEnvironment - The current execution environment
* @param journal - The current journal
* @param address - The contract instance to initialize a context for
* @param calldata - Data/arguments for nested call
* @returns new AvmContext instance
*/
public static prepExternalStaticCallContext(
address: AztecAddress,
calldata: Fr[],
executionEnvironment: AvmExecutionEnvironment,
journal: AvmJournal,
): AvmContext {
const newExecutionEnvironment = executionEnvironment.newStaticCall(address, calldata);
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 mergeJournalSuccess() {
this.journal.mergeSuccessWithParent();
}

/**
* Merge the journal of this call with it's parent
* For when the child call fails ( we still must track state accesses )
*/
public mergeJournalFailure() {
this.journal.mergeFailureWithParent();
}
}

class NoBytecodeFoundInterpreterError extends AvmInterpreterError {
constructor(contractAddress: AztecAddress) {
super(`No bytecode found at: ${contractAddress}`);
this.name = 'NoBytecodeFoundInterpreterError';
public createNestedContractStaticCallContext(address: AztecAddress, calldata: Fr[]): AvmContext {
const newExecutionEnvironment = this.environment.deriveEnvironmentForNestedStaticCall(address, calldata);
const forkedWorldState = this.worldState.fork();
const machineState = AvmMachineState.fromState(this.machineState);
return new AvmContext(forkedWorldState, newExecutionEnvironment, machineState);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested context needs to take in args for l1/l2/daGas as they aren't derived from the parent context but are actually arguments to the call/staticcall instructions.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('Execution Environment', () => {

it('New call should fork execution environment correctly', () => {
const executionEnvironment = initExecutionEnvironment();
const newExecutionEnvironment = executionEnvironment.newCall(newAddress, calldata);
const newExecutionEnvironment = executionEnvironment.deriveEnvironmentForNestedCall(newAddress, calldata);

allTheSameExcept(executionEnvironment, newExecutionEnvironment, {
address: newAddress,
Expand All @@ -30,7 +30,7 @@ describe('Execution Environment', () => {

it('New static call call should fork execution environment correctly', () => {
const executionEnvironment = initExecutionEnvironment();
const newExecutionEnvironment = executionEnvironment.newStaticCall(newAddress, calldata);
const newExecutionEnvironment = executionEnvironment.deriveEnvironmentForNestedStaticCall(newAddress, calldata);

allTheSameExcept(executionEnvironment, newExecutionEnvironment, {
address: newAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class AvmExecutionEnvironment {
public readonly calldata: Fr[],
) {}

public newCall(address: AztecAddress, calldata: Fr[]): AvmExecutionEnvironment {
public deriveEnvironmentForNestedCall(address: AztecAddress, calldata: Fr[]): AvmExecutionEnvironment {
return new AvmExecutionEnvironment(
/*address=*/ address,
/*storageAddress=*/ address,
Expand All @@ -55,7 +55,7 @@ export class AvmExecutionEnvironment {
);
}

public newStaticCall(address: AztecAddress, calldata: Fr[]): AvmExecutionEnvironment {
public deriveEnvironmentForNestedStaticCall(address: AztecAddress, calldata: Fr[]): AvmExecutionEnvironment {
return new AvmExecutionEnvironment(
/*address=*/ address,
/*storageAddress=*/ address,
Expand Down
108 changes: 66 additions & 42 deletions yarn-project/acir-simulator/src/avm/avm_machine_state.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,93 @@
import { Fr } from '@aztec/foundation/fields';
import { Fr } from '@aztec/circuits.js';

import { AvmExecutionEnvironment } from './avm_execution_environment.js';
import { TaggedMemory } from './avm_memory_types.js';
import { AvmContractCallResults } from './avm_message_call_result.js';

/**
* Store's data for an Avm execution frame
* A few fields of machine state are initialized from AVM session inputs or call instruction arguments
*/
export type InitialAvmMachineState = {
l1GasLeft: number;
l2GasLeft: number;
daGasLeft: number;
};

/**
* Avm state modified on an instruction-per-instruction basis.
*/
export class AvmMachineState {
public l1GasLeft: number;
/** gas remaining of the gas allocated for a contract call */
public l2GasLeft: number;
public daGasLeft: number;
/** program counter */
public pc: number = 0;

/**
* Execution environment contains hard coded information that is received from the kernel
* Items like, the block header and global variables fall within this category
* On INTERNALCALL, internal call stack is pushed to with the current pc + 1
* On INTERNALRETURN, value is popped from the internal call stack and assigned to the pc.
*/
public readonly executionEnvironment: AvmExecutionEnvironment;
public internalCallStack: number[] = [];

private returnData: Fr[];

public readonly memory: TaggedMemory;
/** Memory accessible to user code */
public readonly memory: TaggedMemory = new TaggedMemory();

/**
* When an internal_call is invoked, the internal call stack is added to with the current pc + 1
* When internal_return is invoked, the latest value is popped from the internal call stack and set to the pc.
*/
public internalCallStack: number[];
* Signals that execution should end.
* AvmContext execution continues executing instructions until the machine state signals "halted"
* */
public halted: boolean = false;
/** Signals that execution has reverted normally (this does not cover exceptional halts) */
private reverted: boolean = false;
/** Output data must NOT be modified once it is set */
private output: Fr[] = [];

public pc: number;
constructor(l1GasLeft: number, l2GasLeft: number, daGasLeft: number) {
this.l1GasLeft = l1GasLeft;
this.l2GasLeft = l2GasLeft;
this.daGasLeft = daGasLeft;
}

public callStack: number[];
public static fromState(state: InitialAvmMachineState): AvmMachineState {
return new AvmMachineState(state.l1GasLeft, state.l2GasLeft, state.daGasLeft);
}

/**
* If an instruction triggers a halt, then it ends execution of the VM
* Most instructions just increment PC before they complete
*/
public halted: boolean;
/**
* Signifies if the execution has reverted ( due to a revert instruction )
*/
public reverted: boolean;
public incrementPc() {
this.pc++;
}

/**
* Create a new avm context
* @param executionEnvironment - Machine context that is passed to the avm
* Halt as successful
* Output data must NOT be modified once it is set
* @param output
*/
constructor(executionEnvironment: AvmExecutionEnvironment) {
this.returnData = [];
this.memory = new TaggedMemory();
this.internalCallStack = [];

this.pc = 0;
this.callStack = [];

this.halted = false;
this.reverted = false;

this.executionEnvironment = executionEnvironment;
public return(output: Fr[]) {
this.halted = true;
this.output = output;
}

/**
* Return data must NOT be modified once it is set
* @param returnData -
* Halt as reverted
* Output data must NOT be modified once it is set
* @param output
*/
public setReturnData(returnData: Fr[]) {
this.returnData = returnData;
Object.freeze(returnData);
public revert(output: Fr[]) {
this.halted = true;
this.reverted = true;
this.output = output;
}

public getReturnData(): Fr[] {
return this.returnData;
/**
* Get a summary of execution results for a halted machine state
* @returns summary of execution results
*/
public getResults(): AvmContractCallResults {
if (!this.halted) {
throw new Error('Execution results are not ready! Execution is ongoing.');
}
return new AvmContractCallResults(this.reverted, this.output);
}
}
Loading
Loading