diff --git a/packages/spec-test-runner/test/spec/ssz/generic/index.test.ts b/packages/spec-test-runner/test/spec/ssz/generic/index.test.ts index ef77b1f5f955..89d01f76f19a 100644 --- a/packages/spec-test-runner/test/spec/ssz/generic/index.test.ts +++ b/packages/spec-test-runner/test/spec/ssz/generic/index.test.ts @@ -1,73 +1,102 @@ import {expect} from "chai"; -import {join} from "path"; +import path from "path"; +import fs from "fs"; // eslint-disable-next-line no-restricted-imports -import {getInvalidTestcases, getValidTestcases} from "@chainsafe/lodestar-spec-test-util/lib/sszGeneric"; -import {CompositeValue, isCompositeType} from "@chainsafe/ssz"; +import {parseInvalidTestcase, parseValidTestcase} from "@chainsafe/lodestar-spec-test-util/lib/sszGeneric"; +import {CompositeType, isCompositeType, toHexString, Type} from "@chainsafe/ssz"; +import {SPEC_TEST_LOCATION} from "../../../specTestVersioning"; // Test types defined here -import {types} from "./types"; +import {getTestType} from "./types"; -for (const type of types) { - // valid testcases - describe(`ssz generic - valid - ${type.prefix}`, () => { - for (const testcase of getValidTestcases(join(type.path, "valid"), type.prefix, type.type)) { - it(`${testcase.path.split("/").pop()}`, () => { - // test struct round trip serialization/deserialization - expect( - type.type.deserialize(type.type.serialize(testcase.value)), - "Invalid struct round-trip serialization/deserialization" - ).to.deep.equal(testcase.value); +const rootGenericSszPath = path.join(SPEC_TEST_LOCATION, "tests", "general", "phase0", "ssz_generic"); - // test struct serialization - expect(type.type.serialize(testcase.value), "Invalid struct serialization").to.deep.equal(testcase.serialized); +// ssz_generic +// | basic_vector +// | invalid +// | vec_bool_0 +// | serialized.ssz_snappy +// | valid +// | vec_bool_1_max +// | meta.yaml +// | serialized.ssz_snappy +// | value.yaml +// +// Docs: https://github.com/ethereum/eth2.0-specs/blob/master/tests/formats/ssz_generic/README.md - // test deserialization to struct - expect(type.type.deserialize(testcase.serialized), "Invalid deserialization to struct").to.deep.equal( - testcase.value - ); +for (const testType of fs.readdirSync(rootGenericSszPath)) { + const testTypePath = path.join(rootGenericSszPath, testType); - // test struct merkleization - expect(type.type.hashTreeRoot(testcase.value), "Invalid struct merkleization").to.deep.equal(testcase.root); + describe(`${testType} invalid`, () => { + const invalidCasesPath = path.join(testTypePath, "invalid"); + for (const invalidCase of fs.readdirSync(invalidCasesPath)) { + it(invalidCase, () => { + const type = getTestType(testType, invalidCase); + const testData = parseInvalidTestcase(path.join(invalidCasesPath, invalidCase)); - // If the type is composite, test tree-backed ops - if (isCompositeType(type.type)) { - const structValue = testcase.value as CompositeValue; - const treebackedValue = type.type.createTreeBackedFromStruct(structValue); + // Unlike the valid suite, invalid encodings do not have any value or hash tree root. The serialized data + // should simply not be decoded without raising an error. + // Note that for some type declarations in the invalid suite, the type itself may technically be invalid. + // This is a valid way of detecting invalid data too. E.g. a 0-length basic vector. + expect(() => type.deserialize(testData.serialized), "Must throw on deserialize").to.throw(); + }); + } + }); - // test struct / tree-backed equality - expect(type.type.equals(structValue, treebackedValue), "Struct and tree-backed not equal").to.be.true; + describe(`${testType} valid`, () => { + const validCasesPath = path.join(testTypePath, "valid"); + for (const validCase of fs.readdirSync(validCasesPath)) { + // NOTE: ComplexTestStruct tests are not correctly generated. + // where deserialized .d value is D: '0x00'. However the tests guide mark that field as D: Bytes[256]. + // Those test won't be fixed since most implementations staticly compile types. + if (validCase.startsWith("ComplexTestStruct")) { + continue; + } - // test tree-backed to struct - expect( - type.type.tree_convertToStruct(treebackedValue.tree), - "Tree-backed to struct conversion resulted in unequal value" - ).to.deep.equal(structValue); + it(validCase, () => { + const type = getTestType(testType, validCase); - // test tree-backed serialization - expect(treebackedValue.serialize(), "Invalid tree-backed serialization").to.deep.equal(testcase.serialized); + const testData = parseValidTestcase(path.join(validCasesPath, validCase), type); + const testDataSerialized = toHexString(testData.serialized); + const testDataRoot = toHexString(testData.root); - // test deserialization to tree-backed - expect( - type.type.tree_convertToStruct(type.type.tree_deserialize(testcase.serialized)), - "Invalid deserialization to tree-backed" - ).to.deep.equal(structValue); + const serialized = wrapErr(() => type.serialize(testData.value), "type.serialize()"); + const value = wrapErr(() => type.deserialize(testData.serialized), "type.deserialize()"); + const root = wrapErr(() => type.hashTreeRoot(testData.value), "type.hashTreeRoot()"); + const valueSerdes = wrapErr(() => type.deserialize(serialized), "type.deserialize(serialized)"); - // test tree-backed merkleization - expect(treebackedValue.hashTreeRoot(), "Invalid tree-backed merkleization").to.deep.equal(testcase.root); - } - }); - } - }); + expect(valueSerdes).to.deep.equal(testData.value, "round trip serdes"); + expect(toHexString(serialized)).to.equal(testDataSerialized, "struct serialize"); + expect(value).to.deep.equal(testData.value, "struct deserialize"); + expect(toHexString(root)).to.equal(testDataRoot, "struct hashTreeRoot"); + + // If the type is composite, test tree-backed ops + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!isCompositeType(type as Type)) return; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const compositeType = type as CompositeType; - // invalid testcases - describe(`ssz generic - invalid - ${type.prefix}`, () => { - for (const testcase of getInvalidTestcases(join(type.path, "invalid"), type.prefix)) { - it(`${testcase.path.split("/").pop()}`, () => { - // test struct round trip serialization/deserialization - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - expect(() => type.type.deserialize(testcase.serialized), "Invalid data should error during deserialization").to - .throw; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const treebackedValue = compositeType.createTreeBackedFromStruct(testData.value); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const treeToStruct = compositeType.tree_convertToStruct(treebackedValue.tree); + + expect(treeToStruct).to.deep.equal(testData.value, "tree-backed to struct"); + expect(type.equals(testData.value, treebackedValue), "struct - tree-backed type.equals()").to.be.true; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + expect(toHexString(treebackedValue.serialize())).to.equal(testDataSerialized, "tree-backed serialize"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + expect(toHexString(treebackedValue.hashTreeRoot())).to.equal(testDataRoot, "tree-backed hashTreeRoot"); }); } }); } + +function wrapErr(fn: () => T, prefix: string): T { + try { + return fn(); + } catch (e) { + (e as Error).message = `${prefix}: ${(e as Error).message}`; + throw e; + } +} diff --git a/packages/spec-test-runner/test/spec/ssz/generic/types.ts b/packages/spec-test-runner/test/spec/ssz/generic/types.ts index 4418d3a5b1dc..153f5f84d9e6 100644 --- a/packages/spec-test-runner/test/spec/ssz/generic/types.ts +++ b/packages/spec-test-runner/test/spec/ssz/generic/types.ts @@ -1,240 +1,177 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import {SPEC_TEST_LOCATION} from "../../../specTestVersioning"; -import {join} from "path"; import { BigIntUintType, BitListType, BitVectorType, booleanType, - // byteType, + byteType, ContainerType, ListType, Type, VectorType, } from "@chainsafe/ssz"; -const rootGenericSszPath = join(SPEC_TEST_LOCATION, "tests", "general", "phase0", "ssz_generic"); - -export interface IGenericSSZType { - type: Type; - path: string; - prefix: string; -} - -// boolean - -const booleanPath = join(rootGenericSszPath, "boolean"); - -const boolTypes = [ - { - type: booleanType, - prefix: "true", - path: booleanPath, - }, - { - type: booleanType, - prefix: "false", - path: booleanPath, - }, -]; - -// used for basic vector -const boolTypes2 = [ - { - type: booleanType, - prefix: "bool", - path: booleanPath, - }, -]; - -// uints - -const uintsPath = join(rootGenericSszPath, "uints"); +/* eslint-disable @typescript-eslint/naming-convention */ -const uintTypes = [ - { - type: new BigIntUintType({byteLength: 1}), - prefix: "uint_8", - path: uintsPath, - }, - { - type: new BigIntUintType({byteLength: 2}), - prefix: "uint_16", - path: uintsPath, - }, - { - type: new BigIntUintType({byteLength: 4}), - prefix: "uint_32", - path: uintsPath, - }, - { - type: new BigIntUintType({byteLength: 8}), - prefix: "uint_64", - path: uintsPath, - }, - { - type: new BigIntUintType({byteLength: 16}), - prefix: "uint_128", - path: uintsPath, - }, - { - type: new BigIntUintType({byteLength: 32}), - prefix: "uint_256", - path: uintsPath, +// class SingleFieldTestStruct(Container): +// A: byte +const SingleFieldTestStruct = new ContainerType({ + fields: { + a: byteType, }, -]; - -// used for basic vector -const uintTypes2 = uintTypes.map((t) => { - return { - ...t, - prefix: t.prefix.replace("_", ""), - }; }); -const lengths = [1, 2, 3, 4, 5, 8, 16, 31, 512, 513]; - -// basic_vector -const basicVectorPath = join(rootGenericSszPath, "basic_vector"); -const basicVectorTypes = (boolTypes2 as IGenericSSZType[]) - .concat(uintTypes2) - .map((t) => { - return lengths.map((length) => { - return { - type: new VectorType({ - elementType: t.type, - length, - }), - prefix: `vec_${t.prefix}_${length}_`, - path: basicVectorPath, - }; - }); - }) - .flat(1); - -// bitlist -const bitlistPath = join(rootGenericSszPath, "bitlist"); -const bitlistTypes = lengths.map((length) => { - return { - type: new BitListType({ - limit: length, - }), - prefix: `bitlist_${length}_`, - path: bitlistPath, - }; -}); - -// bitvector -const bitvectorPath = join(rootGenericSszPath, "bitvector"); -const bitvectorTypes = lengths.map((length) => { - return { - type: new BitVectorType({ - length, - }), - prefix: `bitvec_${length}_`, - path: bitvectorPath, - }; +// class SmallTestStruct(Container): +// A: uint16 +// B: uint16 +const SmallTestStruct = new ContainerType({ + fields: { + a: new BigIntUintType({byteLength: 16 / 8}), + b: new BigIntUintType({byteLength: 16 / 8}), + }, }); -// containers -const containerPath = join(rootGenericSszPath, "containers"); - +// class FixedTestStruct(Container): +// A: uint8 +// B: uint64 +// C: uint32 const FixedTestStruct = new ContainerType({ fields: { - a: new BigIntUintType({byteLength: 1}), - b: new BigIntUintType({byteLength: 8}), - c: new BigIntUintType({byteLength: 4}), + a: new BigIntUintType({byteLength: 8 / 8}), + b: new BigIntUintType({byteLength: 64 / 8}), + c: new BigIntUintType({byteLength: 32 / 8}), }, }); + +// class VarTestStruct(Container): +// A: uint16 +// B: List[uint16, 1024] +// C: uint8 const VarTestStruct = new ContainerType({ fields: { - a: new BigIntUintType({byteLength: 2}), - b: new ListType({ - elementType: new BigIntUintType({byteLength: 2}), - limit: 1024, - }), - c: new BigIntUintType({byteLength: 1}), + a: new BigIntUintType({byteLength: 16 / 8}), + b: new ListType({elementType: new BigIntUintType({byteLength: 16 / 8}), limit: 1024}), + c: new BigIntUintType({byteLength: 8 / 8}), }, }); -const containerTypes: IGenericSSZType[] = [ - { - type: new ContainerType({ - fields: { - a: new BigIntUintType({byteLength: 1}), - }, - }), - prefix: "SingleFieldTestStruct", - path: containerPath, - }, - { - type: new ContainerType({ - fields: { - a: new BigIntUintType({byteLength: 2}), - b: new BigIntUintType({byteLength: 2}), - }, - }), - prefix: "SmallTestStruct", - path: containerPath, - }, - { - type: FixedTestStruct, - prefix: "FixedTestStruct", - path: containerPath, - }, - { - type: VarTestStruct, - prefix: "VarTestStruct", - path: containerPath, - }, - /* TODO implement ByteList - { - type: new ContainerType({ - fields: { - a: new BigIntUintType({byteLength: 2}), - b: new ListType({ - elementType: new BigIntUintType({byteLength: 2}), - limit: 128, - }), - c: new BigIntUintType({byteLength: 1}), - d: new ListType({ - elementType: byteType, - limit: 256, - }), - e: VarTestStruct, - f: new VectorType({ - elementType: FixedTestStruct, - length: 4, - }), - g: new VectorType({ - elementType: VarTestStruct, - length: 2, - }), - }, - }), - prefix: "ComplexTestStruct", - path: containerPath, +// class ComplexTestStruct(Container): +// A: uint16 +// B: List[uint16, 128] +// C: uint8 +// D: Bytes[256] +// E: VarTestStruct +// F: Vector[FixedTestStruct, 4] +// G: Vector[VarTestStruct, 2] +const ComplexTestStruct = new ContainerType({ + fields: { + a: new BigIntUintType({byteLength: 16 / 8}), + b: new ListType({elementType: new BigIntUintType({byteLength: 16 / 8}), limit: 128}), + c: new BigIntUintType({byteLength: 8 / 8}), + d: new BitListType({limit: 256}), + e: VarTestStruct, + f: new VectorType({elementType: FixedTestStruct, length: 4}), + g: new VectorType({elementType: VarTestStruct, length: 2}), }, - */ - { - type: new ContainerType({ - fields: { - a: new BitListType({limit: 5}), - b: new BitVectorType({length: 2}), - c: new BitVectorType({length: 1}), - d: new BitListType({limit: 6}), - e: new BitVectorType({length: 8}), - }, - }), - prefix: "BitsStruct", - path: containerPath, +}); + +// class BitsStruct(Container): +// A: Bitlist[5] +// B: Bitvector[2] +// C: Bitvector[1] +// D: Bitlist[6] +// E: Bitvector[8] +const BitsStruct = new ContainerType({ + fields: { + a: new BitListType({limit: 5}), + b: new BitVectorType({length: 2}), + c: new BitVectorType({length: 1}), + d: new BitListType({limit: 6}), + e: new BitVectorType({length: 8}), }, -]; - -export const types: IGenericSSZType[] = (boolTypes as IGenericSSZType[]).concat( - uintTypes, - basicVectorTypes, - bitvectorTypes, - bitlistTypes, - containerTypes -); +}); + +const containerTypes = { + SingleFieldTestStruct, + SmallTestStruct, + FixedTestStruct, + VarTestStruct, + ComplexTestStruct, + BitsStruct, +}; + +const vecElementTypes = { + bool: booleanType, + uint8: new BigIntUintType({byteLength: 8 / 8}), + uint16: new BigIntUintType({byteLength: 16 / 8}), + uint32: new BigIntUintType({byteLength: 32 / 8}), + uint64: new BigIntUintType({byteLength: 64 / 8}), + uint128: new BigIntUintType({byteLength: 128 / 8}), + uint256: new BigIntUintType({byteLength: 256 / 8}), +}; + +export function getTestType(testType: string, testCase: string): Type { + switch (testType) { + // `vec_{element type}_{length}` + // {element type}: bool, uint8, uint16, uint32, uint64, uint128, uint256 + // {length}: an unsigned integer + case "basic_vector": { + const match = testCase.match(/vec_([^\W_]+)_([0-9]+)/); + const [, elementTypeStr, lengthStr] = match || []; + const elementType = vecElementTypes[elementTypeStr as keyof typeof vecElementTypes]; + if (elementType === undefined) throw Error(`No vecElementType for ${elementTypeStr}: '${testCase}'`); + const length = parseInt(lengthStr); + if (isNaN(length)) throw Error(`Bad length ${length}: '${testCase}'`); + return new VectorType({elementType, length}); + } + + // `bitlist_{limit}` + // {limit}: the list limit, in bits, of the bitlist. + case "bitlist": { + // Consider case `bitlist_no_delimiter_empty` + const limit = testCase.includes("no_delimiter") ? 0 : parseSecondNum(testCase, "limit"); + // TODO: memoize + return new BitListType({limit}); + } + + // `bitvec_{length}` + // {length}: the length, in bits, of the bitvector. + case "bitvector": { + // TODO: memoize + return new BitVectorType({length: parseSecondNum(testCase, "length")}); + } + + // A boolean has no type variations. Instead, file names just plainly describe the contents for debugging. + case "boolean": + return booleanType; + + // {container name} + // {container name}: Any of the container names listed below (excluding the `(Container)` python super type) + case "containers": { + const match = testCase.match(/([^\W_]+)/); + const containerName = (match || [])[1]; + const containerType = containerTypes[containerName as keyof typeof containerTypes]; + if (containerType === undefined) throw Error(`No containerType for ${containerName}`); + return containerType; + } + + // `uint_{size}` + // {size}: the uint size: 8, 16, 32, 64, 128 or 256. + case "uints": { + // TODO: memoize + return new BigIntUintType({byteLength: parseSecondNum(testCase, "size") / 8}); + } + + default: + throw Error(`Unknown testType ${testType}`); + } +} + +/** + * Parse second num in a underscore string: `uint_8_`, returns 8 + */ +function parseSecondNum(str: string, id: string): number { + const match = str.match(/[^\W_]+_([0-9]+)/); + const num = parseInt((match || [])[1]); + if (isNaN(num)) throw Error(`Bad ${id} ${str}`); + return num; +}