From 6374a328859eefed0346a3c12b3500dd960e0884 Mon Sep 17 00:00:00 2001 From: Ilyas Ridhuan Date: Fri, 10 May 2024 12:30:46 +0100 Subject: [PATCH] feat(avm-simulator): add to_radix_le instruction (#6308) --- avm-transpiler/src/opcodes.rs | 4 + .../barretenberg/vm/avm_trace/avm_opcode.hpp | 3 + .../public-vm/gen/_instruction-set.mdx | 185 +++++++++++------- .../InstructionSet/InstructionSet.js | 33 ++++ yarn-project/simulator/src/avm/avm_gas.ts | 2 + .../src/avm/opcodes/conversion.test.ts | 90 +++++++++ .../simulator/src/avm/opcodes/conversion.ts | 58 ++++++ .../instruction_serialization.ts | 2 + 8 files changed, 308 insertions(+), 69 deletions(-) create mode 100644 yarn-project/simulator/src/avm/opcodes/conversion.test.ts create mode 100644 yarn-project/simulator/src/avm/opcodes/conversion.ts diff --git a/avm-transpiler/src/opcodes.rs b/avm-transpiler/src/opcodes.rs index 2b63c8e987e..206325cfeff 100644 --- a/avm-transpiler/src/opcodes.rs +++ b/avm-transpiler/src/opcodes.rs @@ -69,6 +69,8 @@ pub enum AvmOpcode { POSEIDON2, SHA256, // temp - may be removed, but alot of contracts rely on it PEDERSEN, // temp - may be removed, but alot of contracts rely on it + // Conversions + TORADIXLE, } impl AvmOpcode { @@ -155,6 +157,8 @@ impl AvmOpcode { AvmOpcode::POSEIDON2 => "POSEIDON2", AvmOpcode::SHA256 => "SHA256 ", AvmOpcode::PEDERSEN => "PEDERSEN", + // Conversions + AvmOpcode::TORADIXLE => "TORADIXLE", } } } diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp index 2a4dd1138e9..21423838f43 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp @@ -96,6 +96,9 @@ enum class OpCode : uint8_t { KECCAK, POSEIDON2, + // Conversions + TORADIXLE, + // Sentinel LAST_OPCODE_SENTINEL, }; diff --git a/docs/docs/protocol-specs/public-vm/gen/_instruction-set.mdx b/docs/docs/protocol-specs/public-vm/gen/_instruction-set.mdx index a13fb19a0f5..1c275e77e92 100644 --- a/docs/docs/protocol-specs/public-vm/gen/_instruction-set.mdx +++ b/docs/docs/protocol-specs/public-vm/gen/_instruction-set.mdx @@ -154,98 +154,105 @@ Click on an instruction name to jump to its section. } - 0x14 [`CONTRACTCALLDEPTH`](#isa-section-contractcalldepth) + 0x14 [`TRANSACTIONFEE`](#isa-section-transactionfee) + Get the computed transaction fee during teardown phase, zero otherwise + { + `M[dstOffset] = context.environment.transactionFee` + } + + + 0x15 [`CONTRACTCALLDEPTH`](#isa-section-contractcalldepth) Get how many contract calls deep the current call context is { `M[dstOffset] = context.environment.contractCallDepth` } - 0x15 [`CHAINID`](#isa-section-chainid) + 0x16 [`CHAINID`](#isa-section-chainid) Get this rollup's L1 chain ID { `M[dstOffset] = context.environment.globals.chainId` } - 0x16 [`VERSION`](#isa-section-version) + 0x17 [`VERSION`](#isa-section-version) Get this rollup's L2 version ID { `M[dstOffset] = context.environment.globals.version` } - 0x17 [`BLOCKNUMBER`](#isa-section-blocknumber) + 0x18 [`BLOCKNUMBER`](#isa-section-blocknumber) Get this L2 block's number { `M[dstOffset] = context.environment.globals.blocknumber` } - 0x18 [`TIMESTAMP`](#isa-section-timestamp) + 0x19 [`TIMESTAMP`](#isa-section-timestamp) Get this L2 block's timestamp { `M[dstOffset] = context.environment.globals.timestamp` } - 0x19 [`COINBASE`](#isa-section-coinbase) + 0x1a [`COINBASE`](#isa-section-coinbase) Get the block's beneficiary address { `M[dstOffset] = context.environment.globals.coinbase` } - 0x1a [`BLOCKL2GASLIMIT`](#isa-section-blockl2gaslimit) + 0x1b [`BLOCKL2GASLIMIT`](#isa-section-blockl2gaslimit) Total amount of "L2 gas" that a block can consume { `M[dstOffset] = context.environment.globals.l2GasLimit` } - 0x1b [`BLOCKDAGASLIMIT`](#isa-section-blockdagaslimit) + 0x1c [`BLOCKDAGASLIMIT`](#isa-section-blockdagaslimit) Total amount of "DA gas" that a block can consume { `M[dstOffset] = context.environment.globals.daGasLimit` } - 0x1c [`CALLDATACOPY`](#isa-section-calldatacopy) + 0x1d [`CALLDATACOPY`](#isa-section-calldatacopy) Copy calldata into memory { `M[dstOffset:dstOffset+copySize] = context.environment.calldata[cdOffset:cdOffset+copySize]` } - 0x1d [`L2GASLEFT`](#isa-section-l2gasleft) + 0x1e [`L2GASLEFT`](#isa-section-l2gasleft) Remaining "L2 gas" for this call (after this instruction) { `M[dstOffset] = context.MachineState.l2GasLeft` } - 0x1e [`DAGASLEFT`](#isa-section-dagasleft) + 0x1f [`DAGASLEFT`](#isa-section-dagasleft) Remaining "DA gas" for this call (after this instruction) { `M[dstOffset] = context.machineState.daGasLeft` } - 0x1f [`JUMP`](#isa-section-jump) + 0x20 [`JUMP`](#isa-section-jump) Jump to a location in the bytecode { `context.machineState.pc = loc` } - 0x20 [`JUMPI`](#isa-section-jumpi) + 0x21 [`JUMPI`](#isa-section-jumpi) Conditionally jump to a location in the bytecode { `context.machineState.pc = M[condOffset] > 0 ? loc : context.machineState.pc` } - 0x21 [`INTERNALCALL`](#isa-section-internalcall) + 0x22 [`INTERNALCALL`](#isa-section-internalcall) Make an internal call. Push the current PC to the internal call stack and jump to the target location. {`context.machineState.internalCallStack.push(context.machineState.pc) @@ -253,49 +260,49 @@ context.machineState.pc = loc`} - 0x22 [`INTERNALRETURN`](#isa-section-internalreturn) + 0x23 [`INTERNALRETURN`](#isa-section-internalreturn) Return from an internal call. Pop from the internal call stack and jump to the popped location. { `context.machineState.pc = context.machineState.internalCallStack.pop()` } - 0x23 [`SET`](#isa-section-set) + 0x24 [`SET`](#isa-section-set) Set a memory word from a constant in the bytecode { `M[dstOffset] = const` } - 0x24 [`MOV`](#isa-section-mov) + 0x25 [`MOV`](#isa-section-mov) Move a word from source memory location to destination { `M[dstOffset] = M[srcOffset]` } - 0x25 [`CMOV`](#isa-section-cmov) + 0x26 [`CMOV`](#isa-section-cmov) Move a word (conditionally chosen) from one memory location to another (`d = cond > 0 ? a : b`) { `M[dstOffset] = M[condOffset] > 0 ? M[aOffset] : M[bOffset]` } - 0x26 [`SLOAD`](#isa-section-sload) + 0x27 [`SLOAD`](#isa-section-sload) Load a word from this contract's persistent public storage. Zero is loaded for unwritten slots. {`M[dstOffset] = S[M[slotOffset]]`} - 0x27 [`SSTORE`](#isa-section-sstore) + 0x28 [`SSTORE`](#isa-section-sstore) Write a word to this contract's persistent public storage {`S[M[slotOffset]] = M[srcOffset]`} - 0x28 [`NOTEHASHEXISTS`](#isa-section-notehashexists) + 0x29 [`NOTEHASHEXISTS`](#isa-section-notehashexists) Check whether a note hash exists in the note hash tree (as of the start of the current block) {`exists = context.worldState.noteHashes.has({ @@ -306,7 +313,7 @@ M[existsOffset] = exists`} - 0x29 [`EMITNOTEHASH`](#isa-section-emitnotehash) + 0x2a [`EMITNOTEHASH`](#isa-section-emitnotehash) Emit a new note hash to be inserted into the note hash tree {`context.worldState.noteHashes.append( @@ -315,7 +322,7 @@ M[existsOffset] = exists`} - 0x2a [`NULLIFIEREXISTS`](#isa-section-nullifierexists) + 0x2b [`NULLIFIEREXISTS`](#isa-section-nullifierexists) Check whether a nullifier exists in the nullifier tree (including nullifiers from earlier in the current transaction or from earlier in the current block) {`exists = pendingNullifiers.has(M[addressOffset], M[nullifierOffset]) || context.worldState.nullifiers.has( @@ -325,7 +332,7 @@ M[existsOffset] = exists`} - 0x2b [`EMITNULLIFIER`](#isa-section-emitnullifier) + 0x2c [`EMITNULLIFIER`](#isa-section-emitnullifier) Emit a new nullifier to be inserted into the nullifier tree {`context.worldState.nullifiers.append( @@ -334,7 +341,7 @@ M[existsOffset] = exists`} - 0x2c [`L1TOL2MSGEXISTS`](#isa-section-l1tol2msgexists) + 0x2d [`L1TOL2MSGEXISTS`](#isa-section-l1tol2msgexists) Check if a message exists in the L1-to-L2 message tree {`exists = context.worldState.l1ToL2Messages.has({ @@ -344,7 +351,7 @@ M[existsOffset] = exists`} - 0x2d [`HEADERMEMBER`](#isa-section-headermember) + 0x2e [`HEADERMEMBER`](#isa-section-headermember) Check if a header exists in the [archive tree](../state/archive) and retrieve the specified member if so {`exists = context.worldState.header.has({ @@ -357,7 +364,7 @@ if exists: - 0x2e [`GETCONTRACTINSTANCE`](#isa-section-getcontractinstance) + 0x2f [`GETCONTRACTINSTANCE`](#isa-section-getcontractinstance) Copies contract instance data to memory {`M[dstOffset:dstOffset+CONTRACT_INSTANCE_SIZE+1] = [ @@ -372,7 +379,7 @@ if exists: - 0x2f [`EMITUNENCRYPTEDLOG`](#isa-section-emitunencryptedlog) + 0x30 [`EMITUNENCRYPTEDLOG`](#isa-section-emitunencryptedlog) Emit an unencrypted log {`context.accruedSubstate.unencryptedLogs.append( @@ -385,7 +392,7 @@ if exists: - 0x30 [`SENDL2TOL1MSG`](#isa-section-sendl2tol1msg) + 0x31 [`SENDL2TOL1MSG`](#isa-section-sendl2tol1msg) Send an L2-to-L1 message {`context.accruedSubstate.sentL2ToL1Messages.append( @@ -398,7 +405,7 @@ if exists: - 0x31 [`CALL`](#isa-section-call) + 0x32 [`CALL`](#isa-section-call) Call into another contract {`// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } @@ -412,7 +419,7 @@ updateContextAfterNestedCall(context, instr.args, nestedContext)`} - 0x32 [`STATICCALL`](#isa-section-staticcall) + 0x33 [`STATICCALL`](#isa-section-staticcall) Call into another contract, disallowing World State and Accrued Substate modifications {`// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } @@ -426,7 +433,7 @@ updateContextAfterNestedCall(context, instr.args, nestedContext)`} - 0x33 [`DELEGATECALL`](#isa-section-delegatecall) + 0x34 [`DELEGATECALL`](#isa-section-delegatecall) Call into another contract, but keep the caller's `sender` and `storageAddress` {`// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } @@ -440,7 +447,7 @@ updateContextAfterNestedCall(context, instr.args, nestedContext)`} - 0x34 [`RETURN`](#isa-section-return) + 0x35 [`RETURN`](#isa-section-return) Halt execution within this context (without revert), optionally returning some data {`context.contractCallResults.output = M[retOffset:retOffset+retSize] @@ -448,7 +455,7 @@ halt`} - 0x35 [`REVERT`](#isa-section-revert) + 0x36 [`REVERT`](#isa-section-revert) Halt execution within this context as `reverted`, optionally returning some data {`context.contractCallResults.output = M[retOffset:retOffset+retSize] @@ -456,6 +463,11 @@ context.contractCallResults.reverted = true halt`} + + 0x37 [`TORADIXLE`](#isa-section-to_radix_le) + Convert a word to an array of limbs in little-endian radix form + TBD: Storage of limbs and if T[dstOffset] is constrained to U8 + @@ -862,12 +874,28 @@ Get the fee to be paid per "DA gas" - constant for entire transaction [![](/img/protocol-specs/public-vm/bit-formats/FEEPERDAGAS.png)](/img/protocol-specs/public-vm/bit-formats/FEEPERDAGAS.png) +### `TRANSACTIONFEE` +Get the computed transaction fee during teardown phase, zero otherwise + +[See in table.](#isa-table-transactionfee) + +- **Opcode**: 0x14 +- **Category**: Execution Environment +- **Flags**: + - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. +- **Args**: + - **dstOffset**: memory offset specifying where to store operation's result +- **Expression**: `M[dstOffset] = context.environment.transactionFee` +- **Tag updates**: `T[dstOffset] = u32` +- **Bit-size**: 56 + + ### `CONTRACTCALLDEPTH` Get how many contract calls deep the current call context is [See in table.](#isa-table-contractcalldepth) -- **Opcode**: 0x14 +- **Opcode**: 0x15 - **Category**: Execution Environment - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -885,7 +913,7 @@ Get this rollup's L1 chain ID [See in table.](#isa-table-chainid) -- **Opcode**: 0x15 +- **Opcode**: 0x16 - **Category**: Execution Environment - Globals - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -902,7 +930,7 @@ Get this rollup's L2 version ID [See in table.](#isa-table-version) -- **Opcode**: 0x16 +- **Opcode**: 0x17 - **Category**: Execution Environment - Globals - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -919,7 +947,7 @@ Get this L2 block's number [See in table.](#isa-table-blocknumber) -- **Opcode**: 0x17 +- **Opcode**: 0x18 - **Category**: Execution Environment - Globals - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -936,7 +964,7 @@ Get this L2 block's timestamp [See in table.](#isa-table-timestamp) -- **Opcode**: 0x18 +- **Opcode**: 0x19 - **Category**: Execution Environment - Globals - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -953,7 +981,7 @@ Get the block's beneficiary address [See in table.](#isa-table-coinbase) -- **Opcode**: 0x19 +- **Opcode**: 0x1a - **Category**: Execution Environment - Globals - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -970,7 +998,7 @@ Total amount of "L2 gas" that a block can consume [See in table.](#isa-table-blockl2gaslimit) -- **Opcode**: 0x1a +- **Opcode**: 0x1b - **Category**: Execution Environment - Globals - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -987,7 +1015,7 @@ Total amount of "DA gas" that a block can consume [See in table.](#isa-table-blockdagaslimit) -- **Opcode**: 0x1b +- **Opcode**: 0x1c - **Category**: Execution Environment - Globals - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1004,7 +1032,7 @@ Copy calldata into memory [See in table.](#isa-table-calldatacopy) -- **Opcode**: 0x1c +- **Opcode**: 0x1d - **Category**: Execution Environment - Calldata - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1024,7 +1052,7 @@ Remaining "L2 gas" for this call (after this instruction) [See in table.](#isa-table-l2gasleft) -- **Opcode**: 0x1d +- **Opcode**: 0x1e - **Category**: Machine State - Gas - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1041,7 +1069,7 @@ Remaining "DA gas" for this call (after this instruction) [See in table.](#isa-table-dagasleft) -- **Opcode**: 0x1e +- **Opcode**: 0x1f - **Category**: Machine State - Gas - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1058,7 +1086,7 @@ Jump to a location in the bytecode [See in table.](#isa-table-jump) -- **Opcode**: 0x1f +- **Opcode**: 0x20 - **Category**: Machine State - Control Flow - **Args**: - **loc**: target location to jump to @@ -1073,7 +1101,7 @@ Conditionally jump to a location in the bytecode [See in table.](#isa-table-jumpi) -- **Opcode**: 0x20 +- **Opcode**: 0x21 - **Category**: Machine State - Control Flow - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1091,7 +1119,7 @@ Make an internal call. Push the current PC to the internal call stack and jump t [See in table.](#isa-table-internalcall) -- **Opcode**: 0x21 +- **Opcode**: 0x22 - **Category**: Machine State - Control Flow - **Args**: - **loc**: target location to jump/call to @@ -1103,14 +1131,13 @@ context.machineState.pc = loc`} - **Details**: Target location is an immediate value (a constant in the bytecode). - **Bit-size**: 48 -[![](/img/protocol-specs/public-vm/bit-formats/INTERNALCALL.png)](/img/protocol-specs/public-vm/bit-formats/INTERNALCALL.png) ### `INTERNALRETURN` Return from an internal call. Pop from the internal call stack and jump to the popped location. [See in table.](#isa-table-internalreturn) -- **Opcode**: 0x22 +- **Opcode**: 0x23 - **Category**: Machine State - Control Flow - **Expression**: `context.machineState.pc = context.machineState.internalCallStack.pop()` - **Bit-size**: 16 @@ -1122,7 +1149,7 @@ Set a memory word from a constant in the bytecode [See in table.](#isa-table-set) -- **Opcode**: 0x23 +- **Opcode**: 0x24 - **Category**: Machine State - Memory - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1142,7 +1169,7 @@ Move a word from source memory location to destination [See in table.](#isa-table-mov) -- **Opcode**: 0x24 +- **Opcode**: 0x25 - **Category**: Machine State - Memory - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1160,7 +1187,7 @@ Move a word (conditionally chosen) from one memory location to another (`d = con [See in table.](#isa-table-cmov) -- **Opcode**: 0x25 +- **Opcode**: 0x26 - **Category**: Machine State - Memory - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1181,7 +1208,7 @@ Load a word from this contract's persistent public storage. Zero is loaded for u [See in table.](#isa-table-sload) -- **Opcode**: 0x26 +- **Opcode**: 0x27 - **Category**: World State - Public Storage - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1226,7 +1253,7 @@ Write a word to this contract's persistent public storage [See in table.](#isa-table-sstore) -- **Opcode**: 0x27 +- **Opcode**: 0x28 - **Category**: World State - Public Storage - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1266,7 +1293,7 @@ Check whether a note hash exists in the note hash tree (as of the start of the c [See in table.](#isa-table-notehashexists) -- **Opcode**: 0x28 +- **Opcode**: 0x29 - **Category**: World State - Notes & Nullifiers - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1304,7 +1331,7 @@ Emit a new note hash to be inserted into the note hash tree [See in table.](#isa-table-emitnotehash) -- **Opcode**: 0x29 +- **Opcode**: 0x2a - **Category**: World State - Notes & Nullifiers - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1336,7 +1363,7 @@ Check whether a nullifier exists in the nullifier tree (including nullifiers fro [See in table.](#isa-table-nullifierexists) -- **Opcode**: 0x2a +- **Opcode**: 0x2b - **Category**: World State - Notes & Nullifiers - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1373,7 +1400,7 @@ Emit a new nullifier to be inserted into the nullifier tree [See in table.](#isa-table-emitnullifier) -- **Opcode**: 0x2b +- **Opcode**: 0x2c - **Category**: World State - Notes & Nullifiers - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1405,7 +1432,7 @@ Check if a message exists in the L1-to-L2 message tree [See in table.](#isa-table-l1tol2msgexists) -- **Opcode**: 0x2c +- **Opcode**: 0x2d - **Category**: World State - Messaging - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1444,7 +1471,7 @@ Check if a header exists in the [archive tree](../state/archive) and retrieve th [See in table.](#isa-table-headermember) -- **Opcode**: 0x2d +- **Opcode**: 0x2e - **Category**: World State - Archive Tree & Headers - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1487,7 +1514,7 @@ Copies contract instance data to memory [See in table.](#isa-table-getcontractinstance) -- **Opcode**: 0x2e +- **Opcode**: 0x2f - **Category**: Other - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1517,7 +1544,7 @@ Emit an unencrypted log [See in table.](#isa-table-emitunencryptedlog) -- **Opcode**: 0x2f +- **Opcode**: 0x30 - **Category**: Accrued Substate - Logging - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1544,7 +1571,7 @@ Send an L2-to-L1 message [See in table.](#isa-table-sendl2tol1msg) -- **Opcode**: 0x30 +- **Opcode**: 0x31 - **Category**: Accrued Substate - Messaging - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1570,7 +1597,7 @@ Call into another contract [See in table.](#isa-table-call) -- **Opcode**: 0x31 +- **Opcode**: 0x32 - **Category**: Control Flow - Contract Calls - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1617,7 +1644,7 @@ Call into another contract, disallowing World State and Accrued Substate modific [See in table.](#isa-table-staticcall) -- **Opcode**: 0x32 +- **Opcode**: 0x33 - **Category**: Control Flow - Contract Calls - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1661,7 +1688,7 @@ Call into another contract, but keep the caller's `sender` and `storageAddress` [See in table.](#isa-table-delegatecall) -- **Opcode**: 0x33 +- **Opcode**: 0x34 - **Category**: Control Flow - Contract Calls - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1705,7 +1732,7 @@ Halt execution within this context (without revert), optionally returning some d [See in table.](#isa-table-return) -- **Opcode**: 0x34 +- **Opcode**: 0x35 - **Category**: Control Flow - Contract Calls - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1727,7 +1754,7 @@ Halt execution within this context as `reverted`, optionally returning some data [See in table.](#isa-table-revert) -- **Opcode**: 0x35 +- **Opcode**: 0x36 - **Category**: Control Flow - Contract Calls - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1744,3 +1771,23 @@ halt`} - **Bit-size**: 88 [![](/img/protocol-specs/public-vm/bit-formats/REVERT.png)](/img/protocol-specs/public-vm/bit-formats/REVERT.png) + +### `TORADIXLE` +Convert a word to an array of limbs in little-endian radix form + +[See in table.](#isa-table-to_radix_le) + +- **Opcode**: 0x37 +- **Category**: Conversions +- **Flags**: + - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. +- **Args**: + - **srcOffset**: memory offset of word to convert. + - **dstOffset**: memory offset specifying where the first limb of the radix-conversion result is stored. + - **radix**: the maximum bit-size of each limb. + - **numLimbs**: the number of limbs the word will be converted into. +- **Expression**: TBD: Storage of limbs and if T[dstOffset] is constrained to U8 +- **Details**: The limbs will be stored in a contiguous memory block starting at `dstOffset`. +- **Tag checks**: `T[srcOffset] == field` +- **Bit-size**: 152 + diff --git a/docs/src/preprocess/InstructionSet/InstructionSet.js b/docs/src/preprocess/InstructionSet/InstructionSet.js index a94e3b933ab..fac2b3e0239 100644 --- a/docs/src/preprocess/InstructionSet/InstructionSet.js +++ b/docs/src/preprocess/InstructionSet/InstructionSet.js @@ -1537,6 +1537,39 @@ halt "Tag checks": "", "Tag updates": "", }, + { + id: "to_radix_le", + Name: "`TORADIXLE`", + Category: "Conversions", + Flags: [{ name: "indirect", description: INDIRECT_FLAG_DESCRIPTION }], + Args: [ + { + name: "srcOffset", + description: "memory offset of word to convert.", + }, + { + name: "dstOffset", + description: "memory offset specifying where the first limb of the radix-conversion result is stored.", + }, + { + name: "radix", + description: "the maximum bit-size of each limb.", + mode: "immediate", + type: "u32", + }, + { + name: "numLimbs", + description: "the number of limbs the word will be converted into.", + type: "u32", + mode: "immediate", + } + ], + + Expression: `TBD: Storage of limbs and if T[dstOffset] is constrained to U8`, + Summary: "Convert a word to an array of limbs in little-endian radix form", + Details: "The limbs will be stored in a contiguous memory block starting at `dstOffset`.", + "Tag checks": "`T[srcOffset] == field`", + } ]; const INSTRUCTION_SET = INSTRUCTION_SET_RAW.map((instr) => { instr["Bit-size"] = instructionSize(instr); diff --git a/yarn-project/simulator/src/avm/avm_gas.ts b/yarn-project/simulator/src/avm/avm_gas.ts index 8f140ed03e1..b16b171212f 100644 --- a/yarn-project/simulator/src/avm/avm_gas.ts +++ b/yarn-project/simulator/src/avm/avm_gas.ts @@ -123,6 +123,8 @@ export const GasCosts: Record = { [Opcode.POSEIDON2]: TemporaryDefaultGasCost, [Opcode.SHA256]: TemporaryDefaultGasCost, // temp - may be removed, but alot of contracts rely on i: TemporaryDefaultGasCost, [Opcode.PEDERSEN]: TemporaryDefaultGasCost, // temp - may be removed, but alot of contracts rely on i: TemporaryDefaultGasCost,t + // Conversions + [Opcode.TORADIXLE]: TemporaryDefaultGasCost, }; /** Returns the fixed base gas cost for a given opcode, or throws if set to dynamic. */ diff --git a/yarn-project/simulator/src/avm/opcodes/conversion.test.ts b/yarn-project/simulator/src/avm/opcodes/conversion.test.ts new file mode 100644 index 00000000000..d3278b0871f --- /dev/null +++ b/yarn-project/simulator/src/avm/opcodes/conversion.test.ts @@ -0,0 +1,90 @@ +import { type AvmContext } from '../avm_context.js'; +import { Field, type Uint8, Uint32 } from '../avm_memory_types.js'; +import { initContext } from '../fixtures/index.js'; +import { Addressing, AddressingMode } from './addressing_mode.js'; +import { ToRadixLE } from './conversion.js'; + +describe('Conversion Opcodes', () => { + let context: AvmContext; + + beforeEach(async () => { + context = initContext(); + }); + + describe('To Radix LE', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + ToRadixLE.opcode, // opcode + 1, // indirect + ...Buffer.from('12345678', 'hex'), // inputStateOffset + ...Buffer.from('23456789', 'hex'), // outputStateOffset + ...Buffer.from('00000002', 'hex'), // radix + ...Buffer.from('00000100', 'hex'), // numLimbs + ]); + const inst = new ToRadixLE( + /*indirect=*/ 1, + /*srcOffset=*/ 0x12345678, + /*dstOffset=*/ 0x23456789, + /*radix=*/ 2, + /*numLimbs=*/ 256, + ); + + expect(ToRadixLE.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should decompose correctly - direct', async () => { + const arg = new Field(0b1011101010100n); + const indirect = 0; + const srcOffset = 0; + const radix = 2; // Bit decomposition + const numLimbs = 10; // only the first 10 bits + const dstOffset = 20; + context.machineState.memory.set(srcOffset, arg); + + await new ToRadixLE(indirect, srcOffset, dstOffset, radix, numLimbs).execute(context); + + const resultBuffer: Buffer = Buffer.concat( + context.machineState.memory.getSliceAs(dstOffset, numLimbs).map(byte => byte.toBuffer()), + ); + // The expected result is the first 10 bits of the input, reversed + const expectedResults = '1011101010100'.split('').reverse().slice(0, numLimbs).map(Number); + for (let i = 0; i < numLimbs; i++) { + expect(resultBuffer.readUInt8(i)).toEqual(expectedResults[i]); + } + }); + + it('Should decompose correctly - indirect', async () => { + const arg = new Field(Buffer.from('1234567890abcdef', 'hex')); + const indirect = new Addressing([ + /*srcOffset=*/ AddressingMode.INDIRECT, + /*dstOffset*/ AddressingMode.INDIRECT, + ]).toWire(); + const srcOffset = 0; + const srcOffsetReal = 10; + const dstOffset = 2; + const dstOffsetReal = 30; + context.machineState.memory.set(srcOffset, new Uint32(srcOffsetReal)); + context.machineState.memory.set(dstOffset, new Uint32(dstOffsetReal)); + context.machineState.memory.set(srcOffsetReal, arg); + + const radix = 1 << 8; // Byte decomposition + const numLimbs = 32; // 256-bit decomposition + await new ToRadixLE(indirect, srcOffset, dstOffset, radix, numLimbs).execute(context); + + const resultBuffer: Buffer = Buffer.concat( + context.machineState.memory.getSliceAs(dstOffsetReal, numLimbs).map(byte => byte.toBuffer()), + ); + // The expected result is the input (padded to 256 bits),and reversed + const expectedResults = '1234567890abcdef' + .padStart(64, '0') + .split('') + .reverse() + .map(a => parseInt(a, 16)); + // Checking the value in each byte of the buffer is correct + for (let i = 0; i < numLimbs; i++) { + expect(resultBuffer.readUInt8(i)).toEqual(expectedResults[2 * i] + expectedResults[2 * i + 1] * 16); + } + }); + }); +}); diff --git a/yarn-project/simulator/src/avm/opcodes/conversion.ts b/yarn-project/simulator/src/avm/opcodes/conversion.ts new file mode 100644 index 00000000000..dc9884d9aab --- /dev/null +++ b/yarn-project/simulator/src/avm/opcodes/conversion.ts @@ -0,0 +1,58 @@ +import { assert } from '../../../../foundation/src/json-rpc/js_utils.js'; +import { type AvmContext } from '../avm_context.js'; +import { TypeTag, Uint8 } from '../avm_memory_types.js'; +import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; +import { Addressing } from './addressing_mode.js'; +import { Instruction } from './instruction.js'; + +export class ToRadixLE extends Instruction { + static type: string = 'TORADIXLE'; + static readonly opcode: Opcode = Opcode.TORADIXLE; + + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, // Opcode + OperandType.UINT8, // Indirect + OperandType.UINT32, // src memory address + OperandType.UINT32, // dst memory address + OperandType.UINT32, // radix (immediate) + OperandType.UINT32, // number of limbs (Immediate) + ]; + + constructor( + private indirect: number, + private srcOffset: number, + private dstOffset: number, + private radix: number, + private numLimbs: number, + ) { + assert(radix <= 256, 'Radix cannot be greater than 256'); + super(); + } + + public async execute(context: AvmContext): Promise { + const memory = context.machineState.memory.track(this.type); + const [srcOffset, dstOffset] = Addressing.fromWire(this.indirect).resolve([this.srcOffset, this.dstOffset], memory); + const memoryOperations = { reads: 1, writes: this.numLimbs, indirect: this.indirect }; + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + // The radix gadget only takes in a Field + memory.checkTag(TypeTag.FIELD, srcOffset); + + let value: bigint = memory.get(srcOffset).toBigInt(); + const radixBN: bigint = BigInt(this.radix); + const limbArray = []; + + for (let i = 0; i < this.numLimbs; i++) { + const limb = value % radixBN; + limbArray.push(limb); + value /= radixBN; + } + + const res = [...limbArray].map(byte => new Uint8(byte)); + memory.setSlice(dstOffset, res); + + memory.assert(memoryOperations); + context.machineState.incrementPc(); + } +} diff --git a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts index 569ad1d7eda..dabf361d04c 100644 --- a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts @@ -74,6 +74,8 @@ export enum Opcode { POSEIDON2, SHA256, // temp - may be removed, but alot of contracts rely on it PEDERSEN, // temp - may be removed, but alot of contracts rely on it + // Conversion + TORADIXLE, } // Possible types for an instruction's operand in its wire format. (Keep in sync with CPP code.