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 19bd018ec710..5945bd366b42 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -834,6 +834,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 cb633e0dec3b..0e131e612e37 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; @@ -133,6 +134,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 92374003c487..98ba8507a7b0 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'; @@ -96,6 +97,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();