Skip to content

Commit

Permalink
feat: add optional ssz type (#329)
Browse files Browse the repository at this point in the history
* feat: add optional ssz type

* lint

* lint

* add a todo comment

* chore: pr review

* chore: fix tests

---------

Co-authored-by: Cayman <caymannava@gmail.com>
  • Loading branch information
g11tech and wemeetagain authored Sep 9, 2023
1 parent 2929e8b commit 3b714a2
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/ssz/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {ListCompositeType} from "./type/listComposite";
export {NoneType} from "./type/none";
export {UintBigintType, UintNumberType} from "./type/uint";
export {UnionType} from "./type/union";
export {OptionalType} from "./type/optional";
export {VectorBasicType} from "./type/vectorBasic";
export {VectorCompositeType} from "./type/vectorComposite";

Expand Down
248 changes: 248 additions & 0 deletions packages/ssz/src/type/optional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import {concatGindices, Gindex, Node, Tree, zeroNode} from "@chainsafe/persistent-merkle-tree";
import {mixInLength} from "../util/merkleize";
import {Require} from "../util/types";
import {namedClass} from "../util/named";
import {Type, ByteViews, JsonPath, JsonPathProp} from "./abstract";
import {CompositeType, isCompositeType} from "./composite";
import {addLengthNode, getLengthFromRootNode} from "./arrayBasic";
/* eslint-disable @typescript-eslint/member-ordering */

export type OptionalOpts = {
typeName?: string;
};
type ValueOfType<ElementType extends Type<unknown>> = ElementType extends Type<infer T> ? T | null : never;
const VALUE_GINDEX = BigInt(2);
const SELECTOR_GINDEX = BigInt(3);

/**
* Optional: optional type containing either None or a type
* - Notation: Optional[type], e.g. optional[uint64]
* - merklizes as list of length 0 or 1, essentially acts like
* - like Union[none,type] or
* - list [], [type]
*/
export class OptionalType<ElementType extends Type<unknown>> extends CompositeType<
ValueOfType<ElementType>,
ValueOfType<ElementType>,
ValueOfType<ElementType>
> {
readonly typeName: string;
readonly depth: number;
readonly maxChunkCount: number;
readonly fixedSize = null;
readonly minSize: number;
readonly maxSize: number;
readonly isList = true;
readonly isViewMutable = true;

constructor(readonly elementType: ElementType, opts?: OptionalOpts) {
super();

this.typeName = opts?.typeName ?? `Optional[${elementType.typeName}]`;
this.maxChunkCount = 1;
// Depth includes the extra level for the true/false node
this.depth = elementType.depth + 1;

this.minSize = 0;
// Max size includes prepended 0x01 byte
this.maxSize = elementType.maxSize + 1;
}

static named<ElementType extends Type<unknown>>(
elementType: ElementType,
opts: Require<OptionalOpts, "typeName">
): OptionalType<ElementType> {
return new (namedClass(OptionalType, opts.typeName))(elementType, opts);
}

defaultValue(): ValueOfType<ElementType> {
return null as ValueOfType<ElementType>;
}

// TODO add an OptionalView
getView(tree: Tree): ValueOfType<ElementType> {
return this.tree_toValue(tree.rootNode);
}

// TODO add an OptionalViewDU
getViewDU(node: Node): ValueOfType<ElementType> {
return this.tree_toValue(node);
}

// TODO add an OptionalView
commitView(view: ValueOfType<ElementType>): Node {
return this.value_toTree(view);
}

// TODO add an OptionalViewDU
commitViewDU(view: ValueOfType<ElementType>): Node {
return this.value_toTree(view);
}

// TODO add an OptionalViewDU
cacheOfViewDU(): unknown {
return;
}

value_serializedSize(value: ValueOfType<ElementType>): number {
return value !== null ? 1 + this.elementType.value_serializedSize(value) : 0;
}

value_serializeToBytes(output: ByteViews, offset: number, value: ValueOfType<ElementType>): number {
if (value !== null) {
output.uint8Array[offset] = 1;
return this.elementType.value_serializeToBytes(output, offset + 1, value);
} else {
return offset;
}
}

value_deserializeFromBytes(data: ByteViews, start: number, end: number): ValueOfType<ElementType> {
if (start === end) {
return null as ValueOfType<ElementType>;
} else {
const selector = data.uint8Array[start];
if (selector !== 1) {
throw new Error(`Invalid selector for Optional type: ${selector}`);
}
return this.elementType.value_deserializeFromBytes(data, start + 1, end) as ValueOfType<ElementType>;
}
}

tree_serializedSize(node: Node): number {
const selector = getLengthFromRootNode(node);

if (selector === 0) {
return 0;
} else if (selector === 1) {
return 1 + this.elementType.value_serializedSize(node.left);
} else {
throw new Error(`Invalid selector for Optional type: ${selector}`);
}
}

tree_serializeToBytes(output: ByteViews, offset: number, node: Node): number {
const selector = getLengthFromRootNode(node);

if (selector === 0) {
return offset;
} else if (selector === 1) {
output.uint8Array[offset] = 1;
return this.elementType.tree_serializeToBytes(output, offset + 1, node.left);
} else {
throw new Error(`Invalid selector for Optional type: ${selector}`);
}
}

tree_deserializeFromBytes(data: ByteViews, start: number, end: number): Node {
let valueNode;
let selector;
if (start === end) {
selector = 0;
valueNode = zeroNode(0);
} else {
selector = data.uint8Array[start];
if (selector !== 1) {
throw new Error(`Invalid selector for Optional type: ${selector}`);
}
valueNode = this.elementType.tree_deserializeFromBytes(data, start + 1, end);
}

return addLengthNode(valueNode, selector);
}

// Merkleization

hashTreeRoot(value: ValueOfType<ElementType>): Uint8Array {
const selector = value === null ? 0 : 1;
return mixInLength(super.hashTreeRoot(value), selector);
}

protected getRoots(value: ValueOfType<ElementType>): Uint8Array[] {
const valueRoot = value === null ? new Uint8Array(32) : this.elementType.hashTreeRoot(value);
return [valueRoot];
}

// Proofs

getPropertyGindex(prop: JsonPathProp): Gindex | null {
if (isCompositeType(this.elementType)) {
const propIndex = this.elementType.getPropertyGindex(prop);
return propIndex === null ? propIndex : concatGindices([VALUE_GINDEX, propIndex]);
} else {
throw new Error("not applicable for Optional basic type");
}
}

getPropertyType(prop: JsonPathProp): Type<unknown> {
if (isCompositeType(this.elementType)) {
return this.elementType.getPropertyType(prop);
} else {
throw new Error("not applicable for Optional basic type");
}
}

getIndexProperty(index: number): JsonPathProp | null {
if (isCompositeType(this.elementType)) {
return this.elementType.getIndexProperty(index);
} else {
throw new Error("not applicable for Optional basic type");
}
}

tree_createProofGindexes(node: Node, jsonPaths: JsonPath[]): Gindex[] {
if (isCompositeType(this.elementType)) {
return super.tree_createProofGindexes(node, jsonPaths);
} else {
throw new Error("not applicable for Optional basic type");
}
}

tree_getLeafGindices(rootGindex: bigint, rootNode?: Node): Gindex[] {
if (!rootNode) {
throw new Error("Optional type requires rootNode argument to get leaves");
}

const selector = getLengthFromRootNode(rootNode);

if (isCompositeType(this.elementType) && selector === 1) {
return [
//
...this.elementType.tree_getLeafGindices(concatGindices([rootGindex, VALUE_GINDEX]), rootNode.left),
concatGindices([rootGindex, SELECTOR_GINDEX]),
];
} else if (selector === 0 || selector === 1) {
return [
//
concatGindices([rootGindex, VALUE_GINDEX]),
concatGindices([rootGindex, SELECTOR_GINDEX]),
];
} else {
throw new Error(`Invalid selector for Optional type: ${selector}`);
}
}

// JSON

fromJson(json: unknown): ValueOfType<ElementType> {
return (json === null ? null : this.elementType.fromJson(json)) as ValueOfType<ElementType>;
}

toJson(value: ValueOfType<ElementType>): unknown | Record<string, unknown> {
return value === null ? null : this.elementType.toJson(value);
}

clone(value: ValueOfType<ElementType>): ValueOfType<ElementType> {
return (value === null ? null : this.elementType.clone(value)) as ValueOfType<ElementType>;
}

equals(a: ValueOfType<ElementType>, b: ValueOfType<ElementType>): boolean {
if (a === null && b === null) {
return true;
} else if (a === null || b === null) {
return false;
}

return this.elementType.equals(a, b);
}
}
15 changes: 15 additions & 0 deletions packages/ssz/test/unit/byType/optional/invalid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {UintNumberType, OptionalType} from "../../../../src";
import {runTypeTestInvalid} from "../runTypeTestInvalid";

const byteType = new UintNumberType(1);

runTypeTestInvalid({
type: new OptionalType(byteType),
values: [
{id: "Bad selector", serialized: "0x02ff"},

{id: "Array", json: []},
{id: "incorrect value", json: {}},
{id: "Object stringified", json: JSON.stringify({})},
],
});
38 changes: 38 additions & 0 deletions packages/ssz/test/unit/byType/optional/tree.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {expect} from "chai";
import {OptionalType, ContainerType, UintNumberType, ValueOf, toHexString} from "../../../../src";

const byteType = new UintNumberType(1);
const SimpleObject = new ContainerType({
b: byteType,
a: byteType,
});

describe("Optional view tests", () => {
// unimplemented
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
it.skip("optional simple type", () => {
const type = new OptionalType(byteType);
const value: ValueOf<typeof type> = 9;
const root = type.hashTreeRoot(value);

const view = type.toView(value);
const viewDU = type.toViewDU(value);

expect(toHexString(type.commitView(view).root)).equals(toHexString(root));
expect(toHexString(type.commitViewDU(viewDU).root)).equals(toHexString(root));
});

// unimplemented
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
it.skip("optional composite type", () => {
const type = new OptionalType(SimpleObject);
const value: ValueOf<typeof type> = {a: 9, b: 11};
const root = type.hashTreeRoot(value);

const view = type.toView(value);
const viewDU = type.toViewDU(value);

expect(toHexString(type.commitView(view).root)).equals(toHexString(root));
expect(toHexString(type.commitViewDU(viewDU).root)).equals(toHexString(root));
});
});
56 changes: 56 additions & 0 deletions packages/ssz/test/unit/byType/optional/valid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {OptionalType, UintNumberType, ListBasicType, ContainerType, ListCompositeType} from "../../../../src";
import {runTypeTestValid} from "../runTypeTestValid";

const number8Type = new UintNumberType(1);
const SimpleObject = new ContainerType({
b: number8Type,
a: number8Type,
});

// test for a basic type
runTypeTestValid({
type: new OptionalType(number8Type),
defaultValue: null,
values: [
{serialized: "0x", json: null, root: "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"},
{serialized: "0x0109", json: 9, root: "0xc17ba48dfddbdec0cbfbf24c1aef5ebac372f63b9dad08e99224d0c9a9f22f72"},
],
});

// null should merklize same as empty list or list with 1 value but serializes without optional prefix 0x01
runTypeTestValid({
type: new ListBasicType(number8Type, 1),
defaultValue: [],
values: [
{serialized: "0x", json: [], root: "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"},
{serialized: "0x09", json: [9], root: "0xc17ba48dfddbdec0cbfbf24c1aef5ebac372f63b9dad08e99224d0c9a9f22f72"},
],
});

// test for a composite type
runTypeTestValid({
type: new OptionalType(SimpleObject),
defaultValue: null,
values: [
{serialized: "0x", json: null, root: "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"},
{
serialized: "0x010b09",
json: {a: 9, b: 11},
root: "0xb4fc36ed412e6f56e3002b2f56559c55420e843e182168ed087669bd3e5338a7",
},
],
});

// null should merklize same as empty list or list with 1 value but serializes without optional prefix 0x01
runTypeTestValid({
type: new ListCompositeType(SimpleObject, 1),
defaultValue: [],
values: [
{serialized: "0x", json: [], root: "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"},
{
serialized: "0x0b09",
json: [{a: 9, b: 11}],
root: "0xb4fc36ed412e6f56e3002b2f56559c55420e843e182168ed087669bd3e5338a7",
},
],
});
10 changes: 9 additions & 1 deletion packages/ssz/test/unit/byType/runTypeProofTest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Node} from "@chainsafe/persistent-merkle-tree";
import {expect} from "chai";
import {BitArray, ContainerType, fromHexString, JsonPath, Type} from "../../../src";
import {BitArray, ContainerType, fromHexString, JsonPath, OptionalType, Type} from "../../../src";
import {CompositeTypeAny, isCompositeType} from "../../../src/type/composite";
import {ArrayBasicTreeView} from "../../../src/view/arrayBasic";
import {RootHex} from "../../lodestarTypes";
Expand Down Expand Up @@ -101,6 +101,10 @@ function getJsonPathType(type: CompositeTypeAny, jsonPath: JsonPath): Type<unkno
*/
function getJsonPathView(type: Type<unknown>, view: unknown, jsonPath: JsonPath): unknown {
for (const jsonProp of jsonPath) {
if (type instanceof OptionalType) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
type = type.elementType;
}
if (typeof jsonProp === "number") {
view = (view as ArrayBasicTreeView<any>).get(jsonProp);
} else if (typeof jsonProp === "string") {
Expand Down Expand Up @@ -128,6 +132,10 @@ function getJsonPathView(type: Type<unknown>, view: unknown, jsonPath: JsonPath)
*/
function getJsonPathValue(type: Type<unknown>, json: unknown, jsonPath: JsonPath): unknown {
for (const jsonProp of jsonPath) {
if (type instanceof OptionalType) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
type = type.elementType;
}
if (typeof jsonProp === "number") {
json = (json as unknown[])[jsonProp];
} else if (typeof jsonProp === "string") {
Expand Down

0 comments on commit 3b714a2

Please sign in to comment.