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 5, 2024
1 parent 23eaf11 commit 2c5cb97
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 31 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 @@ -238,6 +238,9 @@ fn handle_foreign_call(
inputs: &Vec<ValueOrArray>,
) {
match function {
"amvOpcodeEmitUnencryptedLog" => {
handle_emit_unencrypted_log(avm_instrs, destinations, inputs)
},
"avmOpcodeNoteHashExists" => handle_note_hash_exists(avm_instrs, destinations, inputs),
"avmOpcodeEmitNoteHash" | "avmOpcodeEmitNullifier" => handle_emit_note_hash_or_nullifier(
function == "avmOpcodeEmitNullifier",
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
12 changes: 12 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,17 @@ impl AVMContext {
#[oracle(avmOpcodeEmitNullifier)]
pub fn emit_nullifier(self, nullifier: Field) {}

/**
* Emit a log with the given event selector and message.
*
* @param event_selector The event selector for the log.
* @param message The message to emit in the log.
* Should be automatically convertible to [Field; N]. For example str<N> works with
* one char per field. Otherwise you can use CompressedString.
*/
#[oracle(amvOpcodeEmitUnencryptedLog)]
pub fn emit_unencrypted_log<T>(self, event_selector: Field, message: T) {}

#[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 @@ -6,3 +6,4 @@ type = "contract"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
compressed_string = { path = "../../../aztec-nr/compressed-string" }
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
contract AvmTest {
// Libs
use dep::aztec::protocol_types::{address::{AztecAddress, EthAddress}, constants::L1_TO_L2_MESSAGE_LENGTH};
use dep::compressed_string::CompressedString;

// avm lib
use dep::aztec::avm::hash::{keccak256, poseidon, sha256};
Expand Down Expand Up @@ -140,6 +141,16 @@ contract AvmTest {
// context.contract_call_depth()
// }

#[aztec(public-vm)]
fn emit_unencrypted_log() {
context.emit_unencrypted_log(/*event_selector=*/ 5, /*message=*/ [10, 20, 30]);
context.emit_unencrypted_log(/*event_selector=*/ 8, /*message=*/ "Hello, world!");
// FIXME: Try this once Brillig codegen produces uniform bit sizes for LT
// FIXME: TagCheckError: Tag mismatch at offset 22, got UINT64, expected UINT32
// let s: CompressedString<1,13> = CompressedString::from_string("Hello, world!");
// context.emit_unencrypted_log(/*event_selector=*/ 10, /*message=*/ s);
}

#[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
40 changes: 40 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,44 @@ 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 unencrypted logs (should be traced)`, async () => {
// Get contract function artifact
const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_emit_unencrypted_log')!;

// Decode bytecode into instructions
const bytecode = Buffer.from(artifact.bytecode, 'base64');

const context = initContext();
jest
.spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode')
.mockReturnValue(Promise.resolve(bytecode));

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

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

const expectedFields = [new Fr(10), new Fr(20), new Fr(30)];
const expectedString = 'Hello, world!'.split('').map(c => new Fr(c.charCodeAt(0)));
// FIXME: Try this once Brillig codegen produces uniform bit sizes for LT
// const expectedCompressedString = Buffer.from('Hello, world!');
expect(context.persistableState.flush().newLogs).toEqual([
new UnencryptedL2Log(
context.environment.address,
new EventSelector(5),
Buffer.concat(expectedFields.map(f => f.toBuffer())),
),
new UnencryptedL2Log(
context.environment.address,
new EventSelector(8),
Buffer.concat(expectedString.map(f => f.toBuffer())),
),
// new UnencryptedL2Log(
// context.environment.address,
// new EventSelector(10),
// expectedCompressedString,
// ),
]);
});
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
41 changes: 30 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,15 @@ 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 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 +170,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,7 +197,18 @@ 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 },
Expand Down Expand Up @@ -228,8 +241,8 @@ 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 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 +252,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 +262,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 +298,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 { AztecAddress, EthAddress, L2ToL1Message } 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
32 changes: 24 additions & 8 deletions yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { UnencryptedL2Log } from '@aztec/circuit-types';
import { EthAddress, Fr } from '@aztec/circuits.js';
import { EventSelector } from '@aztec/foundation/abi';

import { mock } from 'jest-mock-extended';

Expand Down Expand Up @@ -354,28 +356,42 @@ describe('Accrued Substate', () => {
const buf = Buffer.from([
EmitUnencryptedLog.opcode, // opcode
0x01, // indirect
...Buffer.from('02345678', 'hex'), // event selector offset
...Buffer.from('12345678', 'hex'), // offset
...Buffer.from('a2345678', 'hex'), // length
]);
const inst = new EmitUnencryptedLog(/*indirect=*/ 0x01, /*offset=*/ 0x12345678, /*length=*/ 0xa2345678);
const inst = new EmitUnencryptedLog(
/*indirect=*/ 0x01,
/*eventSelectorOffset=*/ 0x02345678,
/*offset=*/ 0x12345678,
/*length=*/ 0xa2345678,
);

expect(EmitUnencryptedLog.deserialize(buf)).toEqual(inst);
expect(inst.serialize()).toEqual(buf);
});

it('Should append unencrypted logs correctly', async () => {
const startOffset = 0;
const eventSelector = 5;
const eventSelectorOffset = 10;

const values = [new Field(69n), new Field(420n), new Field(Field.MODULUS - 1n)];
context.machineState.memory.setSlice(0, values);

const length = values.length;
context.machineState.memory.setSlice(startOffset, values);
context.machineState.memory.set(eventSelectorOffset, new Field(eventSelector));

await new EmitUnencryptedLog(/*indirect=*/ 0, /*offset=*/ startOffset, length).execute(context);
await new EmitUnencryptedLog(
/*indirect=*/ 0,
eventSelectorOffset,
/*offset=*/ startOffset,
values.length,
).execute(context);

const journalState = context.persistableState.flush();
const expected = values.map(v => v.toFr());
expect(journalState.newLogs).toEqual([expected]);
const expectedLog = Buffer.concat(values.map(v => v.toFr().toBuffer()));
expect(journalState.newLogs).toEqual([
new UnencryptedL2Log(context.environment.address, new EventSelector(eventSelector), expectedLog),
]);
});
});

Expand Down Expand Up @@ -423,7 +439,7 @@ describe('Accrued Substate', () => {
const instructions = [
new EmitNoteHash(/*indirect=*/ 0, /*offset=*/ 0),
new EmitNullifier(/*indirect=*/ 0, /*offset=*/ 0),
new EmitUnencryptedLog(/*indirect=*/ 0, /*offset=*/ 0, 1),
new EmitUnencryptedLog(/*indirect=*/ 0, /*eventSelector=*/ 0, /*offset=*/ 0, /*logSize=*/ 1),
new SendL2ToL1Message(/*indirect=*/ 0, /*recipientOffset=*/ 0, /*contentOffset=*/ 1),
];

Expand Down
Loading

0 comments on commit 2c5cb97

Please sign in to comment.