-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* dependency: add @noble/curves * feat: add ECDSA builtin * test: add ecdsa cairo program * fix: add signature values to integration test
- Loading branch information
Showing
8 changed files
with
270 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 (); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters