Skip to content

Commit

Permalink
feat(avm): contract instance opcode
Browse files Browse the repository at this point in the history
  • Loading branch information
fcarreiro committed Mar 27, 2024
1 parent 688accd commit ea16ba5
Show file tree
Hide file tree
Showing 15 changed files with 306 additions and 14 deletions.
2 changes: 2 additions & 0 deletions avm-transpiler/src/opcodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub enum AvmOpcode {
HEADERMEMBER,
EMITUNENCRYPTEDLOG,
SENDL2TOL1MSG,
CONTRACTINSTANCECOPY,
// External calls
CALL,
STATICCALL,
Expand Down Expand Up @@ -148,6 +149,7 @@ impl AvmOpcode {
// Accrued Substate
AvmOpcode::EMITUNENCRYPTEDLOG => "EMITUNENCRYPTEDLOG",
AvmOpcode::SENDL2TOL1MSG => "SENDL2TOL1MSG",
AvmOpcode::CONTRACTINSTANCECOPY => "CONTRACTINSTANCECOPY",

// Control Flow - Contract Calls
AvmOpcode::CALL => "CALL",
Expand Down
37 changes: 37 additions & 0 deletions avm-transpiler/src/transpile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ fn handle_foreign_call(
"avmOpcodePoseidon" => {
handle_single_field_hash_instruction(avm_instrs, function, destinations, inputs)
}
"getContractInstance" => handle_get_contract_instance(avm_instrs, destinations, inputs),
"storageRead" => handle_storage_read(avm_instrs, destinations, inputs),
"storageWrite" => handle_storage_write(avm_instrs, destinations, inputs),
// Getters.
Expand Down Expand Up @@ -955,6 +956,42 @@ fn handle_storage_write(
})
}

/// Emit a CONTRACTINSTANCECOPY opcode
fn handle_get_contract_instance(
avm_instrs: &mut Vec<AvmInstruction>,
destinations: &Vec<ValueOrArray>,
inputs: &Vec<ValueOrArray>,
) {
assert!(inputs.len() == 1);
assert!(destinations.len() == 1);

let address_offset_maybe = inputs[0];
let address_offset = match address_offset_maybe {
ValueOrArray::MemoryAddress(slot_offset) => slot_offset.0,
_ => panic!("CONTRACTINSTANCECOPY address should be a single value"),
};

let dest_offset_maybe = destinations[0];
let dest_offset = match dest_offset_maybe {
ValueOrArray::HeapArray(HeapArray { pointer, .. }) => pointer.0,
_ => panic!("CONTRACTINSTANCECOPY destination should be an array"),
};

avm_instrs.push(AvmInstruction {
opcode: AvmOpcode::CONTRACTINSTANCECOPY,
indirect: Some(FIRST_OPERAND_INDIRECT),
operands: vec![
AvmOperand::U32 {
value: address_offset as u32,
},
AvmOperand::U32 {
value: dest_offset as u32,
},
],
..Default::default()
})
}

/// Emit a storage read opcode
/// The current implementation reads an array of values from storage ( contiguous slots in memory )
fn handle_storage_read(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ enum class OpCode : uint8_t {
// Accrued Substate
EMITUNENCRYPTEDLOG,
SENDL2TOL1MSG, // Messages
CONTRACTINSTANCECOPY,

// Control Flow - Contract Calls
CALL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ contract AvmTest {
// Libs
use dep::aztec::prelude::Map;
use dep::aztec::state_vars::{PublicImmutable, PublicMutable};
use dep::aztec::protocol_types::{address::{AztecAddress, EthAddress}, constants::L1_TO_L2_MESSAGE_LENGTH};
use dep::aztec::protocol_types::{
address::{AztecAddress, EthAddress}, constants::L1_TO_L2_MESSAGE_LENGTH,
contract_instance::ContractInstance
};
use dep::aztec::oracle::get_contract_instance::{get_contract_instance, get_contract_instance_internal};
use dep::aztec::protocol_types::abis::function_selector::FunctionSelector;
use dep::aztec::protocol_types::traits::ToField;
use dep::aztec::protocol_types::constants::RETURN_VALUES_LENGTH;
Expand Down Expand Up @@ -181,6 +185,26 @@ contract AvmTest {
dep::std::hash::pedersen_hash_with_separator(data, 20)
}

/************************************************************************
* Contract instance
************************************************************************/
#[aztec(public-vm)]
fn test_get_contract_instance_raw() {
let fields = get_contract_instance_internal(context.this_address());
assert(fields.len() == 6);
assert(fields[0] == 0x123);
assert(fields[1] == 0x456);
assert(fields[2] == 0x789);
assert(fields[3] == 0x101112);
assert(fields[4] == 0x131415);
assert(fields[5] == 0x161718);
}

#[aztec(public-vm)]
fn test_get_contract_instance() {
get_contract_instance(context.this_address());
}

/************************************************************************
* AvmContext functions
************************************************************************/
Expand Down
13 changes: 10 additions & 3 deletions yarn-project/end-to-end/src/e2e_avm_simulator.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AztecAddress, Wallet } from '@aztec/aztec.js';
import { AztecAddress, TxStatus, Wallet } from '@aztec/aztec.js';
import { AvmTestContract } from '@aztec/noir-contracts.js';

import { jest } from '@jest/globals';
Expand Down Expand Up @@ -43,10 +43,17 @@ describe('e2e_avm_simulator', () => {
});
});

describe('Contract instance', () => {
it('Works', async () => {
const tx = await avmContact.methods.test_get_contract_instance().send().wait();
expect(tx.status).toEqual(TxStatus.MINED);
});
});

describe('Nullifiers', () => {
it('Emit and check', async () => {
await avmContact.methods.emit_nullifier_and_check(123456).send().wait();
// TODO: check NOT reverted
const tx = await avmContact.methods.emit_nullifier_and_check(123456).send().wait();
expect(tx.status).toEqual(TxStatus.MINED);
});
});
});
1 change: 1 addition & 0 deletions yarn-project/simulator/src/avm/avm_gas_cost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const GasCosts = {
[Opcode.HEADERMEMBER]: TemporaryDefaultGasCost,
[Opcode.EMITUNENCRYPTEDLOG]: TemporaryDefaultGasCost,
[Opcode.SENDL2TOL1MSG]: TemporaryDefaultGasCost,
[Opcode.CONTRACTINSTANCECOPY]: TemporaryDefaultGasCost,
// External calls
[Opcode.CALL]: TemporaryDefaultGasCost,
[Opcode.STATICCALL]: TemporaryDefaultGasCost,
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/simulator/src/avm/avm_memory_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ export class TaggedMemory {
assert(offset < TaggedMemory.MAX_MEMORY_SIZE);
const word = this._mem[offset];
TaggedMemory.log(`get(${offset}) = ${word}`);
if (word === undefined) {
TaggedMemory.log.warn(`Memory at offset ${offset} is undefined! This might be OK if it's stack dumping.`);
}
return word as T;
}

Expand All @@ -229,6 +232,7 @@ export class TaggedMemory {
assert(offset + size < TaggedMemory.MAX_MEMORY_SIZE);
const value = this._mem.slice(offset, offset + size);
TaggedMemory.log(`getSlice(${offset}, ${size}) = ${value}`);
assert(!value.some(e => e === undefined), 'Memory slice contains undefined values.');
assert(value.length === size, `Expected slice of size ${size}, got ${value.length}.`);
return value;
}
Expand Down
24 changes: 24 additions & 0 deletions yarn-project/simulator/src/avm/avm_simulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,30 @@ describe('AVM simulator: transpiled Noir contracts', () => {
expect([...storageTrace.values()]).toEqual([[value]]);
});
});

