Skip to content

Commit

Permalink
feat: add ECDSA builtin (#76)
Browse files Browse the repository at this point in the history
* dependency: add @noble/curves

* feat: add ECDSA builtin

* test: add ecdsa cairo program

* fix: add signature values to integration test
  • Loading branch information
zmalatrax authored Jun 4, 2024
1 parent ae2743b commit be68042
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 0 deletions.
Binary file modified bun.lockb
Binary file not shown.
14 changes: 14 additions & 0 deletions cairo_programs/cairo_0/ecdsa_builtin.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
%builtins ecdsa

from starkware.cairo.common.cairo_builtins import SignatureBuiltin

func main{ecdsa_ptr: SignatureBuiltin*}() {
let signature_r = 0x6d2e2e00dfceffd6a375db04764da249a5a1534c7584738dfe01cb3944a33ee;
let signature_s = 0x152d64f9943290feadc803e80b05f5aa36310ee8fe46e623f10f94e33d59f93;
%{ ecdsa_builtin.add_signature(ids.ecdsa_ptr.address_, (ids.signature_r, ids.signature_s)) %}
assert ecdsa_ptr.message = 2718;
assert ecdsa_ptr.pub_key = 0x3d60886c2353d93ec2862e91e23036cd9999a534481166e5a616a983070434d;

let ecdsa_ptr = ecdsa_ptr + SignatureBuiltin.SIZE;
return ();
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"printWidth": 80
},
"dependencies": {
"@noble/curves": "^1.4.0",
"@scure/starknet": "^1.0.0",
"zod": "canary"
}
Expand Down
2 changes: 2 additions & 0 deletions src/builtins/builtin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SegmentValue } from 'primitives/segmentValue';
import { bitwiseHandler } from './bitwise';
import { ecOpHandler } from './ecop';
import { ecdsaHandler } from './ecdsa';

/** Proxy handler to abstract validation & deduction rules off the VM */
export type BuiltinHandler = ProxyHandler<Array<SegmentValue>>;
Expand All @@ -22,6 +23,7 @@ const BUILTIN_HANDLER: {
} = {
bitwise: bitwiseHandler,
ec_op: ecOpHandler,
ecdsa: ecdsaHandler,
};

/** Getter of the object `BUILTIN_HANDLER` */
Expand Down
108 changes: 108 additions & 0 deletions src/builtins/ecdsa.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, expect, test } from 'bun:test';

import { ExpectedFelt } from 'errors/virtualMachine';

import { Felt } from 'primitives/felt';
import { Relocatable } from 'primitives/relocatable';
import { Memory } from 'memory/memory';
import { EcdsaSegment, EcdsaSignature, ecdsaHandler } from './ecdsa';

const DUMMY_SIG: EcdsaSignature = { r: new Felt(0n), s: new Felt(0n) };

