From 7459911f010b3317d405725a71e40c27e6133ad5 Mon Sep 17 00:00:00 2001 From: fcarreiro Date: Fri, 12 Apr 2024 09:21:08 +0000 Subject: [PATCH] feat(avm): keccack as blackbox --- avm-transpiler/src/transpile.rs | 27 ++++++- .../contracts/avm_test_contract/src/main.nr | 6 +- .../aztec.js/src/utils/cheat_codes.ts | 4 +- .../foundation/src/abi/event_selector.ts | 4 +- .../foundation/src/abi/function_selector.ts | 4 +- .../foundation/src/crypto/keccak/index.ts | 2 +- .../pxe/src/kernel_prover/kernel_prover.ts | 2 +- .../simulator/src/avm/avm_simulator.test.ts | 73 ++++++++++++------- .../simulator/src/avm/opcodes/hashing.test.ts | 63 ++++++++-------- .../simulator/src/avm/opcodes/hashing.ts | 35 ++++----- 10 files changed, 132 insertions(+), 88 deletions(-) diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index 34c3388b9246..fd33c0cee48d 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -292,7 +292,7 @@ fn handle_foreign_call( "avmOpcodeNullifierExists" => handle_nullifier_exists(avm_instrs, destinations, inputs), "avmOpcodeL1ToL2MsgExists" => handle_l1_to_l2_msg_exists(avm_instrs, destinations, inputs), "avmOpcodeSendL2ToL1Msg" => handle_send_l2_to_l1_msg(avm_instrs, destinations, inputs), - "avmOpcodeKeccak256" | "avmOpcodeSha256" => { + "avmOpcodeSha256" => { handle_2_field_hash_instruction(avm_instrs, function, destinations, inputs) } "avmOpcodeGetContractInstance" => { @@ -902,6 +902,31 @@ fn handle_black_box_function(avm_instrs: &mut Vec, operation: &B ..Default::default() }); } + BlackBoxOp::Keccak256 { message, output } => { + let message_offset = message.pointer.0; + let message_size_offset = message.size.0; + let dest_offset = output.pointer.0; + assert_eq!(output.size, 32, "Keccak256 output size must be 32!"); + + avm_instrs.push(AvmInstruction { + opcode: AvmOpcode::KECCAK, + indirect: Some( + ZEROTH_OPERAND_INDIRECT | FIRST_OPERAND_INDIRECT | SECOND_OPERAND_INDIRECT, + ), + operands: vec![ + AvmOperand::U32 { + value: dest_offset as u32, + }, + AvmOperand::U32 { + value: message_offset as u32, + }, + AvmOperand::U32 { + value: message_size_offset as u32, + }, + ], + ..Default::default() + }); + } _ => panic!("Transpiler doesn't know how to process {:?}", operation), } } 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 b7efa77cbbe3..356d09a08734 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 @@ -32,7 +32,7 @@ contract AvmTest { use dep::compressed_string::CompressedString; // avm lib - use dep::aztec::avm::hash::{keccak256, sha256}; + use dep::aztec::avm::hash::sha256; #[aztec(storage)] struct Storage { @@ -145,8 +145,8 @@ contract AvmTest { * Hashing functions ************************************************************************/ #[aztec(public-vm)] - fn keccak_hash(data: [Field; 10]) -> pub [Field; 2] { - keccak256(data) + fn keccak_hash(data: [u8; 10]) -> pub [u8; 32] { + dep::std::hash::keccak256(data, data.len() as u32) } #[aztec(public-vm)] diff --git a/yarn-project/aztec.js/src/utils/cheat_codes.ts b/yarn-project/aztec.js/src/utils/cheat_codes.ts index b3c0bcfbe97b..4550cd929182 100644 --- a/yarn-project/aztec.js/src/utils/cheat_codes.ts +++ b/yarn-project/aztec.js/src/utils/cheat_codes.ts @@ -1,7 +1,7 @@ import { type Note, type PXE } from '@aztec/circuit-types'; import { type AztecAddress, type EthAddress, Fr } from '@aztec/circuits.js'; import { toBigIntBE, toHex } from '@aztec/foundation/bigint-buffer'; -import { keccak, pedersenHash } from '@aztec/foundation/crypto'; +import { keccak256, pedersenHash } from '@aztec/foundation/crypto'; import { createDebugLogger } from '@aztec/foundation/log'; import fs from 'fs'; @@ -167,7 +167,7 @@ export class EthCheatCodes { public keccak256(baseSlot: bigint, key: bigint): bigint { // abi encode (removing the 0x) - concat key and baseSlot (both padded to 32 bytes) const abiEncoded = toHex(key, true).substring(2) + toHex(baseSlot, true).substring(2); - return toBigIntBE(keccak(Buffer.from(abiEncoded, 'hex'))); + return toBigIntBE(keccak256(Buffer.from(abiEncoded, 'hex'))); } /** diff --git a/yarn-project/foundation/src/abi/event_selector.ts b/yarn-project/foundation/src/abi/event_selector.ts index f9fa0a5b05e0..d7cbe951a468 100644 --- a/yarn-project/foundation/src/abi/event_selector.ts +++ b/yarn-project/foundation/src/abi/event_selector.ts @@ -1,5 +1,5 @@ import { fromHex, toBigIntBE } from '../bigint-buffer/index.js'; -import { keccak, randomBytes } from '../crypto/index.js'; +import { keccak256, randomBytes } from '../crypto/index.js'; import { type Fr } from '../fields/fields.js'; import { BufferReader } from '../serialize/buffer_reader.js'; import { Selector } from './selector.js'; @@ -44,7 +44,7 @@ export class EventSelector extends Selector { if (/\s/.test(signature)) { throw new Error('Signature cannot contain whitespace'); } - return EventSelector.fromBuffer(keccak(Buffer.from(signature)).subarray(0, Selector.SIZE)); + return EventSelector.fromBuffer(keccak256(Buffer.from(signature)).subarray(0, Selector.SIZE)); } /** diff --git a/yarn-project/foundation/src/abi/function_selector.ts b/yarn-project/foundation/src/abi/function_selector.ts index 3c71482a6a03..2002be8f5963 100644 --- a/yarn-project/foundation/src/abi/function_selector.ts +++ b/yarn-project/foundation/src/abi/function_selector.ts @@ -1,5 +1,5 @@ import { fromHex, toBigIntBE } from '../bigint-buffer/index.js'; -import { keccak, randomBytes } from '../crypto/index.js'; +import { keccak256, randomBytes } from '../crypto/index.js'; import { type Fr } from '../fields/fields.js'; import { BufferReader } from '../serialize/buffer_reader.js'; import { FieldReader } from '../serialize/field_reader.js'; @@ -72,7 +72,7 @@ export class FunctionSelector extends Selector { if (/\s/.test(signature)) { throw new Error('Signature cannot contain whitespace'); } - return FunctionSelector.fromBuffer(keccak(Buffer.from(signature)).subarray(0, Selector.SIZE)); + return FunctionSelector.fromBuffer(keccak256(Buffer.from(signature)).subarray(0, Selector.SIZE)); } /** diff --git a/yarn-project/foundation/src/crypto/keccak/index.ts b/yarn-project/foundation/src/crypto/keccak/index.ts index db93b1ad527d..f4093e113eb0 100644 --- a/yarn-project/foundation/src/crypto/keccak/index.ts +++ b/yarn-project/foundation/src/crypto/keccak/index.ts @@ -6,7 +6,7 @@ import { Keccak } from 'sha3'; * @param input - The input buffer to be hashed. * @returns The computed Keccak-256 hash as a Buffer. */ -export function keccak(input: Buffer) { +export function keccak256(input: Buffer) { const hash = new Keccak(256); return hash.update(input).digest(); } diff --git a/yarn-project/pxe/src/kernel_prover/kernel_prover.ts b/yarn-project/pxe/src/kernel_prover/kernel_prover.ts index 123ee026b925..bf776d13b115 100644 --- a/yarn-project/pxe/src/kernel_prover/kernel_prover.ts +++ b/yarn-project/pxe/src/kernel_prover/kernel_prover.ts @@ -214,7 +214,7 @@ export class KernelProver { await this.oracle.getContractClassIdPreimage(contractClassId); // TODO(#262): Use real acir hash - // const acirHash = keccak(Buffer.from(bytecode, 'hex')); + // const acirHash = keccak235(Buffer.from(bytecode, 'hex')); const acirHash = Fr.fromBuffer(Buffer.alloc(32, 0)); // TODO diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index cd1d28652770..e659edd7911e 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -2,7 +2,7 @@ import { UnencryptedL2Log } from '@aztec/circuit-types'; import { computeVarArgsHash } from '@aztec/circuits.js/hash'; import { EventSelector, FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; -import { keccak, pedersenHash, poseidon2Hash, sha256 } from '@aztec/foundation/crypto'; +import { keccak256, pedersenHash, poseidon2Hash, sha256 } from '@aztec/foundation/crypto'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { AvmNestedCallsTestContractArtifact, AvmTestContractArtifact } from '@aztec/noir-contracts.js'; @@ -12,7 +12,7 @@ import { strict as assert } from 'assert'; import { isAvmBytecode } from '../public/transitional_adaptors.js'; import { AvmMachineState } from './avm_machine_state.js'; -import { TypeTag } from './avm_memory_types.js'; +import { Field, type MemoryValue, TypeTag, Uint8 } from './avm_memory_types.js'; import { AvmSimulator } from './avm_simulator.js'; import { adjustCalldataIndex, @@ -121,37 +121,49 @@ describe('AVM simulator: transpiled Noir contracts', () => { }); describe.each([ - ['sha256_hash', sha256], - ['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 = [...Array(10)].map(_ => Fr.random()); - const hash = hashFunction(Buffer.concat(calldata.map(f => f.toBuffer()))); - - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); - const bytecode = getAvmTestContractBytecode(name); - const results = await new AvmSimulator(context).executeBytecode(bytecode); - - expect(results.reverted).toBe(false); - expect(results.output).toEqual([new Fr(hash.subarray(0, 16)), new Fr(hash.subarray(16, 32))]); - }); - }); - - describe.each([ - ['poseidon2_hash', poseidon2Hash], - ['pedersen_hash', pedersenHash], - ['pedersen_hash_with_index', (m: Buffer[]) => pedersenHash(m, 20)], - ])('Hashes with field returned in noir contracts', (name: string, hashFunction: (data: Buffer[]) => Fr) => { - it(`Should execute contract function that performs ${name} hash`, async () => { - const calldata = [...Array(10)].map(_ => Fr.random()); - const hash = hashFunction(calldata.map(f => f.toBuffer())); + [ + 'sha256_hash', + /*input=*/ randomBytes(10), + /*output=*/ (fields: Field[]) => { + const resBuffer = sha256(Buffer.concat(fields.map(f => f.toBuffer()))); + return [new Fr(resBuffer.subarray(0, 16)), new Fr(resBuffer.subarray(16, 32))]; + }, + ], + [ + 'keccak_hash', + /*input=*/ randomBytes(10), + /*output=*/ (bytes: Uint8[]) => [...keccak256(Buffer.concat(bytes.map(b => b.toBuffer())))].map(b => new Fr(b)), + ], + [ + 'poseidon2_hash', + /*input=*/ randomFields(10), + /*output=*/ (fields: Field[]) => [poseidon2Hash(fields.map(f => f.toBuffer()))], + ], + [ + 'pedersen_hash', + /*input=*/ randomFields(10), + /*output=*/ (fields: Field[]) => [pedersenHash(fields.map(f => f.toBuffer()))], + ], + [ + 'pedersen_hash_with_index', + /*input=*/ randomFields(10), + /*output=*/ (fields: Field[]) => [ + pedersenHash( + fields.map(f => f.toBuffer()), + /*index=*/ 20, + ), + ], + ], + ])('Hashes in noir contracts', (name: string, input: MemoryValue[], output: (msg: any[]) => Fr[]) => { + it(`Should execute contract function that performs ${name}`, async () => { + const calldata = input.map(e => e.toFr()); const context = initContext({ env: initExecutionEnvironment({ calldata }) }); const bytecode = getAvmTestContractBytecode(name); const results = await new AvmSimulator(context).executeBytecode(bytecode); expect(results.reverted).toBe(false); - expect(results.output).toEqual([new Fr(hash)]); + expect(results.output).toEqual(output(input)); }); }); @@ -923,3 +935,10 @@ function getAvmNestedCallsTestContractBytecode(functionName: string): Buffer { ); return artifact.bytecode; } + +function randomBytes(length: number): Uint8[] { + return [...Array(length)].map(_ => new Uint8(Math.floor(Math.random() * 255))); +} +function randomFields(length: number): Field[] { + return [...Array(length)].map(_ => new Field(Fr.random())); +} diff --git a/yarn-project/simulator/src/avm/opcodes/hashing.test.ts b/yarn-project/simulator/src/avm/opcodes/hashing.test.ts index 05b9b749288f..eca64fcc7813 100644 --- a/yarn-project/simulator/src/avm/opcodes/hashing.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/hashing.test.ts @@ -1,8 +1,7 @@ -import { Fr } from '@aztec/circuits.js'; -import { keccak, pedersenHash, sha256 } from '@aztec/foundation/crypto'; +import { keccak256, pedersenHash, sha256 } from '@aztec/foundation/crypto'; import { type AvmContext } from '../avm_context.js'; -import { Field, Uint32 } from '../avm_memory_types.js'; +import { Field, Uint8, Uint32 } from '../avm_memory_types.js'; import { initContext } from '../fixtures/index.js'; import { Addressing, AddressingMode } from './addressing_mode.js'; import { Keccak, Pedersen, Poseidon2, Sha256 } from './hashing.js'; @@ -75,13 +74,13 @@ describe('Hashing Opcodes', () => { 1, // indirect ...Buffer.from('12345678', 'hex'), // dstOffset ...Buffer.from('23456789', 'hex'), // messageOffset - ...Buffer.from('3456789a', 'hex'), // hashSize + ...Buffer.from('3456789a', 'hex'), // messageSizeOffset ]); const inst = new Keccak( /*indirect=*/ 1, /*dstOffset=*/ 0x12345678, /*messageOffset=*/ 0x23456789, - /*hashSize=*/ 0x3456789a, + /*messageSizeOffset=*/ 0x3456789a, ); expect(Keccak.deserialize(buf)).toEqual(inst); @@ -89,47 +88,51 @@ describe('Hashing Opcodes', () => { }); it('Should hash correctly - direct', async () => { - const args = [new Field(1n), new Field(2n), new Field(3n)]; + const args = [...Array(10)].map(_ => new Uint8(Math.floor(Math.random() * 255))); const indirect = 0; const messageOffset = 0; + const messageSizeOffset = 15; + const dstOffset = 20; + context.machineState.memory.set(messageSizeOffset, new Uint32(args.length)); context.machineState.memory.setSlice(messageOffset, args); - const dstOffset = 3; + await new Keccak(indirect, dstOffset, messageOffset, messageSizeOffset).execute(context); - const inputBuffer = Buffer.concat(args.map(field => field.toBuffer())); - const expectedHash = keccak(inputBuffer); - await new Keccak(indirect, dstOffset, messageOffset, 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); + const resultBuffer = Buffer.concat( + context.machineState.memory.getSliceAs(dstOffset, 32).map(byte => byte.toBuffer()), + ); + const inputBuffer = Buffer.concat(args.map(byte => byte.toBuffer())); + const expectedHash = keccak256(inputBuffer); + expect(resultBuffer).toEqual(expectedHash); }); it('Should hash correctly - indirect', async () => { - const args = [new Field(1n), new Field(2n), new Field(3n)]; + const args = [...Array(10)].map(_ => new Uint8(Math.floor(Math.random() * 255))); const indirect = new Addressing([ /*dstOffset=*/ AddressingMode.INDIRECT, /*messageOffset*/ AddressingMode.INDIRECT, + /*messageSizeOffset*/ AddressingMode.INDIRECT, ]).toWire(); const messageOffset = 0; - const argsLocation = 4; - + const messageOffsetReal = 10; + const messageSizeOffset = 1; + const messageSizeOffsetReal = 100; const dstOffset = 2; - const readLocation = 6; + const dstOffsetReal = 30; + context.machineState.memory.set(messageOffset, new Uint32(messageOffsetReal)); + context.machineState.memory.set(dstOffset, new Uint32(dstOffsetReal)); + context.machineState.memory.set(messageSizeOffset, new Uint32(messageSizeOffsetReal)); + context.machineState.memory.set(messageSizeOffsetReal, new Uint32(args.length)); + context.machineState.memory.setSlice(messageOffsetReal, args); - context.machineState.memory.set(messageOffset, new Uint32(argsLocation)); - context.machineState.memory.set(dstOffset, new Uint32(readLocation)); - context.machineState.memory.setSlice(argsLocation, args); + await new Keccak(indirect, dstOffset, messageOffset, messageSizeOffset).execute(context); - const inputBuffer = Buffer.concat(args.map(field => field.toBuffer())); - const expectedHash = keccak(inputBuffer); - await new Keccak(indirect, dstOffset, messageOffset, 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); + const resultBuffer = Buffer.concat( + context.machineState.memory.getSliceAs(dstOffsetReal, 32).map(byte => byte.toBuffer()), + ); + const inputBuffer = Buffer.concat(args.map(byte => byte.toBuffer())); + const expectedHash = keccak256(inputBuffer); + expect(resultBuffer).toEqual(expectedHash); }); }); diff --git a/yarn-project/simulator/src/avm/opcodes/hashing.ts b/yarn-project/simulator/src/avm/opcodes/hashing.ts index 68020ca92b3d..ab2c42d6d083 100644 --- a/yarn-project/simulator/src/avm/opcodes/hashing.ts +++ b/yarn-project/simulator/src/avm/opcodes/hashing.ts @@ -1,8 +1,10 @@ import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; -import { keccak, pedersenHash, poseidon2Permutation, sha256 } from '@aztec/foundation/crypto'; +import { keccak256, pedersenHash, poseidon2Permutation, sha256 } from '@aztec/foundation/crypto'; + +import { strict as assert } from 'assert'; import { type AvmContext } from '../avm_context.js'; -import { Field } from '../avm_memory_types.js'; +import { Field, Uint8 } from '../avm_memory_types.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; import { Instruction } from './instruction.js'; @@ -63,33 +65,28 @@ export class Keccak extends Instruction { private indirect: number, private dstOffset: number, private messageOffset: number, - private messageSize: number, + private messageSizeOffset: number, ) { super(); } - // Note hash output is 32 bytes, so takes up two fields + // pub fn keccak256(input: [u8], message_size: u32) -> [u8; 32] public async execute(context: AvmContext): Promise { - const memoryOperations = { reads: this.messageSize, writes: 2, indirect: this.indirect }; const memory = context.machineState.memory.track(this.type); - context.machineState.consumeGas(this.gasCost(memoryOperations)); - - // We hash a set of field elements - const [dstOffset, messageOffset] = Addressing.fromWire(this.indirect).resolve( - [this.dstOffset, this.messageOffset], + const [dstOffset, messageOffset, messageSizeOffset] = Addressing.fromWire(this.indirect).resolve( + [this.dstOffset, this.messageOffset, this.messageSizeOffset], memory, ); + const messageSize = memory.get(messageSizeOffset).toNumber(); + const memoryOperations = { reads: messageSize + 1, writes: 32, indirect: this.indirect }; + context.machineState.consumeGas(this.gasCost(memoryOperations)); - const hashData = memory.getSlice(messageOffset, this.messageSize).map(word => word.toBuffer()); - - const hash = keccak(Buffer.concat(hashData)); + const messageData = Buffer.concat(memory.getSlice(messageOffset, messageSize).map(word => word.toBuffer())); + const hashBuffer = keccak256(messageData); - // Split output into two fields - const high = new Field(toBigIntBE(hash.subarray(0, 16))); - const low = new Field(toBigIntBE(hash.subarray(16, 32))); - - memory.set(dstOffset, high); - memory.set(dstOffset + 1, low); + // We need to convert the hashBuffer because map doesn't work as expected on an Uint8Array (Buffer). + const res = [...hashBuffer].map(byte => new Uint8(byte)); + memory.setSlice(dstOffset, res); memory.assert(memoryOperations); context.machineState.incrementPc();