Skip to content

Commit

Permalink
chore(avm): refactor AVM Simulator and fix issues (AztecProtocol#4424)
Browse files Browse the repository at this point in the history
  • Loading branch information
fcarreiro authored Feb 5, 2024
1 parent 80703f6 commit 4be9dd0
Show file tree
Hide file tree
Showing 39 changed files with 1,008 additions and 1,075 deletions.
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
* - 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);
}
}
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

0 comments on commit 4be9dd0

Please sign in to comment.