describe('ECDSA', () => {
describe('Signature verification', () => {
test('Should properly verify a correct signature', () => {
const memory = new Memory();
const { segmentId } = memory.addSegment(ecdsaHandler);

const segment = memory.segments[segmentId] as EcdsaSegment;

const signature: EcdsaSignature = {
r: new Felt(
3086480810278599376317923499561306189851900463386393948998357832163236918254n
),
s: new Felt(
598673427589502599949712887611119751108407514580626464031881322743364689811n
),
};

const pubKeyAddr = new Relocatable(segmentId, 0);
const msgAddr = new Relocatable(segmentId, 1);

const pubKey = new Felt(
1735102664668487605176656616876767369909409133946409161569774794110049207117n
);
const msg = new Felt(2718n);

segment.signatures[pubKeyAddr.offset] = signature;
memory.assertEq(pubKeyAddr, pubKey);
memory.assertEq(msgAddr, msg);

expect(memory.get(msgAddr)).toEqual(msg);
});

test('Should properly throw when signature is invalid', () => {
const memory = new Memory();
const { segmentId } = memory.addSegment(ecdsaHandler);

const segment = memory.segments[segmentId] as EcdsaSegment;

const signature = DUMMY_SIG;

const pubKeyAddr = new Relocatable(segmentId, 0);
const msgAddr = new Relocatable(segmentId, 1);

const pubKey = new Felt(14n);
const msg = new Felt(2718n);

segment.signatures[pubKeyAddr.offset] = signature;
memory.assertEq(pubKeyAddr, pubKey);
expect(() => memory.assertEq(msgAddr, msg)).toThrow();
});
});

describe('signatureHandler', () => {
test('should properly update the signature array', () => {
const memory = new Memory();
const { segmentId } = memory.addSegment(ecdsaHandler);
const segment = memory.segments[segmentId] as EcdsaSegment;

const signature: EcdsaSignature = {
r: new Felt(10n),
s: new Felt(15n),
};

const address = new Relocatable(segmentId, 2);
segment.signatures[address.offset] = signature;

expect(segment.signatures[address.offset]).toEqual(signature);
});

test('should throw when adding a signature which is not the expected object', () => {
const memory = new Memory();
const { segmentId } = memory.addSegment(ecdsaHandler);
const segment = memory.segments[segmentId] as EcdsaSegment;

segment.signatures[2] = { r: new Felt(0n), s: new Felt(0n) };
// @ts-expect-error
expect(() => (segment.signatures[4] = 12)).toThrow(new ExpectedFelt());
});

test('should throw when trying to add a signature with a key which is not a number', () => {
const memory = new Memory();
const { segmentId } = memory.addSegment(ecdsaHandler);
const segment = memory.segments[segmentId] as EcdsaSegment;
const signature: EcdsaSignature = { r: new Felt(0n), s: new Felt(0n) };
// @ts-expect-error
expect(() => (segment.signatures['az'] = signature)).toThrow();
});

test('Should not update a signature array for another ProxyHandler', () => {
const memory = new Memory();
const { segmentId } = memory.addSegment();
const segment = memory.segments[segmentId] as EcdsaSegment;
const signature: EcdsaSignature = { r: new Felt(0n), s: new Felt(0n) };
expect(() => (segment.signatures[3] = signature)).toThrow();
});
});
});
119 changes: 119 additions & 0 deletions src/builtins/ecdsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { CURVE, ProjectivePoint, Signature, verify } from '@scure/starknet';

import {
ExpectedOffset,
InvalidSignature,
UndefinedECDSASignature,
UndefinedSignatureDict,
} from 'errors/builtins';
import { ExpectedFelt } from 'errors/virtualMachine';

import { Felt } from 'primitives/felt';
import { SegmentValue, isFelt } from 'primitives/segmentValue';

export type EcdsaSignature = { r: Felt; s: Felt };
type EcdsaSignatureDict = { [key: number]: EcdsaSignature };
export type EcdsaSegment = SegmentValue[] & { signatures: EcdsaSignatureDict };
type EcdsaProxyHandler = ProxyHandler<EcdsaSegment>;

const signatureHandler: ProxyHandler<EcdsaSignatureDict> = {
set(target, prop, newValue): boolean {
if (isNaN(Number(prop))) throw new ExpectedOffset();
if (!isFelt(newValue.r) || !isFelt(newValue.s)) throw new ExpectedFelt();
const key = Number(prop);
target[key] = newValue;
return true;
},
};

export const ecdsaHandler: EcdsaProxyHandler = {
get(target, prop) {
if (prop === 'signatures') {
if (!target.signatures) {
target.signatures = new Proxy({}, signatureHandler);
}
return target.signatures;
}
return Reflect.get(target, prop);
},

set(target, prop, newValue): boolean {
if (prop === 'signatures') {
if (!target.signatures) {
target.signatures = new Proxy({}, signatureHandler);
}
return true;
}

if (isNaN(Number(prop))) throw new ExpectedOffset();

if (!target.signatures) throw new UndefinedSignatureDict();

const cellsPerEcdsa = 2;
const offset = Number(prop);
const ecdsaIndex = offset % cellsPerEcdsa;

const pubKeyXOffset = ecdsaIndex ? offset - 1 : offset;
const msgOffset = ecdsaIndex ? offset : offset + 1;

if (!target[pubKeyXOffset] && !target[msgOffset]) {
if (!isFelt(newValue)) throw new ExpectedFelt();
target[offset] = newValue;
return true;
}

// Trying to assert an already constrained value while the other pair
// (either pub key or message) has not been constrained yet - no sig verif
if (target[offset]) {
return true;
}

if (!isFelt(newValue)) throw new ExpectedFelt();
target[offset] = newValue;

const pubKeyX = target[pubKeyXOffset];
const msg = target[msgOffset];
if (!isFelt(pubKeyX) || !isFelt(msg)) throw new ExpectedFelt();

const { yPos, yNeg } = recoverY(pubKeyX);

const pubKeyPos = ProjectivePoint.fromAffine({
x: pubKeyX.toBigInt(),
y: yPos.toBigInt(),
});

// TODO: Check that a signature has been added to the signatures cache
const sig = target.signatures[pubKeyXOffset];
if (!sig) throw new UndefinedECDSASignature(pubKeyXOffset);

const signature = new Signature(sig.r.toBigInt(), sig.s.toBigInt());

if (!verify(signature, msg.toString(16), pubKeyPos.toHex())) {
const pubKeyNeg = ProjectivePoint.fromAffine({
x: pubKeyX.toBigInt(),
y: yNeg.toBigInt(),
});

if (!verify(signature, msg.toString(16), pubKeyNeg.toHex()))
throw new InvalidSignature();
}
return true;
},
};

/**
* Recover the y-coordinate from the x-coordinate of the public key
*
* Based on the Weierstrass equation of the STARK curve
* $y^2 = x^3 + a*x + b$
*/
const recoverY = (x: Felt) => {
const ax = x.mul(new Felt(CURVE.a));
const x3 = x.mul(x).mul(x);
const b = new Felt(CURVE.b);
const y2 = x3.add(ax).add(b);
// TODO: compute the square root of a STARK felt
const y = y2.sqrt();

return { yPos: y, yNeg: y.neg() };
};
17 changes: 17 additions & 0 deletions src/errors/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,22 @@ export class UndefinedValue extends BuiltinError {
}
}

/** ECDSA signature cannot be retrived from dictionnary at `offset` */
export class UndefinedECDSASignature extends BuiltinError {
constructor(readonly offset: number) {
super();
this.offset = offset;
}
}

/** The ECDSA verification of the signature has failed */
export class InvalidSignature extends BuiltinError {}

/** The signature dictionnary is undefined */
export class UndefinedSignatureDict extends BuiltinError {}

/** An offset of type number is expected */
export class ExpectedOffset extends BuiltinError {}

/** Ladder formula R = P + mQ failed in EcOp builtin */
export class LadderFailed extends BuiltinError {}
9 changes: 9 additions & 0 deletions src/primitives/felt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ForbiddenOperation } from 'errors/primitives';

import { SegmentValue, isFelt, isRelocatable } from './segmentValue';
import { CURVE } from '@scure/starknet';

export class Felt {
private inner: bigint;
Expand Down Expand Up @@ -38,6 +39,10 @@ export class Felt {
return new Felt(this.inner * other.inner);
}

neg(): Felt {
return new Felt(-this.inner);
}

div(other: SegmentValue): Felt {
if (!isFelt(other) || other.inner === 0n) {
throw new ForbiddenOperation();
Expand All @@ -50,6 +55,10 @@ export class Felt {
return !isRelocatable(other) && this.inner === other.inner;
}

sqrt(): Felt {
return new Felt(CURVE.Fp.sqrt(this.inner));
}

toString(radix?: number): string {
return this.inner.toString(radix);
}
Expand Down

0 comments on commit be68042

Please sign in to comment.