Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(avm-simulator): implement EMITUNENCRYPTEDLOG #4926

Merged
merged 2 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
fcarreiro marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading