-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathindex.ts
383 lines (348 loc) · 14.3 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
/*! scure-starknet - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { keccak_256 } from '@noble/hashes/sha3';
import { sha256 } from '@noble/hashes/sha256';
import { utf8ToBytes } from '@noble/hashes/utils';
import { Field, mod, IField, validateField, invert } from '@noble/curves/abstract/modular';
import { poseidon } from '@noble/curves/abstract/poseidon';
import { weierstrass, ProjPointType, SignatureType, DER } from '@noble/curves/abstract/weierstrass';
import * as u from '@noble/curves/abstract/utils';
import type { Hex } from '@noble/curves/abstract/utils';
import { getHash } from '@noble/curves/_shortw_utils';
// Stark-friendly elliptic curve
// https://docs.starkware.co/starkex/stark-curve.html
type ProjectivePoint = ProjPointType<bigint>;
const CURVE_ORDER = BigInt(
'3618502788666131213697322783095070105526743751716087489154079457884512865583'
);
// 2**251, limit for msgHash and Signature.r
export const MAX_VALUE = BigInt(
'0x800000000000000000000000000000000000000000000000000000000000000'
);
const nBitLength = 252;
function bits2int(bytes: Uint8Array): bigint {
while (bytes[0] === 0) bytes = bytes.subarray(1); // strip leading 0s
// Copy-pasted from weierstrass.ts
const delta = bytes.length * 8 - nBitLength;
const num = u.bytesToNumberBE(bytes);
return delta > 0 ? num >> BigInt(delta) : num;
}
function hex0xToBytes(hex: string): Uint8Array {
if (typeof hex === 'string') {
hex = strip0x(hex); // allow 0x prefix
if (hex.length & 1) hex = '0' + hex; // allow unpadded hex
}
return u.hexToBytes(hex);
}
const curve = weierstrass({
a: BigInt(1), // Params: a, b
b: BigInt('3141592653589793238462643383279502884197169399375105820974944592307816406665'),
// Field over which we'll do calculations; 2n**251n + 17n * 2n**192n + 1n
// There is no efficient sqrt for field (P%4==1)
Fp: Field(BigInt('0x800000000000011000000000000000000000000000000000000000000000001')),
n: CURVE_ORDER, // Curve order, total count of valid points in the field.
nBitLength, // len(bin(N).replace('0b',''))
// Base point (x, y) aka generator point
Gx: BigInt('874739451078007766457464989774322083649278607533249481151382481072868806602'),
Gy: BigInt('152666792071518830868575557812948353041420400780739481342941381225525861407'),
h: BigInt(1), // cofactor
lowS: false, // Allow high-s signatures
...getHash(sha256),
// Custom truncation routines for stark curve
bits2int,
bits2int_modN: (bytes: Uint8Array): bigint => {
// 2102820b232636d200cb21f1d330f20d096cae09d1bf3edb1cc333ddee11318 =>
// 2102820b232636d200cb21f1d330f20d096cae09d1bf3edb1cc333ddee113180
const hex = u.bytesToNumberBE(bytes).toString(16); // toHex unpadded
if (hex.length === 63) bytes = hex0xToBytes(hex + '0'); // append trailing 0
return mod(bits2int(bytes), CURVE_ORDER);
},
});
export const _starkCurve = curve;
function ensureBytes(hex: Hex): Uint8Array {
return u.ensureBytes('', typeof hex === 'string' ? hex0xToBytes(hex) : hex);
}
export function normalizePrivateKey(privKey: Hex): string {
return u.bytesToHex(ensureBytes(privKey)).padStart(64, '0');
}
export function getPublicKey(privKey: Hex, isCompressed = false): Uint8Array {
return curve.getPublicKey(normalizePrivateKey(privKey), isCompressed);
}
export function getSharedSecret(privKeyA: Hex, pubKeyB: Hex): Uint8Array {
return curve.getSharedSecret(normalizePrivateKey(privKeyA), pubKeyB);
}
function checkSignature(signature: SignatureType) {
// Signature.s checked inside weierstrass
const { r, s } = signature;
if (r < 0n || r >= MAX_VALUE) throw new Error(`Signature.r should be [1, ${MAX_VALUE})`);
const w = invert(s, CURVE_ORDER);
if (w < 0n || w >= MAX_VALUE) throw new Error(`inv(Signature.s) should be [1, ${MAX_VALUE})`);
}
function checkMessage(msgHash: Hex) {
const bytes = ensureBytes(msgHash);
const num = u.bytesToNumberBE(bytes);
// num < 0 impossible here
if (num >= MAX_VALUE) throw new Error(`msgHash should be [0, ${MAX_VALUE})`);
return bytes;
}
export function sign(msgHash: Hex, privKey: Hex, opts?: any): SignatureType {
const sig = curve.sign(checkMessage(msgHash), normalizePrivateKey(privKey), opts);
checkSignature(sig);
return sig;
}
export function verify(signature: SignatureType | Hex, msgHash: Hex, pubKey: Hex) {
if (!(signature instanceof Signature)) {
const bytes = ensureBytes(signature);
try {
signature = Signature.fromDER(bytes);
} catch (derError) {
if (!(derError instanceof DER.Err)) throw derError;
signature = Signature.fromCompact(bytes);
}
}
checkSignature(signature);
return curve.verify(signature, checkMessage(msgHash), ensureBytes(pubKey));
}
const { CURVE, ProjectivePoint, Signature, utils } = curve;
export { CURVE, ProjectivePoint, Signature, utils };
function extractX(bytes: Uint8Array): string {
const hex = u.bytesToHex(bytes.subarray(1));
const stripped = hex.replace(/^0+/gm, ''); // strip leading 0s
return `0x${stripped}`;
}
function strip0x(hex: string) {
return hex.replace(/^0x/i, '');
}
// seed generation
export function grindKey(seed: Hex) {
const _seed = ensureBytes(seed);
const sha256mask = 2n ** 256n;
const limit = sha256mask - mod(sha256mask, CURVE_ORDER);
for (let i = 0; ; i++) {
const key = sha256Num(u.concatBytes(_seed, u.numberToVarBytesBE(BigInt(i))));
if (key < limit) return mod(key, CURVE_ORDER).toString(16); // key should be in [0, limit)
if (i === 100000) throw new Error('grindKey is broken: tried 100k vals'); // prevent dos
}
}
export function getStarkKey(privateKey: Hex): string {
return extractX(getPublicKey(privateKey, true));
}
export function ethSigToPrivate(signature: string): string {
signature = strip0x(signature);
if (signature.length !== 130) throw new Error('Wrong ethereum signature');
return grindKey(signature.substring(0, 64));
}
const MASK_31 = 2n ** 31n - 1n;
const int31 = (n: bigint) => Number(n & MASK_31);
export function getAccountPath(
layer: string,
application: string,
ethereumAddress: string,
index: number
): string {
const layerNum = int31(sha256Num(layer));
const applicationNum = int31(sha256Num(application));
const eth = u.hexToNumber(strip0x(ethereumAddress));
return `m/2645'/${layerNum}'/${applicationNum}'/${int31(eth)}'/${int31(eth >> 31n)}'/${index}`;
}
// The Pedersen hash uses five different points on the curve.
// This is critical to ensure that they have been generated in a way
// that nobody knows the discrete logarithm of one point regarding another.
//
// Starknet utilizes nothing-up-my-sleeve technique:
// The parameters of the Pedersen hash are generated from the constant 𝜋.
// The x-coordinate of each point is a chunk of 76 decimal digit of 𝜋 modulo 𝑝.
// If it is a quadratic residue then the point is valid
// else the x-coordinate coordinate is incremented by one.
// https://docs.starkware.co/starkex/pedersen-hash-function.html
// https://github.com/starkware-libs/starkex-for-spot-trading/blob/607f0b4ce507e1d95cd018d206a2797f6ba4aab4/src/starkware/crypto/starkware/crypto/signature/nothing_up_my_sleeve_gen.py
const PEDERSEN_POINTS = [
new ProjectivePoint(
2089986280348253421170679821480865132823066470938446095505822317253594081284n,
1713931329540660377023406109199410414810705867260802078187082345529207694986n,
1n
),
new ProjectivePoint(
996781205833008774514500082376783249102396023663454813447423147977397232763n,
1668503676786377725805489344771023921079126552019160156920634619255970485781n,
1n
),
new ProjectivePoint(
2251563274489750535117886426533222435294046428347329203627021249169616184184n,
1798716007562728905295480679789526322175868328062420237419143593021674992973n,
1n
),
new ProjectivePoint(
2138414695194151160943305727036575959195309218611738193261179310511854807447n,
113410276730064486255102093846540133784865286929052426931474106396135072156n,
1n
),
new ProjectivePoint(
2379962749567351885752724891227938183011949129833673362440656643086021394946n,
776496453633298175483985398648758586525933812536653089401905292063708816422n,
1n
),
] as const;
function pedersenPrecompute(p1: ProjectivePoint, p2: ProjectivePoint): ProjectivePoint[] {
const out: ProjectivePoint[] = [];
let p = p1;
for (let i = 0; i < 248; i++) {
out.push(p);
p = p.double();
}
// NOTE: we cannot use wNAF here, because last 4 bits will require full 248 bits multiplication
// We can add support for this to wNAF, but it will complicate wNAF.
p = p2;
for (let i = 0; i < 4; i++) {
out.push(p);
p = p.double();
}
return out;
}
const PEDERSEN_POINTS1 = pedersenPrecompute(PEDERSEN_POINTS[1], PEDERSEN_POINTS[2]);
const PEDERSEN_POINTS2 = pedersenPrecompute(PEDERSEN_POINTS[3], PEDERSEN_POINTS[4]);
type PedersenArg = Hex | bigint | number;
function pedersenArg(arg: PedersenArg): bigint {
let value: bigint;
if (typeof arg === 'bigint') {
value = arg;
} else if (typeof arg === 'number') {
if (!Number.isSafeInteger(arg)) throw new Error(`Invalid pedersenArg: ${arg}`);
value = BigInt(arg);
} else {
value = u.bytesToNumberBE(ensureBytes(arg));
}
if (!(0n <= value && value < curve.CURVE.Fp.ORDER))
throw new Error(`PedersenArg should be 0 <= value < CURVE.P: ${value}`); // [0..Fp)
return value;
}
/**
* Warning: Not algorithmic constant-time.
*/
function pedersenSingle(point: ProjectivePoint, value: PedersenArg, constants: ProjectivePoint[]) {
let x = pedersenArg(value);
for (let j = 0; j < 252; j++) {
const pt = constants[j];
if (!pt) throw new Error('invalid constant index');
if (pt.equals(point)) throw new Error('Same point');
if ((x & 1n) !== 0n) point = point.add(pt);
x >>= 1n;
}
return point;
}
// shift_point + x_low * P_0 + x_high * P1 + y_low * P2 + y_high * P3
export function pedersen(x: PedersenArg, y: PedersenArg): string {
let point: ProjectivePoint = PEDERSEN_POINTS[0];
point = pedersenSingle(point, x, PEDERSEN_POINTS1);
point = pedersenSingle(point, y, PEDERSEN_POINTS2);
return extractX(point.toRawBytes(true));
}
// Same as hashChain, but computes hash even for single element and order is not revesed
export const computeHashOnElements = (data: PedersenArg[], fn = pedersen) =>
[0, ...data, data.length].reduce((x, y) => fn(x, y));
const MASK_250 = u.bitMask(250);
export const keccak = (data: Uint8Array): bigint => u.bytesToNumberBE(keccak_256(data)) & MASK_250;
const sha256Num = (data: Uint8Array | string): bigint => u.bytesToNumberBE(sha256(data));
// Poseidon hash
// Unused for now
// export const Fp253 = Field(
// BigInt('14474011154664525231415395255581126252639794253786371766033694892385558855681')
// ); // 2^253 + 2^199 + 1
export const Fp251 = Field(
BigInt('3618502788666131213697322783095070105623107215331596699973092056135872020481')
); // 2^251 + 17 * 2^192 + 1
function poseidonRoundConstant(Fp: IField<bigint>, name: string, idx: number) {
const val = Fp.fromBytes(sha256(utf8ToBytes(`${name}${idx}`)));
return Fp.create(val);
}
// NOTE: doesn't check eiginvalues and possible can create unsafe matrix. But any filtration here will break compatibility with starknet
// Please use only if you really know what you doing.
// https://eprint.iacr.org/2019/458.pdf Section 2.3 (Avoiding Insecure Matrices)
export function _poseidonMDS(Fp: IField<bigint>, name: string, m: number, attempt = 0) {
const x_values: bigint[] = [];
const y_values: bigint[] = [];
for (let i = 0; i < m; i++) {
x_values.push(poseidonRoundConstant(Fp, `${name}x`, attempt * m + i));
y_values.push(poseidonRoundConstant(Fp, `${name}y`, attempt * m + i));
}
if (new Set([...x_values, ...y_values]).size !== 2 * m)
throw new Error('X and Y values are not distinct');
return x_values.map((x) => y_values.map((y) => Fp.inv(Fp.sub(x, y))));
}
const MDS_SMALL = [
[3, 1, 1],
[1, -1, 1],
[1, 1, -2],
].map((i) => i.map(BigInt));
export type PoseidonOpts = {
Fp: IField<bigint>;
rate: number;
capacity: number;
roundsFull: number;
roundsPartial: number;
};
export type PoseidonFn = ReturnType<typeof poseidon> & {
m: number;
rate: number;
capacity: number;
};
export function poseidonBasic(opts: PoseidonOpts, mds: bigint[][]): PoseidonFn {
validateField(opts.Fp);
if (!Number.isSafeInteger(opts.rate) || !Number.isSafeInteger(opts.capacity))
throw new Error(`Wrong poseidon opts: ${opts}`);
const m = opts.rate + opts.capacity;
const rounds = opts.roundsFull + opts.roundsPartial;
const roundConstants = [];
for (let i = 0; i < rounds; i++) {
const row = [];
for (let j = 0; j < m; j++) row.push(poseidonRoundConstant(opts.Fp, 'Hades', m * i + j));
roundConstants.push(row);
}
const res: Partial<PoseidonFn> = poseidon({
...opts,
t: m,
sboxPower: 3,
reversePartialPowIdx: true, // Why?!
mds,
roundConstants,
});
res.m = m;
res.rate = opts.rate;
res.capacity = opts.capacity;
return res as PoseidonFn;
}
export function poseidonCreate(opts: PoseidonOpts, mdsAttempt = 0) {
const m = opts.rate + opts.capacity;
if (!Number.isSafeInteger(mdsAttempt)) throw new Error(`Wrong mdsAttempt=${mdsAttempt}`);
return poseidonBasic(opts, _poseidonMDS(opts.Fp, 'HadesMDS', m, mdsAttempt));
}
export const poseidonSmall = poseidonBasic(
{ Fp: Fp251, rate: 2, capacity: 1, roundsFull: 8, roundsPartial: 83 },
MDS_SMALL
);
export function poseidonHash(x: bigint, y: bigint, fn = poseidonSmall): bigint {
return fn([x, y, 2n])[0]!;
}
export function poseidonHashFunc(x: Uint8Array, y: Uint8Array, fn = poseidonSmall): Uint8Array {
return u.numberToVarBytesBE(poseidonHash(u.bytesToNumberBE(x), u.bytesToNumberBE(y), fn));
}
export function poseidonHashSingle(x: bigint, fn = poseidonSmall): bigint {
return fn([x, 0n, 1n])[0]!;
}
export function poseidonHashMany(values: bigint[], fn = poseidonSmall): bigint {
const { m, rate } = fn;
if (!Array.isArray(values)) throw new Error('bigint array expected in values');
const padded = Array.from(values); // copy
padded.push(1n);
while (padded.length % rate !== 0) padded.push(0n);
let state: bigint[] = new Array(m).fill(0n);
for (let i = 0; i < padded.length; i += rate) {
for (let j = 0; j < rate; j++) {
const item = padded[i + j];
if (typeof item === 'undefined') throw new Error('invalid index');
if (typeof state[j] === 'undefined') throw new Error('state[j] is undefined');
state[j] = state[j]! + item;
}
state = fn(state);
}
return state[0]!;
}