Skip to content

Commit

Permalink
feat(avm): contract instance opcode (#5487)
Browse files Browse the repository at this point in the history
  • Loading branch information
fcarreiro committed Apr 4, 2024
1 parent a082723 commit ceacba6
Show file tree
Hide file tree
Showing 18 changed files with 419 additions and 40 deletions.
2 changes: 2 additions & 0 deletions avm-transpiler/src/opcodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub enum AvmOpcode {
EMITNULLIFIER,
L1TOL2MSGEXISTS,
HEADERMEMBER,
GETCONTRACTINSTANCE,
EMITUNENCRYPTEDLOG,
SENDL2TOL1MSG,
// External calls
Expand Down Expand Up @@ -148,6 +149,7 @@ impl AvmOpcode {
// Accrued Substate
AvmOpcode::EMITUNENCRYPTEDLOG => "EMITUNENCRYPTEDLOG",
AvmOpcode::SENDL2TOL1MSG => "SENDL2TOL1MSG",
AvmOpcode::GETCONTRACTINSTANCE => "GETCONTRACTINSTANCE",

// Control Flow - Contract Calls
AvmOpcode::CALL => "CALL",
Expand Down
39 changes: 39 additions & 0 deletions avm-transpiler/src/transpile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ fn handle_foreign_call(
"avmOpcodePoseidon" => {
handle_single_field_hash_instruction(avm_instrs, function, destinations, inputs)
}
"avmOpcodeGetContractInstance" => {
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 @@ -969,6 +972,42 @@ fn handle_storage_write(
})
}

/// Emit a GETCONTRACTINSTANCE 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!("GETCONTRACTINSTANCE 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!("GETCONTRACTINSTANCE destination should be an array"),
};

avm_instrs.push(AvmInstruction {
opcode: AvmOpcode::GETCONTRACTINSTANCE,
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 @@ -83,6 +83,7 @@ enum class OpCode : uint8_t {
EMITNULLIFIER, // Notes & Nullifiers
L1TOL2MSGEXISTS, // Messages
HEADERMEMBER, // Archive tree & Headers
GETCONTRACTINSTANCE,

// Accrued Substate
EMITUNENCRYPTEDLOG,
Expand Down
23 changes: 22 additions & 1 deletion noir-projects/aztec-nr/aztec/src/oracle/get_contract_instance.nr
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
use dep::protocol_types::{address::AztecAddress, contract_instance::ContractInstance, constants::CONTRACT_INSTANCE_LENGTH};
use dep::protocol_types::{
address::AztecAddress, contract_instance::ContractInstance, utils::arr_copy_slice,
constants::CONTRACT_INSTANCE_LENGTH, utils::reader::Reader
};

#[oracle(getContractInstance)]
fn get_contract_instance_oracle(_address: AztecAddress) -> [Field; CONTRACT_INSTANCE_LENGTH] {}

// Returns a ContractInstance plus a boolean indicating whether the instance was found.
#[oracle(avmOpcodeGetContractInstance)]
fn get_contract_instance_oracle_avm(_address: AztecAddress) -> [Field; CONTRACT_INSTANCE_LENGTH + 1] {}

unconstrained fn get_contract_instance_internal(address: AztecAddress) -> [Field; CONTRACT_INSTANCE_LENGTH] {
get_contract_instance_oracle(address)
}

unconstrained fn get_contract_instance_internal_avm(address: AztecAddress) -> [Field; CONTRACT_INSTANCE_LENGTH + 1] {
get_contract_instance_oracle_avm(address)
}

pub fn get_contract_instance(address: AztecAddress) -> ContractInstance {
let instance = ContractInstance::deserialize(get_contract_instance_internal(address));
assert(instance.to_address().eq(address));
instance
}

pub fn get_contract_instance_avm(address: AztecAddress) -> Option<ContractInstance> {
let mut reader = Reader::new(get_contract_instance_internal_avm(address));
let found = reader.read();
if found == 0 {
Option::none()
} else {
Option::some(reader.read_struct(ContractInstance::deserialize))
}
}
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_avm, get_contract_instance_internal_avm};
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,28 @@ 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_avm(context.this_address());
assert(fields.len() == 7);
assert(fields[0] == 0x1);
assert(fields[1] == 0x123);
assert(fields[2] == 0x456);
assert(fields[3] == 0x789);
assert(fields[4] == 0x101112);
assert(fields[5] == 0x131415);
assert(fields[6] == 0x161718);
}

#[aztec(public-vm)]
fn test_get_contract_instance() {
let ci = get_contract_instance_avm(context.this_address());
assert(ci.is_some());
}

/************************************************************************
* 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, type Wallet } from '@aztec/aztec.js';
import { AztecAddress, TxStatus, type 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.GETCONTRACTINSTANCE]: 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 @@ -757,6 +757,30 @@ describe('AVM simulator: transpiled Noir contracts', () => {
expect([...storageTrace.values()]).toEqual([[value]]);
});
});

describe('Contract', () => {
it(`GETCONTRACTINSTANCE 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
89 changes: 89 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,89 @@
import { AztecAddress, EthAddress, Fr } from '@aztec/circuits.js';

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

import { type AvmContext } from '../avm_context.js';
import { Field } from '../avm_memory_types.js';
import { initContext } from '../fixtures/index.js';
import { type AvmPersistableStateManager } from '../journal/journal.js';
import { GetContractInstance } 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('GETCONTRACTINSTANCE', () => {
it('Should (de)serialize correctly', () => {
const buf = Buffer.from([
GetContractInstance.opcode, // opcode
0x01, // indirect
...Buffer.from('12345678', 'hex'), // addressOffset
...Buffer.from('a2345678', 'hex'), // dstOffset
]);
const inst = new GetContractInstance(
/*indirect=*/ 0x01,
/*addressOffset=*/ 0x12345678,
/*dstOffset=*/ 0xa2345678,
);

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

it('should copy contract instance to memory if found', 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 GetContractInstance(/*indirect=*/ 0, /*addressOffset=*/ 0, /*dstOffset=*/ 1).execute(context);

const actual = context.machineState.memory.getSlice(1, 7);
expect(actual).toEqual([
new Field(1), // found
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),
]);
});

it('should return zeroes if not found', async () => {
context.machineState.memory.set(0, new Field(address.toField()));
journal.hostStorage.contractsDb.getContractInstance.mockReturnValue(Promise.resolve(undefined));

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

const actual = context.machineState.memory.getSlice(1, 7);
expect(actual).toEqual([
new Field(0), // found
new Field(0),
new Field(0),
new Field(0),
new Field(0),
new Field(0),
new Field(0),
]);
});
});
});
58 changes: 58 additions & 0 deletions yarn-project/simulator/src/avm/opcodes/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { AztecAddress, Fr } from '@aztec/circuits.js';

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

export class GetContractInstance extends Instruction {
static readonly type: string = 'GETCONTRACTINSTANCE';
static readonly opcode: Opcode = Opcode.GETCONTRACTINSTANCE;
// 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);

const data =
instance === undefined
? [
new Field(0), // not found
new Field(0),
new Field(0),
new Field(0),
new Field(0),
new Field(0),
new Field(0),
]
: [
new Fr(1), // found
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

0 comments on commit ceacba6

Please sign in to comment.