From c98325d23897d23c09faddc4355958406d44faa9 Mon Sep 17 00:00:00 2001 From: David Banks <47112877+dbanks12@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:20:03 -0500 Subject: [PATCH] feat(avm-simulator): implement AVM message opcodes (simulator/transpiler/noir-test) (#4852) Closes #4837 Closes #4835 Updated spec for both opcodes according to latest plan (and to more closely match existing Noir interface for private). Misc cleanup. [Latest discussion on `L1TOL2MSGEXISTS` opcode](https://aztecprotocol.slack.com/archives/C03P17YHVK8/p1709072769364739) --- avm-transpiler/src/opcodes.rs | 4 +- avm-transpiler/src/transpile.rs | 95 +++++++++++++++ .../barretenberg/vm/avm_trace/avm_opcode.cpp | 4 +- .../barretenberg/vm/avm_trace/avm_opcode.hpp | 2 +- .../aztec-nr/aztec/src/context/avm.nr | 35 +++++- .../contracts/avm_test_contract/src/main.nr | 18 ++- .../simulator/src/avm/avm_simulator.test.ts | 95 ++++++++++++--- .../simulator/src/avm/fixtures/index.ts | 15 ++- .../simulator/src/avm/journal/journal.test.ts | 77 +++++++++++-- .../simulator/src/avm/journal/journal.ts | 53 ++++++++- .../simulator/src/avm/journal/trace.test.ts | 49 +++++++- .../simulator/src/avm/journal/trace.ts | 18 ++- .../simulator/src/avm/journal/trace_types.ts | 19 ++- .../src/avm/opcodes/accrued_substate.test.ts | 109 +++++++++++++++--- .../src/avm/opcodes/accrued_substate.ts | 42 ++++++- .../serialization/bytecode_serialization.ts | 3 +- .../instruction_serialization.ts | 2 +- yellow-paper/docs/public-vm/execution.md | 8 +- .../docs/public-vm/gen/_instruction-set.mdx | 64 ++++------ yellow-paper/docs/public-vm/state.md | 32 ++--- yellow-paper/docs/public-vm/type-structs.md | 20 ++-- .../InstructionSet/InstructionSet.js | 41 +++---- 22 files changed, 632 insertions(+), 173 deletions(-) diff --git a/avm-transpiler/src/opcodes.rs b/avm-transpiler/src/opcodes.rs index 84414239aa9..b297958b498 100644 --- a/avm-transpiler/src/opcodes.rs +++ b/avm-transpiler/src/opcodes.rs @@ -66,7 +66,7 @@ pub enum AvmOpcode { EMITNOTEHASH, // Notes & Nullifiers NULLIFIEREXISTS, // Notes & Nullifiers EMITNULLIFIER, // Notes & Nullifiers - READL1TOL2MSG, // Messages + L1TOL2MSGEXISTS, // Messages HEADERMEMBER, // Archive tree & Headers // Accrued Substate @@ -154,7 +154,7 @@ impl AvmOpcode { AvmOpcode::EMITNOTEHASH => "EMITNOTEHASH", // Notes & Nullifiers AvmOpcode::NULLIFIEREXISTS => "NULLIFIEREXISTS", // Notes & Nullifiers AvmOpcode::EMITNULLIFIER => "EMITNULLIFIER", // Notes & Nullifiers - AvmOpcode::READL1TOL2MSG => "READL1TOL2MSG", // Messages + AvmOpcode::L1TOL2MSGEXISTS => "L1TOL2MSGEXISTS", // Messages AvmOpcode::HEADERMEMBER => "HEADERMEMBER", // Archive tree & Headers // Accrued Substate diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index 372a273dbe5..58be5e25532 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -246,6 +246,8 @@ fn handle_foreign_call( inputs, ), "nullifierExists" => handle_nullifier_exists(avm_instrs, destinations, inputs), + "l1ToL2MsgExists" => handle_l1_to_l2_msg_exists(avm_instrs, destinations, inputs), + "sendL2ToL1Msg" => handle_send_l2_to_l1_msg(avm_instrs, destinations, inputs), "keccak256" | "sha256" => { handle_2_field_hash_instruction(avm_instrs, function, destinations, inputs) } @@ -382,6 +384,99 @@ fn handle_nullifier_exists( }); } +/// Handle an AVM L1TOL2MSGEXISTS instruction +/// (a l1ToL2MsgExists brillig foreign call was encountered) +/// Adds the new instruction to the avm instructions list. +fn handle_l1_to_l2_msg_exists( + avm_instrs: &mut Vec, + destinations: &Vec, + inputs: &Vec, +) { + if destinations.len() != 1 || inputs.len() != 2 { + panic!( + "Transpiler expects ForeignCall::L1TOL2MSGEXISTS to have 1 destinations and 2 input, got {} and {}", + destinations.len(), + inputs.len() + ); + } + let msg_hash_offset_operand = match &inputs[0] { + ValueOrArray::MemoryAddress(offset) => offset.to_usize() as u32, + _ => panic!( + "Transpiler does not know how to handle ForeignCall::L1TOL2MSGEXISTS with HeapArray/Vector inputs", + ), + }; + let msg_leaf_index_offset_operand = match &inputs[1] { + ValueOrArray::MemoryAddress(offset) => offset.to_usize() as u32, + _ => panic!( + "Transpiler does not know how to handle ForeignCall::L1TOL2MSGEXISTS with HeapArray/Vector inputs", + ), + }; + let exists_offset_operand = match &destinations[0] { + ValueOrArray::MemoryAddress(offset) => offset.to_usize() as u32, + _ => panic!( + "Transpiler does not know how to handle ForeignCall::L1TOL2MSGEXISTS with HeapArray/Vector inputs", + ), + }; + avm_instrs.push(AvmInstruction { + opcode: AvmOpcode::L1TOL2MSGEXISTS, + indirect: Some(ALL_DIRECT), + operands: vec![ + AvmOperand::U32 { + value: msg_hash_offset_operand, + }, + AvmOperand::U32 { + value: msg_leaf_index_offset_operand, + }, + AvmOperand::U32 { + value: exists_offset_operand, + }, + ], + ..Default::default() + }); +} + +/// Handle an AVM SENDL2TOL1MSG +/// (a sendL2ToL1Msg brillig foreign call was encountered) +/// Adds the new instruction to the avm instructions list. +fn handle_send_l2_to_l1_msg( + avm_instrs: &mut Vec, + destinations: &Vec, + inputs: &Vec, +) { + if destinations.len() != 0 || inputs.len() != 2 { + panic!( + "Transpiler expects ForeignCall::SENDL2TOL1MSG to have 0 destinations and 2 inputs, got {} and {}", + destinations.len(), + inputs.len() + ); + } + let recipient_offset_operand = match &inputs[0] { + ValueOrArray::MemoryAddress(offset) => offset.to_usize() as u32, + _ => panic!( + "Transpiler does not know how to handle ForeignCall::SENDL2TOL1MSG with HeapArray/Vector inputs", + ), + }; + let content_offset_operand = match &inputs[1] { + ValueOrArray::MemoryAddress(offset) => offset.to_usize() as u32, + _ => panic!( + "Transpiler does not know how to handle ForeignCall::SENDL2TOL1MSG with HeapArray/Vector inputs", + ), + }; + avm_instrs.push(AvmInstruction { + opcode: AvmOpcode::SENDL2TOL1MSG, + indirect: Some(ALL_DIRECT), + operands: vec![ + AvmOperand::U32 { + value: recipient_offset_operand, + }, + AvmOperand::U32 { + value: content_offset_operand, + }, + ], + ..Default::default() + }); +} + /// Two field hash instructions represent instruction's that's outputs are larger than a field element /// /// This includes: diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.cpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.cpp index c460195db3d..53feb5d75e6 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.cpp @@ -72,7 +72,7 @@ const std::unordered_map Bytecode::OPERANDS_NUM = { //{ OpCode::EMITNOTEHASH, }, // Notes & Nullifiers //{ OpCode::NULLIFIEREXISTS, }, // Notes & Nullifiers //{ OpCode::EMITNULLIFIER, }, // Notes & Nullifiers - //{ OpCode::READL1TOL2MSG, }, // Messages + //{ OpCode::L1TOL2MSGEXISTS, }, // Messages //{ OpCode::HEADERMEMBER, }, //// Accrued Substate @@ -146,7 +146,7 @@ bool Bytecode::has_in_tag(OpCode const op_code) case OpCode::EMITNOTEHASH: case OpCode::NULLIFIEREXISTS: case OpCode::EMITNULLIFIER: - case OpCode::READL1TOL2MSG: + case OpCode::L1TOL2MSGEXISTS: case OpCode::HEADERMEMBER: case OpCode::EMITUNENCRYPTEDLOG: case OpCode::SENDL2TOL1MSG: 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 5fa3cac9add..53fc087b26d 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp @@ -80,7 +80,7 @@ enum class OpCode : uint8_t { EMITNOTEHASH, // Notes & Nullifiers NULLIFIEREXISTS, // Notes & Nullifiers EMITNULLIFIER, // Notes & Nullifiers - READL1TOL2MSG, // Messages + L1TOL2MSGEXISTS, // Messages HEADERMEMBER, // Archive tree & Headers // Accrued Substate diff --git a/noir-projects/aztec-nr/aztec/src/context/avm.nr b/noir-projects/aztec-nr/aztec/src/context/avm.nr index c9d8a228b01..89dcd752551 100644 --- a/noir-projects/aztec-nr/aztec/src/context/avm.nr +++ b/noir-projects/aztec-nr/aztec/src/context/avm.nr @@ -1,4 +1,4 @@ -use dep::protocol_types::address::{AztecAddress, EthAddress}; +use dep::protocol_types::{address::{AztecAddress, EthAddress}, constants::L1_TO_L2_MESSAGE_LENGTH}; // Getters that will be converted by the transpiler into their // own opcodes @@ -10,6 +10,7 @@ impl AVMContext { Self {} } + // OPCODES #[oracle(address)] pub fn address(self) -> AztecAddress {} @@ -56,16 +57,40 @@ impl AVMContext { pub fn emit_note_hash(self, note_hash: Field) {} #[oracle(nullifierExists)] - pub fn check_nullifier_exists(self, nullifier: Field) -> u8 {} + pub fn nullifier_exists(self, nullifier: Field) -> u8 {} #[oracle(emitNullifier)] pub fn emit_nullifier(self, nullifier: Field) {} + #[oracle(l1ToL2MsgExists)] + pub fn l1_to_l2_msg_exists(self, msg_hash: Field, msg_leaf_index: Field) -> u8 {} + + #[oracle(sendL2ToL1Msg)] + pub fn send_l2_to_l1_msg(self, recipient: EthAddress, content: Field) {} + + /////////////////////////////////////////////////////////////////////////// // The functions below allow interface-equivalence with PrivateContext - // for emitting note hashes and nullifiers - pub fn push_new_note_hash(self: &mut Self, note_hash: Field) { - self.emit_note_hash(note_hash); + /////////////////////////////////////////////////////////////////////////// + pub fn this_address(self) -> AztecAddress { + self.address() + } + + #[oracle(sendL2ToL1Msg)] + pub fn message_portal(&mut self, recipient: EthAddress, content: Field) {} + + pub fn consume_l1_to_l2_message( + &mut self, + _msg_key: Field, + _content: Field, + _secret: Field, + _sender: EthAddress + ) { + assert(false, "Not implemented!"); } + + #[oracle(emitNoteHash)] + pub fn push_new_note_hash(self: &mut Self, note_hash: Field) {} + pub fn push_new_nullifier(self: &mut Self, nullifier: Field, _nullified_commitment: Field) { // Cannot nullify pending commitments in AVM, so `nullified_commitment` is not used self.emit_nullifier(nullifier); diff --git a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr index 42f9826b7ed..8ea44b2ca78 100644 --- a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr @@ -1,6 +1,6 @@ contract AvmTest { // Libs - use dep::aztec::protocol_types::address::{AztecAddress, EthAddress}; + use dep::aztec::protocol_types::{address::{AztecAddress, EthAddress}, constants::L1_TO_L2_MESSAGE_LENGTH}; // avm lib use dep::aztec::avm::hash::{keccak256, poseidon, sha256}; @@ -159,15 +159,15 @@ contract AvmTest { // Use the standard context interface to emit a new nullifier #[aztec(public-vm)] - fn check_nullifier_exists(nullifier: Field) -> pub u8 { - context.check_nullifier_exists(nullifier) + fn nullifier_exists(nullifier: Field) -> pub u8 { + context.nullifier_exists(nullifier) } // Use the standard context interface to emit a new nullifier #[aztec(public-vm)] fn emit_nullifier_and_check(nullifier: Field) { context.emit_nullifier(nullifier); - let exists = context.check_nullifier_exists(nullifier); + let exists = context.nullifier_exists(nullifier); assert(exists == 1, "Nullifier was just created, but its existence wasn't detected!"); } @@ -178,4 +178,14 @@ contract AvmTest { // Can't do this twice! context.push_new_nullifier(nullifier, 0); } + + #[aztec(public-vm)] + fn l1_to_l2_msg_exists(msg_hash: Field, msg_leaf_index: Field) -> pub u8 { + context.l1_to_l2_msg_exists(msg_hash, msg_leaf_index) + } + + #[aztec(public-vm)] + fn send_l2_to_l1_msg(recipient: EthAddress, content: Field) { + context.message_portal(recipient, content) + } } diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index ecc2c943b9f..382af6d4be1 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -8,7 +8,12 @@ import { jest } from '@jest/globals'; import { TypeTag } from './avm_memory_types.js'; import { AvmSimulator } from './avm_simulator.js'; -import { initContext, initExecutionEnvironment, initGlobalVariables } from './fixtures/index.js'; +import { + initContext, + initExecutionEnvironment, + initGlobalVariables, + initL1ToL2MessageOracleInput, +} from './fixtures/index.js'; import { Add, CalldataCopy, Return } from './opcodes/index.js'; import { encodeToBytecode } from './serialization/bytecode_serialization.js'; @@ -237,7 +242,7 @@ describe('AVM simulator', () => { }); }); - describe('Test tree access functions from noir contract', () => { + describe('Test tree access functions from noir contract (notes & nullifiers)', () => { it(`Should execute contract function that checks if a note hash exists (it does not)`, async () => { const noteHash = new Fr(42); const leafIndex = new Fr(7); @@ -335,12 +340,12 @@ describe('AVM simulator', () => { expect(context.persistableState.flush().newNullifiers).toEqual([utxo]); }); - it(`Should execute contract function that checks if a nullifier existence (it does not)`, async () => { + it(`Should execute contract function that checks if a nullifier exists (it does not)`, async () => { const utxo = new Fr(42); const calldata = [utxo]; // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_check_nullifier_exists')!; + const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_nullifier_exists')!; // Decode bytecode into instructions const bytecode = Buffer.from(artifact.bytecode, 'base64'); @@ -356,16 +361,16 @@ describe('AVM simulator', () => { expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); // Nullifier existence check should be in trace - const sideEffects = context.persistableState.flush(); - expect(sideEffects.nullifierChecks.length).toEqual(1); - expect(sideEffects.nullifierChecks[0].exists).toEqual(false); + const trace = context.persistableState.flush(); + expect(trace.nullifierChecks.length).toEqual(1); + expect(trace.nullifierChecks[0].exists).toEqual(false); }); - it(`Should execute contract function that checks if a nullifier existence (it does)`, async () => { + it(`Should execute contract function that checks if a nullifier exists (it does)`, async () => { const utxo = new Fr(42); const calldata = [utxo]; // Get contract function artifact - const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_check_nullifier_exists')!; + const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_nullifier_exists')!; // Decode bytecode into instructions const bytecode = Buffer.from(artifact.bytecode, 'base64'); @@ -386,9 +391,9 @@ describe('AVM simulator', () => { expect(results.output).toEqual([/*exists=true*/ new Fr(1)]); // Nullifier existence check should be in trace - const sideEffects = context.persistableState.flush(); - expect(sideEffects.nullifierChecks.length).toEqual(1); - expect(sideEffects.nullifierChecks[0].exists).toEqual(true); + const trace = context.persistableState.flush(); + expect(trace.nullifierChecks.length).toEqual(1); + expect(trace.nullifierChecks[0].exists).toEqual(true); }); it(`Should execute contract function that checks emits a nullifier and checks its existence`, async () => { const utxo = new Fr(42); @@ -410,10 +415,10 @@ describe('AVM simulator', () => { expect(results.reverted).toBe(false); // Nullifier existence check should be in trace - const sideEffects = context.persistableState.flush(); - expect(sideEffects.newNullifiers).toEqual([utxo]); - expect(sideEffects.nullifierChecks.length).toEqual(1); - expect(sideEffects.nullifierChecks[0].exists).toEqual(true); + const trace = context.persistableState.flush(); + expect(trace.newNullifiers).toEqual([utxo]); + expect(trace.nullifierChecks.length).toEqual(1); + expect(trace.nullifierChecks[0].exists).toEqual(true); }); it(`Should execute contract function that emits same nullifier twice (should fail)`, async () => { const utxo = new Fr(42); @@ -438,5 +443,63 @@ describe('AVM simulator', () => { expect(context.persistableState.flush().newNullifiers).toEqual([utxo]); }); }); + describe('Test tree access functions from noir contract (l1ToL2 messages)', () => { + it(`Should execute contract function that checks if a message exists (it does not)`, async () => { + const msgHash = new Fr(42); + const leafIndex = new Fr(24); + const calldata = [msgHash, leafIndex]; + + // Get contract function artifact + const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_l1_to_l2_msg_exists')!; + + // Decode bytecode into instructions + const bytecode = Buffer.from(artifact.bytecode, 'base64'); + + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + jest + .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(bytecode)); + + await new AvmSimulator(context).execute(); + const results = await new AvmSimulator(context).execute(); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); + + // Message existence check should be in trace + const trace = context.persistableState.flush(); + expect(trace.l1ToL2MessageChecks.length).toEqual(1); + expect(trace.l1ToL2MessageChecks[0].exists).toEqual(false); + }); + it(`Should execute contract function that checks if a message exists (it does)`, async () => { + const msgHash = new Fr(42); + const leafIndex = new Fr(24); + const calldata = [msgHash, leafIndex]; + + // Get contract function artifact + const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_l1_to_l2_msg_exists')!; + + // Decode bytecode into instructions + const bytecode = Buffer.from(artifact.bytecode, 'base64'); + + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + jest + .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(bytecode)); + + jest + .spyOn(context.persistableState.hostStorage.commitmentsDb, 'getL1ToL2MembershipWitness') + .mockResolvedValue(initL1ToL2MessageOracleInput(leafIndex.toBigInt())); + + await new AvmSimulator(context).execute(); + const results = await new AvmSimulator(context).execute(); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([/*exists=false*/ new Fr(1)]); + + // Message existence check should be in trace + const trace = context.persistableState.flush(); + expect(trace.l1ToL2MessageChecks.length).toEqual(1); + expect(trace.l1ToL2MessageChecks[0].exists).toEqual(true); + }); + }); }); }); diff --git a/yarn-project/simulator/src/avm/fixtures/index.ts b/yarn-project/simulator/src/avm/fixtures/index.ts index 1df07a4418e..b292074abfb 100644 --- a/yarn-project/simulator/src/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/avm/fixtures/index.ts @@ -1,4 +1,5 @@ -import { GlobalVariables } from '@aztec/circuits.js'; +import { SiblingPath } from '@aztec/circuit-types'; +import { GlobalVariables, L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/circuits.js'; import { FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -7,7 +8,7 @@ import { Fr } from '@aztec/foundation/fields'; import { mock } from 'jest-mock-extended'; import merge from 'lodash.merge'; -import { CommitmentsDB, PublicContractsDB, PublicStateDB } from '../../index.js'; +import { CommitmentsDB, MessageLoadOracleInputs, PublicContractsDB, PublicStateDB } from '../../index.js'; import { AvmContext } from '../avm_context.js'; import { AvmExecutionEnvironment } from '../avm_execution_environment.js'; import { AvmMachineState } from '../avm_machine_state.js'; @@ -100,3 +101,13 @@ export function initMachineState(overrides?: Partial): AvmMachi export function allSameExcept(original: any, overrides: any): any { return merge({}, original, overrides); } + +/** + * Create an empty L1ToL2Message oracle input + */ +export function initL1ToL2MessageOracleInput(leafIndex?: bigint): any { + return new MessageLoadOracleInputs( + leafIndex ? leafIndex : BigInt(0), + new SiblingPath(L1_TO_L2_MSG_TREE_HEIGHT, Array(L1_TO_L2_MSG_TREE_HEIGHT)), + ); +} diff --git a/yarn-project/simulator/src/avm/journal/journal.test.ts b/yarn-project/simulator/src/avm/journal/journal.test.ts index dabeee98751..4a4db8e3304 100644 --- a/yarn-project/simulator/src/avm/journal/journal.test.ts +++ b/yarn-project/simulator/src/avm/journal/journal.test.ts @@ -1,8 +1,10 @@ +import { EthAddress } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { MockProxy, mock } from 'jest-mock-extended'; import { CommitmentsDB, PublicContractsDB, PublicStateDB } from '../../index.js'; +import { initL1ToL2MessageOracleInput } from '../fixtures/index.js'; import { HostStorage } from './host_storage.js'; import { AvmPersistableStateManager, JournalData } from './journal.js'; @@ -53,7 +55,7 @@ describe('journal', () => { }); }); - describe('UTXOs', () => { + describe('UTXOs & messages', () => { it('Should maintain commitments', () => { const utxo = new Fr(1); journal.writeNoteHash(utxo); @@ -90,12 +92,46 @@ describe('journal', () => { const journalUpdates = journal.flush(); expect(journalUpdates.newNullifiers).toEqual([utxo]); }); + it('checkL1ToL2MessageExists works for missing message', async () => { + const utxo = new Fr(2); + const leafIndex = new Fr(42); + + const exists = await journal.checkL1ToL2MessageExists(utxo, leafIndex); + expect(exists).toEqual(false); + + const journalUpdates = journal.flush(); + expect(journalUpdates.l1ToL2MessageChecks.map(c => [c.leafIndex, c.msgHash, c.exists])).toEqual([ + [leafIndex, utxo, false], + ]); + }); + it('checkL1ToL2MessageExists works for existing nullifiers', async () => { + const utxo = new Fr(2); + const leafIndex = new Fr(42); + + commitmentsDb.getL1ToL2MembershipWitness.mockResolvedValue(initL1ToL2MessageOracleInput(leafIndex.toBigInt())); + const exists = await journal.checkL1ToL2MessageExists(utxo, leafIndex); + expect(exists).toEqual(true); + + const journalUpdates = journal.flush(); + expect(journalUpdates.l1ToL2MessageChecks.map(c => [c.leafIndex, c.msgHash, c.exists])).toEqual([ + [leafIndex, utxo, true], + ]); + }); + it('Should maintain nullifiers', async () => { + const contractAddress = new Fr(1); + const utxo = new Fr(2); + await journal.writeNullifier(contractAddress, utxo); + + const journalUpdates = journal.flush(); + expect(journalUpdates.newNullifiers).toEqual([utxo]); + }); it('Should maintain l1 messages', () => { - const utxo = [new Fr(1)]; - journal.writeL1Message(utxo); + const recipient = EthAddress.fromField(new Fr(1)); + const utxo = new Fr(2); + journal.writeL1Message(recipient, utxo); const journalUpdates = journal.flush(); - expect(journalUpdates.newL1Messages).toEqual([utxo]); + expect(journalUpdates.newL1Messages).toEqual([{ recipient, content: utxo }]); }); }); @@ -111,27 +147,32 @@ describe('journal', () => { const key = new Fr(2); const value = new Fr(1); const valueT1 = new Fr(2); + const recipient = EthAddress.fromField(new Fr(42)); const commitment = new Fr(10); const commitmentT1 = new Fr(20); const logs = [new Fr(1), new Fr(2)]; const logsT1 = [new Fr(3), new Fr(4)]; + const index = new Fr(42); + const indexT1 = new Fr(24); journal.writeStorage(contractAddress, key, value); await journal.readStorage(contractAddress, key); journal.writeNoteHash(commitment); journal.writeLog(logs); - journal.writeL1Message(logs); + journal.writeL1Message(recipient, commitment); await journal.writeNullifier(contractAddress, commitment); await journal.checkNullifierExists(contractAddress, commitment); + await journal.checkL1ToL2MessageExists(commitment, index); const childJournal = new AvmPersistableStateManager(journal.hostStorage, journal); childJournal.writeStorage(contractAddress, key, valueT1); await childJournal.readStorage(contractAddress, key); childJournal.writeNoteHash(commitmentT1); childJournal.writeLog(logsT1); - childJournal.writeL1Message(logsT1); + childJournal.writeL1Message(recipient, commitmentT1); await childJournal.writeNullifier(contractAddress, commitmentT1); await childJournal.checkNullifierExists(contractAddress, commitmentT1); + await childJournal.checkL1ToL2MessageExists(commitmentT1, indexT1); journal.acceptNestedCallState(childJournal); @@ -155,12 +196,19 @@ describe('journal', () => { expect(journalUpdates.newNoteHashes).toEqual([commitment, commitmentT1]); expect(journalUpdates.newLogs).toEqual([logs, logsT1]); - expect(journalUpdates.newL1Messages).toEqual([logs, logsT1]); + expect(journalUpdates.newL1Messages).toEqual([ + { recipient, content: commitment }, + { recipient, content: commitmentT1 }, + ]); expect(journalUpdates.nullifierChecks.map(c => [c.nullifier, c.exists])).toEqual([ [commitment, true], [commitmentT1, true], ]); expect(journalUpdates.newNullifiers).toEqual([commitment, commitmentT1]); + expect(journalUpdates.l1ToL2MessageChecks.map(c => [c.leafIndex, c.msgHash, c.exists])).toEqual([ + [index, commitment, false], + [indexT1, commitmentT1, false], + ]); }); it('Should merge failed journals together', async () => { @@ -177,18 +225,22 @@ describe('journal', () => { const key = new Fr(2); const value = new Fr(1); const valueT1 = new Fr(2); + const recipient = EthAddress.fromField(new Fr(42)); const commitment = new Fr(10); const commitmentT1 = new Fr(20); const logs = [new Fr(1), new Fr(2)]; const logsT1 = [new Fr(3), new Fr(4)]; + const index = new Fr(42); + const indexT1 = new Fr(24); journal.writeStorage(contractAddress, key, value); await journal.readStorage(contractAddress, key); journal.writeNoteHash(commitment); await journal.writeNullifier(contractAddress, commitment); await journal.checkNullifierExists(contractAddress, commitment); + await journal.checkL1ToL2MessageExists(commitment, index); journal.writeLog(logs); - journal.writeL1Message(logs); + journal.writeL1Message(recipient, commitment); const childJournal = new AvmPersistableStateManager(journal.hostStorage, journal); childJournal.writeStorage(contractAddress, key, valueT1); @@ -196,8 +248,9 @@ describe('journal', () => { childJournal.writeNoteHash(commitmentT1); await childJournal.writeNullifier(contractAddress, commitmentT1); await childJournal.checkNullifierExists(contractAddress, commitmentT1); + await journal.checkL1ToL2MessageExists(commitmentT1, indexT1); childJournal.writeLog(logsT1); - childJournal.writeL1Message(logsT1); + childJournal.writeL1Message(recipient, commitmentT1); journal.rejectNestedCallState(childJournal); @@ -226,10 +279,14 @@ describe('journal', () => { [commitmentT1, true], ]); expect(journalUpdates.newNullifiers).toEqual([commitment, commitmentT1]); + expect(journalUpdates.l1ToL2MessageChecks.map(c => [c.leafIndex, c.msgHash, c.exists])).toEqual([ + [index, commitment, false], + [indexT1, commitmentT1, false], + ]); // Check that rejected Accrued Substate is absent expect(journalUpdates.newLogs).toEqual([logs]); - expect(journalUpdates.newL1Messages).toEqual([logs]); + expect(journalUpdates.newL1Messages).toEqual([{ recipient, content: commitment }]); }); it('Can fork and merge journals', () => { diff --git a/yarn-project/simulator/src/avm/journal/journal.ts b/yarn-project/simulator/src/avm/journal/journal.ts index c1c73899412..936a3a127f4 100644 --- a/yarn-project/simulator/src/avm/journal/journal.ts +++ b/yarn-project/simulator/src/avm/journal/journal.ts @@ -1,10 +1,11 @@ +import { EthAddress, L2ToL1Message } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { HostStorage } from './host_storage.js'; import { Nullifiers } from './nullifiers.js'; import { PublicStorage } from './public_storage.js'; import { WorldStateAccessTrace } from './trace.js'; -import { TracedNoteHashCheck, TracedNullifierCheck } from './trace_types.js'; +import { TracedL1toL2MessageCheck, TracedNoteHashCheck, TracedNullifierCheck } from './trace_types.js'; /** * Data held within the journal @@ -14,8 +15,9 @@ export type JournalData = { newNoteHashes: Fr[]; nullifierChecks: TracedNullifierCheck[]; newNullifiers: Fr[]; + l1ToL2MessageChecks: TracedL1toL2MessageCheck[]; - newL1Messages: Fr[][]; + newL1Messages: L2ToL1Message[]; newLogs: Fr[][]; /** contract address -\> key -\> value */ @@ -50,7 +52,7 @@ export class AvmPersistableStateManager { private trace: WorldStateAccessTrace; /** Accrued Substate **/ - private newL1Messages: Fr[][] = []; + private newL1Messages: L2ToL1Message[] = []; private newLogs: Fr[][] = []; constructor(hostStorage: HostStorage, parent?: AvmPersistableStateManager) { @@ -111,16 +113,31 @@ export class AvmPersistableStateManager { return Promise.resolve(exists); } + /** + * Write a note hash, trace the write. + * @param noteHash - the unsiloed note hash to write + */ public writeNoteHash(noteHash: Fr) { this.trace.traceNewNoteHash(/*storageAddress*/ Fr.ZERO, noteHash); } + /** + * Check if a nullifier exists, trace the check. + * @param storageAddress - address of the contract that the nullifier is associated with + * @param nullifier - the unsiloed nullifier to check + * @returns exists - whether the nullifier exists in the nullifier set + */ public async checkNullifierExists(storageAddress: Fr, nullifier: Fr): Promise { const [exists, isPending, leafIndex] = await this.nullifiers.checkExists(storageAddress, nullifier); this.trace.traceNullifierCheck(storageAddress, nullifier, exists, isPending, leafIndex); return Promise.resolve(exists); } + /** + * Write a nullifier to the nullifier set, trace the write. + * @param storageAddress - address of the contract that the nullifier is associated with + * @param nullifier - the unsiloed nullifier to write + */ public async writeNullifier(storageAddress: Fr, nullifier: Fr) { // Cache pending nullifiers for later access await this.nullifiers.append(storageAddress, nullifier); @@ -128,8 +145,33 @@ export class AvmPersistableStateManager { this.trace.traceNewNullifier(storageAddress, nullifier); } - public writeL1Message(message: Fr[]) { - this.newL1Messages.push(message); + /** + * Check if an L1 to L2 message exists, trace the check. + * @param msgHash - the message hash to check existence of + * @param msgLeafIndex - the message leaf index to use in the check + * @returns exists - whether the message exists in the L1 to L2 Messages tree + */ + public async checkL1ToL2MessageExists(msgHash: Fr, msgLeafIndex: Fr): Promise { + let exists = false; + try { + const gotMessage = await this.hostStorage.commitmentsDb.getL1ToL2MembershipWitness(msgHash); + exists = gotMessage !== undefined && gotMessage.index == msgLeafIndex.toBigInt(); + } catch { + // error getting message - doesn't exist! + exists = false; + } + this.trace.traceL1ToL2MessageCheck(msgHash, msgLeafIndex, exists); + return Promise.resolve(exists); + } + + /** + * Write an L2 to L1 message. + * @param recipient - L1 contract address to send the message to. + * @param content - Message content. + */ + public writeL1Message(recipient: EthAddress | Fr, content: Fr) { + const recipientAddress = recipient instanceof EthAddress ? recipient : EthAddress.fromField(recipient); + this.newL1Messages.push(new L2ToL1Message(recipientAddress, content)); } public writeLog(log: Fr[]) { @@ -170,6 +212,7 @@ export class AvmPersistableStateManager { newNoteHashes: this.trace.newNoteHashes, nullifierChecks: this.trace.nullifierChecks, newNullifiers: this.trace.newNullifiers, + l1ToL2MessageChecks: this.trace.l1ToL2MessageChecks, newL1Messages: this.newL1Messages, newLogs: this.newLogs, currentStorageValue: this.publicStorage.getCache().cachePerContract, diff --git a/yarn-project/simulator/src/avm/journal/trace.test.ts b/yarn-project/simulator/src/avm/journal/trace.test.ts index 959ff47c9b2..1b2cb5880c1 100644 --- a/yarn-project/simulator/src/avm/journal/trace.test.ts +++ b/yarn-project/simulator/src/avm/journal/trace.test.ts @@ -1,7 +1,7 @@ import { Fr } from '@aztec/foundation/fields'; import { WorldStateAccessTrace } from './trace.js'; -import { TracedNullifierCheck } from './trace_types.js'; +import { TracedL1toL2MessageCheck, TracedNullifierCheck } from './trace_types.js'; describe('world state access trace', () => { let trace: WorldStateAccessTrace; @@ -66,6 +66,19 @@ describe('world state access trace', () => { expect(trace.newNullifiers).toEqual([utxo]); expect(trace.getAccessCounter()).toEqual(1); }); + it('Should trace L1ToL2 Message checks', () => { + const utxo = new Fr(2); + const exists = true; + const leafIndex = new Fr(42); + trace.traceL1ToL2MessageCheck(utxo, leafIndex, exists); + const expectedCheck: TracedL1toL2MessageCheck = { + leafIndex: leafIndex, + msgHash: utxo, + exists: exists, + }; + expect(trace.l1ToL2MessageChecks).toEqual([expectedCheck]); + expect(trace.getAccessCounter()).toEqual(1); + }); }); it('Access counter should properly count accesses', () => { @@ -79,6 +92,9 @@ describe('world state access trace', () => { const noteHash = new Fr(10); const noteHashLeafIndex = new Fr(88); const noteHashExists = false; + const msgExists = false; + const msgLeafIndex = Fr.ZERO; + const msgHash = new Fr(10); let counter = 0; trace.tracePublicStorageWrite(contractAddress, slot, value); @@ -93,6 +109,8 @@ describe('world state access trace', () => { counter++; trace.traceNewNullifier(contractAddress, nullifier); counter++; + trace.traceL1ToL2MessageCheck(msgHash, msgLeafIndex, msgExists); + counter++; trace.tracePublicStorageWrite(contractAddress, slot, value); counter++; trace.tracePublicStorageRead(contractAddress, slot, value); @@ -103,6 +121,8 @@ describe('world state access trace', () => { counter++; trace.traceNewNullifier(contractAddress, nullifier); counter++; + trace.traceL1ToL2MessageCheck(msgHash, msgLeafIndex, msgExists); + counter++; expect(trace.getAccessCounter()).toEqual(counter); }); @@ -128,12 +148,31 @@ describe('world state access trace', () => { const nullifierIsPendingT1 = false; const nullifierLeafIndexT1 = new Fr(42); + const msgExists = false; + const msgLeafIndex = Fr.ZERO; + const msgHash = new Fr(10); + const msgHashT1 = new Fr(20); + const msgExistsT1 = true; + const msgLeafIndexT1 = new Fr(42); + + const expectedMessageCheck = { + leafIndex: msgLeafIndex, + msgHash: msgHash, + exists: msgExists, + }; + const expectedMessageCheckT1 = { + leafIndex: msgLeafIndexT1, + msgHash: msgHashT1, + exists: msgExistsT1, + }; + trace.tracePublicStorageWrite(contractAddress, slot, value); trace.tracePublicStorageRead(contractAddress, slot, value); trace.traceNoteHashCheck(contractAddress, noteHash, noteHashExists, noteHashLeafIndex); trace.traceNewNoteHash(contractAddress, noteHash); trace.traceNullifierCheck(contractAddress, nullifier, nullifierExists, nullifierIsPending, nullifierLeafIndex); trace.traceNewNullifier(contractAddress, nullifier); + trace.traceL1ToL2MessageCheck(msgHash, msgLeafIndex, msgExists); const childTrace = new WorldStateAccessTrace(trace); childTrace.tracePublicStorageWrite(contractAddress, slot, valueT1); @@ -148,6 +187,7 @@ describe('world state access trace', () => { nullifierLeafIndexT1, ); childTrace.traceNewNullifier(contractAddress, nullifierT1); + childTrace.traceL1ToL2MessageCheck(msgHashT1, msgLeafIndexT1, msgExistsT1); const childCounterBeforeMerge = childTrace.getAccessCounter(); trace.acceptAndMerge(childTrace); @@ -177,5 +217,12 @@ describe('world state access trace', () => { expect.objectContaining({ noteHash: noteHash, exists: noteHashExists, leafIndex: noteHashLeafIndex }), expect.objectContaining({ noteHash: noteHashT1, exists: noteHashExistsT1, leafIndex: noteHashLeafIndexT1 }), ]); + expect( + trace.l1ToL2MessageChecks.map(c => ({ + leafIndex: c.leafIndex, + msgHash: c.msgHash, + exists: c.exists, + })), + ).toEqual([expectedMessageCheck, expectedMessageCheckT1]); }); }); diff --git a/yarn-project/simulator/src/avm/journal/trace.ts b/yarn-project/simulator/src/avm/journal/trace.ts index da8f913aa84..620e2e0462c 100644 --- a/yarn-project/simulator/src/avm/journal/trace.ts +++ b/yarn-project/simulator/src/avm/journal/trace.ts @@ -1,6 +1,6 @@ import { Fr } from '@aztec/foundation/fields'; -import { TracedNoteHashCheck, TracedNullifierCheck } from './trace_types.js'; +import { TracedL1toL2MessageCheck, TracedNoteHashCheck, TracedNullifierCheck } from './trace_types.js'; export class WorldStateAccessTrace { public accessCounter: number; @@ -17,7 +17,7 @@ export class WorldStateAccessTrace { public nullifierChecks: TracedNullifierCheck[] = []; //public newNullifiers: TracedNullifier[] = []; public newNullifiers: Fr[] = []; - //public l1toL2MessageReads: TracedL1toL2MessageRead[] = []; + public l1ToL2MessageChecks: TracedL1toL2MessageCheck[] = []; //public archiveChecks: TracedArchiveLeafCheck[] = []; constructor(parentTrace?: WorldStateAccessTrace) { @@ -119,6 +119,19 @@ export class WorldStateAccessTrace { this.incrementAccessCounter(); } + public traceL1ToL2MessageCheck(msgHash: Fr, msgLeafIndex: Fr, exists: boolean) { + // TODO(4805): check if some threshold is reached for max message reads + const traced: TracedL1toL2MessageCheck = { + //callPointer: Fr.ZERO, // FIXME + leafIndex: msgLeafIndex, + msgHash: msgHash, + exists: exists, + //endLifetime: Fr.ZERO, // FIXME + }; + this.l1ToL2MessageChecks.push(traced); + this.incrementAccessCounter(); + } + private incrementAccessCounter() { this.accessCounter++; } @@ -140,6 +153,7 @@ export class WorldStateAccessTrace { this.newNoteHashes = this.newNoteHashes.concat(incomingTrace.newNoteHashes); this.nullifierChecks = this.nullifierChecks.concat(incomingTrace.nullifierChecks); this.newNullifiers = this.newNullifiers.concat(incomingTrace.newNullifiers); + this.l1ToL2MessageChecks = this.l1ToL2MessageChecks.concat(incomingTrace.l1ToL2MessageChecks); // it is assumed that the incoming trace was initialized with this as parent, so accept counter this.accessCounter = incomingTrace.accessCounter; } diff --git a/yarn-project/simulator/src/avm/journal/trace_types.ts b/yarn-project/simulator/src/avm/journal/trace_types.ts index d4fc9b75990..3d065c4fd7d 100644 --- a/yarn-project/simulator/src/avm/journal/trace_types.ts +++ b/yarn-project/simulator/src/avm/journal/trace_types.ts @@ -64,16 +64,15 @@ export type TracedNullifierCheck = { // counter: Fr; // endLifetime: Fr; //}; -// -//export type TracedL1toL2MessageRead = { -// callPointer: Fr; -// portal: Fr; // EthAddress -// leafIndex: Fr; -// entryKey: Fr; -// exists: Fr; -// message: []; // omitted from VM public inputs -//}; -// + +export type TracedL1toL2MessageCheck = { + //callPointer: Fr; + leafIndex: Fr; + msgHash: Fr; + exists: boolean; + //endLifetime: Fr; +}; + //export type TracedArchiveLeafCheck = { // leafIndex: Fr; // leaf: Fr; diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts index 65c22cfb105..9a116891602 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts @@ -1,15 +1,23 @@ +import { EthAddress, Fr } from '@aztec/circuits.js'; + import { mock } from 'jest-mock-extended'; import { CommitmentsDB } from '../../index.js'; import { AvmContext } from '../avm_context.js'; import { Field, Uint8 } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; -import { initContext, initExecutionEnvironment, initHostStorage } from '../fixtures/index.js'; +import { + initContext, + initExecutionEnvironment, + initHostStorage, + initL1ToL2MessageOracleInput, +} from '../fixtures/index.js'; import { AvmPersistableStateManager } from '../journal/journal.js'; import { EmitNoteHash, EmitNullifier, EmitUnencryptedLog, + L1ToL2MessageExists, NoteHashExists, NullifierExists, SendL2ToL1Message, @@ -270,6 +278,73 @@ describe('Accrued Substate', () => { }); }); + describe('L1ToL1MessageExists', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + L1ToL2MessageExists.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // msgHashOffset + ...Buffer.from('456789AB', 'hex'), // msgLeafIndexOffset + ...Buffer.from('CDEF0123', 'hex'), // existsOffset + ]); + const inst = new L1ToL2MessageExists( + /*indirect=*/ 0x01, + /*msgHashOffset=*/ 0x12345678, + /*msgLeafIndexOffset=*/ 0x456789ab, + /*existsOffset=*/ 0xcdef0123, + ); + + expect(L1ToL2MessageExists.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should correctly show false when L1ToL2 message does not exist', async () => { + const msgHash = new Field(69n); + const leafIndex = new Field(42n); + const msgHashOffset = 0; + const msgLeafIndexOffset = 1; + const existsOffset = 2; + + context.machineState.memory.set(msgHashOffset, msgHash); + context.machineState.memory.set(msgLeafIndexOffset, leafIndex); + await new L1ToL2MessageExists(/*indirect=*/ 0, msgHashOffset, msgLeafIndexOffset, existsOffset).execute(context); + + // never created, doesn't exist! + const exists = context.machineState.memory.getAs(existsOffset); + expect(exists).toEqual(new Uint8(0)); + + const journalState = context.persistableState.flush(); + expect(journalState.l1ToL2MessageChecks.length).toEqual(1); + expect(journalState.l1ToL2MessageChecks[0].exists).toEqual(false); + }); + + it('Should correctly show true when L1ToL2 message exists', async () => { + const msgHash = new Field(69n); + const leafIndex = new Field(42n); + const msgHashOffset = 0; + const msgLeafIndexOffset = 1; + const existsOffset = 2; + + // mock commitments db to show message exists + const commitmentsDb = mock(); + commitmentsDb.getL1ToL2MembershipWitness.mockResolvedValue(initL1ToL2MessageOracleInput(leafIndex.toBigInt())); + const hostStorage = initHostStorage({ commitmentsDb }); + context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); + + context.machineState.memory.set(msgHashOffset, msgHash); + context.machineState.memory.set(msgLeafIndexOffset, leafIndex); + await new L1ToL2MessageExists(/*indirect=*/ 0, msgHashOffset, msgLeafIndexOffset, existsOffset).execute(context); + + // never created, doesn't exist! + const exists = context.machineState.memory.getAs(existsOffset); + expect(exists).toEqual(new Uint8(1)); + + const journalState = context.persistableState.flush(); + expect(journalState.l1ToL2MessageChecks.length).toEqual(1); + expect(journalState.l1ToL2MessageChecks[0].exists).toEqual(true); + }); + }); + describe('EmitUnencryptedLog', () => { it('Should (de)serialize correctly', () => { const buf = Buffer.from([ @@ -305,28 +380,36 @@ describe('Accrued Substate', () => { const buf = Buffer.from([ SendL2ToL1Message.opcode, // opcode 0x01, // indirect - ...Buffer.from('12345678', 'hex'), // offset - ...Buffer.from('a2345678', 'hex'), // length + ...Buffer.from('12345678', 'hex'), // recipientOffset + ...Buffer.from('a2345678', 'hex'), // contentOffset ]); - const inst = new SendL2ToL1Message(/*indirect=*/ 0x01, /*offset=*/ 0x12345678, /*length=*/ 0xa2345678); + const inst = new SendL2ToL1Message( + /*indirect=*/ 0x01, + /*recipientOffset=*/ 0x12345678, + /*contentOffset=*/ 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 recipientOffset = 0; + const recipient = new Fr(42); + const contentOffset = 1; + const content = new Fr(69); - const values = [new Field(69n), new Field(420n), new Field(Field.MODULUS - 1n)]; - context.machineState.memory.setSlice(0, values); + context.machineState.memory.set(recipientOffset, new Field(recipient)); + context.machineState.memory.set(contentOffset, new Field(content)); - const length = values.length; - - await new SendL2ToL1Message(/*indirect=*/ 0, /*offset=*/ startOffset, length).execute(context); + await new SendL2ToL1Message( + /*indirect=*/ 0, + /*recipientOffset=*/ recipientOffset, + /*contentOffset=*/ contentOffset, + ).execute(context); const journalState = context.persistableState.flush(); - const expected = values.map(v => v.toFr()); - expect(journalState.newL1Messages).toEqual([expected]); + expect(journalState.newL1Messages).toEqual([{ recipient: EthAddress.fromField(recipient), content }]); }); }); @@ -337,7 +420,7 @@ describe('Accrued Substate', () => { 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), + new SendL2ToL1Message(/*indirect=*/ 0, /*recipientOffset=*/ 0, /*contentOffset=*/ 1), ]; for (const instruction of instructions) { diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts index eb18a030fe5..669c6035a9d 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts @@ -118,6 +118,41 @@ export class EmitNullifier extends Instruction { } } +export class L1ToL2MessageExists extends Instruction { + static type: string = 'L1TOL2MSGEXISTS'; + static readonly opcode: Opcode = Opcode.L1TOL2MSGEXISTS; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, + ]; + + constructor( + private indirect: number, + private msgHashOffset: number, + private msgLeafIndexOffset: number, + private existsOffset: number, + ) { + super(); + } + + async execute(context: AvmContext): Promise { + if (context.environment.isStaticCall) { + throw new StaticCallStorageAlterError(); + } + + const msgHash = context.machineState.memory.get(this.msgHashOffset).toFr(); + const msgLeafIndex = context.machineState.memory.get(this.msgLeafIndexOffset).toFr(); + const exists = await context.persistableState.checkL1ToL2MessageExists(msgHash, msgLeafIndex); + context.machineState.memory.set(this.existsOffset, exists ? new Uint8(1) : new Uint8(0)); + + context.machineState.incrementPc(); + } +} + export class EmitUnencryptedLog extends Instruction { static type: string = 'EMITUNENCRYPTEDLOG'; static readonly opcode: Opcode = Opcode.EMITUNENCRYPTEDLOG; @@ -146,7 +181,7 @@ export class SendL2ToL1Message extends Instruction { // Informs (de)serialization. See Instruction.deserialize. static readonly wireFormat = [OperandType.UINT8, OperandType.UINT8, OperandType.UINT32, OperandType.UINT32]; - constructor(private indirect: number, private msgOffset: number, private msgSize: number) { + constructor(private indirect: number, private recipientOffset: number, private contentOffset: number) { super(); } @@ -155,8 +190,9 @@ export class SendL2ToL1Message extends Instruction { throw new StaticCallStorageAlterError(); } - const msg = context.machineState.memory.getSlice(this.msgOffset, this.msgSize).map(f => f.toFr()); - context.persistableState.writeL1Message(msg); + const recipient = context.machineState.memory.get(this.recipientOffset).toFr(); + const content = context.machineState.memory.get(this.contentOffset).toFr(); + context.persistableState.writeL1Message(recipient, content); context.machineState.incrementPc(); } diff --git a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts index f0c8dd8f16c..8650c34819e 100644 --- a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts @@ -21,6 +21,7 @@ import { InternalReturn, Jump, JumpI, + L1ToL2MessageExists, Lt, Lte, Mov, @@ -117,7 +118,7 @@ const INSTRUCTION_SET = () => [EmitNoteHash.opcode, EmitNoteHash], // Notes & Nullifiers [NullifierExists.opcode, NullifierExists], // Notes & Nullifiers [EmitNullifier.opcode, EmitNullifier], // Notes & Nullifiers - //[Readl1tol2msg.opcode, Readl1tol2msg], // Messages + [L1ToL2MessageExists.opcode, L1ToL2MessageExists], // Messages //[HeaderMember.opcode, HeaderMember], // Header // Accrued Substate diff --git a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts index 22eb3f366dc..963c457c299 100644 --- a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts @@ -55,7 +55,7 @@ export enum Opcode { EMITNOTEHASH, NULLIFIEREXISTS, EMITNULLIFIER, - READL1TOL2MSG, + L1TOL2MSGEXISTS, HEADERMEMBER, EMITUNENCRYPTEDLOG, SENDL2TOL1MSG, diff --git a/yellow-paper/docs/public-vm/execution.md b/yellow-paper/docs/public-vm/execution.md index 1b8e6f865a4..7f957f041cf 100644 --- a/yellow-paper/docs/public-vm/execution.md +++ b/yellow-paper/docs/public-vm/execution.md @@ -215,7 +215,7 @@ The AVM's exceptional halting conditions area listed below: AND worldStateAccessTrace.newNoteHashes.length <= 1024 AND worldStateAccessTrace.nullifierChecks.length <= 1024 AND worldStateAccessTrace.newNullifiers.length <= 1024 - AND worldStateAccessTrace.l1ToL2MessageReads.length <= 1024 + AND worldStateAccessTrace.l1ToL2MessageChecks.length <= 1024 AND worldStateAccessTrace.archiveChecks.length <= 1024 // Storage @@ -237,8 +237,8 @@ The AVM's exceptional halting conditions area listed below: OR newNullifiers.length < 1024 // Read L1 to L2 messages - assert instructions[machineState.pc].opcode != READL1TOL2MSG - OR worldStateAccessTrace.l1ToL2MessagesReads.length < 1024 + assert instructions[machineState.pc].opcode != L1TOL2MSGEXISTS + OR worldStateAccessTrace.l1ToL2MessagesChecks.length < 1024 // Archive tree & Headers assert instructions[machineState.pc].opcode != HEADERMEMBER @@ -250,7 +250,7 @@ The AVM's exceptional halting conditions area listed below: AND accruedSubstate.sentL2ToL1Messages.length <= MAX_SENT_L2_TO_L1_MESSAGES // Unencrypted logs - assert instructions[machineState.pc].opcode != ULOG + assert instructions[machineState.pc].opcode != EMITUNENCRYPTEDLOG OR unencryptedLogs.length < MAX_UNENCRYPTED_LOGS // Sent L2 to L1 messages diff --git a/yellow-paper/docs/public-vm/gen/_instruction-set.mdx b/yellow-paper/docs/public-vm/gen/_instruction-set.mdx index d5da4c5c1df..a3f3f577049 100644 --- a/yellow-paper/docs/public-vm/gen/_instruction-set.mdx +++ b/yellow-paper/docs/public-vm/gen/_instruction-set.mdx @@ -362,17 +362,13 @@ M[existsOffset] = exists`} - 0x30 [`READL1TOL2MSG`](#isa-section-readl1tol2msg) - Check if a message exists in the L1-to-L2 message tree and reads it if so + 0x30 [`L1TOL2MSGEXISTS`](#isa-section-l1tol2msgexists) + Check if a message exists in the L1-to-L2 message tree {`exists = context.worldState.l1ToL2Messages.has({ - leafIndex: M[msgLeafIndex], leaf: M[msgKeyOffset] + leafIndex: M[msgLeafIndexOffset], leaf: M[msgHashOffset] }) -M[existsOffset] = exists -if exists: - M[dstOffset:dstOffset+msgSize] = context.worldState.l1ToL2Messages.get({ - leafIndex: M[msgLeafIndex], leaf: M[msgKeyOffset] - })`} +M[existsOffset] = exists`} @@ -407,8 +403,8 @@ if exists: {`context.accruedSubstate.sentL2ToL1Messages.append( SentL2ToL1Message { address: context.environment.address, - portal: context.environment.portal, - message: M[msgOffset:msgOffset+msgSize] + recipient: M[recipientOffset], + message: M[contentOffset] } )`} @@ -1401,7 +1397,7 @@ Emit a new note hash to be inserted into the note hash tree {`context.worldStateAccessTrace.newNoteHashes.append( TracedNoteHash { callPointer: context.environment.callPointer, - value: M[noteHashOffset], // unsiloed note hash + noteHash: M[noteHashOffset], // unsiloed note hash counter: ++context.worldStateAccessTrace.accessCounter, } )`} @@ -1468,7 +1464,7 @@ Emit a new nullifier to be inserted into the nullifier tree {`context.worldStateAccessTrace.newNullifiers.append( TracedNullifier { callPointer: context.environment.callPointer, - value: M[nullifierOffset], // unsiloed nullifier + nullifier: M[nullifierOffset], // unsiloed nullifier counter: ++context.worldStateAccessTrace.accessCounter, } )`} @@ -1478,54 +1474,44 @@ Emit a new nullifier to be inserted into the nullifier tree [![](./images/bit-formats/EMITNULLIFIER.png)](./images/bit-formats/EMITNULLIFIER.png) -### `READL1TOL2MSG` -Check if a message exists in the L1-to-L2 message tree and reads it if so +### `L1TOL2MSGEXISTS` +Check if a message exists in the L1-to-L2 message tree -[See in table.](#isa-table-readl1tol2msg) +[See in table.](#isa-table-l1tol2msgexists) - **Opcode**: 0x30 - **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]`. - **Args**: - - **msgKeyOffset**: memory offset of the message's key - - **msgLeafIndex**: memory offset of the message's leaf index in the L1-to-L2 message tree + - **msgHashOffset**: memory offset of the message hash + - **msgLeafIndexOffset**: memory offset of the message's leaf index in the L1-to-L2 message tree - **existsOffset**: memory offset specifying where to store operation's result (whether the message exists in the L1-to-L2 message tree) - - **dstOffset**: memory offset to place the 0th word of the message content - - **msgSize**: number of words in the message - **Expression**: {`exists = context.worldState.l1ToL2Messages.has({ - leafIndex: M[msgLeafIndex], leaf: M[msgKeyOffset] + leafIndex: M[msgLeafIndexOffset], leaf: M[msgHashOffset] }) -M[existsOffset] = exists -if exists: - M[dstOffset:dstOffset+msgSize] = context.worldState.l1ToL2Messages.get({ - leafIndex: M[msgLeafIndex], leaf: M[msgKeyOffset] - })`} +M[existsOffset] = exists`} - **World State access tracing**: -{`context.worldStateAccessTrace.l1ToL2MessagesReads.append( - ReadL1ToL2Message { +{`context.worldStateAccessTrace.l1ToL2MessagesChecks.append( + L1ToL2Message { callPointer: context.environment.callPointer, - portal: context.environment.portal, - leafIndex: M[msgLeafIndex], - msgKey: M[msgKeyOffset], + leafIndex: M[msgLeafIndexOffset], + msgHash: M[msgHashOffset], exists: exists, // defined above } )`} -- **Additional AVM circuit checks**: `msgKey == sha256_to_field(msg)` - **Triggers downstream circuit operations**: L1-to-L2 message tree membership check - **Tag updates**: -{`T[existsOffset] = u8, -T[dstOffset:dstOffset+msgSize] = field`} +{`T[existsOffset] = u8,`} -- **Bit-size**: 184 +- **Bit-size**: 120 -[![](./images/bit-formats/READL1TOL2MSG.png)](./images/bit-formats/READL1TOL2MSG.png) ### `HEADERMEMBER` Check if a header exists in the [archive tree](../state/archive) and retrieve the specified member if so @@ -1605,15 +1591,15 @@ Send an L2-to-L1 message - **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**: - - **msgOffset**: memory offset of the message content - - **msgSize**: number of words in the message + - **recipientOffset**: memory offset of the message recipient + - **contentOffset**: memory offset of the message content - **Expression**: {`context.accruedSubstate.sentL2ToL1Messages.append( SentL2ToL1Message { address: context.environment.address, - portal: context.environment.portal, - message: M[msgOffset:msgOffset+msgSize] + recipient: M[recipientOffset], + message: M[contentOffset] } )`} diff --git a/yellow-paper/docs/public-vm/state.md b/yellow-paper/docs/public-vm/state.md index 473168bb3e9..5414036bc1f 100644 --- a/yellow-paper/docs/public-vm/state.md +++ b/yellow-paper/docs/public-vm/state.md @@ -27,11 +27,11 @@ This section describes the types of state maintained by the AVM. | State | Tree | Merkle Tree Type | AVM Access | | --- | --- | --- | --- | -| Public Storage | Public Data Tree | Updatable | membership-checks (latest), reads, writes | -| Note Hashes | Note Hash Tree | Append-only | membership-checks (start-of-block), appends | -| Nullifiers | Nullifier Tree | Indexed | membership-checks (latest), appends | -| L1-to-L2 Messages | L1-to-L2 Message Tree | Append-only | membership-checks (start-of-block), leaf-preimage-reads | -| Headers | Archive Tree | Append-only | membership-checks, leaf-preimage-reads | +| Public Storage | Public Data Tree | Updatable | membership-checks (latest), reads, writes | +| Note Hashes | Note Hash Tree | Append-only | membership-checks (start-of-block), appends | +| Nullifiers | Nullifier Tree | Indexed | membership-checks (latest), appends | +| L1-to-L2 Messages | L1-to-L2 Message Tree | Append-only | membership-checks (start-of-block) | +| Headers | Archive Tree | Append-only | membership-checks, leaf-preimage-reads | | Contracts\* | - | - | - | > \* As described in ["Contract Deployment"](../contract-deployment), contracts are not stored in a dedicated tree. A [contract class](../contract-deployment/classes) is [represented](../contract-deployment/classes#registration) as an unencrypted log containing the `ContractClass` structure (which contains the bytecode) and a nullifier representing the class identifier. A [contract instance](../contract-deployment/instances) is [represented](../contract-deployment/classes#registration) as an unencrypted log containing the `ContractInstance` structure and a nullifier representing the contract address. @@ -52,7 +52,7 @@ The following table defines an AVM context's world state interface: | `publicStorage` | [`SLOAD`](./instruction-set#isa-section-sload) (membership-checks (latest) & reads), [`SSTORE`](./instruction-set#isa-section-sstore) (writes) | | `noteHashes` | [`NOTEHASHEXISTS`](./instruction-set#isa-section-notehashexists) (membership-checks (start-of-block)), [`EMITNOTEHASH`](./instruction-set#isa-section-emitnotehash) (appends) | | `nullifiers` | [`NULLIFIERSEXISTS`](./instruction-set#isa-section-nullifierexists) membership-checks (latest), [`EMITNULLIFIER`](./instruction-set#isa-section-emitnullifier) (appends) | -| `l1ToL2Messages` | [`READL1TOL2MSG`](./instruction-set#isa-section-readl1tol2msg) (membership-checks (start-of-block) & leaf-preimage-reads) | +| `l1ToL2Messages` | [`L1TOL2MSGEXISTS`](./instruction-set#isa-section-l1tol2msgexists) (membership-checks (start-of-block)) | | `headers` | [`HEADERMEMBER`](./instruction-set#isa-section-headermember) (membership-checks & leaf-preimage-reads) | > \* `*CALL` is short for `CALL`/`STATICCALL`/`DELEGATECALL`. @@ -74,15 +74,15 @@ Each entry in the world state access trace is listed below along with its type a | Field | Relevant State | Type | Instructions | | --- | --- | --- | --- | | `accessCounter` | all state | `field` | incremented by all instructions below | -| `contractCalls` | Contracts | `Vector` | [`*CALL`](./instruction-set#isa-section-call) | -| `publicStorageReads` | Public Storage | `Vector` | [`SLOAD`](./instruction-set#isa-section-sload) | -| `publicStorageWrites` | Public Storage | `Vector` | [`SSTORE`](./instruction-set#isa-section-sstore) | -| `noteHashChecks` | Note Hashes | `Vector` | [`NOTEHASHEXISTS`](./instruction-set#isa-section-notehashexists) | -| `newNoteHashes` | Note Hashes | `Vector` | [`EMITNOTEHASH`](./instruction-set#isa-section-emitnotehash) | +| `contractCalls` | Contracts | `Vector` | [`*CALL`](./instruction-set#isa-section-call) | +| `publicStorageReads` | Public Storage | `Vector` | [`SLOAD`](./instruction-set#isa-section-sload) | +| `publicStorageWrites` | Public Storage | `Vector` | [`SSTORE`](./instruction-set#isa-section-sstore) | +| `noteHashChecks` | Note Hashes | `Vector` | [`NOTEHASHEXISTS`](./instruction-set#isa-section-notehashexists) | +| `newNoteHashes` | Note Hashes | `Vector` | [`EMITNOTEHASH`](./instruction-set#isa-section-emitnotehash) | | `nullifierChecks` | Nullifiers | `Vector` | [`NULLIFIERSEXISTS`](./instruction-set#isa-section-nullifierexists) | -| `newNullifiers` | Nullifiers | `Vector` | [`EMITNULLIFIER`](./instruction-set#isa-section-emitnullifier) | -| `l1ToL2MessageReads` | L1-To-L2 Messages | `Vector` | [`READL1TOL2MSG`](./instruction-set#isa-section-readl1tol2msg) | -| `archiveChecks` | Headers | `Vector` | [`HEADERMEMBER`](./instruction-set#isa-section-headermember) | +| `newNullifiers` | Nullifiers | `Vector` | [`EMITNULLIFIER`](./instruction-set#isa-section-emitnullifier) | +| `l1ToL2MessageChecks` | L1-To-L2 Messages | `Vector` | [`L1TOL2MSGEXISTS`](./instruction-set#isa-section-l1tol2msgexists) | +| `archiveChecks` | Headers | `Vector` | [`HEADERMEMBER`](./instruction-set#isa-section-headermember) | > The types tracked in these trace vectors are defined [here](./type-structs). @@ -100,7 +100,7 @@ Each entry in the world state access trace is listed below along with its type a | Field | Type | Instructions | | --- | --- | --- | -| `unencryptedLogs` | `Vector` | [`ULOG`](./instruction-set#isa-secction-ulog) | -| `sentL2ToL1Messages` | `Vector` | [`SENDL1TOL2MSG`](./instruction-set#isa-secction-sendl2tol1msg) | +| `unencryptedLogs` | `Vector` | [`EMITUNENCRYPTEDLOG`](./instruction-set#isa-section-emitunencryptedlog) | +| `sentL2ToL1Messages` | `Vector` | [`SENDL1TOL2MSG`](./instruction-set#isa-section-sendl2tol1msg) | > The types tracked in these vectors are defined [here](./type-structs). diff --git a/yellow-paper/docs/public-vm/type-structs.md b/yellow-paper/docs/public-vm/type-structs.md index c973fc3cec4..a02360fb77c 100644 --- a/yellow-paper/docs/public-vm/type-structs.md +++ b/yellow-paper/docs/public-vm/type-structs.md @@ -12,16 +12,14 @@ This section lists type definitions relevant to AVM State and Circuit I/O. | `counter` | `field` | When did this occur relative to other world state accesses. | | `endLifetime` | `field` | End lifetime of a call. Final `accessCounter` for reverted calls, `endLifetime` of parent for successful calls. Successful initial/top-level calls have infinite (max-value) `endLifetime`. | -#### _TracedL1ToL2MessageRead_ +#### _TracedL1ToL2MessageCheck_ | Field | Type | Description | | --- | --- | --- | | `callPointer` | `field` | Associates this item with a `TracedContractCall` entry in `worldStateAccessTrace.contractCalls` | -| `portal` | `EthAddress` | | | `leafIndex` | `field` | | -| `msgKey` | `field` | The entry key which is also the tree leaf value. | +| `msgHash` | `field` | The message hash which is also the tree leaf value. | | `exists` | `field` | | -| `message` | `[field; MAX_L1_TO_L2_MESSAGE_LENGTH]` | **Omitted from public inputs** | | `endLifetime` | `field` | Equivalent to `endLifetime` of the containing contract call. | #### _TracedStorageRead_ @@ -61,7 +59,7 @@ This section lists type definitions relevant to AVM State and Circuit I/O. | Field | Type | Description | | --- | --- | --- | | `callPointer` | `field` | Associates this item with a `TracedContractCall` entry in `worldStateAccessTrace.contractCalls` | -| `value` | `field` | | +| `noteHash` | `field` | | | `counter` | `field` | | | `endLifetime` | `field` | Equivalent to `endLifetime` of the containing contract call. The last `counter` at which this object should be considered to "exist" if this call or a parent reverted. | @@ -82,7 +80,7 @@ This section lists type definitions relevant to AVM State and Circuit I/O. | Field | Type | Description | | --- | --- | --- | | `callPointer` | `field` | Associates this item with a `TracedContractCall` entry in `worldStateAccessTrace.contractCalls` | -| `value` | `field` | | +| `nullifier` | `field` | | | `counter` | `field` | | | `endLifetime` | `field` | Equivalent to `endLifetime` of the containing contract call. The last `counter` at which this object should be considered to "exist" if this call or a parent reverted. | @@ -102,8 +100,8 @@ This section lists type definitions relevant to AVM State and Circuit I/O. #### _SentL2ToL1Message_ -| Field | Type | Description | -| --- | --- | --- | -| `address` | `AztecAddress` | Contract address that emitted the message. | -| `portal` | `EthAddress` | L1 portal address to send the message to. | -| `message` | `[field, MAX_L2_TO_L1_MESSAGE_LENGTH]` | | +| Field | Type | Description | +| --- | --- | --- | +| `address` | `AztecAddress` | L2 contract address that emitted the message. | +| `recipient` | `EthAddress` | L1 contract address to send the message to. | +| `content` | `field` | Message content. | diff --git a/yellow-paper/src/preprocess/InstructionSet/InstructionSet.js b/yellow-paper/src/preprocess/InstructionSet/InstructionSet.js index 5b89dfa10c5..ebfd3d0ffb3 100644 --- a/yellow-paper/src/preprocess/InstructionSet/InstructionSet.js +++ b/yellow-paper/src/preprocess/InstructionSet/InstructionSet.js @@ -876,7 +876,7 @@ context.worldState.noteHashes.append( context.worldStateAccessTrace.newNoteHashes.append( TracedNoteHash { callPointer: context.environment.callPointer, - value: M[noteHashOffset], // unsiloed note hash + noteHash: M[noteHashOffset], // unsiloed note hash counter: ++context.worldStateAccessTrace.accessCounter, } ) @@ -937,7 +937,7 @@ context.worldState.nullifiers.append( context.worldStateAccessTrace.newNullifiers.append( TracedNullifier { callPointer: context.environment.callPointer, - value: M[nullifierOffset], // unsiloed nullifier + nullifier: M[nullifierOffset], // unsiloed nullifier counter: ++context.worldStateAccessTrace.accessCounter, } ) @@ -947,47 +947,38 @@ context.worldStateAccessTrace.newNullifiers.append( "Tag updates": "", }, { - "id": "readl1tol2msg", - "Name": "`READL1TOL2MSG`", + "id": "l1tol2msgexists", + "Name": "`L1TOL2MSGEXISTS`", "Category": "World State - Messaging", "Flags": [ {"name": "indirect", "description": INDIRECT_FLAG_DESCRIPTION}, ], "Args": [ - {"name": "msgKeyOffset", "description": "memory offset of the message's key"}, - {"name": "msgLeafIndex", "description": "memory offset of the message's leaf index in the L1-to-L2 message tree"}, + {"name": "msgHashOffset", "description": "memory offset of the message hash"}, + {"name": "msgLeafIndexOffset", "description": "memory offset of the message's leaf index in the L1-to-L2 message tree"}, {"name": "existsOffset", "description": "memory offset specifying where to store operation's result (whether the message exists in the L1-to-L2 message tree)"}, - {"name": "dstOffset", "description": "memory offset to place the 0th word of the message content"}, - {"name": "msgSize", "description": "number of words in the message", "mode": "immediate", "type": "u32"}, ], "Expression": ` exists = context.worldState.l1ToL2Messages.has({ - leafIndex: M[msgLeafIndex], leaf: M[msgKeyOffset] + leafIndex: M[msgLeafIndexOffset], leaf: M[msgHashOffset] }) M[existsOffset] = exists -if exists: - M[dstOffset:dstOffset+msgSize] = context.worldState.l1ToL2Messages.get({ - leafIndex: M[msgLeafIndex], leaf: M[msgKeyOffset] - }) `, - "Summary": "Check if a message exists in the L1-to-L2 message tree and reads it if so", + "Summary": "Check if a message exists in the L1-to-L2 message tree", "World State access tracing": ` -context.worldStateAccessTrace.l1ToL2MessagesReads.append( - ReadL1ToL2Message { +context.worldStateAccessTrace.l1ToL2MessagesChecks.append( + L1ToL2Message { callPointer: context.environment.callPointer, - portal: context.environment.portal, - leafIndex: M[msgLeafIndex], - msgKey: M[msgKeyOffset], + leafIndex: M[msgLeafIndexOffset], + msgHash: M[msgHashOffset], exists: exists, // defined above } ) `, - "Additional AVM circuit checks": "`msgKey == sha256_to_field(msg)`", "Triggers downstream circuit operations": "L1-to-L2 message tree membership check", "Tag checks": "", "Tag updates": ` T[existsOffset] = u8, -T[dstOffset:dstOffset+msgSize] = field `, }, { @@ -1060,15 +1051,15 @@ context.accruedSubstate.unencryptedLogs.append( {"name": "indirect", "description": INDIRECT_FLAG_DESCRIPTION}, ], "Args": [ - {"name": "msgOffset", "description": "memory offset of the message content"}, - {"name": "msgSize", "description": "number of words in the message", "mode": "immediate", "type": "u32"}, + {"name": "recipientOffset", "description": "memory offset of the message recipient"}, + {"name": "contentOffset", "description": "memory offset of the message content"}, ], "Expression": ` context.accruedSubstate.sentL2ToL1Messages.append( SentL2ToL1Message { address: context.environment.address, - portal: context.environment.portal, - message: M[msgOffset:msgOffset+msgSize] + recipient: M[recipientOffset], + message: M[contentOffset] } ) `,