diff --git a/avm-transpiler/src/opcodes.rs b/avm-transpiler/src/opcodes.rs index ef91f2b368da..84414239aa9c 100644 --- a/avm-transpiler/src/opcodes.rs +++ b/avm-transpiler/src/opcodes.rs @@ -150,10 +150,10 @@ impl AvmOpcode { // World State AvmOpcode::SLOAD => "SLOAD", // Public Storage AvmOpcode::SSTORE => "SSTORE", // Public Storage - AvmOpcode::NOTEHASHEXISTS => "NOTEHASHEXISTS", // Notes & Nullifiers - AvmOpcode::EMITNOTEHASH => "EMITNOTEHASH", // Notes & Nullifiers + AvmOpcode::NOTEHASHEXISTS => "NOTEHASHEXISTS", // Notes & Nullifiers + AvmOpcode::EMITNOTEHASH => "EMITNOTEHASH", // Notes & Nullifiers AvmOpcode::NULLIFIEREXISTS => "NULLIFIEREXISTS", // Notes & Nullifiers - AvmOpcode::EMITNULLIFIER => "EMITNULLIFIER", // Notes & Nullifiers + AvmOpcode::EMITNULLIFIER => "EMITNULLIFIER", // Notes & Nullifiers AvmOpcode::READL1TOL2MSG => "READL1TOL2MSG", // Messages AvmOpcode::HEADERMEMBER => "HEADERMEMBER", // Archive tree & Headers diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index ad079b4b098c..5dfac47fe6ac 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -1,7 +1,9 @@ use acvm::acir::brillig::Opcode as BrilligOpcode; use acvm::acir::circuit::brillig::Brillig; -use acvm::brillig_vm::brillig::{BinaryFieldOp, BinaryIntOp, MemoryAddress, Value, ValueOrArray}; +use acvm::brillig_vm::brillig::{ + BinaryFieldOp, BinaryIntOp, BlackBoxOp, HeapArray, MemoryAddress, Value, ValueOrArray, +}; use crate::instructions::{ AvmInstruction, AvmOperand, AvmTypeTag, ALL_DIRECT, FIRST_OPERAND_INDIRECT, @@ -93,7 +95,6 @@ pub fn brillig_to_avm(brillig: &Brillig) -> Vec { value: destination.to_usize() as u32, }, ], - ..Default::default() }); } BrilligOpcode::CalldataCopy { destination_address, size, offset } => { @@ -200,9 +201,13 @@ pub fn brillig_to_avm(brillig: &Brillig) -> Vec { ..Default::default() }); }, + BrilligOpcode::Cast { destination, source, bit_size } => { + avm_instrs.push(emit_cast(source.to_usize() as u32, destination.to_usize() as u32, tag_from_bit_size(*bit_size))); + } BrilligOpcode::ForeignCall { function, destinations, inputs, destination_value_types:_, input_value_types:_ } => { handle_foreign_call(&mut avm_instrs, function, destinations, inputs); }, + BrilligOpcode::BlackBox(operation) => handle_black_box_function(&mut avm_instrs, operation), _ => panic!( "Transpiler doesn't know how to process {:?} brillig instruction", brillig_instr @@ -228,9 +233,148 @@ fn handle_foreign_call( function: &String, destinations: &Vec, inputs: &Vec, +) { + match function.as_str() { + "keccak256" | "sha256" => { + emit_2_field_hash_instruction(avm_instrs, function, destinations, inputs) + } + "poseidon" => { + emit_single_field_hash_instruction(avm_instrs, function, destinations, inputs) + } + _ => handle_getter_instruction(avm_instrs, function, destinations, inputs), + } +} + +/// Two field hash instructions represent instruction's that's outputs are larger than a field element +/// +/// This includes: +/// - keccak +/// - sha256 +/// +/// In the future the output of these may expand / contract depending on what is most efficient for the circuit +/// to reason about. In order to decrease user friction we will use two field outputs. +fn emit_2_field_hash_instruction( + avm_instrs: &mut Vec, + function: &String, + destinations: &[ValueOrArray], + inputs: &[ValueOrArray], +) { + // handle field returns differently + let hash_offset_maybe = inputs[0]; + println!("hash_offset_maybe: {:?}", hash_offset_maybe); + let (hash_offset, hash_size) = match hash_offset_maybe { + ValueOrArray::HeapArray(HeapArray { pointer, size }) => (pointer.0, size), + _ => panic!("Keccak | Sha256 address inputs destination should be a single value"), + }; + + assert!(destinations.len() == 1); + let dest_offset_maybe = destinations[0]; + let dest_offset = match dest_offset_maybe { + ValueOrArray::HeapArray(HeapArray { pointer, size }) => { + assert!(size == 2); + pointer.0 + } + _ => panic!("Keccak | Poseidon address destination should be a single value"), + }; + + let opcode = match function.as_str() { + "keccak256" => AvmOpcode::KECCAK, + "sha256" => AvmOpcode::SHA256, + _ => panic!( + "Transpiler doesn't know how to process ForeignCall function {:?}", + function + ), + }; + + avm_instrs.push(AvmInstruction { + opcode, + indirect: Some(3), // 11 - addressing mode, indirect for input and output + operands: vec![ + AvmOperand::U32 { + value: dest_offset as u32, + }, + AvmOperand::U32 { + value: hash_offset as u32, + }, + AvmOperand::U32 { + value: hash_size as u32, + }, + ], + ..Default::default() + }); +} + +/// A single field hash instruction includes hash functions that emit a single field element +/// directly onto the stack. +/// +/// This includes (snark friendly functions): +/// - poseidon2 +/// +/// Pedersen is not implemented this way as the black box function representation has the correct api. +/// As the Poseidon BBF only deals with a single permutation, it is not quite suitable for our current avm +/// representation. +fn emit_single_field_hash_instruction( + avm_instrs: &mut Vec, + function: &String, + destinations: &[ValueOrArray], + inputs: &[ValueOrArray], +) { + // handle field returns differently + let hash_offset_maybe = inputs[0]; + let (hash_offset, hash_size) = match hash_offset_maybe { + ValueOrArray::HeapArray(HeapArray { pointer, size }) => (pointer.0, size), + _ => panic!("Poseidon address inputs destination should be a single value"), + }; + + assert!(destinations.len() == 1); + let dest_offset_maybe = destinations[0]; + let dest_offset = match dest_offset_maybe { + ValueOrArray::MemoryAddress(dest_offset) => dest_offset.0, + _ => panic!("Poseidon address destination should be a single value"), + }; + + let opcode = match function.as_str() { + "poseidon" => AvmOpcode::POSEIDON, + _ => panic!( + "Transpiler doesn't know how to process ForeignCall function {:?}", + function + ), + }; + + avm_instrs.push(AvmInstruction { + opcode, + indirect: Some(1), + operands: vec![ + AvmOperand::U32 { + value: dest_offset as u32, + }, + AvmOperand::U32 { + value: hash_offset as u32, + }, + AvmOperand::U32 { + value: hash_size as u32, + }, + ], + ..Default::default() + }); +} + +/// Getter Instructions are instructions that take NO inputs, and return information +/// from the current execution context. +/// +/// This includes: +/// - Global variables +/// - Caller +/// - storage address +/// - ... +fn handle_getter_instruction( + avm_instrs: &mut Vec, + function: &String, + destinations: &Vec, + inputs: &Vec, ) { // For the foreign calls we want to handle, we do not want inputs, as they are getters - assert!(inputs.len() == 0); + assert!(inputs.is_empty()); assert!(destinations.len() == 1); let dest_offset_maybe = destinations[0]; let dest_offset = match dest_offset_maybe { @@ -257,7 +401,6 @@ fn handle_foreign_call( function ), }; - avm_instrs.push(AvmInstruction { opcode, indirect: Some(ALL_DIRECT), @@ -265,7 +408,7 @@ fn handle_foreign_call( value: dest_offset as u32, }], ..Default::default() - }); + }) } /// Handles Brillig's CONST opcode. @@ -316,7 +459,7 @@ fn emit_set(tag: AvmTypeTag, dest: u32, value: u128) -> AvmInstruction { AvmTypeTag::UINT64 => AvmOperand::U64 { value: value as u64, }, - AvmTypeTag::UINT128 => AvmOperand::U128 { value: value }, + AvmTypeTag::UINT128 => AvmOperand::U128 { value }, _ => panic!("Invalid type tag {:?} for set", tag), }, // dest offset @@ -342,7 +485,7 @@ fn emit_cast(source: u32, destination: u32, dst_tag: AvmTypeTag) -> AvmInstructi fn emit_mov(indirect: Option, source: u32, dest: u32) -> AvmInstruction { AvmInstruction { opcode: AvmOpcode::MOV, - indirect: indirect, + indirect, operands: vec![ AvmOperand::U32 { value: source }, AvmOperand::U32 { value: dest }, @@ -351,6 +494,44 @@ fn emit_mov(indirect: Option, source: u32, dest: u32) -> AvmInstruction { } } +/// Black box functions, for the meantime only covers pedersen operations as the blackbox function api suits our current needs. +/// (array goes in -> field element comes out) +fn handle_black_box_function(avm_instrs: &mut Vec, operation: &BlackBoxOp) { + match operation { + BlackBoxOp::PedersenHash { + inputs, + domain_separator: _, + output, + } => { + let hash_offset = inputs.pointer.0; + let hash_size = inputs.size.0; + + let dest_offset = output.0; + + avm_instrs.push(AvmInstruction { + opcode: AvmOpcode::PEDERSEN, + indirect: Some(1), + operands: vec![ + AvmOperand::U32 { + value: dest_offset as u32, + }, + AvmOperand::U32 { + value: hash_offset as u32, + }, + AvmOperand::U32 { + value: hash_size as u32, + }, + ], + ..Default::default() + }); + } + _ => panic!( + "Transpiler doesn't know how to process BlackBoxOp {:?}", + operation + ), + } +} + /// Compute an array that maps each Brillig pc to an AVM pc. /// This must be done before transpiling to properly transpile jump destinations. /// This is necessary for two reasons: @@ -367,12 +548,7 @@ fn map_brillig_pcs_to_avm_pcs(initial_offset: usize, brillig: &Brillig) -> Vec 2, - BrilligOpcode::Store { .. } => 2, - BrilligOpcode::Const { bit_size, .. } => match bit_size { - 254 => 2, // Field. - _ => 1, - }, + BrilligOpcode::Const { bit_size: 254, .. } => 2, _ => 1, }; // next Brillig pc will map to an AVM pc offset by the @@ -384,6 +560,7 @@ fn map_brillig_pcs_to_avm_pcs(initial_offset: usize, brillig: &Brillig) -> Vec AvmTypeTag { match bit_size { + 1 => AvmTypeTag::UINT8, // temp workaround 8 => AvmTypeTag::UINT8, 16 => AvmTypeTag::UINT16, 32 => AvmTypeTag::UINT32, diff --git a/noir-projects/aztec-nr/aztec/src/avm.nr b/noir-projects/aztec-nr/aztec/src/avm.nr index 3d9885db80d8..cfd9aa3e6f4b 100644 --- a/noir-projects/aztec-nr/aztec/src/avm.nr +++ b/noir-projects/aztec-nr/aztec/src/avm.nr @@ -1 +1,2 @@ mod context; +mod hash; diff --git a/noir-projects/aztec-nr/aztec/src/avm/hash.nr b/noir-projects/aztec-nr/aztec/src/avm/hash.nr new file mode 100644 index 000000000000..2c2ac6f2448b --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/avm/hash.nr @@ -0,0 +1,8 @@ +#[oracle(keccak256)] +pub fn keccak256(input: [Field; N]) -> [Field; 2] {} + +#[oracle(poseidon)] +pub fn poseidon(input: [Field; N]) -> Field {} + +#[oracle(sha256)] +pub fn sha256(input: [Field; N]) -> [Field; 2] {} diff --git a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr index 64fe1644489f..67e3086814cf 100644 --- a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr @@ -1,11 +1,16 @@ -// - contract AvmTest { // Libs use dep::aztec::protocol_types::address::{AztecAddress, EthAddress}; // avm lib - use dep::aztec::avm::context::AvmContext; + use dep::aztec::avm::{ + context::AvmContext, + hash::{ + keccak256, + poseidon, + sha256 + } + }; #[aztec(private)] fn constructor() {} @@ -51,9 +56,32 @@ contract AvmTest { 200 as Field } - /************************************************************************ - * AvmContext functions - ************************************************************************/ + // /************************************************************************ + // * Hashing functions + // ************************************************************************/ + #[aztec(public-vm)] + fn keccak_hash(data: [Field; 3]) -> pub [Field; 2] { + keccak256(data) + } + + #[aztec(public-vm)] + fn poseidon_hash(data: [Field; 3]) -> pub Field { + poseidon(data) + } + + #[aztec(public-vm)] + fn sha256_hash(data: [Field; 3]) -> pub [Field; 2] { + sha256(data) + } + + #[aztec(public-vm)] + fn pedersen_hash(data: [Field; 3]) -> pub Field { + dep::std::hash::pedersen_hash(data) + } + + // /************************************************************************ + // * AvmContext functions + // ************************************************************************/ #[aztec(public-vm)] fn getAddress() -> pub AztecAddress { context.address() diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index 5133f1c5a4d2..8a56550155ae 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -1,4 +1,5 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { keccak, pedersenHash, poseidonHash, sha256 } from '@aztec/foundation/crypto'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { AvmTestContractArtifact } from '@aztec/noir-contracts.js'; @@ -82,6 +83,66 @@ describe('AVM simulator', () => { }); }); + describe.each([ + ['avm_sha256_hash', sha256], + ['avm_keccak_hash', keccak], + ])('Hashes with 2 fields returned in noir contracts', (name: string, hashFunction: (data: Buffer) => Buffer) => { + it(`Should execute contract function that performs ${name} hash`, async () => { + const calldata = [new Fr(1), new Fr(2), new Fr(3)]; + const hash = hashFunction(Buffer.concat(calldata.map(f => f.toBuffer()))); + + // Get contract function artifact + const artifact = AvmTestContractArtifact.functions.find(f => f.name === name)!; + + // Decode bytecode into instructions + const bytecode = Buffer.from(artifact.bytecode, 'base64'); + + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + jest + .spyOn(context.worldState.hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(bytecode)); + + const results = await new AvmSimulator(context).execute(); + + expect(results.reverted).toBe(false); + + const returnData = results.output; + const reconstructedHash = Buffer.concat([ + returnData[0].toBuffer().subarray(16, 32), + returnData[1].toBuffer().subarray(16, 32), + ]); + expect(reconstructedHash).toEqual(hash); + }); + }); + + describe.each([ + ['avm_poseidon_hash', poseidonHash], + ['avm_pedersen_hash', pedersenHash], + ])('Hashes with field returned in noir contracts', (name: string, hashFunction: (data: Buffer[]) => Buffer) => { + it(`Should execute contract function that performs ${name} hash`, async () => { + const calldata = [new Fr(1), new Fr(2), new Fr(3)]; + const hash = hashFunction(calldata.map(f => f.toBuffer())); + + // Get contract function artifact + const artifact = AvmTestContractArtifact.functions.find(f => f.name === name)!; + + // Decode bytecode into instructions + const bytecode = Buffer.from(artifact.bytecode, 'base64'); + + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + jest + .spyOn(context.worldState.hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(bytecode)); + + const results = await new AvmSimulator(context).execute(); + + expect(results.reverted).toBe(false); + + const returnData = results.output; + expect(returnData).toEqual([new Fr(hash)]); + }); + }); + describe('Test env getters from noir contract', () => { const testEnvGetter = async (valueName: string, value: any, functionName: string, globalVar: boolean = false) => { const getterArtifact = AvmTestContractArtifact.functions.find(f => f.name === functionName)!; diff --git a/yarn-project/simulator/src/avm/opcodes/hashing.test.ts b/yarn-project/simulator/src/avm/opcodes/hashing.test.ts index 48e7206385a0..0c7b363df048 100644 --- a/yarn-project/simulator/src/avm/opcodes/hashing.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/hashing.test.ts @@ -2,7 +2,7 @@ import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { keccak, pedersenHash, poseidonHash, sha256 } from '@aztec/foundation/crypto'; import { AvmContext } from '../avm_context.js'; -import { Field } from '../avm_memory_types.js'; +import { Field, Uint32 } from '../avm_memory_types.js'; import { initContext } from '../fixtures/index.js'; import { Keccak, Pedersen, Poseidon2, Sha256 } from './hashing.js'; @@ -17,17 +17,24 @@ describe('Hashing Opcodes', () => { it('Should (de)serialize correctly', () => { const buf = Buffer.from([ Poseidon2.opcode, // opcode + 1, // indirect ...Buffer.from('12345678', 'hex'), // dstOffset ...Buffer.from('23456789', 'hex'), // hashOffset ...Buffer.from('3456789a', 'hex'), // hashSize ]); - const inst = new Poseidon2(/*dstOffset=*/ 0x12345678, /*hashOffset=*/ 0x23456789, /*hashSize=*/ 0x3456789a); + const inst = new Poseidon2( + /*indirect=*/ 1, + /*dstOffset=*/ 0x12345678, + /*hashOffset=*/ 0x23456789, + /*hashSize=*/ 0x3456789a, + ); expect(Poseidon2.deserialize(buf)).toEqual(inst); expect(inst.serialize()).toEqual(buf); }); - it('Should hash correctly', async () => { + it('Should hash correctly - direct', async () => { + const indirect = 0; const args = [new Field(1n), new Field(2n), new Field(3n)]; const hashOffset = 0; context.machineState.memory.setSlice(hashOffset, args); @@ -35,7 +42,25 @@ describe('Hashing Opcodes', () => { const dstOffset = 3; const expectedHash = poseidonHash(args.map(field => field.toBuffer())); - await new Poseidon2(dstOffset, hashOffset, args.length).execute(context); + await new Poseidon2(indirect, dstOffset, hashOffset, args.length).execute(context); + + const result = context.machineState.memory.get(dstOffset); + expect(result).toEqual(new Field(toBigIntBE(expectedHash))); + }); + + it('Should hash correctly - indirect', async () => { + const args = [new Field(1n), new Field(2n), new Field(3n)]; + const indirect = 1; + const hashOffset = 0; + const realLocation = 4; + + context.machineState.memory.set(hashOffset, new Uint32(realLocation)); + context.machineState.memory.setSlice(realLocation, args); + + const dstOffset = 3; + + const expectedHash = poseidonHash(args.map(field => field.toBuffer())); + await new Poseidon2(indirect, dstOffset, hashOffset, args.length).execute(context); const result = context.machineState.memory.get(dstOffset); expect(result).toEqual(new Field(toBigIntBE(expectedHash))); @@ -46,18 +71,25 @@ describe('Hashing Opcodes', () => { it('Should (de)serialize correctly', () => { const buf = Buffer.from([ Keccak.opcode, // opcode + 1, // indirect ...Buffer.from('12345678', 'hex'), // dstOffset ...Buffer.from('23456789', 'hex'), // hashOffset ...Buffer.from('3456789a', 'hex'), // hashSize ]); - const inst = new Keccak(/*dstOffset=*/ 0x12345678, /*hashOffset=*/ 0x23456789, /*hashSize=*/ 0x3456789a); + const inst = new Keccak( + /*indirect=*/ 1, + /*dstOffset=*/ 0x12345678, + /*hashOffset=*/ 0x23456789, + /*hashSize=*/ 0x3456789a, + ); expect(Keccak.deserialize(buf)).toEqual(inst); expect(inst.serialize()).toEqual(buf); }); - it('Should hash correctly', async () => { + it('Should hash correctly - direct', async () => { const args = [new Field(1n), new Field(2n), new Field(3n)]; + const indirect = 0; const hashOffset = 0; context.machineState.memory.setSlice(hashOffset, args); @@ -65,71 +97,151 @@ describe('Hashing Opcodes', () => { const inputBuffer = Buffer.concat(args.map(field => field.toBuffer())); const expectedHash = keccak(inputBuffer); - await new Keccak(dstOffset, hashOffset, args.length).execute(context); + await new Keccak(indirect, dstOffset, hashOffset, args.length).execute(context); const result = context.machineState.memory.getSliceAs(dstOffset, 2); const combined = Buffer.concat([result[0].toBuffer().subarray(16, 32), result[1].toBuffer().subarray(16, 32)]); expect(combined).toEqual(expectedHash); }); + + it('Should hash correctly - indirect', async () => { + const args = [new Field(1n), new Field(2n), new Field(3n)]; + const indirect = 3; // dest and return are indirect + const hashOffset = 0; + const argsLocation = 4; + + const dstOffset = 2; + const readLocation = 6; + + context.machineState.memory.set(hashOffset, new Uint32(argsLocation)); + context.machineState.memory.set(dstOffset, new Uint32(readLocation)); + context.machineState.memory.setSlice(argsLocation, args); + + const inputBuffer = Buffer.concat(args.map(field => field.toBuffer())); + const expectedHash = keccak(inputBuffer); + await new Keccak(indirect, dstOffset, hashOffset, args.length).execute(context); + + const result = context.machineState.memory.getSliceAs(readLocation, 2); + const combined = Buffer.concat([result[0].toBuffer().subarray(16, 32), result[1].toBuffer().subarray(16, 32)]); + + expect(combined).toEqual(expectedHash); + }); + // TODO: indirect }); describe('Sha256', () => { it('Should (de)serialize correctly', () => { const buf = Buffer.from([ Sha256.opcode, // opcode + 1, // indirect ...Buffer.from('12345678', 'hex'), // dstOffset ...Buffer.from('23456789', 'hex'), // hashOffset ...Buffer.from('3456789a', 'hex'), // hashSize ]); - const inst = new Sha256(/*dstOffset=*/ 0x12345678, /*hashOffset=*/ 0x23456789, /*hashSize=*/ 0x3456789a); + const inst = new Sha256( + /*indirect=*/ 1, + /*dstOffset=*/ 0x12345678, + /*hashOffset=*/ 0x23456789, + /*hashSize=*/ 0x3456789a, + ); expect(Sha256.deserialize(buf)).toEqual(inst); expect(inst.serialize()).toEqual(buf); }); - it('Should hash correctly', async () => { + it('Should hash correctly - direct', async () => { const args = [new Field(1n), new Field(2n), new Field(3n)]; const hashOffset = 0; + const indirect = 0; context.machineState.memory.setSlice(hashOffset, args); const dstOffset = 3; const inputBuffer = Buffer.concat(args.map(field => field.toBuffer())); const expectedHash = sha256(inputBuffer); - await new Sha256(dstOffset, hashOffset, args.length).execute(context); + await new Sha256(indirect, dstOffset, hashOffset, args.length).execute(context); const result = context.machineState.memory.getSliceAs(dstOffset, 2); const combined = Buffer.concat([result[0].toBuffer().subarray(16, 32), result[1].toBuffer().subarray(16, 32)]); expect(combined).toEqual(expectedHash); }); + + it('Should hash correctly - indirect', async () => { + const args = [new Field(1n), new Field(2n), new Field(3n)]; + const indirect = 3; // dest and return are indirect + const hashOffset = 0; + const argsLocation = 4; + + const dstOffset = 2; + const readLocation = 6; + + context.machineState.memory.set(hashOffset, new Uint32(argsLocation)); + context.machineState.memory.set(dstOffset, new Uint32(readLocation)); + context.machineState.memory.setSlice(argsLocation, args); + + const inputBuffer = Buffer.concat(args.map(field => field.toBuffer())); + const expectedHash = sha256(inputBuffer); + await new Sha256(indirect, dstOffset, hashOffset, args.length).execute(context); + + const result = context.machineState.memory.getSliceAs(readLocation, 2); + const combined = Buffer.concat([result[0].toBuffer().subarray(16, 32), result[1].toBuffer().subarray(16, 32)]); + + expect(combined).toEqual(expectedHash); + }); }); describe('Pedersen', () => { it('Should (de)serialize correctly', () => { const buf = Buffer.from([ Pedersen.opcode, // opcode + 1, // indirect ...Buffer.from('12345678', 'hex'), // dstOffset ...Buffer.from('23456789', 'hex'), // hashOffset ...Buffer.from('3456789a', 'hex'), // hashSize ]); - const inst = new Pedersen(/*dstOffset=*/ 0x12345678, /*hashOffset=*/ 0x23456789, /*hashSize=*/ 0x3456789a); + const inst = new Pedersen( + /*indirect=*/ 1, + /*dstOffset=*/ 0x12345678, + /*hashOffset=*/ 0x23456789, + /*hashSize=*/ 0x3456789a, + ); expect(Sha256.deserialize(buf)).toEqual(inst); expect(inst.serialize()).toEqual(buf); }); - it('Should hash correctly', async () => { + it('Should hash correctly - direct', async () => { const args = [new Field(1n), new Field(2n), new Field(3n)]; const hashOffset = 0; + const indirect = 0; context.machineState.memory.setSlice(hashOffset, args); const dstOffset = 3; const inputBuffer = args.map(field => field.toBuffer()); const expectedHash = pedersenHash(inputBuffer); - await new Pedersen(dstOffset, hashOffset, args.length).execute(context); + await new Pedersen(indirect, dstOffset, hashOffset, args.length).execute(context); + + const result = context.machineState.memory.get(dstOffset); + expect(result).toEqual(new Field(toBigIntBE(expectedHash))); + }); + + it('Should hash correctly - indirect', async () => { + const args = [new Field(1n), new Field(2n), new Field(3n)]; + const indirect = 1; + const hashOffset = 0; + const realLocation = 4; + + context.machineState.memory.set(hashOffset, new Uint32(realLocation)); + context.machineState.memory.setSlice(realLocation, args); + + const dstOffset = 3; + + const inputBuffer = args.map(field => field.toBuffer()); + const expectedHash = pedersenHash(inputBuffer); + await new Pedersen(indirect, dstOffset, hashOffset, args.length).execute(context); const result = context.machineState.memory.get(dstOffset); expect(result).toEqual(new Field(toBigIntBE(expectedHash))); diff --git a/yarn-project/simulator/src/avm/opcodes/hashing.ts b/yarn-project/simulator/src/avm/opcodes/hashing.ts index 5add968ac6e0..99e64b0a4439 100644 --- a/yarn-project/simulator/src/avm/opcodes/hashing.ts +++ b/yarn-project/simulator/src/avm/opcodes/hashing.ts @@ -4,6 +4,7 @@ import { keccak, pedersenHash, poseidonHash, sha256 } from '@aztec/foundation/cr import { 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 Poseidon2 extends Instruction { @@ -12,21 +13,28 @@ export class Poseidon2 extends Instruction { // Informs (de)serialization. See Instruction.deserialize. static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, OperandType.UINT8, OperandType.UINT32, OperandType.UINT32, OperandType.UINT32, ]; - constructor(private dstOffset: number, private hashOffset: number, private hashSize: number) { + constructor( + private indirect: number, + private dstOffset: number, + private hashOffset: number, + private hashSize: number, + ) { super(); } async execute(context: AvmContext): Promise { // We hash a set of field elements - const hashData = context.machineState.memory - .getSlice(this.hashOffset, this.hashOffset + this.hashSize) - .map(word => word.toBuffer()); + const [hashOffset] = Addressing.fromWire(this.indirect).resolve([this.hashOffset], context.machineState.memory); + + // Memory pointer will be indirect + const hashData = context.machineState.memory.getSlice(hashOffset, this.hashSize).map(word => word.toBuffer()); const hash = poseidonHash(hashData); context.machineState.memory.set(this.dstOffset, new Field(hash)); @@ -41,22 +49,31 @@ export class Keccak extends Instruction { // Informs (de)serialization. See Instruction.deserialize. static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, OperandType.UINT8, OperandType.UINT32, OperandType.UINT32, OperandType.UINT32, ]; - constructor(private dstOffset: number, private hashOffset: number, private hashSize: number) { + constructor( + private indirect: number, + private dstOffset: number, + private hashOffset: number, + private hashSize: number, + ) { super(); } // Note hash output is 32 bytes, so takes up two fields async execute(context: AvmContext): Promise { // We hash a set of field elements - const hashData = context.machineState.memory - .getSlice(this.hashOffset, this.hashOffset + this.hashSize) - .map(word => word.toBuffer()); + const [hashOffset, dstOffset] = Addressing.fromWire(this.indirect).resolve( + [this.hashOffset, this.dstOffset], + context.machineState.memory, + ); + + const hashData = context.machineState.memory.getSlice(hashOffset, this.hashSize).map(word => word.toBuffer()); const hash = keccak(Buffer.concat(hashData)); @@ -64,8 +81,8 @@ export class Keccak extends Instruction { const high = new Field(toBigIntBE(hash.subarray(0, 16))); const low = new Field(toBigIntBE(hash.subarray(16, 32))); - context.machineState.memory.set(this.dstOffset, high); - context.machineState.memory.set(this.dstOffset + 1, low); + context.machineState.memory.set(dstOffset, high); + context.machineState.memory.set(dstOffset + 1, low); context.machineState.incrementPc(); } @@ -77,22 +94,31 @@ export class Sha256 extends Instruction { // Informs (de)serialization. See Instruction.deserialize. static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, OperandType.UINT8, OperandType.UINT32, OperandType.UINT32, OperandType.UINT32, ]; - constructor(private dstOffset: number, private hashOffset: number, private hashSize: number) { + constructor( + private indirect: number, + private dstOffset: number, + private hashOffset: number, + private hashSize: number, + ) { super(); } // Note hash output is 32 bytes, so takes up two fields async execute(context: AvmContext): Promise { + const [hashOffset, dstOffset] = Addressing.fromWire(this.indirect).resolve( + [this.hashOffset, this.dstOffset], + context.machineState.memory, + ); + // We hash a set of field elements - const hashData = context.machineState.memory - .getSlice(this.hashOffset, this.hashOffset + this.hashSize) - .map(word => word.toBuffer()); + const hashData = context.machineState.memory.getSlice(hashOffset, this.hashSize).map(word => word.toBuffer()); const hash = sha256(Buffer.concat(hashData)); @@ -100,8 +126,8 @@ export class Sha256 extends Instruction { const high = new Field(toBigIntBE(hash.subarray(0, 16))); const low = new Field(toBigIntBE(hash.subarray(16, 32))); - context.machineState.memory.set(this.dstOffset, high); - context.machineState.memory.set(this.dstOffset + 1, low); + context.machineState.memory.set(dstOffset, high); + context.machineState.memory.set(dstOffset + 1, low); context.machineState.incrementPc(); } @@ -113,21 +139,27 @@ export class Pedersen extends Instruction { // Informs (de)serialization. See Instruction.deserialize. static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, OperandType.UINT8, OperandType.UINT32, OperandType.UINT32, OperandType.UINT32, ]; - constructor(private dstOffset: number, private hashOffset: number, private hashSize: number) { + constructor( + private indirect: number, + private dstOffset: number, + private hashOffset: number, + private hashSize: number, + ) { super(); } async execute(context: AvmContext): Promise { + const [hashOffset] = Addressing.fromWire(this.indirect).resolve([this.hashOffset], context.machineState.memory); + // We hash a set of field elements - const hashData = context.machineState.memory - .getSlice(this.hashOffset, this.hashOffset + this.hashSize) - .map(word => word.toBuffer()); + const hashData = context.machineState.memory.getSlice(hashOffset, this.hashSize).map(word => word.toBuffer()); // No domain sep for now const hash = pedersenHash(hashData); diff --git a/yarn-project/simulator/src/avm/opcodes/memory.ts b/yarn-project/simulator/src/avm/opcodes/memory.ts index d8c9ad7c0aed..4f8f28e1f862 100644 --- a/yarn-project/simulator/src/avm/opcodes/memory.ts +++ b/yarn-project/simulator/src/avm/opcodes/memory.ts @@ -186,6 +186,7 @@ export class CalldataCopy extends Instruction { const transformedData = context.environment.calldata .slice(this.cdOffset, this.cdOffset + this.copySize) .map(f => new Field(f)); + context.machineState.memory.setSlice(this.dstOffset, transformedData); context.machineState.incrementPc();