Skip to content

Commit

Permalink
feat(avm-simulator): implement AVM message opcodes (simulator/transpi…
Browse files Browse the repository at this point in the history
…ler/noir-test)
  • Loading branch information
dbanks12 committed Feb 29, 2024
1 parent 747fc33 commit 7a90a92
Show file tree
Hide file tree
Showing 19 changed files with 633 additions and 187 deletions.
95 changes: 95 additions & 0 deletions avm-transpiler/src/transpile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,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)
}
Expand Down Expand Up @@ -333,6 +335,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<AvmInstruction>,
destinations: &Vec<ValueOrArray>,
inputs: &Vec<ValueOrArray>,
) {
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::READL1TOL2MSG,
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<AvmInstruction>,
destinations: &Vec<ValueOrArray>,
inputs: &Vec<ValueOrArray>,
) {
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:
Expand Down
35 changes: 30 additions & 5 deletions noir-projects/aztec-nr/aztec/src/context/avm.nr
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,6 +10,7 @@ impl AVMContext {
Self {}
}

// OPCODES
#[oracle(address)]
pub fn address(self) -> AztecAddress {}

Expand Down Expand Up @@ -53,16 +54,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);
Expand Down
4 changes: 3 additions & 1 deletion noir-projects/aztec-nr/aztec/src/messaging.nr
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ mod l1_to_l2_message_getter_data;
use l1_to_l2_message_getter_data::make_l1_to_l2_message_getter_data;

use crate::oracle::get_l1_to_l2_message::get_l1_to_l2_message_call;
use crate::messaging::l1_to_l2_message::L1ToL2Message;
use crate::avm::hash::sha256;

use dep::std::merkle::compute_merkle_root;

use dep::protocol_types::address::{AztecAddress, EthAddress};
use dep::protocol_types::{address::{AztecAddress, EthAddress}, constants::{L1_TO_L2_MESSAGE_LENGTH}, utils::arr_copy_slice};

// Returns the nullifier for the message
pub fn process_l1_to_l2_message(
Expand Down
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -154,15 +154,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!");
}

Expand All @@ -173,4 +173,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)
}
}
95 changes: 79 additions & 16 deletions yarn-project/simulator/src/avm/avm_simulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 to emit note hash (should be traced)`, async () => {
const utxo = new Fr(42);
const calldata = [utxo];
Expand Down Expand Up @@ -280,12 +285,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');
Expand All @@ -301,16 +306,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');
Expand All @@ -331,9 +336,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);
Expand All @@ -355,10 +360,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);
Expand All @@ -383,5 +388,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, 'getL1ToL2Message')
.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);
});
});
});
});
Loading

0 comments on commit 7a90a92

Please sign in to comment.