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): contract instance opcode #5487

Merged
merged 1 commit into from
Apr 4, 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: 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
Loading
Loading