2
0
mirror of synced 2025-02-22 19:18:32 +00:00

Refactoring test case generation scripts.

This commit is contained in:
Richard Moore 2020-10-18 21:55:12 -04:00
parent 15893537c3
commit 7fcae25a78
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651
6 changed files with 424 additions and 0 deletions

View File

@ -0,0 +1,18 @@
{
"name": "private-generation-scripts",
"private": true,
"version": "0.0.1",
"description": "Scripts to geenrate test cases.",
"main": "index.js",
"scripts": {
"auto-build": "npm run build -- -w",
"build": "tsc --build ./tsconfig.json"
},
"devDependencies": {
"@types/node": "^14.11.8",
"eth-sig-util": "^2.5.3",
"typescript": "3.8.3"
},
"author": "Richard Moore <me@ricmoo.com>",
"license": "MIT"
}

View File

@ -0,0 +1,144 @@
import { AbstractTest } from "./test";
export interface AbiType {
name: string;
type: string;
struct?: string;
components?: Array<AbiType>;
create(): any;
}
export abstract class AbstractAbiTest<T = any> extends AbstractTest<T> {
_nextNames: Record<string, number>;
constructor(name: string) {
super(name);
this._nextNames = { };
}
nextName(prefix?: string): string {
if (prefix == null) { prefix = "p"; }
if (!this._nextNames[prefix]) { this._nextNames[prefix] = 1; }
return prefix + (this._nextNames[prefix]++);
}
randomType(dynamicOrType?: boolean | string): AbiType {
if (dynamicOrType == null) { dynamicOrType = true; }
let type: number | string = null;
let dynamic = true;
if (typeof(dynamicOrType) === "boolean") {
dynamic = dynamicOrType;
type = this.randomInteger(0, dynamic ? 8: 6);
} else {
type = dynamicOrType;
}
const name = this.nextName();
switch (type) {
// Static
// address
case 0: case "address":
return { name, type: "address", create: () => {
return this.randomAddress();
} };
// bool
case 1: case "bool":
return { name, type: "bool", create: () => {
return this.randomChoice([ false, true ]);
} };
// intXX and uintXX
case 2: case "number": {
const signed = this.randomChoice([ false, true ]);
const width = this.randomInteger(1, 33);
return { name, type: `${ signed ? "": "u" }int${ width * 8 }`, create: () => {
const bytes = this.randomBytes(width);
let value = BigInt("0x" + bytes.toString("hex"));
if (signed && (bytes[0] & 0x80)) {
bytes[0] &= ~0x80;
value = -BigInt("0x" + bytes.toString("hex"));
}
return value.toString();
} };
}
// bytesXX
case 3: case "bytesX": {
const width = this.randomInteger(1, 33);
return { name, type: `bytes${ width }`, create: () => {
return this.randomHexString(width);
} };
}
// Static or dynamic nested types
// Array
case 4: case "array": {
const baseType = this.randomType(dynamic);
let length = this.randomInteger(0, 4);
if (length == 0) { length = null; }
const lengthString = ((length == null) ? "": String(length))
let struct = undefined;
let components = undefined;
if (baseType.struct) {
struct = `${ baseType.struct }[${ lengthString }]`;
components = baseType.components;
}
return { name, components, struct, type: `${ baseType.type }[${ lengthString }]`, create: () => {
let l = length;
if (l == null) { l = this.randomInteger(0, 4); }
const result = [ ];
for (let i = 0; i < l; i++) {
result.push(baseType.create());
}
return result;
} };
}
// Tuple
case 5: case "tuple": {
const components: Array<AbiType> = [ ];
const length = this.randomInteger(1, 5);
for (let i = 0; i < length; i++) {
components.push(this.randomType(dynamic));
}
const struct = this.nextName("Struct");
const type = `tuple(${ components.map(c => c.type).join(",") })`
return { name, struct, type, components, create: () => {
const result: Record<string, any> = { };
components.forEach((type) => {
result[type.name] = type.create();
});
return result;
} };
}
// Dynamic
// string
case 6: case "string":
return { name, type: "string", create: () => {
return this.randomString(0, 64);
} };
// bytes
case 7: case "bytes":
return { name, type: "bytes", create: () => {
return this.randomHexString(0, 64);
} };
}
throw new Error("should not be reached");
}
}

View File

