Skip to content

Commit

Permalink
feat(avm-simulator): implement EMITUNENCRYPTEDLOG
Browse files Browse the repository at this point in the history
  • Loading branch information
fcarreiro committed Mar 4, 2024
1 parent fa53136 commit 2cf107f
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 30 deletions.
2 changes: 1 addition & 1 deletion avm-transpiler/src/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::opcodes::AvmOpcode;
pub const ALL_DIRECT: u8 = 0b00000000;
pub const ZEROTH_OPERAND_INDIRECT: u8 = 0b00000001;
pub const FIRST_OPERAND_INDIRECT: u8 = 0b00000010;
pub const ZEROTH_FIRST_OPERANDS_INDIRECT: u8 = 0b00000011;
pub const ZEROTH_FIRST_OPERANDS_INDIRECT: u8 = ZEROTH_OPERAND_INDIRECT | FIRST_OPERAND_INDIRECT;

/// A simple representation of an AVM instruction for the purpose
/// of generating an AVM bytecode from Brillig.
Expand Down
40 changes: 40 additions & 0 deletions avm-transpiler/src/transpile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ fn handle_foreign_call(
) {
match function {
"avmOpcodeNoteHashExists" => handle_note_hash_exists(avm_instrs, destinations, inputs),
"amvOpcodeEmitUnencryptedLog" => {
handle_emit_unencrypted_log(avm_instrs, destinations, inputs)
},
"avmOpcodeEmitNoteHash" | "avmOpcodeEmitNullifier" => handle_emit_note_hash_or_nullifier(
function == "avmOpcodeEmitNullifier",
avm_instrs,
Expand Down Expand Up @@ -306,6 +309,43 @@ fn handle_note_hash_exists(
});
}

fn handle_emit_unencrypted_log(
avm_instrs: &mut Vec<AvmInstruction>,
destinations: &Vec<ValueOrArray>,
inputs: &Vec<ValueOrArray>,
) {
if destinations.len() != 0 || inputs.len() != 2 {
panic!(
"Transpiler expects ForeignCall::EMITUNENCRYPTEDLOG to have 0 destinations and 3 inputs, got {} and {}",
destinations.len(),
inputs.len()
);
}
let (event_offset, message_array) = match &inputs[..] {
[ValueOrArray::MemoryAddress(offset), ValueOrArray::HeapArray(array)] => {
(offset.to_usize() as u32, array)
}
_ => panic!("Unexpected inputs for ForeignCall::EMITUNENCRYPTEDLOG: {:?}", inputs),
};
avm_instrs.push(AvmInstruction {
opcode: AvmOpcode::EMITUNENCRYPTEDLOG,
// The message array from Brillig is indirect.
indirect: Some(FIRST_OPERAND_INDIRECT),
operands: vec![
AvmOperand::U32 {
value: event_offset,
},
AvmOperand::U32 {
value: message_array.pointer.to_usize() as u32,
},
AvmOperand::U32 {
value: message_array.size as u32,
},
],
..Default::default()
});
}

/// Handle an AVM EMITNOTEHASH or EMITNULLIFIER instruction
/// (an emitNoteHash or emitNullifier brillig foreign call was encountered)
/// Adds the new instruction to the avm instructions list.
Expand Down
23 changes: 23 additions & 0 deletions noir-projects/aztec-nr/aztec/src/context/avm.nr
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use dep::protocol_types::{address::{AztecAddress, EthAddress}, constants::L1_TO_L2_MESSAGE_LENGTH};
use dep::protocol_types::traits::{Serialize};

// Getters that will be converted by the transpiler into their
// own opcodes
Expand Down Expand Up @@ -62,6 +63,28 @@ impl AVMContext {
#[oracle(avmOpcodeEmitNullifier)]
pub fn emit_nullifier(self, nullifier: Field) {}

#[oracle(amvOpcodeEmitUnencryptedLog)]
pub fn emit_unencrypted_log<N>(self, event_selector: Field, message: [Field; N]) {}

// TODO: document
pub fn emit_unencrypted_log_serialize<T, T_SERIALIZED_LEN>(
self,
event_selector: Field,
message: T
) where T: Serialize<T_SERIALIZED_LEN> {
self.emit_unencrypted_log(event_selector, message.serialize());
}

// Backwards compat?
// pub fn accumulate_unencrypted_logs<T>(
// &mut self,
// _contract_address: AztecAddress,
// event_selector: Field,
// message: T
// ) {
// self.emit_unencrypted_log(event_selector, message);
// }

#[oracle(avmOpcodeL1ToL2MsgExists)]
pub fn l1_to_l2_msg_exists(self, msg_hash: Field, msg_leaf_index: Field) -> u8 {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ contract AvmTest {
// context.contract_call_depth()
// }

#[aztec(public-vm)]
fn emit_unencrypted_log_fields(a: Field, b: Field, c: Field) {
context.emit_unencrypted_log(/*event_selector=*/ 5, /*message=*/ [a, b, c]);
}

#[aztec(public-vm)]
fn emit_unencrypted_log_string() {
context.emit_unencrypted_log(/*event_selector=*/ 8, /*message=*/ "Hello, world!");
}

#[aztec(public-vm)]
fn note_hash_exists(note_hash: Field, leaf_index: Field) -> pub u8 {
context.note_hash_exists(note_hash, leaf_index)
Expand Down
56 changes: 56 additions & 0 deletions yarn-project/simulator/src/avm/avm_simulator.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UnencryptedL2Log } from '@aztec/circuit-types';
import { EventSelector } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { keccak, pedersenHash, poseidonHash, sha256 } from '@aztec/foundation/crypto';
import { EthAddress } from '@aztec/foundation/eth-address';
Expand Down Expand Up @@ -298,6 +300,60 @@ describe('AVM simulator', () => {
const trace = context.persistableState.flush();
expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: true })]);
});
it(`Should execute contract function to emit an unencrypted log from fields (should be traced)`, async () => {
const log = [new Fr(1), new Fr(2), new Fr(3)];
const calldata: Fr[] = log;

// Get contract function artifact
const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_emit_unencrypted_log_fields')!;

// 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));

const results = await new AvmSimulator(context).execute();

expect(results.reverted).toBe(false);

expect(context.persistableState.flush().newLogs).toEqual([
new UnencryptedL2Log(
context.environment.address,
new EventSelector(5),
Buffer.concat(log.map(f => f.toBuffer())),
),
]);
});
it(`Should execute contract function to emit an unencrypted log from strings (should be traced)`, async () => {
const calldata: Fr[] = [];

// Get contract function artifact
const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_emit_unencrypted_log_string')!;

// 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));

const results = await new AvmSimulator(context).execute();

expect(results.reverted).toBe(false);

const expectedLog = "Hello, world!".split('').map(c => new Fr(c.charCodeAt(0)));
expect(context.persistableState.flush().newLogs).toEqual([
new UnencryptedL2Log(
context.environment.address,
new EventSelector(8),
Buffer.concat(expectedLog.map(f => f.toBuffer())),
),
]);
});
it(`Should execute contract function to emit note hash (should be traced)`, async () => {
const utxo = new Fr(42);
const calldata = [utxo];
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/simulator/src/avm/avm_simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class AvmSimulator {
const instruction = instructions[this.context.machineState.pc];
assert(!!instruction); // This should never happen

this.log(`Executing PC=${this.context.machineState.pc}: ${instruction.toString()}`);
this.log.debug(`@${this.context.machineState.pc} ${instruction.toString()}`);
// Execute the instruction.
// Normal returns and reverts will return normally here.
// "Exceptional halts" will throw.
Expand Down
46 changes: 35 additions & 11 deletions yarn-project/simulator/src/avm/journal/journal.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { EthAddress } from '@aztec/circuits.js';
import { UnencryptedL2Log } from '@aztec/circuit-types';
import { AztecAddress, EthAddress } from '@aztec/circuits.js';
import { EventSelector } from '@aztec/foundation/abi';
import { Fr } from '@aztec/foundation/fields';

import { MockProxy, mock } from 'jest-mock-extended';
Expand Down Expand Up @@ -150,15 +152,17 @@ describe('journal', () => {
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 l1Message = [new Fr(1), new Fr(2)];
const l1MessageT1 = [new Fr(3), new Fr(4)];
const log = { address: 10n, selector: 5, data: [new Fr(5), new Fr(6)] };
const logT1 = { address: 20n, selector: 8, data: [new Fr(7), new Fr(8)] };
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.writeLog(new Fr(log.address), new Fr(log.selector), log.data);
journal.writeL1Message(recipient, commitment);
await journal.writeNullifier(contractAddress, commitment);
await journal.checkNullifierExists(contractAddress, commitment);
Expand All @@ -168,7 +172,7 @@ describe('journal', () => {
childJournal.writeStorage(contractAddress, key, valueT1);
await childJournal.readStorage(contractAddress, key);
childJournal.writeNoteHash(commitmentT1);
childJournal.writeLog(logsT1);
childJournal.writeLog(new Fr(logT1.address), new Fr(logT1.selector), logT1.data);
childJournal.writeL1Message(recipient, commitmentT1);
await childJournal.writeNullifier(contractAddress, commitmentT1);
await childJournal.checkNullifierExists(contractAddress, commitmentT1);
Expand All @@ -195,11 +199,23 @@ describe('journal', () => {
expect(slotWrites).toEqual([value, valueT1]);

expect(journalUpdates.newNoteHashes).toEqual([commitment, commitmentT1]);
expect(journalUpdates.newLogs).toEqual([logs, logsT1]);
expect(journalUpdates.newLogs).toEqual([
new UnencryptedL2Log(
AztecAddress.fromBigInt(log.address),
new EventSelector(log.selector),
Buffer.concat(log.data.map(f => f.toBuffer())),
),
new UnencryptedL2Log(
AztecAddress.fromBigInt(logT1.address),
new EventSelector(logT1.selector),
Buffer.concat(logT1.data.map(f => f.toBuffer())),
),
]);
expect(journalUpdates.newL1Messages).toEqual([
{ recipient, content: commitment },
{ recipient, content: commitmentT1 },
]);
expect(journalUpdates.newL1Messages).toEqual([l1Message, l1MessageT1]);
expect(journalUpdates.nullifierChecks).toEqual([
expect.objectContaining({ nullifier: commitment, exists: true }),
expect.objectContaining({ nullifier: commitmentT1, exists: true }),
Expand Down Expand Up @@ -228,8 +244,10 @@ describe('journal', () => {
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 l1Message = [new Fr(1), new Fr(2)];
const l1MessageT1 = [new Fr(3), new Fr(4)];
const log = { address: 10n, selector: 5, data: [new Fr(5), new Fr(6)] };
const logT1 = { address: 20n, selector: 8, data: [new Fr(7), new Fr(8)] };
const index = new Fr(42);
const indexT1 = new Fr(24);

Expand All @@ -239,7 +257,7 @@ describe('journal', () => {
await journal.writeNullifier(contractAddress, commitment);
await journal.checkNullifierExists(contractAddress, commitment);
await journal.checkL1ToL2MessageExists(commitment, index);
journal.writeLog(logs);
journal.writeLog(new Fr(log.address), new Fr(log.selector), log.data);
journal.writeL1Message(recipient, commitment);

const childJournal = new AvmPersistableStateManager(journal.hostStorage, journal);
Expand All @@ -249,7 +267,7 @@ describe('journal', () => {
await childJournal.writeNullifier(contractAddress, commitmentT1);
await childJournal.checkNullifierExists(contractAddress, commitmentT1);
await journal.checkL1ToL2MessageExists(commitmentT1, indexT1);
childJournal.writeLog(logsT1);
childJournal.writeLog(new Fr(logT1.address), new Fr(logT1.selector), logT1.data);
childJournal.writeL1Message(recipient, commitmentT1);

journal.rejectNestedCallState(childJournal);
Expand Down Expand Up @@ -285,7 +303,13 @@ describe('journal', () => {
]);

// Check that rejected Accrued Substate is absent
expect(journalUpdates.newLogs).toEqual([logs]);
expect(journalUpdates.newLogs).toEqual([
new UnencryptedL2Log(
AztecAddress.fromBigInt(log.address),
new EventSelector(log.selector),
Buffer.concat(log.data.map(f => f.toBuffer())),
),
]);
expect(journalUpdates.newL1Messages).toEqual([{ recipient, content: commitment }]);
});

Expand Down
18 changes: 13 additions & 5 deletions yarn-project/simulator/src/avm/journal/journal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { EthAddress, L2ToL1Message } from '@aztec/circuits.js';
import { UnencryptedL2Log } from '@aztec/circuit-types';
import { EthAddress, L2ToL1Message, AztecAddress } from '@aztec/circuits.js';
import { EventSelector } from '@aztec/foundation/abi';
import { Fr } from '@aztec/foundation/fields';

import { HostStorage } from './host_storage.js';
Expand All @@ -18,7 +20,7 @@ export type JournalData = {
l1ToL2MessageChecks: TracedL1toL2MessageCheck[];

newL1Messages: L2ToL1Message[];
newLogs: Fr[][];
newLogs: UnencryptedL2Log[];

/** contract address -\> key -\> value */
currentStorageValue: Map<bigint, Map<bigint, Fr>>;
Expand Down Expand Up @@ -53,7 +55,7 @@ export class AvmPersistableStateManager {

/** Accrued Substate **/
private newL1Messages: L2ToL1Message[] = [];
private newLogs: Fr[][] = [];
private newLogs: UnencryptedL2Log[] = [];

constructor(hostStorage: HostStorage, parent?: AvmPersistableStateManager) {
this.hostStorage = hostStorage;
Expand Down Expand Up @@ -174,8 +176,14 @@ export class AvmPersistableStateManager {
this.newL1Messages.push(new L2ToL1Message(recipientAddress, content));
}

public writeLog(log: Fr[]) {
this.newLogs.push(log);
public writeLog(contractAddress: Fr, event: Fr, log: Fr[]) {
this.newLogs.push(
new UnencryptedL2Log(
AztecAddress.fromField(contractAddress),
EventSelector.fromField(event),
Buffer.concat(log.map(f => f.toBuffer())),
),
);
}

/**
Expand Down
Loading

0 comments on commit 2cf107f

Please sign in to comment.