diff --git a/yarn-project/acir-simulator/src/avm/avm_context.ts b/yarn-project/acir-simulator/src/avm/avm_context.ts index 0fcb86cfde6..9816fda32d8 100644 --- a/yarn-project/acir-simulator/src/avm/avm_context.ts +++ b/yarn-project/acir-simulator/src/avm/avm_context.ts @@ -6,8 +6,10 @@ 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 { decodeBytecode } from './opcodes/decode_bytecode.js'; -import { Instruction } from './opcodes/index.js'; +import { Instruction } from './opcodes/instruction.js'; +import { decodeFromBytecode } from './serialization/bytecode_serialization.js'; + +// FIXME: dependency cycle. /** * Avm Executor manages the execution of the AVM @@ -47,7 +49,7 @@ export class AvmContext { throw new NoBytecodeFoundInterpreterError(this.executionEnvironment.address); } - const instructions: Instruction[] = decodeBytecode(bytecode); + const instructions: Instruction[] = decodeFromBytecode(bytecode); const machineState = new AvmMachineState(this.executionEnvironment); return executeAvm(machineState, this.journal, instructions); diff --git a/yarn-project/acir-simulator/src/avm/index.test.ts b/yarn-project/acir-simulator/src/avm/index.test.ts index f085bc0bc00..9826a89d8e5 100644 --- a/yarn-project/acir-simulator/src/avm/index.test.ts +++ b/yarn-project/acir-simulator/src/avm/index.test.ts @@ -4,12 +4,12 @@ import { AvmTestContractArtifact } from '@aztec/noir-contracts'; import { mock } from 'jest-mock-extended'; import { AvmMachineState } from './avm_machine_state.js'; +import { TypeTag } from './avm_memory_types.js'; import { initExecutionEnvironment } from './fixtures/index.js'; import { executeAvm } from './interpreter/interpreter.js'; import { AvmJournal } from './journal/journal.js'; -import { decodeBytecode } from './opcodes/decode_bytecode.js'; -import { encodeToBytecode } from './opcodes/encode_to_bytecode.js'; -import { Opcode } from './opcodes/opcodes.js'; +import { Add, CalldataCopy, Return } from './opcodes/index.js'; +import { decodeFromBytecode, encodeToBytecode } from './serialization/bytecode_serialization.js'; describe('avm', () => { it('Should execute bytecode that performs basic addition', async () => { @@ -17,17 +17,14 @@ describe('avm', () => { const journal = 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]); + const bytecode = encodeToBytecode([ + new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ 0, /*copySize=*/ 2, /*dstOffset=*/ 0), + new Add(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2), + new Return(/*indirect=*/ 0, /*returnOffset=*/ 2, /*copySize=*/ 1), + ]); // Decode bytecode into instructions - const instructions = decodeBytecode(fullBytecode); + const instructions = decodeFromBytecode(bytecode); // Execute instructions const context = new AvmMachineState(initExecutionEnvironment({ calldata })); @@ -41,7 +38,8 @@ describe('avm', () => { }); describe('testing transpiled Noir contracts', () => { - it('Should execute contract function that performs addition', async () => { + // TODO(https://github.com/AztecProtocol/aztec-packages/issues/4361): sync wire format w/transpiler. + it.skip('Should execute contract function that performs addition', async () => { const calldata: Fr[] = [new Fr(1), new Fr(2)]; const journal = mock(); @@ -49,7 +47,7 @@ describe('avm', () => { const addArtifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_addArgsReturn')!; // Decode bytecode into instructions - const instructions = decodeBytecode(Buffer.from(addArtifact.bytecode, 'base64')); + const instructions = decodeFromBytecode(Buffer.from(addArtifact.bytecode, 'base64')); // Execute instructions const context = new AvmMachineState(initExecutionEnvironment({ calldata })); diff --git a/yarn-project/acir-simulator/src/avm/interpreter/interpreter.test.ts b/yarn-project/acir-simulator/src/avm/interpreter/interpreter.test.ts index e5055cdf903..c0111f067ac 100644 --- a/yarn-project/acir-simulator/src/avm/interpreter/interpreter.test.ts +++ b/yarn-project/acir-simulator/src/avm/interpreter/interpreter.test.ts @@ -3,6 +3,7 @@ import { Fr } from '@aztec/foundation/fields'; import { MockProxy, mock } from 'jest-mock-extended'; import { AvmMachineState } from '../avm_machine_state.js'; +import { TypeTag } from '../avm_memory_types.js'; import { initExecutionEnvironment } from '../fixtures/index.js'; import { AvmJournal } from '../journal/journal.js'; import { Add } from '../opcodes/arithmetic.js'; @@ -22,9 +23,9 @@ describe('interpreter', () => { const calldata: Fr[] = [new Fr(1), new Fr(2)]; const instructions: Instruction[] = [ - new CalldataCopy(/*cdOffset=*/ 0, /*copySize=*/ 2, /*dstOffset=*/ 0), - new Add(/*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2), - new Return(/*returnOffset=*/ 2, /*copySize=*/ 1), + new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ 0, /*copySize=*/ 2, /*dstOffset=*/ 0), + new Add(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2), + new Return(/*indirect=*/ 0, /*returnOffset=*/ 2, /*copySize=*/ 1), ]; const machineState = new AvmMachineState(initExecutionEnvironment({ calldata })); diff --git a/yarn-project/acir-simulator/src/avm/interpreter/interpreter.ts b/yarn-project/acir-simulator/src/avm/interpreter/interpreter.ts index 86385078cac..fb23425f8b8 100644 --- a/yarn-project/acir-simulator/src/avm/interpreter/interpreter.ts +++ b/yarn-project/acir-simulator/src/avm/interpreter/interpreter.ts @@ -3,7 +3,7 @@ import { strict as assert } from 'assert'; import { AvmMachineState } from '../avm_machine_state.js'; import { AvmMessageCallResult } from '../avm_message_call_result.js'; import { AvmJournal } from '../journal/index.js'; -import { Instruction } from '../opcodes/index.js'; +import { Instruction, InstructionExecutionError } from '../opcodes/instruction.js'; /** * Run the avm @@ -36,14 +36,13 @@ export async function executeAvm( } return AvmMessageCallResult.success(returnData); - } catch (_e) { - if (!(_e instanceof AvmInterpreterError)) { - throw _e; + } catch (e) { + if (!(e instanceof AvmInterpreterError || e instanceof InstructionExecutionError)) { + throw e; } - const revertReason: AvmInterpreterError = _e; const revertData = machineState.getReturnData(); - return AvmMessageCallResult.revert(revertData, revertReason); + return AvmMessageCallResult.revert(revertData, /*revertReason=*/ e); } } diff --git a/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.test.ts index fd24b23eda4..72ff0a7c614 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.test.ts @@ -18,56 +18,114 @@ describe('Accrued Substate', () => { machineState = new AvmMachineState(initExecutionEnvironment()); }); - it('Should append a new note hash correctly', async () => { - const value = new Field(69n); - machineState.memory.set(0, value); - - await new EmitNoteHash(0).execute(machineState, journal); - - const journalState = journal.flush(); - const expected = [value.toFr()]; - expect(journalState.newNoteHashes).toEqual(expected); + describe('EmitNoteHash', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + EmitNoteHash.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new EmitNoteHash(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(EmitNoteHash.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should append a new note hash correctly', async () => { + const value = new Field(69n); + machineState.memory.set(0, value); + + await new EmitNoteHash(/*indirect=*/ 0, /*offset=*/ 0).execute(machineState, journal); + + const journalState = journal.flush(); + const expected = [value.toFr()]; + expect(journalState.newNoteHashes).toEqual(expected); + }); }); - it('Should append a new nullifier correctly', async () => { - const value = new Field(69n); - machineState.memory.set(0, value); + describe('EmitNullifier', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + EmitNullifier.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new EmitNullifier(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(EmitNullifier.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should append a new nullifier correctly', async () => { + const value = new Field(69n); + machineState.memory.set(0, value); + + await new EmitNullifier(/*indirect=*/ 0, /*offset=*/ 0).execute(machineState, journal); + + const journalState = journal.flush(); + const expected = [value.toFr()]; + expect(journalState.newNullifiers).toEqual(expected); + }); + }); - await new EmitNullifier(0).execute(machineState, journal); + describe('EmitUnencryptedLog', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + EmitUnencryptedLog.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // offset + ...Buffer.from('a2345678', 'hex'), // length + ]); + const inst = new EmitUnencryptedLog(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678, /*length=*/ 0xa2345678); - const journalState = journal.flush(); - const expected = [value.toFr()]; - expect(journalState.newNullifiers).toEqual(expected); - }); + expect(EmitUnencryptedLog.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); - it('Should append unencrypted logs correctly', async () => { - const startOffset = 0; + it('Should append unencrypted logs correctly', async () => { + const startOffset = 0; - const values = [new Field(69n), new Field(420n), new Field(Field.MODULUS - 1n)]; - machineState.memory.setSlice(0, values); + const values = [new Field(69n), new Field(420n), new Field(Field.MODULUS - 1n)]; + machineState.memory.setSlice(0, values); - const length = values.length; + const length = values.length; - await new EmitUnencryptedLog(startOffset, length).execute(machineState, journal); + await new EmitUnencryptedLog(/*indirect=*/ 0, /*offset=*/ startOffset, length).execute(machineState, journal); - const journalState = journal.flush(); - const expected = values.map(v => v.toFr()); - expect(journalState.newLogs).toEqual([expected]); + const journalState = journal.flush(); + const expected = values.map(v => v.toFr()); + expect(journalState.newLogs).toEqual([expected]); + }); }); - it('Should append l1 to l2 messages correctly', async () => { - const startOffset = 0; + describe('SendL2ToL1Message', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + SendL2ToL1Message.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // offset + ...Buffer.from('a2345678', 'hex'), // length + ]); + const inst = new SendL2ToL1Message(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678, /*length=*/ 0xa2345678); + + expect(SendL2ToL1Message.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should append l1 to l2 messages correctly', async () => { + const startOffset = 0; - const values = [new Field(69n), new Field(420n), new Field(Field.MODULUS - 1n)]; - machineState.memory.setSlice(0, values); + const values = [new Field(69n), new Field(420n), new Field(Field.MODULUS - 1n)]; + machineState.memory.setSlice(0, values); - const length = values.length; + const length = values.length; - await new SendL2ToL1Message(startOffset, length).execute(machineState, journal); + await new SendL2ToL1Message(/*indirect=*/ 0, /*offset=*/ startOffset, length).execute(machineState, journal); - const journalState = journal.flush(); - const expected = values.map(v => v.toFr()); - expect(journalState.newLogs).toEqual([expected]); + const journalState = journal.flush(); + const expected = values.map(v => v.toFr()); + expect(journalState.newLogs).toEqual([expected]); + }); }); it('All substate instructions should fail within a static call', async () => { @@ -75,15 +133,14 @@ describe('Accrued Substate', () => { machineState = new AvmMachineState(executionEnvironment); const instructions = [ - new EmitNoteHash(0), - new EmitNullifier(0), - new EmitUnencryptedLog(0, 1), - new SendL2ToL1Message(0, 1), + new EmitNoteHash(/*indirect=*/ 0, /*offset=*/ 0), + new EmitNullifier(/*indirect=*/ 0, /*offset=*/ 0), + new EmitUnencryptedLog(/*indirect=*/ 0, /*offset=*/ 0, 1), + new SendL2ToL1Message(/*indirect=*/ 0, /*offset=*/ 0, 1), ]; for (const instruction of instructions) { - const inst = () => instruction.execute(machineState, journal); - await expect(inst()).rejects.toThrowError(StaticCallStorageAlterError); + await expect(instruction.execute(machineState, journal)).rejects.toThrow(StaticCallStorageAlterError); } }); }); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.ts b/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.ts index de54edaa0c7..00c78f6879f 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.ts @@ -1,13 +1,16 @@ import { AvmMachineState } from '../avm_machine_state.js'; import { AvmJournal } from '../journal/journal.js'; +import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Instruction } from './instruction.js'; import { StaticCallStorageAlterError } from './storage.js'; export class EmitNoteHash extends Instruction { static type: string = 'EMITNOTEHASH'; - static numberOfOperands = 1; + static readonly opcode: Opcode = Opcode.EMITNOTEHASH; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat = [OperandType.UINT8, OperandType.UINT8, OperandType.UINT32]; - constructor(private noteHashOffset: number) { + constructor(private indirect: number, private noteHashOffset: number) { super(); } @@ -25,9 +28,11 @@ export class EmitNoteHash extends Instruction { export class EmitNullifier extends Instruction { static type: string = 'EMITNULLIFIER'; - static numberOfOperands = 1; + static readonly opcode: Opcode = Opcode.EMITNULLIFIER; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat = [OperandType.UINT8, OperandType.UINT8, OperandType.UINT32]; - constructor(private nullifierOffset: number) { + constructor(private indirect: number, private nullifierOffset: number) { super(); } @@ -45,9 +50,11 @@ export class EmitNullifier extends Instruction { export class EmitUnencryptedLog extends Instruction { static type: string = 'EMITUNENCRYPTEDLOG'; - static numberOfOperands = 2; + static readonly opcode: Opcode = Opcode.EMITUNENCRYPTEDLOG; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat = [OperandType.UINT8, OperandType.UINT8, OperandType.UINT32, OperandType.UINT32]; - constructor(private logOffset: number, private logSize: number) { + constructor(private indirect: number, private logOffset: number, private logSize: number) { super(); } @@ -65,9 +72,11 @@ export class EmitUnencryptedLog extends Instruction { export class SendL2ToL1Message extends Instruction { static type: string = 'EMITUNENCRYPTEDLOG'; - static numberOfOperands = 2; + static readonly opcode: Opcode = Opcode.SENDL2TOL1MSG; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat = [OperandType.UINT8, OperandType.UINT8, OperandType.UINT32, OperandType.UINT32]; - constructor(private msgOffset: number, private msgSize: number) { + constructor(private indirect: number, private msgOffset: number, private msgSize: number) { super(); } diff --git a/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.test.ts index a9911f8d82d..f54db2d82a7 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.test.ts @@ -1,7 +1,7 @@ import { MockProxy, mock } from 'jest-mock-extended'; import { AvmMachineState } from '../avm_machine_state.js'; -import { Field } from '../avm_memory_types.js'; +import { Field, TypeTag } from '../avm_memory_types.js'; import { initExecutionEnvironment } from '../fixtures/index.js'; import { AvmJournal } from '../journal/journal.js'; import { Add, Div, Mul, Sub } from './arithmetic.js'; @@ -16,6 +16,27 @@ describe('Arithmetic Instructions', () => { }); describe('Add', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Add.opcode, // opcode + 0x01, // indirect + TypeTag.FIELD, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Add( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Add.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should add correctly over field elements', async () => { const a = new Field(1n); const b = new Field(2n); @@ -23,7 +44,13 @@ describe('Arithmetic Instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Add(0, 1, 2).execute(machineState, journal); + await new Add( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = new Field(3n); const actual = machineState.memory.get(2); @@ -37,7 +64,13 @@ describe('Arithmetic Instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Add(0, 1, 2).execute(machineState, journal); + await new Add( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = new Field(0n); const actual = machineState.memory.get(2); @@ -46,6 +79,27 @@ describe('Arithmetic Instructions', () => { }); describe('Sub', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Sub.opcode, // opcode + 0x01, // indirect + TypeTag.FIELD, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Sub( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Sub.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should subtract correctly over field elements', async () => { const a = new Field(1n); const b = new Field(2n); @@ -53,7 +107,13 @@ describe('Arithmetic Instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Sub(0, 1, 2).execute(machineState, journal); + await new Sub( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = new Field(Field.MODULUS - 1n); const actual = machineState.memory.get(2); @@ -62,6 +122,27 @@ describe('Arithmetic Instructions', () => { }); describe('Mul', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Mul.opcode, // opcode + 0x01, // indirect + TypeTag.FIELD, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Mul( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Mul.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should multiply correctly over field elements', async () => { const a = new Field(2n); const b = new Field(3n); @@ -69,7 +150,13 @@ describe('Arithmetic Instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Mul(0, 1, 2).execute(machineState, journal); + await new Mul( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = new Field(6n); const actual = machineState.memory.get(2); @@ -83,7 +170,13 @@ describe('Arithmetic Instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Mul(0, 1, 2).execute(machineState, journal); + await new Mul( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = new Field(Field.MODULUS - 3n); const actual = machineState.memory.get(2); @@ -92,6 +185,27 @@ describe('Arithmetic Instructions', () => { }); describe('Div', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Div.opcode, // opcode + 0x01, // indirect + TypeTag.FIELD, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Div( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Div.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should perform field division', async () => { const a = new Field(2n); const b = new Field(3n); @@ -99,9 +213,14 @@ describe('Arithmetic Instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Div(0, 1, 2).execute(machineState, journal); + await new Div( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); - // Note const actual = machineState.memory.get(2); const recovered = actual.mul(b); expect(recovered).toEqual(a); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.ts b/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.ts index 0b1910f0dd7..71a571b45dd 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/arithmetic.ts @@ -1,13 +1,14 @@ import { AvmMachineState } from '../avm_machine_state.js'; import { AvmJournal } from '../journal/index.js'; -import { Instruction } from './instruction.js'; +import { Opcode } from '../serialization/instruction_serialization.js'; +import { ThreeOperandInstruction } from './instruction_impl.js'; -export class Add extends Instruction { - static type: string = 'ADD'; - static numberOfOperands = 3; +export class Add extends ThreeOperandInstruction { + static readonly type: string = 'ADD'; + static readonly opcode = Opcode.ADD; - constructor(private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { @@ -21,12 +22,12 @@ export class Add extends Instruction { } } -export class Sub extends Instruction { - static type: string = 'SUB'; - static numberOfOperands = 3; +export class Sub extends ThreeOperandInstruction { + static readonly type: string = 'SUB'; + static readonly opcode = Opcode.SUB; - constructor(private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { @@ -40,12 +41,12 @@ export class Sub extends Instruction { } } -export class Mul extends Instruction { +export class Mul extends ThreeOperandInstruction { static type: string = 'MUL'; - static numberOfOperands = 3; + static readonly opcode = Opcode.MUL; - constructor(private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { @@ -59,12 +60,12 @@ export class Mul extends Instruction { } } -export class Div extends Instruction { +export class Div extends ThreeOperandInstruction { static type: string = 'DIV'; - static numberOfOperands = 3; + static readonly opcode = Opcode.DIV; - constructor(private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { diff --git a/yarn-project/acir-simulator/src/avm/opcodes/bitwise.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/bitwise.test.ts index ae8ce802724..d41c34611e0 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/bitwise.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/bitwise.test.ts @@ -15,45 +15,153 @@ describe('Bitwise instructions', () => { journal = mock(); }); - it('Should AND correctly over integral types', async () => { - machineState.memory.set(0, new Uint32(0b11111110010011100100n)); - machineState.memory.set(1, new Uint32(0b11100100111001001111n)); + describe('AND', () => { + it('Should deserialize correctly', () => { + const buf = Buffer.from([ + And.opcode, // opcode + 0x01, // indirect + TypeTag.UINT64, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new And( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.UINT64, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(And.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should AND correctly over integral types', async () => { + machineState.memory.set(0, new Uint32(0b11111110010011100100n)); + machineState.memory.set(1, new Uint32(0b11100100111001001111n)); - await new And(TypeTag.UINT32, 0, 1, 2).execute(machineState, journal); + await new And( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.UINT32, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); - const actual = machineState.memory.get(2); - expect(actual).toEqual(new Uint32(0b11100100010001000100n)); + const actual = machineState.memory.get(2); + expect(actual).toEqual(new Uint32(0b11100100010001000100n)); + }); }); - it('Should OR correctly over integral types', async () => { - const a = new Uint32(0b11111110010011100100n); - const b = new Uint32(0b11100100111001001111n); + describe('OR', () => { + it('Should deserialize correctly', () => { + const buf = Buffer.from([ + Or.opcode, // opcode + 0x01, // indirect + TypeTag.UINT64, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Or( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.UINT64, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Or.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); - machineState.memory.set(0, a); - machineState.memory.set(1, b); + it('Should OR correctly over integral types', async () => { + const a = new Uint32(0b11111110010011100100n); + const b = new Uint32(0b11100100111001001111n); - await new Or(TypeTag.UINT32, 0, 1, 2).execute(machineState, journal); + machineState.memory.set(0, a); + machineState.memory.set(1, b); - const expected = new Uint32(0b11111110111011101111n); - const actual = machineState.memory.get(2); - expect(actual).toEqual(expected); + await new Or( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.UINT32, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); + + const expected = new Uint32(0b11111110111011101111n); + const actual = machineState.memory.get(2); + expect(actual).toEqual(expected); + }); }); - it('Should XOR correctly over integral types', async () => { - const a = new Uint32(0b11111110010011100100n); - const b = new Uint32(0b11100100111001001111n); + describe('XOR', () => { + it('Should deserialize correctly', () => { + const buf = Buffer.from([ + Xor.opcode, // opcode + 0x01, // indirect + TypeTag.UINT64, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Xor( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.UINT64, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Xor.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); - machineState.memory.set(0, a); - machineState.memory.set(1, b); + it('Should XOR correctly over integral types', async () => { + const a = new Uint32(0b11111110010011100100n); + const b = new Uint32(0b11100100111001001111n); - await new Xor(TypeTag.UINT32, 0, 1, 2).execute(machineState, journal); + machineState.memory.set(0, a); + machineState.memory.set(1, b); + + await new Xor( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.UINT32, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); - const expected = new Uint32(0b00011010101010101011n); - const actual = machineState.memory.get(2); - expect(actual).toEqual(expected); + const expected = new Uint32(0b00011010101010101011n); + const actual = machineState.memory.get(2); + expect(actual).toEqual(expected); + }); }); describe('SHR', () => { + it('Should deserialize correctly', () => { + const buf = Buffer.from([ + Shr.opcode, // opcode + 0x01, // indirect + TypeTag.UINT64, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Shr( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.UINT64, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Shr.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should shift correctly 0 positions over integral types', async () => { const a = new Uint32(0b11111110010011100100n); const b = new Uint32(0n); @@ -61,7 +169,13 @@ describe('Bitwise instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Shr(TypeTag.UINT32, 0, 1, 2).execute(machineState, journal); + await new Shr( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.UINT32, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = a; const actual = machineState.memory.get(2); @@ -75,7 +189,13 @@ describe('Bitwise instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Shr(TypeTag.UINT32, 0, 1, 2).execute(machineState, journal); + await new Shr( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.UINT32, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = new Uint32(0b00111111100100111001n); const actual = machineState.memory.get(2); @@ -89,7 +209,13 @@ describe('Bitwise instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Shr(TypeTag.UINT32, 0, 1, 2).execute(machineState, journal); + await new Shr( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.UINT32, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = new Uint32(0b01n); const actual = machineState.memory.get(2); @@ -98,6 +224,27 @@ describe('Bitwise instructions', () => { }); describe('SHL', () => { + it('Should deserialize correctly', () => { + const buf = Buffer.from([ + Shl.opcode, // opcode + 0x01, // indirect + TypeTag.UINT64, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Shl( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.UINT64, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Shl.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should shift correctly 0 positions over integral types', async () => { const a = new Uint32(0b11111110010011100100n); const b = new Uint32(0n); @@ -105,7 +252,13 @@ describe('Bitwise instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Shl(TypeTag.UINT32, 0, 1, 2).execute(machineState, journal); + await new Shl( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.UINT32, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = a; const actual = machineState.memory.get(2); @@ -119,7 +272,13 @@ describe('Bitwise instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Shl(TypeTag.UINT32, 0, 1, 2).execute(machineState, journal); + await new Shl( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.UINT32, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = new Uint32(0b1111111001001110010000n); const actual = machineState.memory.get(2); @@ -133,7 +292,13 @@ describe('Bitwise instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Shl(TypeTag.UINT16, 0, 1, 2).execute(machineState, journal); + await new Shl( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.UINT16, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = new Uint16(0n); const actual = machineState.memory.get(2); @@ -147,7 +312,13 @@ describe('Bitwise instructions', () => { machineState.memory.set(0, a); machineState.memory.set(1, b); - await new Shl(TypeTag.UINT16, 0, 1, 2).execute(machineState, journal); + await new Shl( + /*indirect=*/ 0, + /*inTag=*/ TypeTag.UINT16, + /*aOffset=*/ 0, + /*bOffset=*/ 1, + /*dstOffset=*/ 2, + ).execute(machineState, journal); const expected = new Uint16(0b1001001110011100n); const actual = machineState.memory.get(2); @@ -155,15 +326,39 @@ describe('Bitwise instructions', () => { }); }); - it('Should NOT correctly over integral types', async () => { - const a = new Uint16(0b0110010011100100n); + describe('NOT', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Not.opcode, // opcode + 0x01, // indirect + TypeTag.UINT64, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Not( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.UINT64, + /*aOffset=*/ 0x12345678, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Not.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); - machineState.memory.set(0, a); + it('Should NOT correctly over integral types', async () => { + const a = new Uint16(0b0110010011100100n); - await new Not(TypeTag.UINT16, 0, 1).execute(machineState, journal); + machineState.memory.set(0, a); - const expected = new Uint16(0b1001101100011011n); // high bits! - const actual = machineState.memory.get(1); - expect(actual).toEqual(expected); + await new Not(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT16, /*aOffset=*/ 0, /*dstOffset=*/ 1).execute( + machineState, + journal, + ); + + const expected = new Uint16(0b1001101100011011n); // high bits! + const actual = machineState.memory.get(1); + expect(actual).toEqual(expected); + }); }); }); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/bitwise.ts b/yarn-project/acir-simulator/src/avm/opcodes/bitwise.ts index e439a8bd447..f30be9b5053 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/bitwise.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/bitwise.ts @@ -1,14 +1,16 @@ import { AvmMachineState } from '../avm_machine_state.js'; -import { IntegralValue, TypeTag } from '../avm_memory_types.js'; +import { IntegralValue } from '../avm_memory_types.js'; import { AvmJournal } from '../journal/index.js'; +import { Opcode } from '../serialization/instruction_serialization.js'; import { Instruction } from './instruction.js'; +import { ThreeOperandInstruction, TwoOperandInstruction } from './instruction_impl.js'; -export class And extends Instruction { - static type: string = 'AND'; - static numberOfOperands = 3; +export class And extends ThreeOperandInstruction { + static readonly type: string = 'AND'; + static readonly opcode = Opcode.AND; - constructor(private inTag: TypeTag, private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { @@ -24,12 +26,12 @@ export class And extends Instruction { } } -export class Or extends Instruction { - static type: string = 'OR'; - static numberOfOperands = 3; +export class Or extends ThreeOperandInstruction { + static readonly type: string = 'OR'; + static readonly opcode = Opcode.OR; - constructor(private inTag: TypeTag, private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { @@ -45,12 +47,12 @@ export class Or extends Instruction { } } -export class Xor extends Instruction { - static type: string = 'XOR'; - static numberOfOperands = 3; +export class Xor extends ThreeOperandInstruction { + static readonly type: string = 'XOR'; + static readonly opcode = Opcode.XOR; - constructor(private inTag: TypeTag, private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { @@ -66,12 +68,12 @@ export class Xor extends Instruction { } } -export class Not extends Instruction { - static type: string = 'NOT'; - static numberOfOperands = 2; +export class Not extends TwoOperandInstruction { + static readonly type: string = 'NOT'; + static readonly opcode = Opcode.NOT; - constructor(private inTag: TypeTag, private aOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { @@ -86,12 +88,12 @@ export class Not extends Instruction { } } -export class Shl extends Instruction { - static type: string = 'SHL'; - static numberOfOperands = 3; +export class Shl extends ThreeOperandInstruction { + static readonly type: string = 'SHL'; + static readonly opcode = Opcode.SHL; - constructor(private inTag: TypeTag, private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { @@ -107,12 +109,12 @@ export class Shl extends Instruction { } } -export class Shr extends Instruction { - static type: string = 'SHR'; - static numberOfOperands = 3; +export class Shr extends ThreeOperandInstruction { + static readonly type: string = 'SHR'; + static readonly opcode = Opcode.SHR; - constructor(private inTag: TypeTag, private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { diff --git a/yarn-project/acir-simulator/src/avm/opcodes/comparators.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/comparators.test.ts index ff604dbf2cb..b707b138aa7 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/comparators.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/comparators.test.ts @@ -17,13 +17,34 @@ describe('Comparators', () => { }); describe('Eq', () => { + it('Should deserialize correctly', () => { + const buf = Buffer.from([ + Eq.opcode, // opcode + 0x01, // indirect + TypeTag.UINT64, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Eq( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.UINT64, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Eq.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Works on integral types', async () => { machineState.memory.setSlice(0, [new Uint32(1), new Uint32(2), new Uint32(3), new Uint32(1)]); [ - new Eq(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), - new Eq(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 11), - new Eq(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 3, /*dstOffset=*/ 12), + new Eq(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), + new Eq(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 11), + new Eq(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 3, /*dstOffset=*/ 12), ].forEach(i => i.execute(machineState, journal)); const actual = machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 4); @@ -34,9 +55,9 @@ describe('Comparators', () => { machineState.memory.setSlice(0, [new Field(1), new Field(2), new Field(3), new Field(1)]); [ - new Eq(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), - new Eq(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 11), - new Eq(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 3, /*dstOffset=*/ 12), + new Eq(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), + new Eq(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 11), + new Eq(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 3, /*dstOffset=*/ 12), ].forEach(i => i.execute(machineState, journal)); const actual = machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 4); @@ -47,10 +68,10 @@ describe('Comparators', () => { machineState.memory.setSlice(0, [new Field(1), new Uint32(2), new Uint16(3)]); const ops = [ - new Eq(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), - new Eq(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 10), - new Eq(TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 10), - new Eq(TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 1, /*dstOffset=*/ 10), + new Eq(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), + new Eq(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 10), + new Eq(/*indirect=*/ 0, TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 10), + new Eq(/*indirect=*/ 0, TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 1, /*dstOffset=*/ 10), ]; for (const o of ops) { @@ -60,13 +81,34 @@ describe('Comparators', () => { }); describe('Lt', () => { + it('Should deserialize correctly', () => { + const buf = Buffer.from([ + Lt.opcode, // opcode + 0x01, // indirect + TypeTag.UINT64, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Lt( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.UINT64, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Lt.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Works on integral types', async () => { machineState.memory.setSlice(0, [new Uint32(1), new Uint32(2), new Uint32(0)]); [ - new Lt(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), - new Lt(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), - new Lt(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), + new Lt(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), + new Lt(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), + new Lt(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), ].forEach(i => i.execute(machineState, journal)); const actual = machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 4); @@ -77,9 +119,9 @@ describe('Comparators', () => { machineState.memory.setSlice(0, [new Field(1), new Field(2), new Field(0)]); [ - new Lt(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), - new Lt(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), - new Lt(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), + new Lt(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), + new Lt(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), + new Lt(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), ].forEach(i => i.execute(machineState, journal)); const actual = machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 4); @@ -90,10 +132,10 @@ describe('Comparators', () => { machineState.memory.setSlice(0, [new Field(1), new Uint32(2), new Uint16(3)]); const ops = [ - new Lt(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), - new Lt(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 10), - new Lt(TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 10), - new Lt(TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 1, /*dstOffset=*/ 10), + new Lt(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), + new Lt(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 10), + new Lt(/*indirect=*/ 0, TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 10), + new Lt(/*indirect=*/ 0, TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 1, /*dstOffset=*/ 10), ]; for (const o of ops) { @@ -103,13 +145,34 @@ describe('Comparators', () => { }); describe('Lte', () => { + it('Should deserialize correctly', () => { + const buf = Buffer.from([ + Lte.opcode, // opcode + 0x01, // indirect + TypeTag.UINT64, // inTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Lte( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.UINT64, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Lte.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Works on integral types', async () => { machineState.memory.setSlice(0, [new Uint32(1), new Uint32(2), new Uint32(0)]); [ - new Lte(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), - new Lte(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), - new Lte(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), + new Lte(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), + new Lte(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), + new Lte(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), ].forEach(i => i.execute(machineState, journal)); const actual = machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 4); @@ -120,9 +183,9 @@ describe('Comparators', () => { machineState.memory.setSlice(0, [new Field(1), new Field(2), new Field(0)]); [ - new Lte(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), - new Lte(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), - new Lte(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), + new Lte(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), + new Lte(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), + new Lte(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), ].forEach(i => i.execute(machineState, journal)); const actual = machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 4); @@ -133,10 +196,10 @@ describe('Comparators', () => { machineState.memory.setSlice(0, [new Field(1), new Uint32(2), new Uint16(3)]); const ops = [ - new Lte(TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), - new Lte(TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 10), - new Lte(TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 10), - new Lte(TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 1, /*dstOffset=*/ 10), + new Lte(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), + new Lte(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 10), + new Lte(/*indirect=*/ 0, TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 10), + new Lte(/*indirect=*/ 0, TypeTag.UINT16, /*aOffset=*/ 1, /*bOffset=*/ 1, /*dstOffset=*/ 10), ]; for (const o of ops) { diff --git a/yarn-project/acir-simulator/src/avm/opcodes/comparators.ts b/yarn-project/acir-simulator/src/avm/opcodes/comparators.ts index f89d450dbb9..a4cffa19d4a 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/comparators.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/comparators.ts @@ -1,14 +1,15 @@ import { AvmMachineState } from '../avm_machine_state.js'; -import { TypeTag } from '../avm_memory_types.js'; import { AvmJournal } from '../journal/index.js'; +import { Opcode } from '../serialization/instruction_serialization.js'; import { Instruction } from './instruction.js'; +import { ThreeOperandInstruction } from './instruction_impl.js'; -export class Eq extends Instruction { - static type: string = 'EQ'; - static numberOfOperands = 3; +export class Eq extends ThreeOperandInstruction { + static readonly type: string = 'EQ'; + static readonly opcode = Opcode.EQ; - constructor(private inTag: TypeTag, private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { @@ -25,12 +26,12 @@ export class Eq extends Instruction { } } -export class Lt extends Instruction { - static type: string = 'Lt'; - static numberOfOperands = 3; +export class Lt extends ThreeOperandInstruction { + static readonly type: string = 'LT'; + static readonly opcode = Opcode.LT; - constructor(private inTag: TypeTag, private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { @@ -47,12 +48,12 @@ export class Lt extends Instruction { } } -export class Lte extends Instruction { - static type: string = 'LTE'; - static numberOfOperands = 3; +export class Lte extends ThreeOperandInstruction { + static readonly type: string = 'LTE'; + static readonly opcode = Opcode.LTE; - constructor(private inTag: TypeTag, private aOffset: number, private bOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { + super(indirect, inTag, aOffset, bOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { diff --git a/yarn-project/acir-simulator/src/avm/opcodes/control_flow.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/control_flow.test.ts index 7c04e08f246..82879c619b9 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/control_flow.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/control_flow.test.ts @@ -3,15 +3,11 @@ import { Fr } from '@aztec/foundation/fields'; import { MockProxy, mock } from 'jest-mock-extended'; import { AvmMachineState } from '../avm_machine_state.js'; -import { Field, TypeTag, Uint16 } from '../avm_memory_types.js'; +import { Field, Uint16 } from '../avm_memory_types.js'; import { initExecutionEnvironment } from '../fixtures/index.js'; import { AvmJournal } from '../journal/journal.js'; -import { Add, Mul, Sub } from './arithmetic.js'; -import { And, Not, Or, Shl, Shr, Xor } from './bitwise.js'; -import { Eq, Lt, Lte } from './comparators.js'; import { InternalCall, InternalReturn, Jump, JumpI, Return, Revert } from './control_flow.js'; import { InstructionExecutionError } from './instruction.js'; -import { CMov, CalldataCopy, Cast, Mov, Set } from './memory.js'; describe('Control Flow Opcodes', () => { let journal: MockProxy; @@ -22,7 +18,18 @@ describe('Control Flow Opcodes', () => { machineState = new AvmMachineState(initExecutionEnvironment()); }); - describe('Jumps', () => { + describe('JUMP', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Jump.opcode, // opcode + ...Buffer.from('12345678', 'hex'), // loc + ]); + const inst = new Jump(/*loc=*/ 0x12345678); + + expect(Jump.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should implement JUMP', async () => { const jumpLocation = 22; @@ -32,6 +39,21 @@ describe('Control Flow Opcodes', () => { await instruction.execute(machineState, journal); expect(machineState.pc).toBe(jumpLocation); }); + }); + + describe('JUMPI', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + JumpI.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // loc + ...Buffer.from('a2345678', 'hex'), // condOffset + ]); + const inst = new JumpI(/*indirect=*/ 1, /*loc=*/ 0x12345678, /*condOffset=*/ 0xa2345678); + + expect(JumpI.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); it('Should implement JUMPI - truthy', async () => { const jumpLocation = 22; @@ -42,12 +64,12 @@ describe('Control Flow Opcodes', () => { machineState.memory.set(0, new Uint16(1n)); machineState.memory.set(1, new Uint16(2n)); - const instruction = new JumpI(jumpLocation, 0); + const instruction = new JumpI(/*indirect=*/ 0, jumpLocation, /*condOffset=*/ 0); await instruction.execute(machineState, journal); expect(machineState.pc).toBe(jumpLocation); // Truthy can be greater than 1 - const instruction1 = new JumpI(jumpLocation1, 1); + const instruction1 = new JumpI(/*indirect=*/ 0, jumpLocation1, /*condOffset=*/ 1); await instruction1.execute(machineState, journal); expect(machineState.pc).toBe(jumpLocation1); }); @@ -59,10 +81,23 @@ describe('Control Flow Opcodes', () => { machineState.memory.set(0, new Uint16(0n)); - const instruction = new JumpI(jumpLocation, 0); + const instruction = new JumpI(/*indirect=*/ 0, jumpLocation, /*condOffset=*/ 0); await instruction.execute(machineState, journal); expect(machineState.pc).toBe(1); }); + }); + + describe('INTERNALCALL and RETURN', () => { + it('INTERNALCALL Should (de)serialize correctly', () => { + const buf = Buffer.from([ + InternalCall.opcode, // opcode + ...Buffer.from('12345678', 'hex'), // loc + ]); + const inst = new InternalCall(/*loc=*/ 0x12345678); + + expect(InternalCall.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); it('Should implement Internal Call and Return', async () => { const jumpLocation = 22; @@ -79,6 +114,13 @@ describe('Control Flow Opcodes', () => { expect(machineState.pc).toBe(1); }); + it('Should error if Internal Return is called without a corresponding Internal Call', async () => { + const returnInstruction = () => new InternalReturn().execute(machineState, journal); + await expect(returnInstruction()).rejects.toThrow(InstructionExecutionError); + }); + }); + + describe('General flow', () => { it('Should chain series of control flow instructions', async () => { const jumpLocation0 = 22; const jumpLocation1 = 69; @@ -113,47 +155,22 @@ describe('Control Flow Opcodes', () => { expect(machineState.pc).toBe(expectedPcs[i]); } }); + }); - it('Should error if Internal Return is called without a corresponding Internal Call', async () => { - const returnInstruction = () => new InternalReturn().execute(machineState, journal); - await expect(returnInstruction()).rejects.toThrow(InstructionExecutionError); - }); - - it('Should increment PC on All other Instructions', async () => { - const instructions = [ - new Add(0, 1, 2), - new Sub(0, 1, 2), - new Mul(0, 1, 2), - new Lt(TypeTag.UINT16, 0, 1, 2), - new Lte(TypeTag.UINT16, 0, 1, 2), - new Eq(TypeTag.UINT16, 0, 1, 2), - new Xor(TypeTag.UINT16, 0, 1, 2), - new And(TypeTag.UINT16, 0, 1, 2), - new Or(TypeTag.UINT16, 0, 1, 2), - new Shl(TypeTag.UINT16, 0, 1, 2), - new Shr(TypeTag.UINT16, 0, 1, 2), - new Not(TypeTag.UINT16, 0, 2), - new CalldataCopy(0, 1, 2), - new Set(TypeTag.UINT16, 0n, 1), - new Mov(0, 1), - new CMov(0, 1, 2, 3), - new Cast(TypeTag.UINT16, 0, 1), - ]; - - for (const instruction of instructions) { - // Use a fresh machine state each run - const innerMachineState = new AvmMachineState(initExecutionEnvironment()); - innerMachineState.memory.set(0, new Uint16(4n)); - innerMachineState.memory.set(1, new Uint16(8n)); - innerMachineState.memory.set(2, new Uint16(12n)); - expect(innerMachineState.pc).toBe(0); - - await instruction.execute(innerMachineState, journal); - } + describe('RETURN', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Return.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // returnOffset + ...Buffer.from('a2345678', 'hex'), // copySize + ]); + const inst = new Return(/*indirect=*/ 0x01, /*returnOffset=*/ 0x12345678, /*copySize=*/ 0xa2345678); + + expect(Return.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); }); - }); - describe('Halting Opcodes', () => { it('Should return data from the return opcode', async () => { const returnData = [new Fr(1n), new Fr(2n), new Fr(3n)]; @@ -161,13 +178,28 @@ describe('Control Flow Opcodes', () => { machineState.memory.set(1, new Field(2n)); machineState.memory.set(2, new Field(3n)); - const instruction = new Return(0, returnData.length); + const instruction = new Return(/*indirect=*/ 0, /*returnOffset=*/ 0, returnData.length); await instruction.execute(machineState, journal); expect(machineState.getReturnData()).toEqual(returnData); expect(machineState.halted).toBe(true); expect(machineState.reverted).toBe(false); }); + }); + + describe('REVERT', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Revert.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // returnOffset + ...Buffer.from('a2345678', 'hex'), // retSize + ]); + const inst = new Revert(/*indirect=*/ 0x01, /*returnOffset=*/ 0x12345678, /*retSize=*/ 0xa2345678); + + expect(Revert.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); it('Should return data and revert from the revert opcode', async () => { const returnData = [new Fr(1n), new Fr(2n), new Fr(3n)]; @@ -176,7 +208,7 @@ describe('Control Flow Opcodes', () => { machineState.memory.set(1, new Field(2n)); machineState.memory.set(2, new Field(3n)); - const instruction = new Revert(0, returnData.length); + const instruction = new Revert(/*indirect=*/ 0, /*returnOffset=*/ 0, returnData.length); await instruction.execute(machineState, journal); expect(machineState.getReturnData()).toEqual(returnData); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/control_flow.ts b/yarn-project/acir-simulator/src/avm/opcodes/control_flow.ts index c890fc700e3..1527a677116 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/control_flow.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/control_flow.ts @@ -1,13 +1,21 @@ import { AvmMachineState } from '../avm_machine_state.js'; import { IntegralValue } from '../avm_memory_types.js'; import { AvmJournal } from '../journal/journal.js'; +import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Instruction, InstructionExecutionError } from './instruction.js'; export class Return extends Instruction { static type: string = 'RETURN'; - static numberOfOperands = 2; - - constructor(private returnOffset: number, private copySize: number) { + static readonly opcode: Opcode = Opcode.RETURN; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + ]; + + constructor(private indirect: number, private returnOffset: number, private copySize: number) { super(); } @@ -22,9 +30,16 @@ export class Return extends Instruction { export class Revert extends Instruction { static type: string = 'RETURN'; - static numberOfOperands = 2; - - constructor(private returnOffset: number, private retSize: number) { + static readonly opcode: Opcode = Opcode.REVERT; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + ]; + + constructor(private indirect: number, private returnOffset: number, private retSize: number) { super(); } @@ -40,7 +55,9 @@ export class Revert extends Instruction { export class Jump extends Instruction { static type: string = 'JUMP'; - static numberOfOperands = 1; + static readonly opcode: Opcode = Opcode.JUMP; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [OperandType.UINT8, OperandType.UINT32]; constructor(private jumpOffset: number) { super(); @@ -53,9 +70,17 @@ export class Jump extends Instruction { export class JumpI extends Instruction { static type: string = 'JUMPI'; - static numberOfOperands = 1; + static readonly opcode: Opcode = Opcode.JUMPI; + + // Instruction wire format with opcode. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + ]; - constructor(private jumpOffset: number, private condOffset: number) { + constructor(private indirect: number, private loc: number, private condOffset: number) { super(); } @@ -66,28 +91,32 @@ export class JumpI extends Instruction { if (condition.toBigInt() == 0n) { this.incrementPc(machineState); } else { - machineState.pc = this.jumpOffset; + machineState.pc = this.loc; } } } export class InternalCall extends Instruction { - static type: string = 'INTERNALCALL'; - static numberOfOperands = 1; + static readonly type: string = 'INTERNALCALL'; + static readonly opcode: Opcode = Opcode.INTERNALCALL; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [OperandType.UINT8, OperandType.UINT32]; - constructor(private jumpOffset: number) { + constructor(private loc: number) { super(); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { machineState.internalCallStack.push(machineState.pc + 1); - machineState.pc = this.jumpOffset; + machineState.pc = this.loc; } } export class InternalReturn extends Instruction { - static type: string = 'INTERNALRETURN'; - static numberOfOperands = 0; + static readonly type: string = 'INTERNALRETURN'; + static readonly opcode: Opcode = Opcode.INTERNALRETURN; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [OperandType.UINT8]; constructor() { super(); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.test.ts deleted file mode 100644 index 57502440e2d..00000000000 --- a/yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Add, Sub } from './arithmetic.js'; -import { decodeBytecode } from './decode_bytecode.js'; -import { AVM_OPCODE_BYTE_LENGTH, AVM_OPERAND_BYTE_LENGTH, Instruction } from './instruction.js'; - -describe('Avm Decoder', () => { - 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 read bytecode buffer into a list of opcodes', () => { - const opcode = 1; - const opcode2 = 2; - const a = 1; - const b = 2; - const c = 3; - - const ops = toByte(opcode); - const ops2 = toByte(opcode2); - const as = to4Byte(a); - const bs = to4Byte(b); - const cs = to4Byte(c); - const bytecode = Buffer.concat([ops, as, bs, cs, ops2, as, bs, cs]); - - const expectedInstructions: Instruction[] = [new Add(a, b, c), new Sub(a, b, c)]; - - const instructions = decodeBytecode(bytecode); - expect(instructions).toEqual(expectedInstructions); - }); -}); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.ts b/yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.ts deleted file mode 100644 index 33533f31abb..00000000000 --- a/yarn-project/acir-simulator/src/avm/opcodes/decode_bytecode.ts +++ /dev/null @@ -1,41 +0,0 @@ -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'; - -/** - * Convert a buffer of bytecode into an array of instructions - * @param bytecode - Buffer of bytecode - * @returns Bytecode decoded into an ordered array of Instructions - */ -export function decodeBytecode(bytecode: Buffer): Instruction[] { - let bytePtr = 0; - const bytecodeLength = bytecode.length; - - const instructions: Instruction[] = []; - - while (bytePtr < bytecodeLength) { - const opcodeByte = bytecode[bytePtr]; - bytePtr += AVM_OPCODE_BYTE_LENGTH; - if (!(opcodeByte in Opcode)) { - throw new Error(`Opcode 0x${opcodeByte.toString(16)} not implemented`); - } - const opcode = opcodeByte as Opcode; - - const instructionType = INSTRUCTION_SET.get(opcode); - if (instructionType === undefined) { - throw new Error(`Opcode 0x${opcode.toString(16)} not implemented`); - } - const numberOfOperands = instructionType.numberOfOperands; - const operands: number[] = []; - for (let i = 0; i < numberOfOperands; i++) { - // TODO: support constants which might not be u32s - const operand = bytecode.readUInt32BE(bytePtr); - bytePtr += AVM_OPERAND_BYTE_LENGTH; - operands.push(operand); - } - - instructions.push(new instructionType(...operands)); - } - - return instructions; -} 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 deleted file mode 100644 index 8b1ea033dee..00000000000 --- a/yarn-project/acir-simulator/src/avm/opcodes/encode_to_bytecode.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 105c0f808ab..00000000000 --- a/yarn-project/acir-simulator/src/avm/opcodes/encode_to_bytecode.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 0x${opcode.toString(16)} not implemented`); - } - - const numberOfOperands = instructionType.numberOfOperands; - if (args.length !== numberOfOperands) { - throw new Error( - `Opcode 0x${opcode.toString(16)} 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/environment_getters.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/environment_getters.test.ts index 4dd6b0abb85..2acf4729546 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/environment_getters.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/environment_getters.test.ts @@ -37,44 +37,156 @@ describe('Environment getters instructions', () => { expect(actual).toEqual(value); }; - it('Should read address correctly', async () => { - const address = new Fr(123456n); - await envGetterTest('address', address, new Address(0)); + describe('Address', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Address.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new Address(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(Address.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read address correctly', async () => { + const address = new Fr(123456n); + await envGetterTest('address', address, new Address(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); - it('Should read storage address correctly', async () => { - const address = new Fr(123456n); - await envGetterTest('storageAddress', address, new StorageAddress(0)); + describe('StorageAddress', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + StorageAddress.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new StorageAddress(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(StorageAddress.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read storage address correctly', async () => { + const address = new Fr(123456n); + await envGetterTest('storageAddress', address, new StorageAddress(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); - it('Should read Portal correctly', async () => { - const portal = new Fr(123456n); - await envGetterTest('portal', portal, new Portal(0)); + describe('Portal', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Portal.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new Portal(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(Portal.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read Portal correctly', async () => { + const portal = new Fr(123456n); + await envGetterTest('portal', portal, new Portal(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); - it('Should read FeePerL1Gas correctly', async () => { - const feePerL1Gas = new Fr(123456n); - await envGetterTest('feePerL1Gas', feePerL1Gas, new FeePerL1Gas(0)); + describe('FeePerL1Gas', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + FeePerL1Gas.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new FeePerL1Gas(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(FeePerL1Gas.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read FeePerL1Gas correctly', async () => { + const feePerL1Gas = new Fr(123456n); + await envGetterTest('feePerL1Gas', feePerL1Gas, new FeePerL1Gas(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); - it('Should read FeePerL2Gas correctly', async () => { - const feePerL2Gas = new Fr(123456n); - await envGetterTest('feePerL2Gas', feePerL2Gas, new FeePerL2Gas(0)); + describe('FeePerL2Gas', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + FeePerL2Gas.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new FeePerL2Gas(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(FeePerL2Gas.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read FeePerL2Gas correctly', async () => { + const feePerL2Gas = new Fr(123456n); + await envGetterTest('feePerL2Gas', feePerL2Gas, new FeePerL2Gas(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); - it('Should read FeePerDAGas correctly', async () => { - const feePerDaGas = new Fr(123456n); - await envGetterTest('feePerDaGas', feePerDaGas, new FeePerDAGas(0)); + describe('FeePerDAGas', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + FeePerDAGas.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new FeePerDAGas(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(FeePerDAGas.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read FeePerDAGas correctly', async () => { + const feePerDaGas = new Fr(123456n); + await envGetterTest('feePerDaGas', feePerDaGas, new FeePerDAGas(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); - it('Should read Origin correctly', async () => { - const origin = new Fr(123456n); - await envGetterTest('origin', origin, new Origin(0)); + describe('Origin', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Origin.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new Origin(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(Origin.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read Origin correctly', async () => { + const origin = new Fr(123456n); + await envGetterTest('origin', origin, new Origin(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); - it('Should read Sender correctly', async () => { - const sender = new Fr(123456n); - await envGetterTest('sender', sender, new Sender(0)); + describe('Sender', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Sender.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new Sender(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(Sender.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read Sender correctly', async () => { + const sender = new Fr(123456n); + await envGetterTest('sender', sender, new Sender(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); describe('Global Variables', () => { @@ -88,24 +200,80 @@ describe('Environment getters instructions', () => { expect(actual).toEqual(value); }; - it('Should read chainId', async () => { - const chainId = new Fr(123456n); - await readGlobalVariableTest('chainId', chainId, new ChainId(0)); + describe('chainId', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + ChainId.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new ChainId(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(ChainId.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read chainId', async () => { + const chainId = new Fr(123456n); + await readGlobalVariableTest('chainId', chainId, new ChainId(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); - it('Should read version', async () => { - const version = new Fr(123456n); - await readGlobalVariableTest('version', version, new Version(0)); + describe('version', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Version.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new Version(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(Version.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read version', async () => { + const version = new Fr(123456n); + await readGlobalVariableTest('version', version, new Version(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); - it('Should read block number', async () => { - const blockNumber = new Fr(123456n); - await readGlobalVariableTest('blockNumber', blockNumber, new BlockNumber(0)); + describe('block', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + BlockNumber.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new BlockNumber(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(BlockNumber.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read block number', async () => { + const blockNumber = new Fr(123456n); + await readGlobalVariableTest('blockNumber', blockNumber, new BlockNumber(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); - it('Should read timestamp', async () => { - const timestamp = new Fr(123456n); - await readGlobalVariableTest('timestamp', timestamp, new Timestamp(0)); + describe('timestamp', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Timestamp.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // dstOffset + ]); + const inst = new Timestamp(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + + expect(Timestamp.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should read timestamp', async () => { + const timestamp = new Fr(123456n); + await readGlobalVariableTest('timestamp', timestamp, new Timestamp(/*indirect=*/ 0, /*dstOffset=*/ 0)); + }); }); }); }); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/environment_getters.ts b/yarn-project/acir-simulator/src/avm/opcodes/environment_getters.ts index 18e97575b96..8dc59330e94 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/environment_getters.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/environment_getters.ts @@ -1,211 +1,136 @@ +import { AvmExecutionEnvironment } from '../avm_execution_environment.js'; import { AvmMachineState } from '../avm_machine_state.js'; import { Field } from '../avm_memory_types.js'; import { AvmJournal } from '../journal/journal.js'; +import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Instruction } from './instruction.js'; -export class Address extends Instruction { - static type: string = 'ADDRESS'; - static numberOfOperands = 1; +abstract class GetterInstruction extends Instruction { + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [OperandType.UINT8, OperandType.UINT8, OperandType.UINT32]; - constructor(private destOffset: number) { + constructor(protected indirect: number, protected dstOffset: number) { super(); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { address } = machineState.executionEnvironment; - - machineState.memory.set(this.destOffset, new Field(address)); + const res = new Field(this.getIt(machineState.executionEnvironment)); + machineState.memory.set(this.dstOffset, res); this.incrementPc(machineState); } + + protected abstract getIt(env: AvmExecutionEnvironment): any; } -export class StorageAddress extends Instruction { - static type: string = 'STORAGEADDRESS'; - static numberOfOperands = 1; +export class Address extends GetterInstruction { + static type: string = 'ADDRESS'; + static readonly opcode: Opcode = Opcode.ADDRESS; - constructor(private destOffset: number) { - super(); + protected getIt(env: AvmExecutionEnvironment): any { + return env.address; } +} - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { storageAddress } = machineState.executionEnvironment; +export class StorageAddress extends GetterInstruction { + static type: string = 'STORAGEADDRESS'; + static readonly opcode: Opcode = Opcode.STORAGEADDRESS; - machineState.memory.set(this.destOffset, new Field(storageAddress)); - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.storageAddress; } } -export class Sender extends Instruction { +export class Sender extends GetterInstruction { static type: string = 'SENDER'; - static numberOfOperands = 1; + static readonly opcode: Opcode = Opcode.SENDER; - constructor(private destOffset: number) { - super(); - } - - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { sender } = machineState.executionEnvironment; - - machineState.memory.set(this.destOffset, new Field(sender)); - - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.sender; } } -export class Origin extends Instruction { +export class Origin extends GetterInstruction { static type: string = 'ORIGIN'; - static numberOfOperands = 1; - - constructor(private destOffset: number) { - super(); - } + static readonly opcode: Opcode = Opcode.ORIGIN; - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { origin } = machineState.executionEnvironment; - - machineState.memory.set(this.destOffset, new Field(origin)); - - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.origin; } } -export class FeePerL1Gas extends Instruction { +export class FeePerL1Gas extends GetterInstruction { static type: string = 'FEEPERL1GAS'; - static numberOfOperands = 1; - - constructor(private destOffset: number) { - super(); - } - - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { feePerL1Gas } = machineState.executionEnvironment; - - machineState.memory.set(this.destOffset, new Field(feePerL1Gas)); + static readonly opcode: Opcode = Opcode.FEEPERL1GAS; - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.feePerL1Gas; } } -export class FeePerL2Gas extends Instruction { +export class FeePerL2Gas extends GetterInstruction { static type: string = 'FEEPERL2GAS'; - static numberOfOperands = 1; - - constructor(private destOffset: number) { - super(); - } - - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { feePerL2Gas } = machineState.executionEnvironment; - - machineState.memory.set(this.destOffset, new Field(feePerL2Gas)); + static readonly opcode: Opcode = Opcode.FEEPERL2GAS; - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.feePerL2Gas; } } -export class FeePerDAGas extends Instruction { +export class FeePerDAGas extends GetterInstruction { static type: string = 'FEEPERDAGAS'; - static numberOfOperands = 1; - - constructor(private destOffset: number) { - super(); - } - - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { feePerDaGas } = machineState.executionEnvironment; + static readonly opcode: Opcode = Opcode.FEEPERDAGAS; - machineState.memory.set(this.destOffset, new Field(feePerDaGas)); - - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.feePerDaGas; } } -export class Portal extends Instruction { +export class Portal extends GetterInstruction { static type: string = 'PORTAL'; - static numberOfOperands = 1; - - constructor(private destOffset: number) { - super(); - } - - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { portal } = machineState.executionEnvironment; + static readonly opcode: Opcode = Opcode.PORTAL; - machineState.memory.set(this.destOffset, new Field(portal.toField())); - - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.portal.toField(); } } -export class ChainId extends Instruction { +export class ChainId extends GetterInstruction { static type: string = 'CHAINID'; - static numberOfOperands = 1; - - constructor(private destOffset: number) { - super(); - } + static readonly opcode: Opcode = Opcode.CHAINID; - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { chainId } = machineState.executionEnvironment.globals; - - machineState.memory.set(this.destOffset, new Field(chainId)); - - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.globals.chainId; } } -export class Version extends Instruction { +export class Version extends GetterInstruction { static type: string = 'VERSION'; - static numberOfOperands = 1; - - constructor(private destOffset: number) { - super(); - } - - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { version } = machineState.executionEnvironment.globals; - - machineState.memory.set(this.destOffset, new Field(version)); + static readonly opcode: Opcode = Opcode.VERSION; - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.globals.version; } } -export class BlockNumber extends Instruction { +export class BlockNumber extends GetterInstruction { static type: string = 'BLOCKNUMBER'; - static numberOfOperands = 1; - - constructor(private destOffset: number) { - super(); - } - - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { blockNumber } = machineState.executionEnvironment.globals; - - machineState.memory.set(this.destOffset, new Field(blockNumber)); + static readonly opcode: Opcode = Opcode.BLOCKNUMBER; - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.globals.blockNumber; } } -export class Timestamp extends Instruction { +export class Timestamp extends GetterInstruction { static type: string = 'TIMESTAMP'; - static numberOfOperands = 1; - - constructor(private destOffset: number) { - super(); - } - - async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const { timestamp } = machineState.executionEnvironment.globals; + static readonly opcode: Opcode = Opcode.TIMESTAMP; - machineState.memory.set(this.destOffset, new Field(timestamp)); - - this.incrementPc(machineState); + protected getIt(env: AvmExecutionEnvironment): any { + return env.globals.timestamp; } } -// export class Coinbase extends Instruction { +// export class Coinbase extends GetterInstruction { // static type: string = 'COINBASE'; // static numberOfOperands = 1; @@ -223,7 +148,7 @@ export class Timestamp extends Instruction { // } // // TODO: are these even needed within the block? (both block gas limit variables - why does the execution env care?) -// export class BlockL1GasLimit extends Instruction { +// export class BlockL1GasLimit extends GetterInstruction { // static type: string = 'BLOCKL1GASLIMIT'; // static numberOfOperands = 1; @@ -240,7 +165,7 @@ export class Timestamp extends Instruction { // } // } -// export class BlockL2GasLimit extends Instruction { +// export class BlockL2GasLimit extends GetterInstruction { // static type: string = 'BLOCKL2GASLIMIT'; // static numberOfOperands = 1; @@ -257,7 +182,7 @@ export class Timestamp extends Instruction { // } // } -// export class BlockDAGasLimit extends Instruction { +// export class BlockDAGasLimit extends GetterInstruction { // static type: string = 'BLOCKDAGASLIMIT'; // static numberOfOperands = 1; diff --git a/yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts index 50c29128ec5..2395cc32479 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.test.ts @@ -9,9 +9,12 @@ import { Field } from '../avm_memory_types.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'; +import { encodeToBytecode } from '../serialization/bytecode_serialization.js'; +import { Return } from './control_flow.js'; +import { Call, StaticCall } from './external_calls.js'; +import { Instruction } from './instruction.js'; +import { CalldataCopy } from './memory.js'; +import { SStore } from './storage.js'; describe('External Calls', () => { let machineState: AvmMachineState; @@ -31,44 +34,68 @@ describe('External Calls', () => { }); describe('Call', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Call.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // gasOffset + ...Buffer.from('a2345678', 'hex'), // addrOffset + ...Buffer.from('b2345678', 'hex'), // argsOffset + ...Buffer.from('c2345678', 'hex'), // argsSize + ...Buffer.from('d2345678', 'hex'), // retOffset + ...Buffer.from('e2345678', 'hex'), // retSize + ...Buffer.from('f2345678', 'hex'), // successOffset + ]); + const inst = new Call( + /*indirect=*/ 0x01, + /*gasOffset=*/ 0x12345678, + /*addrOffset=*/ 0xa2345678, + /*argsOffset=*/ 0xb2345678, + /*argsSize=*/ 0xc2345678, + /*retOffset=*/ 0xd2345678, + /*retSize=*/ 0xe2345678, + /*successOffset=*/ 0xf2345678, + ); + + expect(Call.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + // 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 Field(1n), new Field(2n), new Field(3n)]; const argsSize = args.length; - const retOffset = 8; const retSize = 2; - const successOffset = 7; + const otherContextInstructionsBytecode = encodeToBytecode([ + new CalldataCopy(/*indirect=*/ 0, /*csOffset=*/ 0, /*copySize=*/ argsSize, /*dstOffset=*/ 0), + new SStore(/*indirect=*/ 0, /*srcOffset=*/ 0, /*slotOffset=*/ 0), + new Return(/*indirect=*/ 0, /*retOffset=*/ 0, /*size=*/ 2), + ]); machineState.memory.set(0, new Field(gas)); machineState.memory.set(1, new Field(addr)); machineState.memory.setSlice(2, args); - - const otherContextInstructions: [Opcode, any[]][] = [ - // Place [1,2,3] into memory - [Opcode.CALLDATACOPY, [/*value=*/ 0, /*copySize=*/ argsSize, /*dstOffset=*/ 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); + const instruction = new Call( + /*indirect=*/ 0, + gasOffset, + addrOffset, + argsOffset, + argsSize, + retOffset, + retSize, + successOffset, + ); await instruction.execute(machineState, journal); const successValue = machineState.memory.get(successOffset); @@ -91,6 +118,33 @@ describe('External Calls', () => { }); describe('Static Call', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + StaticCall.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // gasOffset + ...Buffer.from('a2345678', 'hex'), // addrOffset + ...Buffer.from('b2345678', 'hex'), // argsOffset + ...Buffer.from('c2345678', 'hex'), // argsSize + ...Buffer.from('d2345678', 'hex'), // retOffset + ...Buffer.from('e2345678', 'hex'), // retSize + ...Buffer.from('f2345678', 'hex'), // successOffset + ]); + const inst = new StaticCall( + /*indirect=*/ 0x01, + /*gasOffset=*/ 0x12345678, + /*addrOffset=*/ 0xa2345678, + /*argsOffset=*/ 0xb2345678, + /*argsSize=*/ 0xc2345678, + /*retOffset=*/ 0xd2345678, + /*retSize=*/ 0xe2345678, + /*successOffset=*/ 0xf2345678, + ); + + expect(StaticCall.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should fail if a static call attempts to touch storage', async () => { const gasOffset = 0; const gas = new Field(0); @@ -108,20 +162,27 @@ describe('External Calls', () => { machineState.memory.set(1, addr); machineState.memory.setSlice(2, args); - const otherContextInstructions: [Opcode, any[]][] = [ - // Place [1,2,3] into memory - [Opcode.CALLDATACOPY, [/*value=*/ 0, /*copySize=*/ argsSize, /*dstOffset=*/ 0]], - [Opcode.SSTORE, [/*slotOffset*/ 1, /*dataOffset=*/ 0]], + const otherContextInstructions: Instruction[] = [ + new CalldataCopy(/*indirect=*/ 0, /*csOffset=*/ 0, /*copySize=*/ argsSize, /*dstOffset=*/ 0), + new SStore(/*indirect=*/ 0, /*srcOffset=*/ 1, /*slotOffset=*/ 0), ]; - const otherContextInstructionsBytecode = Buffer.concat( - otherContextInstructions.map(([opcode, args]) => encodeToBytecode(opcode, args)), - ); + const otherContextInstructionsBytecode = encodeToBytecode(otherContextInstructions); + jest .spyOn(journal.hostStorage.contractsDb, 'getBytecode') .mockReturnValue(Promise.resolve(otherContextInstructionsBytecode)); - const instruction = new Call(gasOffset, addrOffset, argsOffset, argsSize, retOffset, retSize, successOffset); + const instruction = new StaticCall( + /*indirect=*/ 0, + gasOffset, + addrOffset, + argsOffset, + argsSize, + retOffset, + retSize, + successOffset, + ); await instruction.execute(machineState, journal); // No revert has occurred, but the nested execution has failed diff --git a/yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts index 280b1284f02..7d9fe5558fb 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/external_calls.ts @@ -4,17 +4,31 @@ import { AvmContext } from '../avm_context.js'; import { AvmMachineState } from '../avm_machine_state.js'; import { Field } from '../avm_memory_types.js'; import { AvmJournal } from '../journal/journal.js'; +import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Instruction } from './instruction.js'; export class Call extends Instruction { static type: string = 'CALL'; - static numberOfOperands = 7; + static readonly opcode: Opcode = Opcode.CALL; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + ]; constructor( - private /* Unused due to no formal gas implementation at this moment */ _gasOffset: number, + private indirect: number, + private _gasOffset: number /* Unused due to no formal gas implementation at this moment */, private addrOffset: number, private argsOffset: number, - private argSize: number, + private argsSize: number, private retOffset: number, private retSize: number, private successOffset: number, @@ -25,7 +39,7 @@ export class Call extends Instruction { // TODO(https://github.com/AztecProtocol/aztec-packages/issues/3992): there is no concept of remaining / available gas at this moment async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { const callAddress = machineState.memory.getAs(this.addrOffset); - const calldata = machineState.memory.getSlice(this.argsOffset, this.argSize).map(f => new Fr(f.toBigInt())); + const calldata = machineState.memory.getSlice(this.argsOffset, this.argsSize).map(f => new Fr(f.toBigInt())); const avmContext = AvmContext.prepExternalCallContext( new Fr(callAddress.toBigInt()), @@ -55,13 +69,26 @@ export class Call extends Instruction { export class StaticCall extends Instruction { static type: string = 'STATICCALL'; - static numberOfOperands = 7; + static readonly opcode: Opcode = Opcode.STATICCALL; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + ]; constructor( - private /* Unused due to no formal gas implementation at this moment */ _gasOffset: number, + private indirect: number, + private _gasOffset: number /* Unused due to no formal gas implementation at this moment */, private addrOffset: number, private argsOffset: number, - private argSize: number, + private argsSize: number, private retOffset: number, private retSize: number, private successOffset: number, @@ -71,7 +98,7 @@ export class StaticCall extends Instruction { async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { const callAddress = machineState.memory.get(this.addrOffset); - const calldata = machineState.memory.getSlice(this.argsOffset, this.argSize).map(f => new Fr(f.toBigInt())); + const calldata = machineState.memory.getSlice(this.argsOffset, this.argsSize).map(f => new Fr(f.toBigInt())); const avmContext = AvmContext.prepExternalStaticCallContext( new Fr(callAddress.toBigInt()), diff --git a/yarn-project/acir-simulator/src/avm/opcodes/index.ts b/yarn-project/acir-simulator/src/avm/opcodes/index.ts index 10ce6b4d0f7..2fa19f7b04a 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/index.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/index.ts @@ -1,5 +1,11 @@ export * from './arithmetic.js'; +export * from './bitwise.js'; export * from './control_flow.js'; export * from './instruction.js'; export * from './comparators.js'; export * from './memory.js'; +export * from './storage.js'; +// TODO(https://github.com/AztecProtocol/aztec-packages/issues/4359): dependency cycle +// export * from './external_calls.js'; +export * from './environment_getters.js'; +export * from './accrued_substate.js'; diff --git a/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts b/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts index 8eb73212fd2..ff5fdd5e96f 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts @@ -1,15 +1,17 @@ +import { assert } from 'console'; + import { AvmMachineState } from '../avm_machine_state.js'; import { TypeTag } from '../avm_memory_types.js'; import { AvmJournal } from '../journal/index.js'; - -export const AVM_OPERAND_BYTE_LENGTH = 4; // Keep in sync with cpp code -export const AVM_OPCODE_BYTE_LENGTH = 1; // Keep in sync with cpp code +import { BufferCursor } from '../serialization/buffer_cursor.js'; +import { OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; /** - * Opcode base class + * Parent class for all AVM instructions. + * It's most important aspects are execution and (de)serialization. */ export abstract class Instruction { - abstract execute(machineState: AvmMachineState, journal: AvmJournal): Promise; + public abstract execute(machineState: AvmMachineState, journal: AvmJournal): Promise; incrementPc(machineState: AvmMachineState): void { machineState.pc++; @@ -35,6 +37,27 @@ export abstract class Instruction { checkTag(machineState, tag, offset); } } + + /** + * Deserializes a subclass of Instruction from a Buffer. + * If you want to use this, your subclass should specify a {@code static wireFormat: OperandType[]}. + * @param this Class object to deserialize to. + * @param buf Buffer to read from. + * @returns Constructed instance of Class. + */ + public static deserialize; wireFormat: OperandType[] }>( + this: T, + buf: BufferCursor | Buffer, + ): InstanceType { + const res = deserialize(buf, this.wireFormat); + const args = res.slice(1) as ConstructorParameters; // Remove opcode. + return new this(...args); + } + + public serialize(this: any): Buffer { + assert(this instanceof Instruction); + return serialize(this.constructor.wireFormat, this); + } } /** diff --git a/yarn-project/acir-simulator/src/avm/opcodes/instruction_impl.ts b/yarn-project/acir-simulator/src/avm/opcodes/instruction_impl.ts new file mode 100644 index 00000000000..fb8b8621a71 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/opcodes/instruction_impl.ts @@ -0,0 +1,52 @@ +import { OperandType } from '../serialization/instruction_serialization.js'; +import { Instruction } from './instruction.js'; + +/** + * Covers (de)serialization for an instruction with: + * indirect, inTag, and two UINT32s. + */ +export abstract class TwoOperandInstruction extends Instruction { + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + ]; + + constructor( + protected indirect: number, + protected inTag: number, + protected aOffset: number, + protected dstOffset: number, + ) { + super(); + } +} + +/** + * Covers (de)serialization for an instruction with: + * indirect, inTag, and three UINT32s. + */ +export abstract class ThreeOperandInstruction extends Instruction { + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + ]; + + constructor( + protected indirect: number, + protected inTag: number, + protected aOffset: number, + protected bOffset: number, + protected dstOffset: number, + ) { + super(); + } +} diff --git a/yarn-project/acir-simulator/src/avm/opcodes/instruction_set.ts b/yarn-project/acir-simulator/src/avm/opcodes/instruction_set.ts deleted file mode 100644 index e63fe02dc20..00000000000 --- a/yarn-project/acir-simulator/src/avm/opcodes/instruction_set.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Add, Div, Mul, Sub } from './arithmetic.js'; -import { And, Not, Or, Shl, Shr, Xor } from './bitwise.js'; -import { InternalCall, InternalReturn, Jump, JumpI, Return } from './control_flow.js'; -// import { Call } from './external_calls.js'; -import { Instruction } from './instruction.js'; -import { CMov, CalldataCopy, Cast, Mov, Set } from './memory.js'; -import { Opcode } from './opcodes.js'; -//import { Eq, Lt, Lte } from './comparators.js'; -import { SLoad, SStore } from './storage.js'; - -type InstructionConstructor = new (...args: any[]) => Instruction; - -type InstructionConstructorAndMembers = InstructionConstructor & { - numberOfOperands: number; -}; - -export const INSTRUCTION_SET: Map = new Map( - new Array<[Opcode, InstructionConstructorAndMembers]>( - // Compute - // Compute - Arithmetic - [Opcode.ADD, Add], - [Opcode.SUB, Sub], - [Opcode.MUL, Mul], - [Opcode.DIV, Div], - //// Compute - Comparators - //[Opcode.EQ, Eq], - //[Opcode.LT, Lt], - //[Opcode.LTE, Lte], - //// Compute - Bitwise - [Opcode.AND, And], - [Opcode.OR, Or], - [Opcode.XOR, Xor], - [Opcode.NOT, Not], - [Opcode.SHL, Shl], - [Opcode.SHR, Shr], - //// Compute - Type Conversions - [Opcode.CAST, Cast], - - //// Execution Environment - //[Opcode.ADDRESS, Address], - //[Opcode.STORAGEADDRESS, Storageaddress], - //[Opcode.ORIGIN, Origin], - //[Opcode.SENDER, Sender], - //[Opcode.PORTAL, Portal], - //[Opcode.FEEPERL1GAS, Feeperl1gas], - //[Opcode.FEEPERL2GAS, Feeperl2gas], - //[Opcode.FEEPERDAGAS, Feeperdagas], - //[Opcode.CONTRACTCALLDEPTH, Contractcalldepth], - //// Execution Environment - Globals - //[Opcode.CHAINID, Chainid], - //[Opcode.VERSION, Version], - //[Opcode.BLOCKNUMBER, Blocknumber], - //[Opcode.TIMESTAMP, Timestamp], - //[Opcode.COINBASE, Coinbase], - //[Opcode.BLOCKL1GASLIMIT, Blockl1gaslimit], - //[Opcode.BLOCKL2GASLIMIT, Blockl2gaslimit], - //[Opcode.BLOCKDAGASLIMIT, Blockdagaslimit], - // Execution Environment - Calldata - [Opcode.CALLDATACOPY, CalldataCopy], - - //// Machine State - // Machine State - Gas - //[Opcode.L1GASLEFT, L1gasleft], - //[Opcode.L2GASLEFT, L2gasleft], - //[Opcode.DAGASLEFT, Dagasleft], - //// Machine State - Internal Control Flow - [Opcode.JUMP, Jump], - [Opcode.JUMPI, JumpI], - [Opcode.INTERNALCALL, InternalCall], - [Opcode.INTERNALRETURN, InternalReturn], - //// Machine State - Memory - [Opcode.SET, Set], - [Opcode.MOV, Mov], - [Opcode.CMOV, CMov], - - //// World State - //[Opcode.BLOCKHEADERBYNUMBER, Blockheaderbynumber], - [Opcode.SLOAD, SLoad], // Public Storage - [Opcode.SSTORE, SStore], // Public Storage - //[Opcode.READL1TOL2MSG, Readl1tol2msg], // Messages - //[Opcode.SENDL2TOL1MSG, Sendl2tol1msg], // Messages - //[Opcode.EMITNOTEHASH, Emitnotehash], // Notes & Nullifiers - //[Opcode.EMITNULLIFIER, Emitnullifier], // Notes & Nullifiers - - //// Accrued Substate - //[Opcode.EMITUNENCRYPTEDLOG, Emitunencryptedlog], - - //// Control Flow - Contract Calls - // [Opcode.CALL, Call], - //[Opcode.STATICCALL, Staticcall], - [Opcode.RETURN, Return], - //[Opcode.REVERT, Revert], - - //// Gadgets - //[Opcode.KECCAK, Keccak], - //[Opcode.POSEIDON, Poseidon], - ), -); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/memory.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/memory.test.ts index 1a10c5be15b..ce4e6165db4 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/memory.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/memory.test.ts @@ -18,8 +18,30 @@ describe('Memory instructions', () => { }); describe('SET', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Set.opcode, // opcode + 0x01, // indirect + TypeTag.FIELD, // inTag + ...Buffer.from('12345678123456781234567812345678', 'hex'), // const (will be 128 bit) + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Set( + /*indirect=*/ 0x01, + /*inTag=*/ TypeTag.FIELD, + /*value=*/ 0x12345678123456781234567812345678n, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Set.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('should correctly set value and tag (uninitialized)', async () => { - await new Set(TypeTag.UINT16, /*value=*/ 1234n, /*offset=*/ 1).execute(machineState, journal); + await new Set(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT16, /*value=*/ 1234n, /*offset=*/ 1).execute( + machineState, + journal, + ); const actual = machineState.memory.get(1); const tag = machineState.memory.getTag(1); @@ -31,7 +53,10 @@ describe('Memory instructions', () => { it('should correctly set value and tag (overwriting)', async () => { machineState.memory.set(1, new Field(27)); - await new Set(TypeTag.UINT32, /*value=*/ 1234n, /*offset=*/ 1).execute(machineState, journal); + await new Set(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*value=*/ 1234n, /*offset=*/ 1).execute( + machineState, + journal, + ); const actual = machineState.memory.get(1); const tag = machineState.memory.getTag(1); @@ -42,6 +67,25 @@ describe('Memory instructions', () => { }); describe('CAST', () => { + it('Should deserialize correctly', () => { + const buf = Buffer.from([ + Cast.opcode, // opcode + 0x01, // indirect + TypeTag.FIELD, // dstTag + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Cast( + /*indirect=*/ 0x01, + /*dstTag=*/ TypeTag.FIELD, + /*aOffset=*/ 0x12345678, + /*dstOffset=*/ 0x3456789a, + ); + + expect(Cast.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should upcast between integral types', () => { machineState.memory.set(0, new Uint8(20n)); machineState.memory.set(1, new Uint16(65000n)); @@ -50,11 +94,11 @@ describe('Memory instructions', () => { machineState.memory.set(4, new Uint128(1n << 100n)); [ - new Cast(TypeTag.UINT16, /*aOffset=*/ 0, /*dstOffset=*/ 10), - new Cast(TypeTag.UINT32, /*aOffset=*/ 1, /*dstOffset=*/ 11), - new Cast(TypeTag.UINT64, /*aOffset=*/ 2, /*dstOffset=*/ 12), - new Cast(TypeTag.UINT128, /*aOffset=*/ 3, /*dstOffset=*/ 13), - new Cast(TypeTag.UINT128, /*aOffset=*/ 4, /*dstOffset=*/ 14), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT16, /*aOffset=*/ 0, /*dstOffset=*/ 10), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT32, /*aOffset=*/ 1, /*dstOffset=*/ 11), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT64, /*aOffset=*/ 2, /*dstOffset=*/ 12), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT128, /*aOffset=*/ 3, /*dstOffset=*/ 13), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT128, /*aOffset=*/ 4, /*dstOffset=*/ 14), ].forEach(i => i.execute(machineState, journal)); const actual = machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 5); @@ -77,11 +121,11 @@ describe('Memory instructions', () => { machineState.memory.set(4, new Uint128((1n << 100n) - 1n)); [ - new Cast(TypeTag.UINT8, /*aOffset=*/ 0, /*dstOffset=*/ 10), - new Cast(TypeTag.UINT8, /*aOffset=*/ 1, /*dstOffset=*/ 11), - new Cast(TypeTag.UINT16, /*aOffset=*/ 2, /*dstOffset=*/ 12), - new Cast(TypeTag.UINT32, /*aOffset=*/ 3, /*dstOffset=*/ 13), - new Cast(TypeTag.UINT64, /*aOffset=*/ 4, /*dstOffset=*/ 14), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT8, /*aOffset=*/ 0, /*dstOffset=*/ 10), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*dstOffset=*/ 11), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT16, /*aOffset=*/ 2, /*dstOffset=*/ 12), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT32, /*aOffset=*/ 3, /*dstOffset=*/ 13), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT64, /*aOffset=*/ 4, /*dstOffset=*/ 14), ].forEach(i => i.execute(machineState, journal)); const actual = machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 5); @@ -104,11 +148,11 @@ describe('Memory instructions', () => { machineState.memory.set(4, new Uint128(1n << 100n)); [ - new Cast(TypeTag.FIELD, /*aOffset=*/ 0, /*dstOffset=*/ 10), - new Cast(TypeTag.FIELD, /*aOffset=*/ 1, /*dstOffset=*/ 11), - new Cast(TypeTag.FIELD, /*aOffset=*/ 2, /*dstOffset=*/ 12), - new Cast(TypeTag.FIELD, /*aOffset=*/ 3, /*dstOffset=*/ 13), - new Cast(TypeTag.FIELD, /*aOffset=*/ 4, /*dstOffset=*/ 14), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 0, /*dstOffset=*/ 10), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 1, /*dstOffset=*/ 11), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 2, /*dstOffset=*/ 12), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 3, /*dstOffset=*/ 13), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 4, /*dstOffset=*/ 14), ].forEach(i => i.execute(machineState, journal)); const actual = machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 5); @@ -131,11 +175,11 @@ describe('Memory instructions', () => { machineState.memory.set(4, new Field((1n << 200n) - 1n)); [ - new Cast(TypeTag.UINT8, /*aOffset=*/ 0, /*dstOffset=*/ 10), - new Cast(TypeTag.UINT16, /*aOffset=*/ 1, /*dstOffset=*/ 11), - new Cast(TypeTag.UINT32, /*aOffset=*/ 2, /*dstOffset=*/ 12), - new Cast(TypeTag.UINT64, /*aOffset=*/ 3, /*dstOffset=*/ 13), - new Cast(TypeTag.UINT128, /*aOffset=*/ 4, /*dstOffset=*/ 14), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT8, /*aOffset=*/ 0, /*dstOffset=*/ 10), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT16, /*aOffset=*/ 1, /*dstOffset=*/ 11), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT32, /*aOffset=*/ 2, /*dstOffset=*/ 12), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT64, /*aOffset=*/ 3, /*dstOffset=*/ 13), + new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT128, /*aOffset=*/ 4, /*dstOffset=*/ 14), ].forEach(i => i.execute(machineState, journal)); const actual = machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 5); @@ -153,7 +197,10 @@ describe('Memory instructions', () => { it('Should cast between field elements', async () => { machineState.memory.set(0, new Field(12345678n)); - await new Cast(TypeTag.FIELD, /*aOffset=*/ 0, /*dstOffset=*/ 1).execute(machineState, journal); + await new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 0, /*dstOffset=*/ 1).execute( + machineState, + journal, + ); const actual = machineState.memory.get(1); expect(actual).toEqual(new Field(12345678n)); @@ -163,23 +210,36 @@ describe('Memory instructions', () => { }); describe('MOV', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + Mov.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // srcOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new Mov(/*indirect=*/ 0x01, /*srcOffset=*/ 0x12345678, /*dstOffset=*/ 0x3456789a); + + expect(Mov.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should move integrals on different memory cells', async () => { - machineState.memory.set(1, new Uint16(27)); - await new Mov(/*offsetA=*/ 1, /*offsetA=*/ 2).execute(machineState, journal); + machineState.memory.set(0, new Uint16(27)); + await new Mov(/*indirect=*/ 0, /*srcOffset=*/ 0, /*dstOffset=*/ 1).execute(machineState, journal); - const actual = machineState.memory.get(2); - const tag = machineState.memory.getTag(2); + const actual = machineState.memory.get(1); + const tag = machineState.memory.getTag(1); expect(actual).toEqual(new Uint16(27n)); expect(tag).toEqual(TypeTag.UINT16); }); it('Should move field elements on different memory cells', async () => { - machineState.memory.set(1, new Field(27)); - await new Mov(/*offsetA=*/ 1, /*offsetA=*/ 2).execute(machineState, journal); + machineState.memory.set(0, new Field(27)); + await new Mov(/*indirect=*/ 0, /*srcOffset=*/ 0, /*dstOffset=*/ 1).execute(machineState, journal); - const actual = machineState.memory.get(2); - const tag = machineState.memory.getTag(2); + const actual = machineState.memory.get(1); + const tag = machineState.memory.getTag(1); expect(actual).toEqual(new Field(27n)); expect(tag).toEqual(TypeTag.FIELD); @@ -187,12 +247,33 @@ describe('Memory instructions', () => { }); describe('CMOV', () => { + it('Should deserialize correctly', () => { + const buf = Buffer.from([ + CMov.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('a2345678', 'hex'), // bOffset + ...Buffer.from('b2345678', 'hex'), // condOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new CMov( + /*indirect=*/ 0x01, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0xa2345678, + /*condOffset=*/ 0xb2345678, + /*dstOffset=*/ 0x3456789a, + ); + + expect(CMov.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Should move A if COND is true, on different memory cells (integral condition)', async () => { machineState.memory.set(0, new Uint32(123)); // A machineState.memory.set(1, new Uint16(456)); // B machineState.memory.set(2, new Uint8(2)); // Condition - await new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( + await new CMov(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( machineState, journal, ); @@ -208,7 +289,7 @@ describe('Memory instructions', () => { machineState.memory.set(1, new Uint16(456)); // B machineState.memory.set(2, new Uint8(0)); // Condition - await new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( + await new CMov(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( machineState, journal, ); @@ -224,7 +305,7 @@ describe('Memory instructions', () => { machineState.memory.set(1, new Uint16(456)); // B machineState.memory.set(2, new Field(1)); // Condition - await new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( + await new CMov(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( machineState, journal, ); @@ -240,7 +321,7 @@ describe('Memory instructions', () => { machineState.memory.set(1, new Uint16(456)); // B machineState.memory.set(2, new Field(0)); // Condition - await new CMov(/*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( + await new CMov(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*condOffset=*/ 2, /*dstOffset=*/ 3).execute( machineState, journal, ); @@ -253,12 +334,34 @@ describe('Memory instructions', () => { }); describe('CALLDATACOPY', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + CalldataCopy.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // cdOffset + ...Buffer.from('23456789', 'hex'), // copysize + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new CalldataCopy( + /*indirect=*/ 0x01, + /*cdOffset=*/ 0x12345678, + /*copysize=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(CalldataCopy.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + it('Writes nothing if size is 0', async () => { const calldata = [new Fr(1n), new Fr(2n), new Fr(3n)]; machineState = new AvmMachineState(initExecutionEnvironment({ calldata })); machineState.memory.set(0, new Uint16(12)); // Some previous data to be overwritten - await new CalldataCopy(/*cdOffset=*/ 0, /*copySize=*/ 0, /*dstOffset=*/ 0).execute(machineState, journal); + await new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ 0, /*copySize=*/ 0, /*dstOffset=*/ 0).execute( + machineState, + journal, + ); const actual = machineState.memory.get(0); expect(actual).toEqual(new Uint16(12)); @@ -269,7 +372,10 @@ describe('Memory instructions', () => { machineState = new AvmMachineState(initExecutionEnvironment({ calldata })); machineState.memory.set(0, new Uint16(12)); // Some previous data to be overwritten - await new CalldataCopy(/*cdOffset=*/ 0, /*copySize=*/ 3, /*dstOffset=*/ 0).execute(machineState, journal); + await new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ 0, /*copySize=*/ 3, /*dstOffset=*/ 0).execute( + machineState, + journal, + ); const actual = machineState.memory.getSlice(/*offset=*/ 0, /*size=*/ 3); expect(actual).toEqual([new Field(1), new Field(2), new Field(3)]); @@ -280,7 +386,10 @@ describe('Memory instructions', () => { machineState = new AvmMachineState(initExecutionEnvironment({ calldata })); machineState.memory.set(0, new Uint16(12)); // Some previous data to be overwritten - await new CalldataCopy(/*cdOffset=*/ 1, /*copySize=*/ 2, /*dstOffset=*/ 0).execute(machineState, journal); + await new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ 1, /*copySize=*/ 2, /*dstOffset=*/ 0).execute( + machineState, + journal, + ); const actual = machineState.memory.getSlice(/*offset=*/ 0, /*size=*/ 2); expect(actual).toEqual([new Field(2), new Field(3)]); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/memory.ts b/yarn-project/acir-simulator/src/avm/opcodes/memory.ts index 2524684dc02..31b05c95969 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/memory.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/memory.ts @@ -1,13 +1,23 @@ import { AvmMachineState } from '../avm_machine_state.js'; import { Field, TaggedMemory, TypeTag } from '../avm_memory_types.js'; import { AvmJournal } from '../journal/index.js'; +import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Instruction } from './instruction.js'; +import { TwoOperandInstruction } from './instruction_impl.js'; export class Set extends Instruction { - static type: string = 'SET'; - static numberOfOperands = 3; - - constructor(private inTag: TypeTag, private value: bigint, private dstOffset: number) { + static readonly type: string = 'SET'; + static readonly opcode: Opcode = Opcode.SET; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT128, + OperandType.UINT32, + ]; + + constructor(private indirect: number, private inTag: number, private value: bigint, private dstOffset: number) { super(); } @@ -20,69 +30,99 @@ export class Set extends Instruction { } } -export class Cast extends Instruction { - static type: string = 'CAST'; - static numberOfOperands = 3; - - constructor(private dstTag: TypeTag, private aOffset: number, private dstOffset: number) { +export class CMov extends Instruction { + static readonly type: string = 'CMOV'; + static readonly opcode: Opcode = Opcode.CMOV; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + ]; + + constructor( + private indirect: number, + private aOffset: number, + private bOffset: number, + private condOffset: number, + private dstOffset: number, + ) { super(); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); + const b = machineState.memory.get(this.bOffset); + const cond = machineState.memory.get(this.condOffset); - // TODO: consider not using toBigInt() - const casted = - this.dstTag == TypeTag.FIELD ? new Field(a.toBigInt()) : TaggedMemory.integralFromTag(a.toBigInt(), this.dstTag); - - machineState.memory.set(this.dstOffset, casted); + // TODO: reconsider toBigInt() here + machineState.memory.set(this.dstOffset, cond.toBigInt() > 0 ? a : b); this.incrementPc(machineState); } } -export class Mov extends Instruction { - static type: string = 'MOV'; - static numberOfOperands = 2; +export class Cast extends TwoOperandInstruction { + static readonly type: string = 'CAST'; + static readonly opcode = Opcode.CAST; - constructor(private aOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, dstTag: number, aOffset: number, dstOffset: number) { + super(indirect, dstTag, aOffset, dstOffset); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { const a = machineState.memory.get(this.aOffset); - machineState.memory.set(this.dstOffset, a); + // TODO: consider not using toBigInt() + const casted = + this.inTag == TypeTag.FIELD ? new Field(a.toBigInt()) : TaggedMemory.integralFromTag(a.toBigInt(), this.inTag); + + machineState.memory.set(this.dstOffset, casted); this.incrementPc(machineState); } } -export class CMov extends Instruction { - static type: string = 'CMOV'; - static numberOfOperands = 4; - - constructor(private aOffset: number, private bOffset: number, private condOffset: number, private dstOffset: number) { +export class Mov extends Instruction { + static readonly type: string = 'MOV'; + static readonly opcode: Opcode = Opcode.MOV; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + ]; + + constructor(private indirect: number, private srcOffset: number, private dstOffset: number) { super(); } async execute(machineState: AvmMachineState, _journal: AvmJournal): Promise { - const a = machineState.memory.get(this.aOffset); - const b = machineState.memory.get(this.bOffset); - const cond = machineState.memory.get(this.condOffset); + const a = machineState.memory.get(this.srcOffset); - // TODO: reconsider toBigInt() here - machineState.memory.set(this.dstOffset, cond.toBigInt() > 0 ? a : b); + machineState.memory.set(this.dstOffset, a); this.incrementPc(machineState); } } export class CalldataCopy extends Instruction { - static type: string = 'CALLDATACOPY'; - static numberOfOperands = 3; - - constructor(private cdOffset: number, private copySize: number, private dstOffset: number) { + static readonly type: string = 'CALLDATACOPY'; + static readonly opcode: Opcode = Opcode.CALLDATACOPY; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + ]; + + constructor(private indirect: number, private cdOffset: number, private copySize: number, private dstOffset: number) { super(); } diff --git a/yarn-project/acir-simulator/src/avm/opcodes/opcodes.ts b/yarn-project/acir-simulator/src/avm/opcodes/opcodes.ts deleted file mode 100644 index 97bdfa3181d..00000000000 --- a/yarn-project/acir-simulator/src/avm/opcodes/opcodes.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * All AVM opcodes. (Keep in sync with cpp counterpart code AvmMini_opcode.hpp). - * Source: https://yp-aztec.netlify.app/docs/public-vm/instruction-set - */ -export enum Opcode { - // Compute - // Compute - Arithmetic - ADD = 0x00, - SUB = 0x01, - MUL = 0x02, - DIV = 0x03, - // Compute - Comparators - EQ = 0x04, - LT = 0x05, - LTE = 0x06, - // Compute - Bitwise - AND = 0x07, - OR = 0x08, - XOR = 0x09, - NOT = 0x0a, - SHL = 0x0b, - SHR = 0x0c, - // Compute - Type Conversions - CAST = 0x0d, - - // Execution Environment - ADDRESS = 0x0e, - STORAGEADDRESS = 0x0f, - ORIGIN = 0x10, - SENDER = 0x11, - PORTAL = 0x12, - FEEPERL1GAS = 0x13, - FEEPERL2GAS = 0x14, - FEEPERDAGAS = 0x15, - CONTRACTCALLDEPTH = 0x16, - // Execution Environment - Globals - CHAINID = 0x17, - VERSION = 0x18, - BLOCKNUMBER = 0x19, - TIMESTAMP = 0x1a, - COINBASE = 0x1b, - BLOCKL1GASLIMIT = 0x1c, - BLOCKL2GASLIMIT = 0x1d, - BLOCKDAGASLIMIT = 0x1e, - // Execution Environment - Calldata - CALLDATACOPY = 0x1f, - - // Machine State - // Machine State - Gas - L1GASLEFT = 0x20, - L2GASLEFT = 0x21, - DAGASLEFT = 0x22, - // Machine State - Internal Control Flow - JUMP = 0x23, - JUMPI = 0x24, - INTERNALCALL = 0x25, - INTERNALRETURN = 0x26, - // Machine State - Memory - SET = 0x27, - MOV = 0x28, - CMOV = 0x29, - - // World State - BLOCKHEADERBYNUMBER = 0x2a, - SLOAD = 0x2b, // Public Storage - SSTORE = 0x2c, // Public Storage - READL1TOL2MSG = 0x2d, // Messages - SENDL2TOL1MSG = 0x2e, // Messages - EMITNOTEHASH = 0x2f, // Notes & Nullifiers - EMITNULLIFIER = 0x30, // Notes & Nullifiers - - // Accrued Substate - EMITUNENCRYPTEDLOG = 0x31, - - // Control Flow - Contract Calls - CALL = 0x32, - STATICCALL = 0x33, - RETURN = 0x34, - REVERT = 0x35, - - // Gadgets - KECCAK = 0x36, - POSEIDON = 0x37, - - // Add new opcodes before this - TOTAL_OPCODES_NUMBER, -} diff --git a/yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts index e62b0abc561..8f704b036a3 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/storage.test.ts @@ -21,48 +21,79 @@ describe('Storage Instructions', () => { machineState = new AvmMachineState(executionEnvironment); }); - it('Sstore should Write into storage', async () => { - const a = new Field(1n); - const b = new Field(2n); - - machineState.memory.set(0, a); - machineState.memory.set(1, b); - - await new SStore(0, 1).execute(machineState, journal); - - expect(journal.writeStorage).toBeCalledWith(address, new Fr(a.toBigInt()), new Fr(b.toBigInt())); + describe('SSTORE', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + SStore.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // srcOffset + ...Buffer.from('a2345678', 'hex'), // slotOffset + ]); + const inst = new SStore(/*indirect=*/ 0x01, /*srcOffset=*/ 0x12345678, /*slotOffset=*/ 0xa2345678); + + expect(SStore.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Sstore should Write into storage', async () => { + const a = new Field(1n); + const b = new Field(2n); + + machineState.memory.set(0, a); + machineState.memory.set(1, b); + + await new SStore(/*indirect=*/ 0, /*srcOffset=*/ 0, /*slotOffset=*/ 1).execute(machineState, journal); + + expect(journal.writeStorage).toHaveBeenCalledWith(address, new Fr(a.toBigInt()), new Fr(b.toBigInt())); + }); + + it('Should not be able to write to storage in a static call', async () => { + const executionEnvironment = initExecutionEnvironment({ isStaticCall: true }); + machineState = new AvmMachineState(executionEnvironment); + + const a = new Field(1n); + const b = new Field(2n); + + machineState.memory.set(0, a); + machineState.memory.set(1, b); + + const instruction = () => + new SStore(/*indirect=*/ 0, /*srcOffset=*/ 0, /*slotOffset=*/ 1).execute(machineState, journal); + await expect(instruction()).rejects.toThrow(StaticCallStorageAlterError); + }); }); - it('Should not be able to write to storage in a static call', async () => { - const executionEnvironment = initExecutionEnvironment({ isStaticCall: true }); - machineState = new AvmMachineState(executionEnvironment); + describe('SLOAD', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + SLoad.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // slotOffset + ...Buffer.from('a2345678', 'hex'), // dstOffset + ]); + const inst = new SLoad(/*indirect=*/ 0x01, /*slotOffset=*/ 0x12345678, /*dstOffset=*/ 0xa2345678); - const a = new Field(1n); - const b = new Field(2n); - - machineState.memory.set(0, a); - machineState.memory.set(1, b); - - const instruction = () => new SStore(0, 1).execute(machineState, journal); - await expect(instruction()).rejects.toThrowError(StaticCallStorageAlterError); - }); + expect(SLoad.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); - it('Sload should Read into storage', async () => { - // Mock response - const expectedResult = new Fr(1n); - journal.readStorage.mockReturnValueOnce(Promise.resolve(expectedResult)); + it('Sload should Read into storage', async () => { + // Mock response + const expectedResult = new Fr(1n); + journal.readStorage.mockReturnValueOnce(Promise.resolve(expectedResult)); - const a = new Field(1n); - const b = new Field(2n); + const a = new Field(1n); + const b = new Field(2n); - machineState.memory.set(0, a); - machineState.memory.set(1, b); + machineState.memory.set(0, a); + machineState.memory.set(1, b); - await new SLoad(0, 1).execute(machineState, journal); + await new SLoad(/*indirect=*/ 0, /*slotOffset=*/ 0, /*dstOffset=*/ 1).execute(machineState, journal); - expect(journal.readStorage).toBeCalledWith(address, new Fr(a.toBigInt())); + expect(journal.readStorage).toHaveBeenCalledWith(address, new Fr(a.toBigInt())); - const actual = machineState.memory.get(1); - expect(actual).toEqual(new Field(expectedResult)); + const actual = machineState.memory.get(1); + expect(actual).toEqual(new Field(expectedResult)); + }); }); }); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/storage.ts b/yarn-project/acir-simulator/src/avm/opcodes/storage.ts index c226522b3db..5cd9bee9ceb 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/storage.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/storage.ts @@ -2,25 +2,39 @@ import { Fr } from '@aztec/foundation/fields'; import { AvmMachineState } from '../avm_machine_state.js'; import { Field } from '../avm_memory_types.js'; -import { AvmInterpreterError } from '../interpreter/interpreter.js'; import { AvmJournal } from '../journal/journal.js'; -import { Instruction } from './instruction.js'; +import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; +import { Instruction, InstructionExecutionError } from './instruction.js'; -export class SStore extends Instruction { - static type: string = 'SSTORE'; - static numberOfOperands = 2; +abstract class BaseStorageInstruction extends Instruction { + // Informs (de)serialization. See Instruction.deserialize. + public static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + ]; - constructor(private slotOffset: number, private dataOffset: number) { + constructor(protected indirect: number, protected aOffset: number, protected bOffset: number) { super(); } +} + +export class SStore extends BaseStorageInstruction { + static readonly type: string = 'SSTORE'; + static readonly opcode = Opcode.SSTORE; + + constructor(indirect: number, srcOffset: number, slotOffset: number) { + super(indirect, srcOffset, slotOffset); + } async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { if (machineState.executionEnvironment.isStaticCall) { throw new StaticCallStorageAlterError(); } - const slot = machineState.memory.get(this.slotOffset); - const data = machineState.memory.get(this.dataOffset); + const slot = machineState.memory.get(this.aOffset); + const data = machineState.memory.get(this.bOffset); journal.writeStorage( machineState.executionEnvironment.storageAddress, @@ -32,23 +46,23 @@ export class SStore extends Instruction { } } -export class SLoad extends Instruction { - static type: string = 'SLOAD'; - static numberOfOperands = 2; +export class SLoad extends BaseStorageInstruction { + static readonly type: string = 'SLOAD'; + static readonly opcode = Opcode.SLOAD; - constructor(private slotOffset: number, private dstOffset: number) { - super(); + constructor(indirect: number, slotOffset: number, dstOffset: number) { + super(indirect, slotOffset, dstOffset); } async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { - const slot = machineState.memory.get(this.slotOffset); + const slot = machineState.memory.get(this.aOffset); const data: Fr = await journal.readStorage( machineState.executionEnvironment.storageAddress, new Fr(slot.toBigInt()), ); - machineState.memory.set(this.dstOffset, new Field(data)); + machineState.memory.set(this.bOffset, new Field(data)); this.incrementPc(machineState); } @@ -57,7 +71,7 @@ export class SLoad extends Instruction { /** * Error is thrown when a static call attempts to alter storage */ -export class StaticCallStorageAlterError extends AvmInterpreterError { +export class StaticCallStorageAlterError extends InstructionExecutionError { constructor() { super('Static calls cannot alter storage'); this.name = 'StaticCallStorageAlterError'; diff --git a/yarn-project/acir-simulator/src/avm/serialization/buffer_cursor.ts b/yarn-project/acir-simulator/src/avm/serialization/buffer_cursor.ts new file mode 100644 index 00000000000..237d9ae24f1 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/serialization/buffer_cursor.ts @@ -0,0 +1,109 @@ +import { strict as assert } from 'assert'; + +/* + * A Buffer-like class that automatically advances the position. + */ +export class BufferCursor { + constructor(private _buffer: Buffer, private _position: number = 0) {} + + public position(): number { + return this._position; + } + + public eof(): boolean { + return this._position === this._buffer.length; + } + + public bufferAtPosition(): Buffer { + return this._buffer.subarray(this._position); + } + + public advance(n: number): void { + this._position += n; + assert(n < this._buffer.length); + } + + public readUint8(): number { + const ret = this._buffer.readUint8(this._position); + this._position += 1; + return ret; + } + + public readUint16LE(): number { + const ret = this._buffer.readUint16LE(this._position); + this._position += 2; + return ret; + } + + public readUint16BE(): number { + const ret = this._buffer.readUint16BE(this._position); + this._position += 2; + return ret; + } + + public readUint32LE(): number { + const ret = this._buffer.readUint32LE(this._position); + this._position += 4; + return ret; + } + + public readUint32BE(): number { + const ret = this._buffer.readUint32BE(this._position); + this._position += 4; + return ret; + } + + public readBigInt64LE(): bigint { + const ret = this._buffer.readBigInt64LE(this._position); + this._position += 8; + return ret; + } + + public readBigInt64BE(): bigint { + const ret = this._buffer.readBigInt64BE(this._position); + this._position += 8; + return ret; + } + + public writeUint8(v: number) { + const ret = this._buffer.writeUint8(v, this._position); + this._position += 1; + return ret; + } + + public writeUint16LE(v: number) { + const ret = this._buffer.writeUint16LE(v, this._position); + this._position += 2; + return ret; + } + + public writeUint16BE(v: number) { + const ret = this._buffer.writeUint16BE(v, this._position); + this._position += 2; + return ret; + } + + public writeUint32LE(v: number) { + const ret = this._buffer.writeUint32LE(v, this._position); + this._position += 4; + return ret; + } + + public writeUint32BE(v: number) { + const ret = this._buffer.writeUint32BE(v, this._position); + this._position += 4; + return ret; + } + + public writeBigInt64LE(v: bigint) { + const ret = this._buffer.writeBigInt64LE(v, this._position); + this._position += 8; + return ret; + } + + public writeBigInt64BE(v: bigint) { + const ret = this._buffer.writeBigInt64BE(v, this._position); + this._position += 8; + return ret; + } +} diff --git a/yarn-project/acir-simulator/src/avm/serialization/bytecode_serialization.test.ts b/yarn-project/acir-simulator/src/avm/serialization/bytecode_serialization.test.ts new file mode 100644 index 00000000000..adca61ef609 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/serialization/bytecode_serialization.test.ts @@ -0,0 +1,98 @@ +import { strict as assert } from 'assert'; + +import { Add, Address, Sub } from '../opcodes/index.js'; +import { BufferCursor } from './buffer_cursor.js'; +import { InstructionSet, decodeFromBytecode, encodeToBytecode } from './bytecode_serialization.js'; +import { Opcode } from './instruction_serialization.js'; + +class InstA { + constructor(private n: number) {} + static readonly opcode: number = 1; + + // Expects opcode. + public static deserialize(buf: BufferCursor): InstA { + const opcode: number = buf.readUint8(); + assert(opcode == InstA.opcode); + return new InstA(buf.readUint16BE()); + } + + // Includes opcode. + public serialize(): Buffer { + const buf = Buffer.alloc(1 + 2); + buf.writeUint8(InstA.opcode); + buf.writeUint16BE(this.n, 1); + return buf; + } +} + +class InstB { + constructor(private n: bigint) {} + static readonly opcode: number = 2; + + // Expects opcode. + public static deserialize(buf: BufferCursor): InstB { + const opcode: number = buf.readUint8(); + assert(opcode == InstB.opcode); + return new InstB(buf.readBigInt64BE()); + } + + // Includes opcode. + public serialize(): Buffer { + const buf = Buffer.alloc(1 + 8); + buf.writeUint8(InstB.opcode); + buf.writeBigInt64BE(this.n, 1); + return buf; + } +} + +describe('Bytecode Serialization', () => { + it('Should deserialize using instruction set', () => { + const instructionSet: InstructionSet = new Map([ + [InstA.opcode, InstA], + [InstB.opcode, InstB], + ]); + const a = new InstA(0x1234); + const b = new InstB(0x5678n); + const bytecode = Buffer.concat([a.serialize(), b.serialize()]); + + const actual = decodeFromBytecode(bytecode, instructionSet); + + expect(actual).toEqual([a, b]); + }); + + it('Should serialize using instruction.serialize()', () => { + const a = new InstA(1234); + const b = new InstB(5678n); + + const actual = encodeToBytecode([a, b]); + + const expected = Buffer.concat([a.serialize(), b.serialize()]); + expect(actual).toEqual(expected); + }); + + it('Should deserialize real instructions', () => { + const instructions = [ + new Add(/*indirect=*/ 0, /*inTag=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2), + new Sub(/*indirect=*/ 0, /*inTag=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2), + new Address(/*indirect=*/ 0, /*dstOffset=*/ 1), + ]; + const bytecode = Buffer.concat(instructions.map(i => i.serialize())); + + const actual = decodeFromBytecode(bytecode); + + expect(actual).toEqual(instructions); + }); + + it('Should serialize real instructions', () => { + const instructions = [ + new Add(/*indirect=*/ 0, /*inTag=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2), + new Sub(/*indirect=*/ 0, /*inTag=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2), + new Address(/*indirect=*/ 0, /*dstOffset=*/ 1), + ]; + + const actual = encodeToBytecode(instructions); + + const expected = Buffer.concat(instructions.map(i => i.serialize())); + expect(actual).toEqual(expected); + }); +}); diff --git a/yarn-project/acir-simulator/src/avm/serialization/bytecode_serialization.ts b/yarn-project/acir-simulator/src/avm/serialization/bytecode_serialization.ts new file mode 100644 index 00000000000..b0285018980 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/serialization/bytecode_serialization.ts @@ -0,0 +1,165 @@ +import { + Add, + Address, + And, + BlockNumber, + CMov, // Call, + CalldataCopy, + Cast, + ChainId, + Div, + EmitNoteHash, + EmitNullifier, + EmitUnencryptedLog, + Eq, + FeePerDAGas, + FeePerL1Gas, + FeePerL2Gas, + InternalCall, + InternalReturn, + Jump, + JumpI, + Lt, + Lte, + Mov, + Mul, + Not, + Or, + Origin, + Portal, + Return, + Revert, + SLoad, + SStore, + SendL2ToL1Message, + Sender, + Set, + Shl, + Shr, // StaticCall, + StorageAddress, + Sub, + Timestamp, + Version, + Xor, +} from '../opcodes/index.js'; +import { Instruction } from '../opcodes/instruction.js'; +import { BufferCursor } from './buffer_cursor.js'; +import { Opcode } from './instruction_serialization.js'; + +interface DeserializableInstruction { + deserialize(buf: BufferCursor | Buffer): Instruction; + opcode: Opcode; +} + +export type InstructionSet = Map; +const INSTRUCTION_SET: InstructionSet = new Map( + [ + [Add.opcode, Add], + [Sub.opcode, Sub], + [Sub.opcode, Sub], + [Mul.opcode, Mul], + [Div.opcode, Div], + [Eq.opcode, Eq], + [Lt.opcode, Lt], + [Lte.opcode, Lte], + [And.opcode, And], + [Or.opcode, Or], + [Xor.opcode, Xor], + [Not.opcode, Not], + [Shl.opcode, Shl], + [Shr.opcode, Shr], + [Cast.opcode, Cast], + [Address.opcode, Address], + [StorageAddress.opcode, StorageAddress], + [Origin.opcode, Origin], + [Sender.opcode, Sender], + [Portal.opcode, Portal], + [FeePerL1Gas.opcode, FeePerL1Gas], + [FeePerL2Gas.opcode, FeePerL2Gas], + [FeePerDAGas.opcode, FeePerDAGas], + // [Contractcalldepth.opcode, Contractcalldepth], + // Execution Environment - Globals + [ChainId.opcode, ChainId], + [Version.opcode, Version], + [BlockNumber.opcode, BlockNumber], + [Timestamp.opcode, Timestamp], + // [Coinbase.opcode, Coinbase], + // [Blockl1gaslimit.opcode, Blockl1gaslimit], + // [Blockl2gaslimit.opcode, Blockl2gaslimit], + // [Blockdagaslimit.opcode, Blockdagaslimit], + // // Execution Environment - Calldata + [CalldataCopy.opcode, CalldataCopy], + + // //// Machine State + // // Machine State - Gas + // //[L1gasleft.opcode, L1gasleft], + // //[L2gasleft.opcode, L2gasleft], + // //[Dagasleft.opcode, Dagasleft], + // //// Machine State - Internal Control Flow + [Jump.opcode, Jump], + [JumpI.opcode, JumpI], + [InternalCall.opcode, InternalCall], + [InternalReturn.opcode, InternalReturn], + [Set.opcode, Set], + [Mov.opcode, Mov], + [CMov.opcode, CMov], + + // //// World State + // //[Blockheaderbynumber.opcode, Blockheaderbynumber], + [SLoad.opcode, SLoad], // Public Storage + [SStore.opcode, SStore], // Public Storage + // //[Readl1tol2msg.opcode, Readl1tol2msg], // Messages + [SendL2ToL1Message.opcode, SendL2ToL1Message], // Messages + [EmitNoteHash.opcode, EmitNoteHash], // Notes & Nullifiers + [EmitNullifier.opcode, EmitNullifier], // Notes & Nullifiers + + // //// Accrued Substate + [EmitUnencryptedLog.opcode, EmitUnencryptedLog], + + // //// Control Flow - Contract Calls + // [Call.opcode, Call], + // [StaticCall.opcode, StaticCall], + [Return.opcode, Return], + [Revert.opcode, Revert], + + // //// Gadgets + // //[Keccak.opcode, Keccak], + // //[Poseidon.opcode, Poseidon], + ], //), +); + +interface Serializable { + serialize(): Buffer; +} + +/** + * Serializes an array of instructions to bytecode. + */ +export function encodeToBytecode(instructions: Serializable[]): Buffer { + return Buffer.concat(instructions.map(i => i.serialize())); +} + +/** + * Convert a buffer of bytecode into an array of instructions. + * @param bytecode Buffer of bytecode. + * @param instructionSet Optional {@code InstructionSet} to be used for deserialization. + * @returns Bytecode decoded into an ordered array of Instructions + */ +export function decodeFromBytecode(bytecode: Buffer, instructionSet: InstructionSet = INSTRUCTION_SET): Instruction[] { + const instructions: Instruction[] = []; + const cursor = new BufferCursor(bytecode); + + while (!cursor.eof()) { + const opcode: Opcode = cursor.bufferAtPosition().readUint8(); // peek. + const instructionDeserializerOrUndef = instructionSet.get(opcode); + if (instructionDeserializerOrUndef === undefined) { + throw new Error(`Opcode 0x${opcode.toString(16)} not implemented`); + } + + const instructionDeserializer: DeserializableInstruction = instructionDeserializerOrUndef; + const i: Instruction = instructionDeserializer.deserialize(cursor); + instructions.push(i); + } + + return instructions; +} diff --git a/yarn-project/acir-simulator/src/avm/serialization/instruction_serialization.test.ts b/yarn-project/acir-simulator/src/avm/serialization/instruction_serialization.test.ts new file mode 100644 index 00000000000..3f7d8905c98 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/serialization/instruction_serialization.test.ts @@ -0,0 +1,70 @@ +import { BufferCursor } from './buffer_cursor.js'; +import { OperandType, deserialize, serialize } from './instruction_serialization.js'; + +class InstA { + constructor(private a: number, private b: number, private c: number, private d: bigint, private e: bigint) {} + + static readonly opcode: number = 1; + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT16, + OperandType.UINT32, + OperandType.UINT64, + OperandType.UINT128, + ]; +} + +describe('Instruction Serialization', () => { + it('Should serialize all types from OperandType[]', () => { + const instance = new InstA(0x12, 0x1234, 0x12345678, 0x1234567887654321n, 0x1234567887654321abcdef0000fedcban); + const actual: Buffer = serialize(InstA.wireFormat, instance); + + expect(actual).toEqual( + Buffer.from( + [ + // opcode + '01', + // a + '12', + // b + '1234', + // c + '12345678', + // d + '1234567887654321', + // e + '1234567887654321ABCDEF0000FEDCBA', + ].join(''), + 'hex', + ), + ); + }); + + it('Should deserialize all types from OperandType[]', () => { + const buffer = Buffer.from( + [ + // opcode + '01', + // a + '12', + // b + '1234', + // c + '12345678', + // d + '1234567887654321', + // e + '1234567887654321ABCDEF0000FEDCBA', + ].join(''), + 'hex', + ); + + const deserializedParams = deserialize(new BufferCursor(buffer), InstA.wireFormat); + const params = deserializedParams.slice(1) as ConstructorParameters; // Drop opcode. + + const actual = new InstA(...params); + const expected = new InstA(0x12, 0x1234, 0x12345678, 0x1234567887654321n, 0x1234567887654321abcdef0000fedcban); + expect(actual).toEqual(expected); + }); +}); diff --git a/yarn-project/acir-simulator/src/avm/serialization/instruction_serialization.ts b/yarn-project/acir-simulator/src/avm/serialization/instruction_serialization.ts new file mode 100644 index 00000000000..0cba3caef37 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/serialization/instruction_serialization.ts @@ -0,0 +1,162 @@ +import { strict as assert } from 'assert'; + +import { BufferCursor } from './buffer_cursor.js'; + +/** + * All AVM opcodes. (Keep in sync with cpp counterpart code AvmMini_opcode.hpp). + * Source: https://yp-aztec.netlify.app/docs/public-vm/instruction-set + */ +export enum Opcode { + ADD, + SUB, + MUL, + DIV, + EQ, + LT, + LTE, + AND, + OR, + XOR, + NOT, + SHL, + SHR, + CAST, + ADDRESS, + STORAGEADDRESS, + ORIGIN, + SENDER, + PORTAL, + FEEPERL1GAS, + FEEPERL2GAS, + FEEPERDAGAS, + CONTRACTCALLDEPTH, + CHAINID, + VERSION, + BLOCKNUMBER, + TIMESTAMP, + COINBASE, + BLOCKL1GASLIMIT, + BLOCKL2GASLIMIT, + BLOCKDAGASLIMIT, + CALLDATACOPY, + L1GASLEFT, + L2GASLEFT, + DAGASLEFT, + JUMP, + JUMPI, + INTERNALCALL, + INTERNALRETURN, + SET, + MOV, + CMOV, + BLOCKHEADERBYNUMBER, + SLOAD, // Public Storage + SSTORE, // Public Storage + READL1TOL2MSG, // Messages + SENDL2TOL1MSG, // Messages + EMITNOTEHASH, // Notes & Nullifiers + EMITNULLIFIER, // Notes & Nullifiers + EMITUNENCRYPTEDLOG, + CALL, + STATICCALL, + RETURN, + REVERT, + KECCAK, + POSEIDON, + // Add new opcodes before this + TOTAL_OPCODES_NUMBER, +} + +// Possible types for an instruction's operand in its wire format. +export enum OperandType { + UINT8, + UINT16, + UINT32, + UINT64, + UINT128, +} + +type OperandNativeType = number | bigint; +type OperandWriter = (value: any) => void; + +// Specifies how to read and write each operand type. +const OPERAND_SPEC = new Map OperandNativeType, OperandWriter]>([ + [OperandType.UINT8, [1, Buffer.prototype.readUint8, Buffer.prototype.writeUint8]], + [OperandType.UINT16, [2, Buffer.prototype.readUint16BE, Buffer.prototype.writeUint16BE]], + [OperandType.UINT32, [4, Buffer.prototype.readUint32BE, Buffer.prototype.writeUint32BE]], + [OperandType.UINT64, [8, Buffer.prototype.readBigInt64BE, Buffer.prototype.writeBigInt64BE]], + [OperandType.UINT128, [16, readBigInt128BE, writeBigInt128BE]], +]); + +function readBigInt128BE(this: Buffer): bigint { + const totalBytes = 16; + let ret: bigint = 0n; + for (let i = 0; i < totalBytes; ++i) { + ret <<= 8n; + ret |= BigInt(this.readUint8(i)); + } + return ret; +} + +function writeBigInt128BE(this: Buffer, value: bigint): void { + const totalBytes = 16; + for (let offset = totalBytes - 1; offset >= 0; --offset) { + this.writeUint8(Number(value & 0xffn), offset); + value >>= 8n; + } +} + +/** + * Reads an array of operands from a buffer. + * @param cursor Buffer to read from. Might be longer than needed. + * @param operands Specification of the operand types. + * @returns An array as big as {@code operands}, with the converted TS values. + */ +export function deserialize(cursor: BufferCursor | Buffer, operands: OperandType[]): (number | bigint)[] { + const argValues = []; + if (cursor instanceof Buffer) { + cursor = new BufferCursor(cursor); + } + + for (const op of operands) { + const opType = op; + const [sizeBytes, reader, _writer] = OPERAND_SPEC.get(opType)!; + argValues.push(reader.call(cursor.bufferAtPosition())); + cursor.advance(sizeBytes); + } + + return argValues; +} + +/** + * Serializes a class using the specified operand types. + * More specifically, this serializes {@code [cls.constructor.opcode, ...Object.values(cls)]}. + * Observe in particular that: + * (1) the first operand type specified must correspond to the opcode; + * (2) the rest of the operand types must be specified in the order returned by {@code Object.values()}. + * @param operands Type specification for the values to be serialized. + * @param cls The class to be serialized. + * @returns + */ +export function serialize(operands: OperandType[], cls: any): Buffer { + const chunks: Buffer[] = []; + + // TODO: infer opcode not in this loop + assert(cls.constructor.opcode !== undefined && cls.constructor.opcode !== null); + const rawClassValues: any[] = [cls.constructor.opcode, ...Object.values(cls)]; + assert( + rawClassValues.length === operands.length, + `Got ${rawClassValues.length} values but only ${operands.length} serialization operands are specified!`, + ); + const classValues = rawClassValues as OperandNativeType[]; + + for (let i = 0; i < operands.length; i++) { + const opType = operands[i]; + const [sizeBytes, _reader, writer] = OPERAND_SPEC.get(opType)!; + const buf = Buffer.alloc(sizeBytes); + writer.call(buf, classValues[i]); + chunks.push(buf); + } + + return Buffer.concat(chunks); +}