@ -0,0 +1,139 @@
import { TypedDataUtils } from "eth-sig-util";
import { AbstractAbiTest, AbiType } from "./abi-test";
import { saveTests, TestCase } from "./test";
function fill(testcase: Partial<TestCase.Eip712>): TestCase.Eip712 {
const domainType: Array<{ name: string, type:string }> = [];
if (testcase.domain.name != null) {
domainType.push({ name: "name", type: "string" });
}
if (testcase.domain.version != null) {
domainType.push({ name: "version", type: "string" });
}
if (testcase.domain.chainId != null) {
domainType.push({ name: "chainId", type: "uint256" });
}
if (testcase.domain.verifyingContract != null) {
domainType.push({ name: "verifyingContract", type: "address" });
}
if (testcase.domain.salt != null) {
domainType.push({ name: "salt", type: "bytes32" });
}
const typesWithDomain: Record<string, Array<{ name: string, type: string }>> = {
EIP712Domain: domainType
};
for (const key in testcase.types) { typesWithDomain[key] = testcase.types[key]; }
testcase.encoded = "0x" + TypedDataUtils.encodeData(testcase.primaryType, testcase.data, testcase.types).toString("hex");
testcase.digest = "0x" + TypedDataUtils.sign({
types: <any>typesWithDomain,
domain: testcase.domain,
primaryType: testcase.primaryType,
message: testcase.data
}, true).toString("hex");
return <TestCase.Eip712>testcase;
}
export class Eip712Test extends AbstractAbiTest<TestCase.Eip712> {
generateTest(): TestCase.Eip712 {
const type = this.randomType("tuple");
const types: Record<string, Array<{ name: string, type: string }>> = { };
function spelunk(type: AbiType): void {
if (type.struct) {
types[type.struct.split("[")[0]] = type.components.map((t) => {
spelunk(t);
return { name: t.name, type: (t.struct || t.type) };
});;
}
}
spelunk(type);
const primaryType = type.struct;
const data = type.create();
const domain: any = { };
if (this.randomChoice([ false, true])) {
domain.name = this.randomString(1, 64);
}
if (this.randomChoice([ false, true])) {
domain.version = [
this.randomInteger(0, 50),
this.randomInteger(0, 50),
this.randomInteger(0, 50),
].join(".");
}
if (this.randomChoice([ false, true])) {
domain.chainId = this.randomInteger(0, 1337);
}
if (this.randomChoice([ false, true])) {
domain.verifyingContract = this.randomAddress();
}
if (this.randomChoice([ false, true])) {
domain.salt = this.randomHexString(32);
}
return fill({
domain,
type: type.type,
seed: this.seed,
primaryType, types, data
});
}
}
if (require.main === module) {
const tests: Array<TestCase.Eip712> = [ ];
for (let i = 0; i < 1024; i++) {
const test = new Eip712Test(String(i));
tests.push(test.generateTest());
}
tests.sort((a, b) => (a.type.length - b.type.length));
tests.forEach((t, i) => { t.name = `random-${ i }`; });
tests.push({
name: "EIP712 example",
domain: {
name: 'Ether Mail',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
},
primaryType: "Mail",
types: {
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' }
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'string' }
]
},
data: {
from: {
name: 'Cow',
wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'
},
to: {
name: 'Bob',
wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
},
contents: 'Hello, Bob!'
},
encoded: "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8",
digest: "0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2",
privateKey: "0xc85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4",
signature: "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
});
saveTests("eip712", tests);
}

View File

@ -0,0 +1,91 @@
import { createHash } from "crypto";
import { saveTests as _saveTests } from "../../lib/index";
import { ethers } from "../../../ethers";
import * as TestCase from "../../lib/testcases";
export { TestCase };
function sha256(value: Buffer): Buffer {
return createHash("sha256").update(value).digest();
}
const words = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum".split(" ");
export abstract class AbstractTest<T = any> {
readonly seed: string;
_seed: Buffer;
constructor(seed: string) {
this.seed = seed;
this._seed = sha256(Buffer.from(seed));
}
_nextWord(): Buffer {
const result = this._seed;
this._seed = sha256(this._seed);
return result;
}
randomBytes(lower: number, upper?: number): Buffer {
if (!upper) { upper = lower; }
if (upper === 0 && upper === lower) { return Buffer.alloc(0); }
let result = this._nextWord();
while (result.length < upper) {
result = Buffer.concat([ result, this._nextWord() ]);
}
const top = this._nextWord();
const percent = ((top[0] << 16) | (top[1] << 8) | top[2]) / 0x01000000;
return result.slice(0, lower + Math.floor((upper - lower) * percent));
}
randomFloat(): number {
const top = this._nextWord();
return ((top[0] << 16) | (top[1] << 8) | top[2]) / 0x01000000;
}
randomInteger(lower: number, upper: number): number {
return lower + Math.floor((upper - lower) * this.randomFloat());
}
randomChoice<T>(choice: Array<T>): T {
return choice[this.randomInteger(0, choice.length)];
}
randomAddress(): string {
while (true) {
const address = this.randomHexString(20);
if (address.match(/[a-f]/i)) {
return ethers.utils.getAddress(address);
}
}
}
randomHexString(lower: number, upper?: number): string {
return "0x" + this.randomBytes(lower, upper).toString("hex");
}
randomString(lower: number, upper?: number): string {
if (!upper) { upper = lower; }
if (upper === 0 && upper === lower) { return ""; }
const length = this.randomInteger(lower, upper);
let result = "";
while (result.length < length + 1) {
result += this.randomChoice(words) + " ";
}
return result.substring(0, length);
}
abstract generateTest(): T;
}
export function saveTests(tag: string, tests: Array<any>): void {
// @TODO : copy defn files over for testcase.ts and testcase.d.ts
_saveTests(tag, tests);
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"esModuleInterop": true,
"lib": ["es2015", "es5", "dom"],
"module": "commonjs",
"target": "es2015",
"moduleResolution": "node",
"rootDir": "./src.ts",
"outDir": "./lib/",
"declaration": true,
"preserveSymlinks": true,
"preserveWatchOutput": true,
"pretty": false,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true
},
"include": [
"./thirdparty.d.ts",
"./src.ts/*.ts"
],
"exclude": [ ]
}