UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

1,041 lines (981 loc) 31.3 kB
import { customTypes, Layout, TypeMap, Json, AccountUpdate, ZkappCommand, empty, } from '../../bindings/mina-transaction/gen/transaction-bigint.js'; import { AuthRequired, Bool, Events, Field, Actions, ActionState, VerificationKeyHash, ReceiptChainHash, Sign, TokenId, TokenSymbol, ZkappUri, PublicKey, StateHash, } from '../../bindings/mina-transaction/transaction-leaves-bigint.js'; import { genericLayoutFold } from '../../bindings/lib/from-layout.js'; import { jsLayout } from '../../bindings/mina-transaction/gen/js-layout.js'; import { PrimitiveTypeMap, primitiveTypeMap, } from '../../bindings/lib/generic.js'; import { Scalar, PrivateKey, Group, } from '../../mina-signer/src/curve-bigint.js'; import { Signature } from '../../mina-signer/src/signature.js'; import { randomBytes } from '../../bindings/crypto/random.js'; import { alphabet } from '../util/base58.js'; import { bytesToBigInt } from '../../bindings/crypto/bigint-helpers.js'; import { Memo } from '../../mina-signer/src/memo.js'; import { Signable } from '../../mina-signer/src/derivers-bigint.js'; import { tokenSymbolLength } from '../../bindings/mina-transaction/derived-leaves.js'; import { stringLengthInBytes } from '../../bindings/lib/binable.js'; import { mocks } from '../../bindings/crypto/constants.js'; import type { FiniteField } from '../../bindings/crypto/finite-field.js'; export { Random, sample, withHardCoded }; type Random<T> = { create(): () => T; invalid?: Random<T>; }; type RandomWithInvalid<T> = Required<Random<T>>; function Random_<T>( next: () => T, toInvalid?: (valid: Random<T>) => Random<T> ): Random<T> { let rng: Random<T> = { create: () => next }; if (toInvalid !== undefined) rng.invalid = toInvalid(rng); return rng; } function sample<T>(rng: Random<T>, size: number) { let next = rng.create(); return Array.from({ length: size }, next); } const boolean = Random_(() => drawOneOf8() < 4); const bool = map(boolean, Bool); const uint8 = biguintWithInvalid(8); const uint32 = biguintWithInvalid(32); const uint64 = biguintWithInvalid(64); const byte = Random_(drawRandomByte); const field = fieldWithInvalid(Field); const scalar = fieldWithInvalid(Scalar); const sign = map(boolean, (b) => Sign(b ? 1 : -1)); const privateKey = Random_(PrivateKey.random); const publicKey = publicKeyWithInvalid(); const keypair = map(privateKey, (privatekey) => ({ privatekey, publicKey: PrivateKey.toPublicKey(privatekey), })); const tokenId = oneOf(TokenId.empty(), field); const stateHash = field; const authRequired = map( oneOf<Json.AuthRequired[]>( 'None', 'Proof', 'Signature', 'Either', 'Impossible' ), AuthRequired.fromJSON ); const tokenSymbolString = reject( string(nat(tokenSymbolLength)), (s) => stringLengthInBytes(s) > 6 ); const tokenSymbol = map(tokenSymbolString, TokenSymbol.fromJSON); const events = mapWithInvalid( array(array(field, int(1, 5)), nat(2)), Events.fromList ); const actions = mapWithInvalid( array(array(field, int(1, 5)), nat(2)), Actions.fromList ); const actionState = oneOf(ActionState.empty(), field); const verificationKeyHash = oneOf(VerificationKeyHash.empty(), field); const receiptChainHash = oneOf(ReceiptChainHash.empty(), field); const zkappUri = map(string(nat(50)), ZkappUri.fromJSON); type Types = typeof TypeMap & typeof customTypes & PrimitiveTypeMap<bigint>; type Generators = { [K in keyof Types]: Types[K] extends Signable<infer U, any> ? Random<U> : never; }; const Generators: Generators = { Field: field, Bool: bool, UInt32: uint32, UInt64: uint64, Sign: sign, PublicKey: publicKey, TokenId: tokenId, StateHash: stateHash, AuthRequired: authRequired, TokenSymbol: tokenSymbol, Events: events, Actions: actions, ActionState: actionState, VerificationKeyHash: verificationKeyHash, ReceiptChainHash: receiptChainHash, ZkappUri: zkappUri, null: constant(null), string: base58(nat(50)), // TODO replace various strings, like signature, with parsed types number: nat(3), TransactionVersion: uint32, }; let typeToBigintGenerator = new Map<Signable<any, any>, Random<any>>( [TypeMap, primitiveTypeMap, customTypes] .map(Object.entries) .flat() .map(([key, value]) => [value, Generators[key as keyof Generators]]) ); // transaction stuff const accountUpdate = mapWithInvalid( generatorFromLayout<AccountUpdate>(jsLayout.AccountUpdate as any, { isJson: false, }), (a) => { // TODO set proof to none since we can't generate a valid random one a.authorization.proof = undefined; // TODO set signature to null since the deriver encodes it as arbitrary string a.authorization.signature = undefined; // ensure authorization kind is valid let { isProved, isSigned } = a.body.authorizationKind; if (isProved && isSigned) { a.body.authorizationKind.isProved = Bool(false); } if (!a.body.authorizationKind.isProved) { a.body.authorizationKind.verificationKeyHash = VerificationKeyHash.empty(); } // ensure mayUseToken is valid let { inheritFromParent, parentsOwnToken } = a.body.mayUseToken; if (inheritFromParent && parentsOwnToken) { a.body.mayUseToken.inheritFromParent = Bool(false); } return a; } ); const feePayer = generatorFromLayout<ZkappCommand['feePayer']>( jsLayout.ZkappCommand.entries.feePayer as any, { isJson: false } ); const memoString = reject(string(nat(32)), (s) => stringLengthInBytes(s) > 32); const memo = map(memoString, (s) => Memo.toBase58(Memo.fromString(s))); const signature = record({ r: field, s: scalar }); // invalid json inputs can contain invalid stringified numbers, but also non-numeric strings const toString = <T>(rng: Random<T>) => map(rng, String); const nonInteger = map(uint32, fraction(3), (x, frac) => Number(x) + frac); const nonNumericString = reject( string(nat(20)), (str: any) => !isNaN(str) && !isNaN(parseFloat(str)) ); const invalidUint8Json = toString( oneOf(uint8.invalid, nonInteger, nonNumericString) ); const invalidUint32Json = toString( oneOf(uint32.invalid, nonInteger, nonNumericString) ); const invalidUint64Json = toString( oneOf(uint64.invalid, nonInteger, nonNumericString) ); // some json versions of those types let json_ = { uint32: { ...toString(uint32), invalid: invalidUint32Json }, uint64: { ...toString(uint64), invalid: invalidUint64Json }, publicKey: withInvalidBase58(mapWithInvalid(publicKey, PublicKey.toBase58)), privateKey: withInvalidBase58(map(privateKey, PrivateKey.toBase58)), keypair: map(keypair, ({ privatekey, publicKey }) => ({ privateKey: PrivateKey.toBase58(privatekey), publicKey: PublicKey.toBase58(publicKey), })), signature: withInvalidBase58(map(signature, Signature.toBase58)), signatureJson: map(signature, Signature.toJSON), field: mapWithInvalid(field, Field.toJSON), }; function withInvalidRandomString<T extends string>(rng: Random<T>) { return { ...rng, invalid: string(30) as Random<T> }; } type JsonGenerators = { [K in keyof Types]: Types[K] extends Signable<any, infer J> ? Random<J> : never; }; const JsonGenerators: JsonGenerators = { Field: json_.field, Bool: boolean, UInt32: json_.uint32, UInt64: json_.uint64, Sign: withInvalidRandomString(map(sign, Sign.toJSON)), PublicKey: json_.publicKey, TokenId: withInvalidBase58(map(tokenId, TokenId.toJSON)), StateHash: withInvalidBase58(map(stateHash, StateHash.toJSON)), AuthRequired: withInvalidRandomString(map(authRequired, AuthRequired.toJSON)), TokenSymbol: Object.assign(tokenSymbolString, { invalid: string(int(tokenSymbolLength + 1, 20)), }), Events: mapWithInvalid(events, Events.toJSON), Actions: mapWithInvalid(actions, Actions.toJSON), ActionState: mapWithInvalid(actionState, ActionState.toJSON), VerificationKeyHash: mapWithInvalid(verificationKeyHash, Field.toJSON), ReceiptChainHash: mapWithInvalid(receiptChainHash, ReceiptChainHash.toJSON), ZkappUri: string(nat(50)), null: constant(null), string: base58(nat(50)), number: nat(3), TransactionVersion: json_.uint32, }; let typeToJsonGenerator = new Map<Signable<any, any>, Random<any>>( [TypeMap, primitiveTypeMap, customTypes] .map(Object.entries) .flat() .map(([key, value]) => [value, JsonGenerators[key as keyof JsonGenerators]]) ); const accountUpdateJson = mapWithInvalid( generatorFromLayout<Json.AccountUpdate>(jsLayout.AccountUpdate as any, { isJson: true, }), (a) => { // TODO set proof to null since we can't generate a valid random one a.authorization.proof = null; // TODO set signature to null since the deriver encodes it as arbitrary string a.authorization.signature = null; // ensure authorization kind is valid let { isProved, isSigned } = a.body.authorizationKind; if (isProved && isSigned) { a.body.authorizationKind.isProved = false; } if (!a.body.authorizationKind.isProved) { a.body.authorizationKind.verificationKeyHash = mocks.dummyVerificationKeyHash; } // ensure mayUseToken is valid let { inheritFromParent, parentsOwnToken } = a.body.mayUseToken; if (inheritFromParent && parentsOwnToken) { a.body.mayUseToken.inheritFromParent = false; } return a; } ); const feePayerJson = generatorFromLayout<Json.ZkappCommand['feePayer']>( jsLayout.ZkappCommand.entries.feePayer as any, { isJson: true } ); const json = { ...json_, accountUpdate: accountUpdateJson, feePayer: feePayerJson, memoString, }; const Random = Object.assign(Random_, { constant, int, nat, fraction, boolean, byte, bytes, string, base58, array: Object.assign(array, { ofSize: arrayOfSizeValid }), record, map: Object.assign(map, { withInvalid: mapWithInvalid }), step, oneOf, withHardCoded, dependent, apply, reject, dice: Object.assign(dice, { ofSize: diceOfSize() }), field, otherField: fieldWithInvalid, bool, uint8, uint32, uint64, biguint: biguintWithInvalid, bignat: bignatWithInvalid, privateKey, publicKey, scalar, signature, accountUpdate, feePayer, memo, json, }); function generatorFromLayout<T>( typeData: Layout, { isJson }: { isJson: boolean } ): Random<T> { let typeToGenerator = isJson ? typeToJsonGenerator : typeToBigintGenerator; return genericLayoutFold< Signable<any, any>, undefined, Random<any>, TypeMap, Json.TypeMap >( TypeMap, customTypes, { map(type, _, name) { let rng = typeToGenerator.get(type); if (rng === undefined) throw Error(`could not find generator for type ${name}`); return rng; }, reduceArray(_, typeData) { let element = generatorFromLayout(typeData.inner, { isJson }); let size = typeData.staticLength ?? Random.nat(20); return array(element, size); }, reduceObject(keys, object) { // hack to not sample invalid vk hashes (because vk hash is correlated with other fields, and has to be overridden) if (keys.includes('verificationKeyHash')) { (object as any).verificationKeyHash = noInvalid( (object as any).verificationKeyHash ); } return record(object); }, reduceFlaggedOption({ isSome, value }, typeData) { if (isJson) { return oneOf(null, value); } else { return mapWithInvalid(isSome, value, (isSome, value) => { let isSomeBoolean = TypeMap.Bool.toJSON(isSome); if (!isSomeBoolean) return empty(typeData); return { isSome, value }; }); } }, reduceOrUndefined(_, innerTypeData) { return oneOf( isJson ? null : undefined, generatorFromLayout(innerTypeData, { isJson }) ); }, }, typeData, undefined ); } function constant<T>(t: T) { return Random_(() => t); } function bytes(size: number | Random<number>): Random<number[]> { return arrayValid(byte, size); } function uniformBytes(size: number | Random<number>): Random<number[]> { let size_ = typeof size === 'number' ? constant(size) : size; return { create() { let nextSize = size_.create(); return () => [...randomBytes(nextSize())]; }, }; } function string(size: number | Random<number>) { return map(uniformBytes(size), (b) => String.fromCharCode(...b)); } function base58(size: number | Random<number>) { return map(arrayValid(oneOf(...alphabet), size), (a) => a.join('')); } function isGenerator<T>(rng: any): rng is Random<T> { return typeof rng === 'object' && rng && 'create' in rng; } function oneOf<Types extends readonly any[]>( ...values: { [K in keyof Types]: | Random<Types[K]> | RandomWithInvalid<Types[K]> | Types[K]; } ): Random<Types[number]> { let gens = values.map(maybeConstant); let valid = { create() { let nexts = gens.map((rng) => rng.create()); return () => { let i = drawUniformUint(values.length - 1); return nexts[i](); }; }, }; let invalidGens = gens .filter((g) => g.invalid !== undefined) .map((g) => g.invalid!); let nInvalid = invalidGens.length; if (nInvalid === 0) return valid; let invalid = { create() { let nexts = invalidGens.map((rng) => rng.create()); return () => { let i = drawUniformUint(nInvalid - 1); return nexts[i](); }; }, }; return Object.assign(valid, { invalid }); } /** * map a list of generators to a new generator, by specifying the transformation which maps samples * of the input generators to a sample of the result. */ function map<T extends readonly any[], S>( ...args: [...rngs: { [K in keyof T]: Random<T[K]> }, to: (...values: T) => S] ): Random<S> { const to = args.pop()! as (...values: T) => S; let rngs = args as { [K in keyof T]: Random<T[K]> }; return { create() { let nexts = rngs.map((rng) => rng.create()); return () => to(...(nexts.map((next) => next()) as any)); }, }; } /** * dependent is like {@link map}, with the difference that the mapping contains a free variable * whose samples have to be provided as inputs separately. this is useful to create correlated generators, where * multiple generators are all dependent on the same extra variable which is sampled independently. * * dependent can be used in two different ways: * - as a function from a random generator of the free variable to a random generator of the result * - as a random generator whose samples are _functions_ from free variable to result: `Random<(arg: Free) => Result>` */ function dependent<T extends readonly any[], Result, Free>( ...args: [ ...rngs: { [K in keyof T]: Random<T[K]> }, to: (free: Free, values: T) => Result ] ): Random<(arg: Free) => Result> & ((arg: Random<Free>) => Random<Result>) { const to = args.pop()! as (free: Free, values: T) => Result; let rngs = args as { [K in keyof T]: Random<T[K]> }; let rng: Random<(arg: Free) => Result> = { create() { let nexts = rngs.map((rng) => rng.create()); return () => (free) => to(free, nexts.map((next) => next()) as any); }, }; return Object.assign(function (free: Random<Free>): Random<Result> { return { create() { let freeNext = free.create(); let nexts = rngs.map((rng) => rng.create()); return () => to(freeNext(), nexts.map((next) => next()) as any); }, }; }, rng); } function step<T extends readonly any[], S>( ...args: [ ...rngs: { [K in keyof T]: Random<T[K]> }, step: (current: S, ...values: T) => S, initial: S ] ): Random<S> { let initial = args.pop()! as S; const step = args.pop()! as (current: S, ...values: T) => S; let rngs = args as { [K in keyof T]: Random<T[K]> }; return { create() { let nexts = rngs.map((rng) => rng.create()); let next = initial; let current = initial; return () => { current = next; next = step(current, ...(nexts.map((next) => next()) as any as T)); return current; }; }, }; } function arrayValid<T>( element: Random<T>, size: number | Random<number>, { reset = false } = {} ): Random<T[]> { let size_ = typeof size === 'number' ? constant(size) : size; return { create() { let nextSize = size_.create(); let nextElement = element.create(); return () => { let nextElement_ = reset ? element.create() : nextElement; return Array.from({ length: nextSize() }, nextElement_); }; }, }; } function arrayOfSizeValid<T>( element: Random<T>, { reset = false } = {} ): Random<(n: number) => T[]> { return { create() { let nextElement = element.create(); return () => (length: number) => { let nextElement_ = reset ? element.create() : nextElement; return Array.from({ length }, nextElement_); }; }, }; } function recordValid<T extends {}>(gens: { [K in keyof T]: Random<T[K]>; }): Random<T> { return { create() { let keys = Object.keys(gens); let nexts = keys.map((key) => gens[key as keyof T].create()); return () => Object.fromEntries(keys.map((key, i) => [key, nexts[i]()])) as T; }, }; } function tupleValid<T extends readonly any[]>( gens: { [i in keyof T & number]: Random<T[i]>; } & Random<any>[] ): Random<T> { return { create() { let nexts = gens.map((gen) => gen.create()); return () => nexts.map((next) => next()) as any; }, }; } function reject<T>(rng: Random<T>, isRejected: (t: T) => boolean): Random<T> { return { create() { let next = rng.create(); return () => { while (true) { let t = next(); if (!isRejected(t)) return t; } }; }, }; } type Action<S> = Random<(s: S) => S>; function apply<S>( rng: Random<S>, howMany: number | Random<number>, ...actions: Action<S>[] ): Random<S> { let howMany_ = maybeConstant(howMany); let action = oneOf(...actions); return { create() { let next = rng.create(); let nextSize = howMany_.create(); let nextAction = action.create(); return () => { let state = next(); let size = nextSize(); for (let i = 0; i < size; i++) { let action = nextAction(); state = action(state); } return state; }; }, }; } function withHardCoded<T>(rng: Random<T>, ...hardCoded: T[]): Random<T> { return { create() { let next = rng.create(); let i = 0; return () => { if (i < hardCoded.length) return hardCoded[i++]; return next(); }; }, }; } function maybeConstant<T>(c: T | Random<T>): Random<T> { return isGenerator(c) ? c : constant(c); } /** * uniform distribution over range [min, max] * with bias towards special values 0, 1, -1, 2, min, max */ function int(min: number, max: number): Random<number> { if (max < min) throw Error('max < min'); // set of special numbers that will appear more often in tests let specialSet = new Set<number>(); if (-1 >= min && -1 <= max) specialSet.add(-1); if (1 >= min && 1 <= max) specialSet.add(1); if (2 >= min && 2 <= max) specialSet.add(2); specialSet.add(min); specialSet.add(max); let special = [...specialSet]; if (0 >= min && 0 <= max) special.unshift(0, 0); let nSpecial = special.length; return { create: () => () => { // 25% of test cases are special numbers if (drawOneOf8() < 3) { let i = drawUniformUint(nSpecial - 1); return special[i]; } // the remaining follow a uniform distribution return min + drawUniformUint(max - min); }, }; } /** * log-uniform distribution over range [0, max] * with bias towards 0, 1, 2 */ function bignat(max: bigint): Random<bigint> { if (max < 0n) throw Error('max < 0'); if (max === 0n) return constant(0n); let bits = max.toString(2).length; let bitBits = bits.toString(2).length; // set of special numbers that will appear more often in tests let special = [0n, 0n, 1n]; if (max > 1n) special.push(2n); let nSpecial = special.length; return { create: () => () => { // 25% of test cases are special numbers if (drawOneOf8() < 3) { let i = drawUniformUint(nSpecial - 1); return special[i]; } // the remaining follow a log-uniform / cut off exponential distribution: // we sample a bit length (within a target range) and then a number with that length while (true) { // draw bit length from [1, 2**bitBits); reject if > bit length of max let bitLength = 1 + drawUniformUintBits(bitBits); if (bitLength > bits) continue; // draw number from [0, 2**bitLength); reject if > max let n = drawUniformBigUintBits(bitLength); if (n <= max) return n; } }, }; } /** * log-uniform distribution over range [0, max] * with bias towards 0, 1, 2 */ function nat(max: number): Random<number> { return map(bignat(BigInt(max)), (n) => Number(n)); } function fraction(fixedPrecision = 3) { let denom = 10 ** fixedPrecision; if (fixedPrecision < 1) throw Error('precision must be > 1'); let next = () => (drawUniformUint(denom - 2) + 1) / denom; return { create: () => next }; } /** * unbiased, uniform distribution over range [0, max-1] */ function dice(max: number): Random<number> { if (max < 1) throw Error('max as to be > 0'); return { create: () => () => drawUniformUint(max - 1), }; } function diceOfSize(): Random<(size: number) => number> { return { create: () => () => (max: number) => { if (max < 1) throw Error('max as to be > 0'); return drawUniformUint(max - 1); }, }; } let specialBytes = [0, 0, 0, 1, 1, 2, 255, 255]; /** * log-uniform distribution over range [0, 255] * with bias towards 0, 1, 2, 255 */ function drawRandomByte() { // 25% of test cases are special numbers if (drawOneOf8() < 2) return specialBytes[drawOneOf8()]; // the remaining follow log-uniform / cut off exponential distribution: // we sample a bit length from [1, 8] and then a number with that length let bitLength = 1 + drawOneOf8(); return drawUniformUintBits(bitLength); } /** * log-uniform distribution over 2^n-bit range * with bias towards 0, 1, 2, max * outputs are bigints */ function biguint(bits: number): Random<bigint> { let max = (1n << BigInt(bits)) - 1n; let special = [0n, 0n, 0n, 1n, 1n, 2n, max, max]; let bitsBits = Math.log2(bits); if (!Number.isInteger(bitsBits)) throw Error('bits must be a power of 2'); return { create: () => () => { // 25% of test cases are special numbers if (drawOneOf8() < 2) return special[drawOneOf8()]; // the remaining follow log-uniform / cut off exponential distribution: // we sample a bit length from [1, 8] and then a number with that length let bitLength = 1 + drawUniformUintBits(bitsBits); return drawUniformBigUintBits(bitLength); }, }; } /** * uniform positive integer in [0, max] drawn from secure randomness, */ function drawUniformUint(max: number) { if (max === 0) return 0; let bitLength = Math.floor(Math.log2(max)) + 1; while (true) { // values with same bit length can be too large by a factor of at most 2; those are rejected let n = drawUniformUintBits(bitLength); if (n <= max) return n; } } /** * uniform positive integer drawn from secure randomness, * given a target bit length */ function drawUniformUintBits(bitLength: number) { let byteLength = Math.ceil(bitLength / 8); // draw random bytes, zero the excess bits let bytes = randomBytes(byteLength); if (bitLength % 8 !== 0) { bytes[byteLength - 1] &= (1 << bitLength % 8) - 1; } // accumulate bytes to integer let n = 0; let bitPosition = 0; for (let byte of bytes) { n += byte << bitPosition; bitPosition += 8; } return n; } /** * uniform positive bigint drawn from secure randomness, * given a target bit length */ function drawUniformBigUintBits(bitLength: number) { let byteLength = Math.ceil(bitLength / 8); // draw random bytes, zero the excess bits let bytes = randomBytes(byteLength); if (bitLength % 8 !== 0) { bytes[byteLength - 1] &= (1 << bitLength % 8) - 1; } return bytesToBigInt(bytes); } /** * draw number between 0,..,7 using secure randomness */ function drawOneOf8() { return randomBytes(1)[0] >> 5; } // generators for invalid samples // note: these only cover invalid samples with a _valid type_. // for example, numbers that are out of range or base58 strings with invalid characters. // what we don't cover is something like passing numbers where strings are expected // convention is that invalid generators sit next to valid ones // so you can use uint64.invalid, array(uint64, 10).invalid, etc /** * we get invalid uints by sampling from a larger range plus negative numbers */ function biguintWithInvalid(bits: number): RandomWithInvalid<bigint> { let valid = biguint(bits); let max = 1n << BigInt(bits); let double = biguint(2 * bits); let negative = map(double, (uint) => -uint - 1n); let tooLarge = map(valid, (uint) => uint + max); let invalid = oneOf(negative, tooLarge); return Object.assign(valid, { invalid }); } function bignatWithInvalid(max: bigint): RandomWithInvalid<bigint> { let valid = bignat(max); let double = bignat(2n * max); let negative = map(double, (uint) => -uint - 1n); let tooLarge = map(valid, (uint) => uint + max); let invalid = oneOf(negative, tooLarge); return Object.assign(valid, { invalid }); } function fieldWithInvalid(F: FiniteField): RandomWithInvalid<bigint> { let randomField = Random_(F.random); let specialField = oneOf(0n, 1n, F.negate(1n)); let roughLogSize = 1 << Math.ceil(Math.log2(F.sizeInBits) - 1); let uint = biguint(roughLogSize); let field = oneOf<bigint[]>(randomField, randomField, uint, specialField); let tooLarge = map(field, (x) => x + F.modulus); let negative = map(field, (x) => -x - 1n); let invalid = oneOf(tooLarge, negative); return Object.assign(field, { invalid }); } function publicKeyWithInvalid() { let publicKey = map(privateKey, PrivateKey.toPublicKey); let invalidX = reject(field, (x) => Field.isSquare(Field.add(Field.power(x, 3n), Group.b)) ); let invalid = map(invalidX, bool, (x, isOdd): PublicKey => ({ x, isOdd })); return Object.assign(publicKey, { invalid }); } /** * invalid arrays are sampled by generating an array with exactly one invalid input (and any number of valid inputs); * (note: invalid arrays have the same length distribution as valid ones, except that they are never empty) */ function array<T>( element: Random<T>, size: number | Random<number>, options?: { reset?: boolean } ): Random<T[]> { let valid = arrayValid(element, size, options); if (element.invalid === undefined) return valid; let invalid = map(valid, element.invalid, (arr, invalid) => { if (arr.length === 0) return [invalid]; let i = drawUniformUint(arr.length - 1); arr[i] = invalid; return arr; }); return { ...valid, invalid }; } /** * invalid records are similar to arrays: randomly choose one of the fields that have an invalid generator, * and set it to its invalid value */ function record<T extends {}>(gens: { [K in keyof T]: Random<T[K]>; }): Random<T> { let valid = recordValid(gens); let invalidFields: [string & keyof T, Random<any>][] = []; for (let key in gens) { let invalid = gens[key].invalid; if (invalid !== undefined) { invalidFields.push([key, invalid]); } } let nInvalid = invalidFields.length; if (nInvalid === 0) return valid; let invalid = { create() { let next = valid.create(); let invalidNexts = invalidFields.map( ([key, rng]) => [key, rng.create()] as const ); return () => { let value = next(); let i = drawUniformUint(nInvalid - 1); let [key, invalidNext] = invalidNexts[i]; value[key] = invalidNext(); return value; }; }, }; return { ...valid, invalid }; } /** * invalid tuples are like invalid records */ function tuple<T extends readonly any[]>( gens: { [K in keyof T & number]: Random<T[K]>; } & Random<any>[] ): Random<T> { let valid = tupleValid<T>(gens); let invalidFields: [number & keyof T, Random<any>][] = []; gens.forEach((gen, i) => { let invalid = gen.invalid; if (invalid !== undefined) { invalidFields.push([i, invalid]); } }); let nInvalid = invalidFields.length; if (nInvalid === 0) return valid; let invalid = { create() { let next = valid.create(); let invalidNexts = invalidFields.map( ([key, rng]) => [key, rng.create()] as const ); return () => { let value = next(); let i = drawUniformUint(nInvalid - 1); let [key, invalidNext] = invalidNexts[i]; value[key] = invalidNext(); return value; }; }, }; return { ...valid, invalid }; } /** * map assuming that invalid inputs can be mapped just like valid ones. * _one_ of the inputs is sampled as invalid */ function mapWithInvalid<T extends readonly any[], S>( ...args: [...rngs: { [K in keyof T]: Random<T[K]> }, to: (...values: T) => S] ): Random<S> { const to = args.pop()! as (...values: T) => S; let rngs = args as { [K in keyof T]: Random<T[K]> }; let valid = map<T, S>(...rngs, to); let invalidInput = tuple<T>(rngs as Random<any>[]).invalid; if (invalidInput === undefined) return valid; let invalid = { create() { let nextInput = invalidInput!.create(); return () => to(...nextInput()); }, }; return { ...valid, invalid }; } function noInvalid<T>(rng: Random<T>): Random<T> { return { ...rng, invalid: undefined }; } // functions to create invalid base58 let n = alphabet.length; function replaceCharacter(string: string, i: number, char: string) { return string.slice(0, i) + char + string.slice(i + 1); } function makeCheckSumInvalid(base58: string) { if (base58.length === 0) return base58; // pick any character, and change it to any different one let iChar = drawUniformUint(base58.length - 1); let iAlph = alphabet.indexOf(base58[iChar]); let iAlphNew = (iAlph + 1 + drawUniformUint(n - 2)) % n; return replaceCharacter(base58, iChar, alphabet[iAlphNew]); } function makeBase58Invalid(base58: string) { let iChar = drawUniformUint(base58.length - 1); // sample a character that is not in the alphabet let char: string; while (true) { let [byte] = randomBytes(1); char = String.fromCharCode(byte); if (!alphabet.includes(char)) break; } return replaceCharacter(base58, iChar, char); } function withInvalidBase58(rng: Random<string>): RandomWithInvalid<string> { let invalidBase58 = apply( rng, 1, constant(makeBase58Invalid), constant(makeCheckSumInvalid) ); let invalid = rng.invalid === undefined ? invalidBase58 : oneOf(invalidBase58, rng.invalid); return { ...rng, invalid }; }