From de6e2edc09b823542f45e14b45c32ec10894f6c7 Mon Sep 17 00:00:00 2001 From: David Banks <47112877+dbanks12@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:57:23 -0500 Subject: [PATCH] feat(avm): encode TS AVM instructions as bytecode, especially for testing (#4115) --- .../acir-simulator/src/avm/avm_context.ts | 4 +- .../acir-simulator/src/avm/index.test.ts | 41 +++++++++++++++++++ ...tecode.test.ts => decode_bytecode.test.ts} | 14 +++---- .../{from_bytecode.ts => decode_bytecode.ts} | 23 ++++------- .../avm/opcodes/encode_to_bytecode.test.ts | 30 ++++++++++++++ .../src/avm/opcodes/encode_to_bytecode.ts | 32 +++++++++++++++ .../src/avm/opcodes/instruction.ts | 3 ++ 7 files changed, 124 insertions(+), 23 deletions(-) create mode 100644 yarn-project/acir-simulator/src/avm/index.test.ts rename yarn-project/acir-simulator/src/avm/opcodes/{from_bytecode.test.ts => decode_bytecode.test.ts} (63%) rename yarn-project/acir-simulator/src/avm/opcodes/{from_bytecode.ts => decode_bytecode.ts} (61%) create mode 100644 yarn-project/acir-simulator/src/avm/opcodes/encode_to_bytecode.test.ts create mode 100644 yarn-project/acir-simulator/src/avm/opcodes/encode_to_bytecode.ts diff --git a/yarn-project/acir-simulator/src/avm/avm_context.ts b/yarn-project/acir-simulator/src/avm/avm_context.ts index 9d52f869acf..79e763f79f4 100644 --- a/yarn-project/acir-simulator/src/avm/avm_context.ts +++ b/yarn-project/acir-simulator/src/avm/avm_context.ts @@ -4,7 +4,7 @@ import { AvmMachineState } from './avm_machine_state.js'; import { AvmMessageCallResult } from './avm_message_call_result.js'; import { AvmStateManager } from './avm_state_manager.js'; import { AvmInterpreter } from './interpreter/index.js'; -import { interpretBytecode } from './opcodes/from_bytecode.js'; +import { decodeBytecode } from './opcodes/decode_bytecode.js'; import { Instruction } from './opcodes/index.js'; /** @@ -34,7 +34,7 @@ export class AvmContext { // const bytecode = stateManager.journal.hostStorage.contractsDb.getBytecode(contractAddress); const bytecode = Buffer.from('0x01000100020003'); - const instructions: Instruction[] = interpretBytecode(bytecode); + const instructions: Instruction[] = decodeBytecode(bytecode); const context = new AvmMachineState(calldata); const interpreter = new AvmInterpreter(context, this.stateManager, instructions); diff --git a/yarn-project/acir-simulator/src/avm/index.test.ts b/yarn-project/acir-simulator/src/avm/index.test.ts new file mode 100644 index 00000000000..1db8306447a --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/index.test.ts @@ -0,0 +1,41 @@ +import { Fr } from '@aztec/foundation/fields'; + +import { mock } from 'jest-mock-extended'; + +import { AvmMachineState } from './avm_machine_state.js'; +import { AvmStateManager } from './avm_state_manager.js'; +import { AvmInterpreter } from './interpreter/interpreter.js'; +import { decodeBytecode } from './opcodes/decode_bytecode.js'; +import { encodeToBytecode } from './opcodes/encode_to_bytecode.js'; +import { Opcode } from './opcodes/opcodes.js'; + +describe('avm', () => { + it('Should execute bytecode', () => { + const calldata: Fr[] = [new Fr(1), new Fr(2)]; + const stateManager = mock(); + + // Construct bytecode + const calldataCopyArgs = [0, 2, 0]; + const addArgs = [0, 1, 2]; + const returnArgs = [2, 1]; + + const calldataCopyBytecode = encodeToBytecode(Opcode.CALLDATACOPY, calldataCopyArgs); + const addBytecode = encodeToBytecode(Opcode.ADD, addArgs); + const returnBytecode = encodeToBytecode(Opcode.RETURN, returnArgs); + const fullBytecode = Buffer.concat([calldataCopyBytecode, addBytecode, returnBytecode]); + + // Decode bytecode into instructions + const instructions = decodeBytecode(fullBytecode); + + // Execute instructions + const context = new AvmMachineState(calldata); + const interpreter = new AvmInterpreter(context, stateManager, instructions); + const avmReturnData = interpreter.run(); + + expect(avmReturnData.reverted).toBe(false); + + const returnData = avmReturnData.output; + expect(returnData.length).toBe(1); + expect(returnData).toEqual([new Fr(3)]); + }); +}); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/from_bytecode.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.test.ts similarity index 63% rename from yarn-project/acir-simulator/src/avm/opcodes/from_bytecode.test.ts rename to yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.test.ts index b5ba48bd23a..57502440e2d 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/from_bytecode.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.test.ts @@ -1,20 +1,20 @@ import { Add, Sub } from './arithmetic.js'; -import { OPCODE_BYTE_LENGTH, OPERAND_BYTE_LENGTH, interpretBytecode } from './from_bytecode.js'; -import { Instruction } from './instruction.js'; +import { decodeBytecode } from './decode_bytecode.js'; +import { AVM_OPCODE_BYTE_LENGTH, AVM_OPERAND_BYTE_LENGTH, Instruction } from './instruction.js'; -describe('Avm Interpreter', () => { +describe('Avm Decoder', () => { const toByte = (num: number): Buffer => { - const buf = Buffer.alloc(OPCODE_BYTE_LENGTH); + const buf = Buffer.alloc(AVM_OPCODE_BYTE_LENGTH); buf.writeUInt8(num); return buf; }; const to4Byte = (num: number): Buffer => { - const buf = Buffer.alloc(OPERAND_BYTE_LENGTH); + const buf = Buffer.alloc(AVM_OPERAND_BYTE_LENGTH); buf.writeUInt32BE(num); return buf; }; - it('Should read bytecode string into a list of opcodes', () => { + it('Should read bytecode buffer into a list of opcodes', () => { const opcode = 1; const opcode2 = 2; const a = 1; @@ -30,7 +30,7 @@ describe('Avm Interpreter', () => { const expectedInstructions: Instruction[] = [new Add(a, b, c), new Sub(a, b, c)]; - const instructions = interpretBytecode(bytecode); + const instructions = decodeBytecode(bytecode); expect(instructions).toEqual(expectedInstructions); }); }); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/from_bytecode.ts b/yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.ts similarity index 61% rename from yarn-project/acir-simulator/src/avm/opcodes/from_bytecode.ts rename to yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.ts index 3225123f00b..624546cfbeb 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/from_bytecode.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.ts @@ -1,26 +1,21 @@ -import { Instruction } from './instruction.js'; +import { AVM_OPCODE_BYTE_LENGTH, AVM_OPERAND_BYTE_LENGTH, Instruction } from './instruction.js'; import { INSTRUCTION_SET } from './instruction_set.js'; import { Opcode } from './opcodes.js'; -export const OPERAND_BIT_LENGTH = 32; -export const OPERAND_BYTE_LENGTH = 4; -export const OPCODE_BIT_LENGTH = 8; -export const OPCODE_BYTE_LENGTH = 1; - /** * Convert a buffer of bytecode into an array of instructions * @param bytecode - Buffer of bytecode - * @returns Bytecode interpreted into an ordered array of Instructions + * @returns Bytecode decoded into an ordered array of Instructions */ -export function interpretBytecode(bytecode: Buffer): Instruction[] { - let readPtr = 0; +export function decodeBytecode(bytecode: Buffer): Instruction[] { + let bytePtr = 0; const bytecodeLength = bytecode.length; const instructions: Instruction[] = []; - while (readPtr < bytecodeLength) { - const opcodeByte = bytecode[readPtr]; - readPtr += 1; + while (bytePtr < bytecodeLength) { + const opcodeByte = bytecode[bytePtr]; + bytePtr += AVM_OPCODE_BYTE_LENGTH; if (!(opcodeByte in Opcode)) { throw new Error(`Opcode ${opcodeByte} not implemented`); } @@ -33,8 +28,8 @@ export function interpretBytecode(bytecode: Buffer): Instruction[] { const numberOfOperands = instructionType.numberOfOperands; const operands: number[] = []; for (let i = 0; i < numberOfOperands; i++) { - const operand = bytecode.readUInt32BE(readPtr); - readPtr += OPERAND_BYTE_LENGTH; + const operand = bytecode.readUInt32BE(bytePtr); + bytePtr += AVM_OPERAND_BYTE_LENGTH; operands.push(operand); } diff --git a/yarn-project/acir-simulator/src/avm/opcodes/encode_to_bytecode.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/encode_to_bytecode.test.ts new file mode 100644 index 00000000000..8b1ea033dee --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/opcodes/encode_to_bytecode.test.ts @@ -0,0 +1,30 @@ +import { encodeToBytecode } from './encode_to_bytecode.js'; +import { AVM_OPCODE_BYTE_LENGTH, AVM_OPERAND_BYTE_LENGTH } from './instruction.js'; +import { Opcode } from './opcodes.js'; + +describe('Avm Encoder', () => { + const toByte = (num: number): Buffer => { + const buf = Buffer.alloc(AVM_OPCODE_BYTE_LENGTH); + buf.writeUInt8(num); + return buf; + }; + const to4Byte = (num: number): Buffer => { + const buf = Buffer.alloc(AVM_OPERAND_BYTE_LENGTH); + buf.writeUInt32BE(num); + return buf; + }; + + it('Should properly encode instructions into bytecode buffers', () => { + const addArgs = [0, 1, 2]; + const subArgs = [3, 4, 5]; + + const addBytecode = encodeToBytecode(Opcode.ADD, addArgs); + const subBytecode = encodeToBytecode(Opcode.SUB, subArgs); + + const expectedAddBytecode = Buffer.concat([toByte(Opcode.ADD), to4Byte(0), to4Byte(1), to4Byte(2)]); + const expectedSubBytecode = Buffer.concat([toByte(Opcode.SUB), to4Byte(3), to4Byte(4), to4Byte(5)]); + + expect(addBytecode).toEqual(expectedAddBytecode); + expect(subBytecode).toEqual(expectedSubBytecode); + }); +}); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/encode_to_bytecode.ts b/yarn-project/acir-simulator/src/avm/opcodes/encode_to_bytecode.ts new file mode 100644 index 00000000000..186706847d3 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/opcodes/encode_to_bytecode.ts @@ -0,0 +1,32 @@ +import { AVM_OPCODE_BYTE_LENGTH, AVM_OPERAND_BYTE_LENGTH } from './instruction.js'; +import { INSTRUCTION_SET } from './instruction_set.js'; +import { Opcode } from './opcodes.js'; + +/** + * Encode an instruction (opcode & arguments) to bytecode. + * @param opcode - the opcode to encode + * @param args - the arguments to encode + * @returns the bytecode for this one instruction + */ +export function encodeToBytecode(opcode: Opcode, args: number[]): Buffer { + const instructionType = INSTRUCTION_SET.get(opcode); + if (instructionType === undefined) { + throw new Error(`Opcode ${opcode} not implemented`); + } + + const numberOfOperands = instructionType.numberOfOperands; + if (args.length !== numberOfOperands) { + throw new Error(`Opcode ${opcode} expects ${numberOfOperands} arguments, but ${args.length} were provided`); + } + + const bytecode = Buffer.alloc(AVM_OPCODE_BYTE_LENGTH + numberOfOperands * AVM_OPERAND_BYTE_LENGTH); + + let bytePtr = 0; + bytecode.writeUInt8(opcode as number, bytePtr); + bytePtr += AVM_OPCODE_BYTE_LENGTH; + for (let i = 0; i < args.length; i++) { + bytecode.writeUInt32BE(args[i], bytePtr); + bytePtr += AVM_OPERAND_BYTE_LENGTH; + } + return bytecode; +} diff --git a/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts b/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts index 0411251b705..9a97ab21a00 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts @@ -1,6 +1,9 @@ import { AvmMachineState } from '../avm_machine_state.js'; import { AvmStateManager } from '../avm_state_manager.js'; +export const AVM_OPERAND_BYTE_LENGTH = 4; +export const AVM_OPCODE_BYTE_LENGTH = 1; + /** * Opcode base class */