From de4c57a0530c4febc4e0cd232e259390e4b9e9da Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Tue, 4 Jun 2024 08:57:10 +0100 Subject: [PATCH 1/4] ecadd op code --- yarn-project/foundation/src/fields/fields.ts | 8 ++ .../foundation/src/fields/point.test.ts | 27 ++++++ yarn-project/foundation/src/fields/point.ts | 88 ++++++++++++++++++- yarn-project/simulator/src/avm/avm_gas.ts | 1 + .../simulator/src/avm/opcodes/ec_add.test.ts | 73 +++++++++++++++ .../simulator/src/avm/opcodes/ec_add.ts | 60 +++++++++++++ .../serialization/bytecode_serialization.ts | 2 + .../instruction_serialization.ts | 1 + yarn-project/tsconfig.json | 3 +- 9 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 yarn-project/foundation/src/fields/point.test.ts create mode 100644 yarn-project/simulator/src/avm/opcodes/ec_add.test.ts create mode 100644 yarn-project/simulator/src/avm/opcodes/ec_add.ts diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index c01abe041c63..a0233d91a995 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -248,6 +248,14 @@ export class Fr extends BaseField { return new Fr((this.toBigInt() + rhs.toBigInt()) % Fr.MODULUS); } + square() { + return new Fr((this.toBigInt() * this.toBigInt()) % Fr.MODULUS); + } + + negate() { + return new Fr(Fr.MODULUS - this.toBigInt()); + } + sub(rhs: Fr) { const result = this.toBigInt() - rhs.toBigInt(); return new Fr(result < 0 ? result + Fr.MODULUS : result); diff --git a/yarn-project/foundation/src/fields/point.test.ts b/yarn-project/foundation/src/fields/point.test.ts new file mode 100644 index 000000000000..b72576efdb4f --- /dev/null +++ b/yarn-project/foundation/src/fields/point.test.ts @@ -0,0 +1,27 @@ +import { Point } from './point.js'; + +describe('Point', () => { + it('equals', () => { + expect(Point.G).toEqual(Point.G); + }); + + it('double, add, sub', () => { + const G2 = Point.G.double(); + expect(G2.equals(Point.G.add(Point.G))).toBe(true); + expect(G2.is_on_grumpkin()).toBe(true); + expect(G2.equals(Point.G)).toBe(false); + + const G3 = Point.G.add(Point.G).add(Point.G); + expect(G3.is_on_grumpkin()).toBe(true); + expect(G3.equals(G2)).toBe(false); + + const G4 = G2.double(); + expect(G4.equals(G3)).toBe(false); + expect(G4.is_on_grumpkin()).toBe(true); + + expect(G4.sub(Point.G)).toEqual(G3); + expect(G4.sub(G2)).toEqual(G2); + expect(G4.sub(G3)).toEqual(Point.G); + expect(Point.G.sub(Point.G)).toEqual(Point.ZERO); + }); +}); diff --git a/yarn-project/foundation/src/fields/point.ts b/yarn-project/foundation/src/fields/point.ts index 9fd8e8fdf03e..97bba691a841 100644 --- a/yarn-project/foundation/src/fields/point.ts +++ b/yarn-project/foundation/src/fields/point.ts @@ -10,6 +10,7 @@ import { Fr } from './fields.js'; export class Point { static ZERO = new Point(Fr.ZERO, Fr.ZERO); static SIZE_IN_BYTES = Fr.SIZE_IN_BYTES * 2; + static G = new Point(new Fr(1n), new Fr(17631683881184975370165255887551781615748388533673675138860n)); /** Used to differentiate this class from AztecAddress */ public readonly kind = 'point'; @@ -23,7 +24,9 @@ export class Point { * The point's y coordinate */ public readonly y: Fr, - ) {} + ) { + // TODO: Do we want to check if the point is on the curve here? + } /** * Generate a random Point instance. @@ -134,6 +137,89 @@ export class Point { hash() { return poseidon2Hash(this.toFields()); } + + is_on_grumpkin() { + // allow zero point to represent infinity + if (this.isZero()) { + return true; + } + + // p.y * p.y == p.x * p.x * p.x - 17 + const A = new Fr(17); + const lhs = this.y.square(); + const rhs = this.x.square().mul(this.x).sub(A); + return lhs.equals(rhs); + } + + /** + * Double the current Point instance. + * Returns a new Point instance representing the result of the doubling operation. + * + * @returns A new Point instance representing the result of the doubling operation. + */ + double() { + if (this.isZero()) { + return this; + } + + const two = new Fr(2); + const three = new Fr(3); + + const lambda = this.x.square().mul(three).div(this.y.mul(two)); + const x = lambda.square().sub(this.x.mul(two)); + const y = lambda.mul(this.x.sub(x)).sub(this.y); + return new Point(x, y); + } + + /** + * Add another Point instance to the current instance. + * Returns a new Point instance representing the sum of the two points. + * + * @param rhs - The Point instance to add to the current instance. + * @returns A new Point instance representing the sum of the two points. + */ + add(rhs: Point) { + if (this.isZero()) { + return rhs; + } + if (rhs.isZero()) { + return this; + } + + if (this.equals(rhs)) { + return this.double(); + } + + if (this.x.equals(rhs.x) && this.y.equals(rhs.y.negate())) { + return Point.ZERO; + } + + const lambda = this.y.sub(rhs.y).div(this.x.sub(rhs.x)); + const x = lambda.square().sub(this.x).sub(rhs.x); + const y = lambda.mul(this.x.sub(x)).sub(this.y); + return new Point(x, y); + } + + /** + * Negate the current Point instance. + * Returns a new Point instance representing the negation of the current instance. + * + * @returns A new Point instance representing the negation of the current instance. + */ + negate() { + return new Point(this.x, this.y.negate()); + } + + /** + * Subtract another Point instance from the current instance. + * Returns a new Point instance representing the difference of the two points. + * + * @param rhs - The Point instance to subtract from the current instance. + * @returns A new Point instance representing the difference of the two points. + */ + sub(rhs: Point) { + return this.add(rhs.negate()); + } } /** diff --git a/yarn-project/simulator/src/avm/avm_gas.ts b/yarn-project/simulator/src/avm/avm_gas.ts index b7616012f40a..7802d4177d1e 100644 --- a/yarn-project/simulator/src/avm/avm_gas.ts +++ b/yarn-project/simulator/src/avm/avm_gas.ts @@ -122,6 +122,7 @@ const BaseGasCosts: Record = { [Opcode.POSEIDON2]: DefaultBaseGasCost, [Opcode.SHA256]: DefaultBaseGasCost, [Opcode.PEDERSEN]: DefaultBaseGasCost, + [Opcode.ECADD]: DefaultBaseGasCost, // Conversions [Opcode.TORADIXLE]: DefaultBaseGasCost, }; diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts new file mode 100644 index 000000000000..b5bd6df7ac4c --- /dev/null +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts @@ -0,0 +1,73 @@ +import { Point } from '@aztec/circuits.js'; + +import { beforeEach } from '@jest/globals'; + +import { type AvmContext } from '../avm_context.js'; +import { Field } from '../avm_memory_types.js'; +import { initContext } from '../fixtures/index.js'; +import { EcAdd } from './ec_add.js'; + +describe('EC Instructions', () => { + let context: AvmContext; + + beforeEach(() => { + context = initContext(); + }); + + describe('EcAdd', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + EcAdd.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // aOffset + ...Buffer.from('23456789', 'hex'), // bOffset + ...Buffer.from('3456789a', 'hex'), // dstOffset + ]); + const inst = new EcAdd( + /*indirect=*/ 0x01, + /*aOffset=*/ 0x12345678, + /*bOffset=*/ 0x23456789, + /*dstOffset=*/ 0x3456789a, + ); + + expect(EcAdd.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it(`Should double correctly`, async () => { + const x = new Field(Point.G.x); + const y = new Field(Point.G.y); + + context.machineState.memory.set(0, x); + context.machineState.memory.set(1, y); + context.machineState.memory.set(2, x); + context.machineState.memory.set(3, y); + + await new EcAdd(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 4).execute(context); + + const actual = new Point(context.machineState.memory.get(4).toFr(), context.machineState.memory.get(5).toFr()); + const expected = Point.G.double(); + expect(actual).toEqual(expected); + }); + + it('Should add correctly', async () => { + const G2 = Point.G.add(Point.G); + + const x1 = new Field(Point.G.x); + const y1 = new Field(Point.G.y); + const x2 = new Field(G2.x); + const y2 = new Field(G2.y); + + context.machineState.memory.set(0, x1); + context.machineState.memory.set(1, y1); + context.machineState.memory.set(2, x2); + context.machineState.memory.set(3, y2); + + await new EcAdd(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 4).execute(context); + + const actual = new Point(context.machineState.memory.get(4).toFr(), context.machineState.memory.get(5).toFr()); + const G3 = G2.add(Point.G); + expect(actual).toEqual(G3); + }); + }); +}); diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.ts new file mode 100644 index 000000000000..e739e7d0e62c --- /dev/null +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.ts @@ -0,0 +1,60 @@ +import { Point } from '@aztec/foundation/fields'; + +import { type AvmContext } from '../avm_context.js'; +import { getBaseGasCost, getGasCostForTypeTag, getMemoryGasCost, sumGas } from '../avm_gas.js'; +import { Field, MemoryOperations, TypeTag } from '../avm_memory_types.js'; +import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; +import { Instruction } from './instruction.js'; + +export class EcAdd extends Instruction { + static type: string = 'ECADD'; + static readonly opcode = Opcode.ECADD; + + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat: OperandType[] = [ + OperandType.UINT8, // reserved + OperandType.UINT8, // indirect + OperandType.UINT32, // p1 + OperandType.UINT32, // p2 + OperandType.UINT32, // dst + ]; + + constructor(private indirect: number, private p1Offset: number, private p2Offset: number, private dstOffset: number) { + super(); + } + + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 4, writes: 2, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + memory.checkTags(TypeTag.FIELD, this.p1Offset, this.p2Offset); + + const p1X = memory.get(this.p1Offset); + const p1Y = memory.get(this.p1Offset + 1); + const p1 = new Point(p1X.toFr(), p1Y.toFr()); + if (!p1.is_on_grumpkin()) { + throw new Error(`Point1 at offset ${this.p1Offset} is not on the curve`); + } + + const p2X = memory.get(this.p2Offset); + const p2Y = memory.get(this.p2Offset + 1); + const p2 = new Point(p2X.toFr(), p2Y.toFr()); + if (!p2.is_on_grumpkin()) { + throw new Error(`Point2 at offset ${this.p2Offset} is not on the curve`); + } + + const dest = p1.add(p2); + memory.set(this.dstOffset, new Field(dest.x)); + memory.set(this.dstOffset + 1, new Field(dest.y)); + + memory.assert(memoryOperations); + context.machineState.incrementPc(); + } + + protected override gasCost(memoryOps: Partial) { + const baseGasCost = getGasCostForTypeTag(TypeTag.FIELD, getBaseGasCost(this.opcode)); + const memoryGasCost = getMemoryGasCost(memoryOps); + return sumGas(baseGasCost, memoryGasCost); + } +} diff --git a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts index f7906fffc265..0cf22ba0a5c4 100644 --- a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts @@ -1,4 +1,5 @@ import { DAGasLeft, L2GasLeft } from '../opcodes/context_getters.js'; +import { EcAdd } from '../opcodes/ec_add.js'; import { Keccak, Pedersen, Poseidon2, Sha256 } from '../opcodes/hashing.js'; import type { Instruction } from '../opcodes/index.js'; import { @@ -137,6 +138,7 @@ const INSTRUCTION_SET = () => [DebugLog.opcode, DebugLog], // Gadgets + [EcAdd.opcode, EcAdd], [Keccak.opcode, Keccak], [Poseidon2.opcode, Poseidon2], [Sha256.opcode, Sha256], diff --git a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts index dca3368b89b8..d8ccbd918409 100644 --- a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts @@ -76,6 +76,7 @@ export enum Opcode { POSEIDON2, SHA256, // temp - may be removed, but alot of contracts rely on it PEDERSEN, // temp - may be removed, but alot of contracts rely on it + ECADD, // Conversion TORADIXLE, } diff --git a/yarn-project/tsconfig.json b/yarn-project/tsconfig.json index 96d001f5ded6..52b02c625b21 100644 --- a/yarn-project/tsconfig.json +++ b/yarn-project/tsconfig.json @@ -49,5 +49,6 @@ { "path": "entrypoints/tsconfig.json" }, { "path": "cli/tsconfig.json" } ], - "files": ["./@types/jest/index.d.ts"] + "files": ["./@types/jest/index.d.ts"], + "exclude": ["node_modules"] } From 7202991833ab489bb621bb438aa15d6ebd96309a Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Tue, 4 Jun 2024 12:44:20 +0100 Subject: [PATCH 2/4] plumb through transpiler use barretenberg wasm for ec ops --- avm-transpiler/src/opcodes.rs | 2 + avm-transpiler/src/transpile.rs | 22 +++++ .../contracts/avm_test_contract/src/main.nr | 10 +++ .../foundation/src/fields/point.test.ts | 27 ------ yarn-project/foundation/src/fields/point.ts | 76 +---------------- .../simulator/src/avm/avm_simulator.test.ts | 16 +++- .../simulator/src/avm/opcodes/ec_add.test.ts | 85 ++++++++++++++----- .../simulator/src/avm/opcodes/ec_add.ts | 56 ++++++++---- 8 files changed, 153 insertions(+), 141 deletions(-) delete mode 100644 yarn-project/foundation/src/fields/point.test.ts diff --git a/avm-transpiler/src/opcodes.rs b/avm-transpiler/src/opcodes.rs index 172531271374..e4d642dd8298 100644 --- a/avm-transpiler/src/opcodes.rs +++ b/avm-transpiler/src/opcodes.rs @@ -72,6 +72,7 @@ pub enum AvmOpcode { POSEIDON2, SHA256, // temp - may be removed, but alot of contracts rely on it PEDERSEN, // temp - may be removed, but alot of contracts rely on it + ECADD, // Conversions TORADIXLE, } @@ -163,6 +164,7 @@ impl AvmOpcode { AvmOpcode::POSEIDON2 => "POSEIDON2", AvmOpcode::SHA256 => "SHA256 ", AvmOpcode::PEDERSEN => "PEDERSEN", + AvmOpcode::ECADD => "ECADD", // Conversions AvmOpcode::TORADIXLE => "TORADIXLE", } diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index 099dfcbd06fb..057f18665954 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -831,6 +831,28 @@ fn handle_black_box_function(avm_instrs: &mut Vec, operation: &B ], }); } + BlackBoxOp::EmbeddedCurveAdd { + input1_x, + input1_y, + input1_infinite, + input2_x, + input2_y, + input2_infinite, + result, + } => avm_instrs.push(AvmInstruction { + opcode: AvmOpcode::ECADD, + indirect: Some(ALL_DIRECT), + operands: vec![ + AvmOperand::U32 { value: input1_x.0 as u32 }, + AvmOperand::U32 { value: input1_y.0 as u32 }, + AvmOperand::U8 { value: input1_infinite.0 as u8 }, + AvmOperand::U32 { value: input2_x.0 as u32 }, + AvmOperand::U32 { value: input2_y.0 as u32 }, + AvmOperand::U8 { value: input2_infinite.0 as u8 }, + AvmOperand::U32 { value: result.pointer.0 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 bd54379030ff..5b92424f0aaf 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 @@ -24,6 +24,7 @@ contract AvmTest { global big_field_136_bits: Field = 0x991234567890abcdef1234567890abcdef; // Libs + use dep::std::embedded_curve_ops::EmbeddedCurvePoint; use dep::aztec::protocol_types::constants::CONTRACT_INSTANCE_LENGTH; use dep::aztec::prelude::{Map, Deserialize}; use dep::aztec::state_vars::PublicMutable; @@ -134,6 +135,15 @@ contract AvmTest { a % 2 } + #[aztec(public)] + fn elliptic_curve_add_and_double() -> EmbeddedCurvePoint { + let g = EmbeddedCurvePoint { x: 1, y: 17631683881184975370165255887551781615748388533673675138860, is_infinite: false }; + + let doubled = g + g; + let added = g + doubled; + added + } + /************************************************************************ * Misc ************************************************************************/ diff --git a/yarn-project/foundation/src/fields/point.test.ts b/yarn-project/foundation/src/fields/point.test.ts deleted file mode 100644 index b72576efdb4f..000000000000 --- a/yarn-project/foundation/src/fields/point.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Point } from './point.js'; - -describe('Point', () => { - it('equals', () => { - expect(Point.G).toEqual(Point.G); - }); - - it('double, add, sub', () => { - const G2 = Point.G.double(); - expect(G2.equals(Point.G.add(Point.G))).toBe(true); - expect(G2.is_on_grumpkin()).toBe(true); - expect(G2.equals(Point.G)).toBe(false); - - const G3 = Point.G.add(Point.G).add(Point.G); - expect(G3.is_on_grumpkin()).toBe(true); - expect(G3.equals(G2)).toBe(false); - - const G4 = G2.double(); - expect(G4.equals(G3)).toBe(false); - expect(G4.is_on_grumpkin()).toBe(true); - - expect(G4.sub(Point.G)).toEqual(G3); - expect(G4.sub(G2)).toEqual(G2); - expect(G4.sub(G3)).toEqual(Point.G); - expect(Point.G.sub(Point.G)).toEqual(Point.ZERO); - }); -}); diff --git a/yarn-project/foundation/src/fields/point.ts b/yarn-project/foundation/src/fields/point.ts index 97bba691a841..b152bffcc6a2 100644 --- a/yarn-project/foundation/src/fields/point.ts +++ b/yarn-project/foundation/src/fields/point.ts @@ -10,7 +10,6 @@ import { Fr } from './fields.js'; export class Point { static ZERO = new Point(Fr.ZERO, Fr.ZERO); static SIZE_IN_BYTES = Fr.SIZE_IN_BYTES * 2; - static G = new Point(new Fr(1n), new Fr(17631683881184975370165255887551781615748388533673675138860n)); /** Used to differentiate this class from AztecAddress */ public readonly kind = 'point'; @@ -47,7 +46,7 @@ export class Point { */ static fromBuffer(buffer: Buffer | BufferReader) { const reader = BufferReader.asReader(buffer); - return new this(Fr.fromBuffer(reader.readBytes(32)), Fr.fromBuffer(reader.readBytes(32))); + return new this(Fr.fromBuffer(reader), Fr.fromBuffer(reader)); } /** @@ -138,8 +137,7 @@ export class Point { return poseidon2Hash(this.toFields()); } - is_on_grumpkin() { - // allow zero point to represent infinity + isOnGrumpkin() { if (this.isZero()) { return true; } @@ -150,76 +148,6 @@ export class Point { const rhs = this.x.square().mul(this.x).sub(A); return lhs.equals(rhs); } - - /** - * Double the current Point instance. - * Returns a new Point instance representing the result of the doubling operation. - * - * @returns A new Point instance representing the result of the doubling operation. - */ - double() { - if (this.isZero()) { - return this; - } - - const two = new Fr(2); - const three = new Fr(3); - - const lambda = this.x.square().mul(three).div(this.y.mul(two)); - const x = lambda.square().sub(this.x.mul(two)); - const y = lambda.mul(this.x.sub(x)).sub(this.y); - return new Point(x, y); - } - - /** - * Add another Point instance to the current instance. - * Returns a new Point instance representing the sum of the two points. - * - * @param rhs - The Point instance to add to the current instance. - * @returns A new Point instance representing the sum of the two points. - */ - add(rhs: Point) { - if (this.isZero()) { - return rhs; - } - if (rhs.isZero()) { - return this; - } - - if (this.equals(rhs)) { - return this.double(); - } - - if (this.x.equals(rhs.x) && this.y.equals(rhs.y.negate())) { - return Point.ZERO; - } - - const lambda = this.y.sub(rhs.y).div(this.x.sub(rhs.x)); - const x = lambda.square().sub(this.x).sub(rhs.x); - const y = lambda.mul(this.x.sub(x)).sub(this.y); - return new Point(x, y); - } - - /** - * Negate the current Point instance. - * Returns a new Point instance representing the negation of the current instance. - * - * @returns A new Point instance representing the negation of the current instance. - */ - negate() { - return new Point(this.x, this.y.negate()); - } - - /** - * Subtract another Point instance from the current instance. - * Returns a new Point instance representing the difference of the two points. - * - * @param rhs - The Point instance to subtract from the current instance. - * @returns A new Point instance representing the difference of the two points. - */ - sub(rhs: Point) { - return this.add(rhs.negate()); - } } /** diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index f043c4279bcd..8f97bbb67cb1 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -1,9 +1,10 @@ import { UnencryptedL2Log } from '@aztec/circuit-types'; +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { computeVarArgsHash } from '@aztec/circuits.js/hash'; import { EventSelector, FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { keccak256, pedersenHash, poseidon2Hash, sha256 } from '@aztec/foundation/crypto'; -import { Fr } from '@aztec/foundation/fields'; +import { Fq, Fr } from '@aztec/foundation/fields'; import { type Fieldable } from '@aztec/foundation/serialize'; import { jest } from '@jest/globals'; @@ -95,6 +96,19 @@ describe('AVM simulator: transpiled Noir contracts', () => { expect(await isAvmBytecode(bytecode)); }); + it('elliptic curve operations', async () => { + const calldata: Fr[] = []; + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + + const bytecode = getAvmTestContractBytecode('elliptic_curve_add_and_double'); + const results = await new AvmSimulator(context).executeBytecode(bytecode); + + expect(results.reverted).toBe(false); + const grumpkin = new Grumpkin(); + const g3 = grumpkin.mul(grumpkin.generator(), new Fq(3)); + expect(results.output).toEqual([g3.x, g3.y, Fr.ZERO]); + }); + describe('U128 addition and overflows', () => { it('U128 addition', async () => { const calldata: Fr[] = [ diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts index b5bd6df7ac4c..d42617570d5b 100644 --- a/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts @@ -1,14 +1,16 @@ -import { Point } from '@aztec/circuits.js'; +import { Fr, Point } from '@aztec/circuits.js'; +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { beforeEach } from '@jest/globals'; import { type AvmContext } from '../avm_context.js'; -import { Field } from '../avm_memory_types.js'; +import { Field, Uint8, Uint32 } from '../avm_memory_types.js'; import { initContext } from '../fixtures/index.js'; import { EcAdd } from './ec_add.js'; describe('EC Instructions', () => { let context: AvmContext; + const grumpkin: Grumpkin = new Grumpkin(); beforeEach(() => { context = initContext(); @@ -19,15 +21,23 @@ describe('EC Instructions', () => { const buf = Buffer.from([ EcAdd.opcode, // opcode 0x01, // indirect - ...Buffer.from('12345678', 'hex'), // aOffset - ...Buffer.from('23456789', 'hex'), // bOffset - ...Buffer.from('3456789a', 'hex'), // dstOffset + ...Buffer.from('12345670', 'hex'), // p1x + ...Buffer.from('12345671', 'hex'), // p1y + ...Buffer.from('00', 'hex'), // p1IsInfinite + ...Buffer.from('12345672', 'hex'), // p2x + ...Buffer.from('12345673', 'hex'), // p2y + ...Buffer.from('01', 'hex'), // p2IsInfinite + ...Buffer.from('12345674', 'hex'), // dstOffset ]); const inst = new EcAdd( /*indirect=*/ 0x01, - /*aOffset=*/ 0x12345678, - /*bOffset=*/ 0x23456789, - /*dstOffset=*/ 0x3456789a, + /*p1X=*/ 0x12345670, + /*p1Y=*/ 0x12345671, + /*p1IsInfinite=*/ 0, + /*p2X=*/ 0x12345672, + /*p2Y=*/ 0x12345673, + /*p2IsInfinite=*/ 1, + /*dstOffset=*/ 0x12345674, ); expect(EcAdd.deserialize(buf)).toEqual(inst); @@ -35,39 +45,68 @@ describe('EC Instructions', () => { }); it(`Should double correctly`, async () => { - const x = new Field(Point.G.x); - const y = new Field(Point.G.y); + const x = new Field(grumpkin.generator().x); + const y = new Field(grumpkin.generator().y); + const zero = new Uint8(0); context.machineState.memory.set(0, x); context.machineState.memory.set(1, y); - context.machineState.memory.set(2, x); - context.machineState.memory.set(3, y); + context.machineState.memory.set(2, zero); + context.machineState.memory.set(3, x); + context.machineState.memory.set(4, y); + context.machineState.memory.set(5, zero); + context.machineState.memory.set(6, new Uint32(6)); - await new EcAdd(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 4).execute(context); + await new EcAdd( + /*indirect=*/ 0, + /*p1X=*/ 0, + /*p1Y=*/ 1, + /*p1IsInfinite=*/ 2, + /*p2X=*/ 3, + /*p2Y=*/ 4, + /*p2IsInfinite=*/ 5, + /*dstOffset=*/ 6, + ).execute(context); - const actual = new Point(context.machineState.memory.get(4).toFr(), context.machineState.memory.get(5).toFr()); - const expected = Point.G.double(); + const actual = new Point(context.machineState.memory.get(6).toFr(), context.machineState.memory.get(7).toFr()); + const expected = grumpkin.add(grumpkin.generator(), grumpkin.generator()); expect(actual).toEqual(expected); + expect(context.machineState.memory.get(8).toFr().equals(Fr.ZERO)).toBe(true); }); it('Should add correctly', async () => { - const G2 = Point.G.add(Point.G); + console.log(grumpkin.generator()); + const G2 = grumpkin.add(grumpkin.generator(), grumpkin.generator()); + const zero = new Uint8(0); - const x1 = new Field(Point.G.x); - const y1 = new Field(Point.G.y); + const x1 = new Field(grumpkin.generator().x); + const y1 = new Field(grumpkin.generator().y); const x2 = new Field(G2.x); const y2 = new Field(G2.y); context.machineState.memory.set(0, x1); context.machineState.memory.set(1, y1); - context.machineState.memory.set(2, x2); - context.machineState.memory.set(3, y2); + context.machineState.memory.set(2, zero); + context.machineState.memory.set(3, x2); + context.machineState.memory.set(4, y2); + context.machineState.memory.set(5, zero); + context.machineState.memory.set(6, new Uint32(6)); - await new EcAdd(/*indirect=*/ 0, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 4).execute(context); + await new EcAdd( + /*indirect=*/ 0, + /*p1X=*/ 0, + /*p1Y=*/ 1, + /*p1IsInfinite=*/ 2, + /*p2X=*/ 3, + /*p2Y=*/ 4, + /*p2IsInfinite=*/ 5, + /*dstOffset=*/ 6, + ).execute(context); - const actual = new Point(context.machineState.memory.get(4).toFr(), context.machineState.memory.get(5).toFr()); - const G3 = G2.add(Point.G); + const actual = new Point(context.machineState.memory.get(6).toFr(), context.machineState.memory.get(7).toFr()); + const G3 = grumpkin.add(grumpkin.generator(), G2); expect(actual).toEqual(G3); + expect(context.machineState.memory.get(8).toFr().equals(Fr.ZERO)).toBe(true); }); }); }); diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.ts index e739e7d0e62c..2acbda553c19 100644 --- a/yarn-project/simulator/src/avm/opcodes/ec_add.ts +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.ts @@ -1,3 +1,4 @@ +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { Point } from '@aztec/foundation/fields'; import { type AvmContext } from '../avm_context.js'; @@ -14,39 +15,62 @@ export class EcAdd extends Instruction { static readonly wireFormat: OperandType[] = [ OperandType.UINT8, // reserved OperandType.UINT8, // indirect - OperandType.UINT32, // p1 - OperandType.UINT32, // p2 + OperandType.UINT32, // p1X + OperandType.UINT32, // p1Y + OperandType.UINT8, // p1IsInfinite + OperandType.UINT32, // p2X + OperandType.UINT32, // p2Y + OperandType.UINT8, // p2IsInfinite OperandType.UINT32, // dst ]; - constructor(private indirect: number, private p1Offset: number, private p2Offset: number, private dstOffset: number) { + constructor( + private indirect: number, + private p1X: number, + private p1Y: number, + private p1IsInfinite: number, + private p2X: number, + private p2Y: number, + private p2IsInfinite: number, + private dstOffset: number, + ) { super(); } public async execute(context: AvmContext): Promise { - const memoryOperations = { reads: 4, writes: 2, indirect: this.indirect }; + const memoryOperations = { reads: 5, writes: 3, indirect: this.indirect }; const memory = context.machineState.memory.track(this.type); context.machineState.consumeGas(this.gasCost(memoryOperations)); - memory.checkTags(TypeTag.FIELD, this.p1Offset, this.p2Offset); + memory.checkTags(TypeTag.FIELD, this.p1X, this.p1Y, this.p2X, this.p2Y); + memory.checkTags(TypeTag.UINT8, this.p1IsInfinite, this.p2IsInfinite); - const p1X = memory.get(this.p1Offset); - const p1Y = memory.get(this.p1Offset + 1); + const p1X = memory.get(this.p1X); + const p1Y = memory.get(this.p1Y); + // unused. Point doesn't store this information + // const p1IsInfinite = memory.get(this.p1IsInfinite); const p1 = new Point(p1X.toFr(), p1Y.toFr()); - if (!p1.is_on_grumpkin()) { - throw new Error(`Point1 at offset ${this.p1Offset} is not on the curve`); + if (!p1.isOnGrumpkin()) { + throw new Error(`Point1 is not on the curve`); } - const p2X = memory.get(this.p2Offset); - const p2Y = memory.get(this.p2Offset + 1); + const p2X = memory.get(this.p2X); + const p2Y = memory.get(this.p2Y); + // unused. Point doesn't store this information + // const p2IsInfinite = memory.get(this.p2IsInfinite); const p2 = new Point(p2X.toFr(), p2Y.toFr()); - if (!p2.is_on_grumpkin()) { - throw new Error(`Point2 at offset ${this.p2Offset} is not on the curve`); + if (!p2.isOnGrumpkin()) { + throw new Error(`Point1 is not on the curve`); } - const dest = p1.add(p2); - memory.set(this.dstOffset, new Field(dest.x)); - memory.set(this.dstOffset + 1, new Field(dest.y)); + // const dest = p1.add(p2); + const grumpkin = new Grumpkin(); + const dest = grumpkin.add(p1, p2); + const dstOffsetResolved = Number(memory.get(this.dstOffset).toBigInt()); + + memory.set(dstOffsetResolved, new Field(dest.x)); + memory.set(dstOffsetResolved + 1, new Field(dest.y)); + memory.set(dstOffsetResolved + 2, new Field(dest.equals(Point.ZERO) ? 1 : 0)); memory.assert(memoryOperations); context.machineState.incrementPc(); From fa63a71a4a9378b2f93ed0d180b0f011e52417ca Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Tue, 4 Jun 2024 12:49:08 +0100 Subject: [PATCH 3/4] remove log --- yarn-project/simulator/src/avm/opcodes/ec_add.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts index d42617570d5b..894f47b6dfe3 100644 --- a/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts @@ -75,7 +75,6 @@ describe('EC Instructions', () => { }); it('Should add correctly', async () => { - console.log(grumpkin.generator()); const G2 = grumpkin.add(grumpkin.generator(), grumpkin.generator()); const zero = new Uint8(0); From 763e6bf6799308faca8134d853e0fb7dd9e91abb Mon Sep 17 00:00:00 2001 From: IlyasRidhuan Date: Tue, 4 Jun 2024 09:29:38 +0000 Subject: [PATCH 4/4] feat(avm): ecc --- avm-transpiler/src/transpile.rs | 28 ++--- .../vm/avm_trace/avm_deserialization.cpp | 10 ++ .../vm/avm_trace/avm_execution.cpp | 10 ++ .../vm/avm_trace/avm_gas_trace.hpp | 3 +- .../barretenberg/vm/avm_trace/avm_opcode.hpp | 1 + .../barretenberg/vm/avm_trace/avm_trace.cpp | 107 ++++++++++++++++++ .../barretenberg/vm/avm_trace/avm_trace.hpp | 11 ++ .../vm/avm_trace/gadgets/avm_ecc.cpp | 33 ++++++ .../vm/avm_trace/gadgets/avm_ecc.hpp | 29 +++++ .../vm/tests/avm_execution.test.cpp | 62 ++++++++++ .../simulator/src/avm/avm_simulator.test.ts | 3 +- .../simulator/src/avm/opcodes/ec_add.test.ts | 16 +-- .../simulator/src/avm/opcodes/ec_add.ts | 74 ++++++------ 13 files changed, 330 insertions(+), 57 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.cpp create mode 100644 barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index 057f18665954..67d0f8043a6e 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -831,24 +831,26 @@ fn handle_black_box_function(avm_instrs: &mut Vec, operation: &B ], }); } + // This will be changed to utilise relative memory offsets BlackBoxOp::EmbeddedCurveAdd { - input1_x, - input1_y, - input1_infinite, - input2_x, - input2_y, - input2_infinite, + input1_x: p1_x_offset, + input1_y: p1_y_offset, + input1_infinite: p1_infinite_offset, + input2_x: p2_x_offset, + input2_y: p2_y_offset, + input2_infinite: p2_infinite_offset, result, } => avm_instrs.push(AvmInstruction { opcode: AvmOpcode::ECADD, - indirect: Some(ALL_DIRECT), + // The result (SIXTH operand) is indirect. + indirect: Some(0b1000000), operands: vec![ - AvmOperand::U32 { value: input1_x.0 as u32 }, - AvmOperand::U32 { value: input1_y.0 as u32 }, - AvmOperand::U8 { value: input1_infinite.0 as u8 }, - AvmOperand::U32 { value: input2_x.0 as u32 }, - AvmOperand::U32 { value: input2_y.0 as u32 }, - AvmOperand::U8 { value: input2_infinite.0 as u8 }, + AvmOperand::U32 { value: p1_x_offset.0 as u32 }, + AvmOperand::U32 { value: p1_y_offset.0 as u32 }, + AvmOperand::U32 { value: p1_infinite_offset.0 as u32 }, + AvmOperand::U32 { value: p2_x_offset.0 as u32 }, + AvmOperand::U32 { value: p2_y_offset.0 as u32 }, + AvmOperand::U32 { value: p2_infinite_offset.0 as u32 }, AvmOperand::U32 { value: result.pointer.0 as u32 }, ], ..Default::default() diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_deserialization.cpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_deserialization.cpp index 358eba7e3c87..6b10ab40afc1 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_deserialization.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_deserialization.cpp @@ -148,6 +148,16 @@ const std::unordered_map> OPCODE_WIRE_FORMAT = { OpCode::SHA256, { OperandType::INDIRECT, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32 } }, { OpCode::PEDERSEN, { OperandType::INDIRECT, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32 } }, + // TEMP ECADD without relative memory + { OpCode::ECADD, + { OperandType::INDIRECT, + OperandType::UINT32, // lhs.x + OperandType::UINT32, // lhs.y + OperandType::UINT32, // lhs.is_infinite + OperandType::UINT32, // rhs.x + OperandType::UINT32, // rhs.y + OperandType::UINT32, // rhs.is_infinite + OperandType::UINT32 } }, // dst_offset // Gadget - Conversion { OpCode::TORADIXLE, { OperandType::INDIRECT, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32, OperandType::UINT32 } }, diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_execution.cpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_execution.cpp index 27dc5548b79a..46f037e42ea9 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_execution.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_execution.cpp @@ -664,6 +664,16 @@ std::vector Execution::gen_trace(std::vector const& instructio std::get(inst.operands.at(3)), std::get(inst.operands.at(4))); break; + case OpCode::ECADD: + trace_builder.op_ec_add(std::get(inst.operands.at(0)), + std::get(inst.operands.at(1)), + std::get(inst.operands.at(2)), + std::get(inst.operands.at(3)), + std::get(inst.operands.at(4)), + std::get(inst.operands.at(5)), + std::get(inst.operands.at(6)), + std::get(inst.operands.at(7))); + break; case OpCode::REVERT: trace_builder.op_revert(std::get(inst.operands.at(0)), std::get(inst.operands.at(1)), diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_gas_trace.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_gas_trace.hpp index cc8e53aeb366..9934164ad238 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_gas_trace.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_gas_trace.hpp @@ -98,6 +98,7 @@ static const inline std::unordered_map GAS_COST_TABLE = { { OpCode::POSEIDON2, temp_default_gas_entry }, { OpCode::SHA256, temp_default_gas_entry }, { OpCode::PEDERSEN, temp_default_gas_entry }, + { OpCode::ECADD, temp_default_gas_entry }, // Conversions { OpCode::TORADIXLE, temp_default_gas_entry }, @@ -146,4 +147,4 @@ class AvmGasTraceBuilder { uint32_t remaining_da_gas = 0; }; -} // namespace bb::avm_trace \ No newline at end of file +} // namespace bb::avm_trace diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp index 87e0f03fe7b2..83de32e65682 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp @@ -104,6 +104,7 @@ enum class OpCode : uint8_t { POSEIDON2, SHA256, PEDERSEN, + ECADD, // Conversions TORADIXLE, // Future Gadgets -- pending changes in noir diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.cpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.cpp index a45bde206d73..fb9e8b7e576d 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.cpp @@ -3520,6 +3520,113 @@ void AvmTraceBuilder::op_pedersen_hash(uint8_t indirect, write_slice_to_memory( call_ptr, clk, output_offset, AvmMemoryTag::FF, AvmMemoryTag::FF, FF(internal_return_ptr), { output }); } + +void AvmTraceBuilder::op_ec_add(uint8_t indirect, + uint32_t lhs_x_offset, + uint32_t lhs_y_offset, + uint32_t lhs_is_inf_offset, + uint32_t rhs_x_offset, + uint32_t rhs_y_offset, + uint32_t rhs_is_inf_offset, + uint32_t output_offset) +{ + auto clk = static_cast(main_trace.size()) + 1; + // Load lhs point + auto lhs_x_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IA, lhs_x_offset, AvmMemoryTag::FF, AvmMemoryTag::U0); + auto lhs_y_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IB, lhs_y_offset, AvmMemoryTag::FF, AvmMemoryTag::U0); + // Load rhs point + auto rhs_x_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IC, rhs_x_offset, AvmMemoryTag::FF, AvmMemoryTag::U0); + auto rhs_y_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::ID, rhs_y_offset, AvmMemoryTag::FF, AvmMemoryTag::U0); + + // Save this clk time to line up with the gadget op. + auto ecc_clk = clk; + main_trace.push_back(Row{ + .avm_main_clk = clk, + .avm_main_ia = lhs_x_read.val, + .avm_main_ib = lhs_y_read.val, + .avm_main_ic = rhs_x_read.val, + .avm_main_id = rhs_y_read.val, + .avm_main_internal_return_ptr = FF(internal_return_ptr), + .avm_main_mem_idx_a = FF(lhs_x_offset), + .avm_main_mem_idx_b = FF(lhs_y_offset), + .avm_main_mem_idx_c = FF(rhs_x_offset), + .avm_main_mem_idx_d = FF(rhs_y_offset), + .avm_main_mem_op_a = FF(1), + .avm_main_mem_op_b = FF(1), + .avm_main_mem_op_c = FF(1), + .avm_main_mem_op_d = FF(1), + .avm_main_pc = FF(pc++), + .avm_main_r_in_tag = FF(static_cast(AvmMemoryTag::FF)), + }); + clk++; + // Load the infinite bools separately since they have a different memory tag + auto lhs_is_inf_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IA, lhs_is_inf_offset, AvmMemoryTag::U8, AvmMemoryTag::U0); + auto rhs_is_inf_read = mem_trace_builder.read_and_load_from_memory( + call_ptr, clk, IntermRegister::IB, rhs_is_inf_offset, AvmMemoryTag::U8, AvmMemoryTag::U0); + + main_trace.push_back(Row{ + .avm_main_clk = clk, + .avm_main_ia = lhs_is_inf_read.val, + .avm_main_ib = rhs_is_inf_read.val, + .avm_main_internal_return_ptr = FF(internal_return_ptr), + .avm_main_mem_idx_a = FF(lhs_is_inf_offset), + .avm_main_mem_idx_b = FF(rhs_is_inf_offset), + .avm_main_mem_op_a = FF(1), + .avm_main_mem_op_b = FF(1), + .avm_main_pc = FF(pc), + .avm_main_r_in_tag = FF(static_cast(AvmMemoryTag::U8)), + }); + clk++; + grumpkin::g1::affine_element lhs = uint8_t(lhs_is_inf_read.val) == 1 + ? grumpkin::g1::affine_element::infinity() + : grumpkin::g1::affine_element{ lhs_x_read.val, lhs_y_read.val }; + grumpkin::g1::affine_element rhs = uint8_t(rhs_is_inf_read.val) == 1 + ? grumpkin::g1::affine_element::infinity() + : grumpkin::g1::affine_element{ rhs_x_read.val, rhs_y_read.val }; + auto result = ecc_trace_builder.embedded_curve_add(lhs, rhs, ecc_clk); + // Write across two lines since we have different mem_tags + uint32_t direct_output_offset = output_offset; + bool indirect_flag_output = is_operand_indirect(indirect, 6); + if (indirect_flag_output) { + auto read_ind_output = + mem_trace_builder.indirect_read_and_load_from_memory(call_ptr, clk, IndirectRegister::IND_A, output_offset); + direct_output_offset = uint32_t(read_ind_output.val); + } + + mem_trace_builder.write_into_memory( + call_ptr, clk, IntermRegister::IA, direct_output_offset, result.x, AvmMemoryTag::U0, AvmMemoryTag::FF); + mem_trace_builder.write_into_memory( + call_ptr, clk, IntermRegister::IB, direct_output_offset + 1, result.y, AvmMemoryTag::U0, AvmMemoryTag::FF); + main_trace.push_back(Row{ + .avm_main_clk = clk, + .avm_main_ia = result.x, + .avm_main_ib = result.y, + .avm_main_ind_a = indirect_flag_output ? FF(output_offset) : FF(0), + .avm_main_ind_op_a = FF(static_cast(indirect_flag_output)), + .avm_main_internal_return_ptr = FF(internal_return_ptr), + .avm_main_mem_idx_a = FF(direct_output_offset), + .avm_main_mem_idx_b = FF(direct_output_offset + 1), + .avm_main_mem_op_a = FF(1), + .avm_main_mem_op_b = FF(1), + .avm_main_pc = FF(pc), + .avm_main_rwa = FF(1), + .avm_main_rwb = FF(1), + .avm_main_w_in_tag = FF(static_cast(AvmMemoryTag::FF)), + }); + clk++; + write_slice_to_memory(call_ptr, + clk, + direct_output_offset + 2, + AvmMemoryTag::U8, + AvmMemoryTag::U8, + FF(internal_return_ptr), + { result.is_point_at_infinity() }); +} // Finalise Lookup Counts // // For log derivative lookups, we require a column that contains the number of times each lookup is consumed diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.hpp index 70d45adb3d55..b0e99b791d69 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_trace.hpp @@ -11,6 +11,7 @@ #include "barretenberg/vm/avm_trace/avm_opcode.hpp" #include "barretenberg/vm/avm_trace/constants.hpp" #include "barretenberg/vm/avm_trace/gadgets/avm_conversion_trace.hpp" +#include "barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp" #include "barretenberg/vm/avm_trace/gadgets/avm_keccak.hpp" #include "barretenberg/vm/avm_trace/gadgets/avm_pedersen.hpp" #include "barretenberg/vm/avm_trace/gadgets/avm_poseidon2.hpp" @@ -192,6 +193,15 @@ class AvmTraceBuilder { uint32_t output_offset, uint32_t input_offset, uint32_t input_size_offset); + // Embedded EC Add - the offsets are temporary + void op_ec_add(uint8_t indirect, + uint32_t lhs_x_offset, + uint32_t lhs_y_offset, + uint32_t lhs_is_inf_offset, + uint32_t rhs_x_offset, + uint32_t rhs_y_offset, + uint32_t rhs_is_inf_offset, + uint32_t output_offset); private: // Used for the standard indirect address resolution of three operands opcode. @@ -217,6 +227,7 @@ class AvmTraceBuilder { AvmPoseidon2TraceBuilder poseidon2_trace_builder; AvmKeccakTraceBuilder keccak_trace_builder; AvmPedersenTraceBuilder pedersen_trace_builder; + AvmEccTraceBuilder ecc_trace_builder; /** * @brief Create a kernel lookup opcode object diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.cpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.cpp new file mode 100644 index 000000000000..fd3fc8955d13 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.cpp @@ -0,0 +1,33 @@ +#include "barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp" +#include "barretenberg/vm/avm_trace/avm_common.hpp" + +namespace bb::avm_trace { +using element = grumpkin::g1::affine_element; + +AvmEccTraceBuilder::AvmEccTraceBuilder() +{ + ecc_trace.reserve(AVM_TRACE_SIZE); +} + +std::vector AvmEccTraceBuilder::finalize() +{ + return std::move(ecc_trace); +} + +void AvmEccTraceBuilder::reset() +{ + ecc_trace.clear(); +} + +element AvmEccTraceBuilder::embedded_curve_add(element lhs, element rhs, uint32_t clk) +{ + element result = lhs + rhs; + std::tuple p1 = { lhs.x, lhs.y, lhs.is_point_at_infinity() }; + std::tuple p2 = { rhs.x, rhs.y, rhs.is_point_at_infinity() }; + std::tuple result_tuple = { result.x, result.y, result.is_point_at_infinity() }; + ecc_trace.push_back({ clk, p1, p2, result_tuple }); + + return result; +} + +} // namespace bb::avm_trace diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp new file mode 100644 index 000000000000..6450a33db399 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/gadgets/avm_ecc.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" +#include "barretenberg/ecc/groups/affine_element.hpp" +#include "barretenberg/vm/avm_trace/avm_common.hpp" + +namespace bb::avm_trace { +class AvmEccTraceBuilder { + public: + struct EccTraceEntry { + uint32_t clk = 0; + std::tuple p1; // x, y, is_infinity + std::tuple p2; + std::tuple result; + }; + + AvmEccTraceBuilder(); + void reset(); + // Finalize the trace + std::vector finalize(); + grumpkin::g1::affine_element embedded_curve_add(grumpkin::g1::affine_element lhs, + grumpkin::g1::affine_element rhs, + uint32_t clk); + + private: + std::vector ecc_trace; +}; + +} // namespace bb::avm_trace diff --git a/barretenberg/cpp/src/barretenberg/vm/tests/avm_execution.test.cpp b/barretenberg/cpp/src/barretenberg/vm/tests/avm_execution.test.cpp index b3a227b9dc51..69d47e3f7d4f 100644 --- a/barretenberg/cpp/src/barretenberg/vm/tests/avm_execution.test.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/tests/avm_execution.test.cpp @@ -1331,6 +1331,68 @@ TEST_F(AvmExecutionTests, pedersenHashOpCode) validate_trace(std::move(trace)); } +// +// Positive test with EmbeddedCurveAdd +TEST_F(AvmExecutionTests, embeddedCurveAddOpCode) +{ + // TODO: Look for hardcoded test vectors since bb is missing them + grumpkin::g1::affine_element a = grumpkin::g1::affine_element::random_element(); + auto a_is_inf = a.is_point_at_infinity(); + grumpkin::g1::affine_element b = grumpkin::g1::affine_element::random_element(); + auto b_is_inf = b.is_point_at_infinity(); + grumpkin::g1::affine_element res = a + b; + auto expected_output = std::vector{ res.x, res.y, res.is_point_at_infinity() }; + std::string bytecode_hex = to_hex(OpCode::CALLDATACOPY) + // Calldatacopy + "00" // Indirect flag + "00000000" // cd_offset + "00000002" // copy_size + "00000000" // dst_offset + + to_hex(OpCode::SET) + // opcode SET for direct src_length + "00" // Indirect flag + "01" // U8 + + to_hex(a_is_inf) + // + "00000002" // dst_offset + + to_hex(OpCode::CALLDATACOPY) + // calldatacopy + "00" // Indirect flag + "00000002" // cd_offset + "00000002" // copy_size + "00000003" // dst_offset + + to_hex(OpCode::SET) + // opcode SET for direct src_length + "00" // Indirect flag + "01" // U32 + + to_hex(b_is_inf) + // value 2 + "00000005" // dst_offset + + to_hex(OpCode::SET) + // opcode SET for direct src_length + "00" // Indirect flag + "03" // U32 + "00000007" // value + "00000006" // dst_offset + + to_hex(OpCode::ECADD) + // opcode ECADD + "40" // Indirect flag (sixth operand indirect) + "00000000" // hash_index offset (direct) + "00000001" // dest offset (direct) + "00000002" // input offset (indirect) + "00000003" // length offset (direct) + "00000004" // length offset (direct) + "00000005" // length offset (direct) + "00000006" // length offset (direct) + + to_hex(OpCode::RETURN) + // opcode RETURN + "00" // Indirect flag + "00000007" // ret offset 3 + "00000003"; // ret size 1 + + auto bytecode = hex_to_bytes(bytecode_hex); + auto instructions = Deserialization::parse(bytecode); + + // Assign a vector that we will mutate internally in gen_trace to store the return values; + std::vector returndata; + std::vector calldata = { a.x, a.y, b.x, b.y }; + auto trace = Execution::gen_trace(instructions, returndata, calldata, public_inputs_vec); + + EXPECT_EQ(returndata, expected_output); + + validate_trace(std::move(trace)); +} // Positive test for Kernel Input opcodes TEST_F(AvmExecutionTests, kernelInputOpcodes) diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index 8f97bbb67cb1..f1e8c98fb904 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -97,8 +97,7 @@ describe('AVM simulator: transpiled Noir contracts', () => { }); it('elliptic curve operations', async () => { - const calldata: Fr[] = []; - const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + const context = initContext(); const bytecode = getAvmTestContractBytecode('elliptic_curve_add_and_double'); const results = await new AvmSimulator(context).executeBytecode(bytecode); diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts index 894f47b6dfe3..2c665378238e 100644 --- a/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.test.ts @@ -4,7 +4,7 @@ import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { beforeEach } from '@jest/globals'; import { type AvmContext } from '../avm_context.js'; -import { Field, Uint8, Uint32 } from '../avm_memory_types.js'; +import { Field, Uint32 } from '../avm_memory_types.js'; import { initContext } from '../fixtures/index.js'; import { EcAdd } from './ec_add.js'; @@ -20,17 +20,17 @@ describe('EC Instructions', () => { it('Should (de)serialize correctly', () => { const buf = Buffer.from([ EcAdd.opcode, // opcode - 0x01, // indirect + 0x20, // indirect ...Buffer.from('12345670', 'hex'), // p1x ...Buffer.from('12345671', 'hex'), // p1y - ...Buffer.from('00', 'hex'), // p1IsInfinite + ...Buffer.from('00000000', 'hex'), // p1IsInfinite ...Buffer.from('12345672', 'hex'), // p2x ...Buffer.from('12345673', 'hex'), // p2y - ...Buffer.from('01', 'hex'), // p2IsInfinite + ...Buffer.from('00000001', 'hex'), // p2IsInfinite ...Buffer.from('12345674', 'hex'), // dstOffset ]); const inst = new EcAdd( - /*indirect=*/ 0x01, + /*indirect=*/ 0x20, /*p1X=*/ 0x12345670, /*p1Y=*/ 0x12345671, /*p1IsInfinite=*/ 0, @@ -47,7 +47,7 @@ describe('EC Instructions', () => { it(`Should double correctly`, async () => { const x = new Field(grumpkin.generator().x); const y = new Field(grumpkin.generator().y); - const zero = new Uint8(0); + const zero = new Uint32(0); context.machineState.memory.set(0, x); context.machineState.memory.set(1, y); @@ -55,7 +55,7 @@ describe('EC Instructions', () => { context.machineState.memory.set(3, x); context.machineState.memory.set(4, y); context.machineState.memory.set(5, zero); - context.machineState.memory.set(6, new Uint32(6)); + // context.machineState.memory.set(6, new Uint32(6)); await new EcAdd( /*indirect=*/ 0, @@ -76,7 +76,7 @@ describe('EC Instructions', () => { it('Should add correctly', async () => { const G2 = grumpkin.add(grumpkin.generator(), grumpkin.generator()); - const zero = new Uint8(0); + const zero = new Uint32(0); const x1 = new Field(grumpkin.generator().x); const y1 = new Field(grumpkin.generator().y); diff --git a/yarn-project/simulator/src/avm/opcodes/ec_add.ts b/yarn-project/simulator/src/avm/opcodes/ec_add.ts index 2acbda553c19..044ca5e31a44 100644 --- a/yarn-project/simulator/src/avm/opcodes/ec_add.ts +++ b/yarn-project/simulator/src/avm/opcodes/ec_add.ts @@ -2,9 +2,9 @@ import { Grumpkin } from '@aztec/circuits.js/barretenberg'; import { Point } from '@aztec/foundation/fields'; import { type AvmContext } from '../avm_context.js'; -import { getBaseGasCost, getGasCostForTypeTag, getMemoryGasCost, sumGas } from '../avm_gas.js'; -import { Field, MemoryOperations, TypeTag } from '../avm_memory_types.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 EcAdd extends Instruction { @@ -17,68 +17,76 @@ export class EcAdd extends Instruction { OperandType.UINT8, // indirect OperandType.UINT32, // p1X OperandType.UINT32, // p1Y - OperandType.UINT8, // p1IsInfinite + OperandType.UINT32, // p1IsInfinite OperandType.UINT32, // p2X OperandType.UINT32, // p2Y - OperandType.UINT8, // p2IsInfinite + OperandType.UINT32, // p2IsInfinite OperandType.UINT32, // dst ]; constructor( private indirect: number, - private p1X: number, - private p1Y: number, - private p1IsInfinite: number, - private p2X: number, - private p2Y: number, - private p2IsInfinite: number, + private p1XOffset: number, + private p1YOffset: number, + private p1IsInfiniteOffset: number, + private p2XOffset: number, + private p2YOffset: number, + private p2IsInfiniteOffset: number, private dstOffset: number, ) { super(); } public async execute(context: AvmContext): Promise { - const memoryOperations = { reads: 5, writes: 3, indirect: this.indirect }; + const memoryOperations = { reads: 6, writes: 3, indirect: this.indirect }; const memory = context.machineState.memory.track(this.type); context.machineState.consumeGas(this.gasCost(memoryOperations)); - memory.checkTags(TypeTag.FIELD, this.p1X, this.p1Y, this.p2X, this.p2Y); - memory.checkTags(TypeTag.UINT8, this.p1IsInfinite, this.p2IsInfinite); + const [p1XOffset, p1YOffset, p1IsInfiniteOffset, p2XOffset, p2YOffset, p2IsInfiniteOffset, dstOffset] = + Addressing.fromWire(this.indirect).resolve( + [ + this.p1XOffset, + this.p1YOffset, + this.p1IsInfiniteOffset, + this.p2XOffset, + this.p2YOffset, + this.p2IsInfiniteOffset, + this.dstOffset, + ], + memory, + ); - const p1X = memory.get(this.p1X); - const p1Y = memory.get(this.p1Y); - // unused. Point doesn't store this information - // const p1IsInfinite = memory.get(this.p1IsInfinite); + const p1X = memory.get(p1XOffset); + const p1Y = memory.get(p1YOffset); + const p1IsInfinite = memory.get(p1IsInfiniteOffset).toNumber() === 1; const p1 = new Point(p1X.toFr(), p1Y.toFr()); if (!p1.isOnGrumpkin()) { throw new Error(`Point1 is not on the curve`); } - const p2X = memory.get(this.p2X); - const p2Y = memory.get(this.p2Y); + const p2X = memory.get(p2XOffset); + const p2Y = memory.get(p2YOffset); // unused. Point doesn't store this information - // const p2IsInfinite = memory.get(this.p2IsInfinite); + const p2IsInfinite = memory.get(p2IsInfiniteOffset).toNumber() === 1; const p2 = new Point(p2X.toFr(), p2Y.toFr()); if (!p2.isOnGrumpkin()) { throw new Error(`Point1 is not on the curve`); } - // const dest = p1.add(p2); const grumpkin = new Grumpkin(); - const dest = grumpkin.add(p1, p2); - const dstOffsetResolved = Number(memory.get(this.dstOffset).toBigInt()); - - memory.set(dstOffsetResolved, new Field(dest.x)); - memory.set(dstOffsetResolved + 1, new Field(dest.y)); - memory.set(dstOffsetResolved + 2, new Field(dest.equals(Point.ZERO) ? 1 : 0)); + let dest = grumpkin.add(p1, p2); + // Temporary, + if (p1IsInfinite) { + dest = p2; + } else if (p2IsInfinite) { + dest = p1; + } + memory.set(dstOffset, new Field(dest.x)); + memory.set(dstOffset + 1, new Field(dest.y)); + // Check representation of infinity for grumpkin + memory.set(dstOffset + 2, new Field(dest.equals(Point.ZERO) ? 1 : 0)); memory.assert(memoryOperations); context.machineState.incrementPc(); } - - protected override gasCost(memoryOps: Partial) { - const baseGasCost = getGasCostForTypeTag(TypeTag.FIELD, getBaseGasCost(this.opcode)); - const memoryGasCost = getMemoryGasCost(memoryOps); - return sumGas(baseGasCost, memoryGasCost); - } }