describe('Contract', () => {
it(`CONTRACTINSTANCECOPY deserializes correctly`, async () => {
const context = initContext();
const contractInstance = {
address: AztecAddress.random(),
version: 1 as const,
salt: new Fr(0x123),
deployer: AztecAddress.fromBigInt(0x456n),
contractClassId: new Fr(0x789),
initializationHash: new Fr(0x101112),
portalContractAddress: EthAddress.fromField(new Fr(0x131415)),
publicKeysHash: new Fr(0x161718),
};

jest
.spyOn(context.persistableState.hostStorage.contractsDb, 'getContractInstance')
.mockReturnValue(Promise.resolve(contractInstance));
const bytecode = getAvmTestContractBytecode('test_get_contract_instance_raw');
const results = await new AvmSimulator(context).executeBytecode(bytecode);

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

function getAvmTestContractBytecode(functionName: string): Buffer {
Expand Down
70 changes: 70 additions & 0 deletions yarn-project/simulator/src/avm/opcodes/contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { AztecAddress, EthAddress, Fr } from '@aztec/circuits.js';

import { DeepMockProxy, mockDeep } from 'jest-mock-extended';

import { AvmContext } from '../avm_context.js';
import { Field } from '../avm_memory_types.js';
import { initContext } from '../fixtures/index.js';
import { AvmPersistableStateManager } from '../journal/journal.js';
import { ContractInstanceCopy } from './contract.js';

describe('Contract opcodes', () => {
let context: AvmContext;
let journal: DeepMockProxy<AvmPersistableStateManager>;
const address = AztecAddress.random();

beforeEach(async () => {
journal = mockDeep<AvmPersistableStateManager>();
context = initContext({
persistableState: journal,
});
});

describe('CONTRACTINSTANCECOPY', () => {
it('Should (de)serialize correctly', () => {
const buf = Buffer.from([
ContractInstanceCopy.opcode, // opcode
0x01, // indirect
...Buffer.from('12345678', 'hex'), // addressOffset
...Buffer.from('a2345678', 'hex'), // dstOffset
]);
const inst = new ContractInstanceCopy(
/*indirect=*/ 0x01,
/*addressOffset=*/ 0x12345678,
/*dstOffset=*/ 0xa2345678,
);

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

it('should copy contract instance to memory', async () => {
context.machineState.memory.set(0, new Field(address.toField()));

const contractInstance = {
address: address,
version: 1 as const,
salt: new Fr(20),
contractClassId: new Fr(30),
initializationHash: new Fr(40),
portalContractAddress: EthAddress.random(),
publicKeysHash: new Fr(50),
deployer: AztecAddress.random(),
};

journal.hostStorage.contractsDb.getContractInstance.mockReturnValue(Promise.resolve(contractInstance));

await new ContractInstanceCopy(/*indirect=*/ 0, /*addressOffset=*/ 0, /*dstOffset=*/ 1).execute(context);

const actual = context.machineState.memory.getSlice(1, 6);
expect(actual).toEqual([
new Field(contractInstance.salt),
new Field(contractInstance.deployer),
new Field(contractInstance.contractClassId),
new Field(contractInstance.initializationHash),
new Field(contractInstance.portalContractAddress.toField()),
new Field(contractInstance.publicKeysHash),
]);
});
});
});
50 changes: 50 additions & 0 deletions yarn-project/simulator/src/avm/opcodes/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { AztecAddress } from '@aztec/circuits.js';

import type { AvmContext } from '../avm_context.js';
import { Field } from '../avm_memory_types.js';
import { InstructionExecutionError } from '../errors.js';
import { Opcode, OperandType } from '../serialization/instruction_serialization.js';
import { Addressing } from './addressing_mode.js';
import { Instruction } from './instruction.js';

export class ContractInstanceCopy extends Instruction {
static readonly type: string = 'CONTRACTINSTANCECOPY';
static readonly opcode: Opcode = Opcode.CONTRACTINSTANCECOPY;
// Informs (de)serialization. See Instruction.deserialize.
static readonly wireFormat: OperandType[] = [
OperandType.UINT8,
OperandType.UINT8,
OperandType.UINT32,
OperandType.UINT32,
];

constructor(private indirect: number, private addressOffset: number, private dstOffset: number) {
super();
}

async execute(context: AvmContext): Promise<void> {
const [addressOffset, dstOffset] = Addressing.fromWire(this.indirect).resolve(
[this.addressOffset, this.dstOffset],
context.machineState.memory,
);

const address = AztecAddress.fromField(context.machineState.memory.get(addressOffset).toFr());
const instance = await context.persistableState.hostStorage.contractsDb.getContractInstance(address);

if (!instance) {
throw new InstructionExecutionError(`Contract instance not found: ${address}`);
}
const data = [
instance.salt,
instance.deployer.toField(),
instance.contractClassId,
instance.initializationHash,
instance.portalContractAddress.toField(),
instance.publicKeysHash,
].map(f => new Field(f));

context.machineState.memory.setSlice(dstOffset, data);

context.machineState.incrementPc();
}
}
1 change: 1 addition & 0 deletions yarn-project/simulator/src/avm/opcodes/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './arithmetic.js';
export * from './bitwise.js';
export * from './control_flow.js';
export * from './contract.js';
export * from './instruction.js';
export * from './comparators.js';
export * from './memory.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CalldataCopy,
Cast,
ChainId,
ContractInstanceCopy,
Div,
EmitNoteHash,
EmitNullifier,
Expand Down Expand Up @@ -126,6 +127,7 @@ const INSTRUCTION_SET = () =>
// Accrued Substate
[EmitUnencryptedLog.opcode, EmitUnencryptedLog],
[SendL2ToL1Message.opcode, SendL2ToL1Message],
[ContractInstanceCopy.opcode, ContractInstanceCopy],

// Control Flow - Contract Calls
[Call.opcode, Call],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export enum Opcode {
HEADERMEMBER,
EMITUNENCRYPTEDLOG,
SENDL2TOL1MSG,
CONTRACTINSTANCECOPY,
// External calls
CALL,
STATICCALL,
Expand Down
Loading

0 comments on commit ea16ba5

Please sign in to comment.