Skip to content

Commit

Permalink
feat: add optional ssz type
Browse files Browse the repository at this point in the history
  • Loading branch information
g11tech committed Aug 27, 2023
1 parent 2929e8b commit 5ac9b83
Show file tree
Hide file tree
Showing 6 changed files with 343 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
224 changes: 224 additions & 0 deletions packages/ssz/src/type/optional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import {concatGindices, getNode, 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} 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 = 1;
readonly maxChunkCount = 1;
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.minSize = 0;
this.maxSize = elementType.maxSize;
}

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>;
}

getView(tree: Tree): ValueOfType<ElementType> {
return this.tree_toValue(tree.rootNode);
}

getViewDU(node: Node): ValueOfType<ElementType> {
return this.tree_toValue(node);
}

cacheOfViewDU(): unknown {
return;
}

commitView(view: ValueOfType<ElementType>): Node {
return this.value_toTree(view);
}

commitViewDU(view: ValueOfType<ElementType>): Node {
return this.value_toTree(view);
}

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 Error(`Invalid selector=${selector} for Optional type`);
}
return this.elementType.value_deserializeFromBytes(data, start + 1, end) as ValueOfType<ElementType>;
}
}

tree_serializedSize(node: Node): number {
const length = getLengthFromRootNode(node);
return length === 1 ? 1 + this.elementType.value_serializedSize(node.left) : 0;
}

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

const valueNode = node.left;
if (selector === 0) {
return offset;
} else {
output.uint8Array[offset] = 1;
}
return this.elementType.tree_serializeToBytes(output, offset + 1, valueNode);
}

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 Error(`Invalid selector=${selector} for Optional type`);
}
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 ? this.elementType.hashTreeRoot(value) : new Uint8Array(32);
return [valueRoot];
}

// Proofs

getPropertyGindex(prop: string): bigint {
if (isCompositeType(this.elementType)) {
const propIndex = this.elementType.getPropertyGindex(prop);
if (propIndex === null) {
throw Error(`index not found for property=${prop}`);
}
return concatGindices([VALUE_GINDEX, propIndex]);
} else {
throw new Error("not applicable for Optional basic type");
}
}

getPropertyType(): Type<unknown> {
return this.elementType;
}

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

tree_createProofGindexes(node: Node, jsonPaths: JsonPath[]): Gindex[] {
if (isCompositeType(this.elementType)) {
const valueNode = node.left;
const gindices = this.elementType.tree_createProofGindexes(valueNode, jsonPaths);
return gindices.map((gindex) => concatGindices([VALUE_GINDEX, gindex]));
} else {
throw Error("not applicable for Optional basic type");
}
}

tree_getLeafGindices(rootGindex: bigint, rootNode?: Node): bigint[] {
if (!rootNode) {
throw Error("rootNode required");
}

const gindices: Gindex[] = [concatGindices([rootGindex, SELECTOR_GINDEX])];
const selector = getLengthFromRootNode(rootNode);
const extendedFieldGindex = concatGindices([rootGindex, VALUE_GINDEX]);
if (selector !== 0 && isCompositeType(this.elementType)) {
gindices.push(...this.elementType.tree_getLeafGindices(extendedFieldGindex, getNode(rootNode, VALUE_GINDEX)));
} else {
gindices.push(extendedFieldGindex);
}
return gindices;
}

// 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);
}
}
16 changes: 16 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,16 @@
import {expect} from "chai";
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({})},
],
});
35 changes: 35 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,35 @@
import {expect} from "chai";
import {OptionalType, ContainerType, UintNumberType, NoneType, ValueOf, toHexString} from "../../../../src";

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

describe("Optional view tests", () => {
// Not using runViewTestMutation because the View of optional simple is a value
it("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));
});

it("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",
},
],
});
12 changes: 11 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, Type, OptionalType} from "../../../src";
import {CompositeTypeAny, isCompositeType} from "../../../src/type/composite";
import {ArrayBasicTreeView} from "../../../src/view/arrayBasic";
import {RootHex} from "../../lodestarTypes";
Expand Down Expand Up @@ -88,6 +88,9 @@ function getJsonPathsFromValue(value: unknown, parentPath: JsonPath = [], jsonPa
* Returns the end type of a JSON path
*/
function getJsonPathType(type: CompositeTypeAny, jsonPath: JsonPath): Type<unknown> {
if (type instanceof OptionalType) {
type = type.getPropertyType() as CompositeTypeAny;
}
for (const jsonProp of jsonPath) {
type = type.getPropertyType(jsonProp) as CompositeTypeAny;
}
Expand All @@ -104,6 +107,9 @@ function getJsonPathView(type: Type<unknown>, view: unknown, jsonPath: JsonPath)
if (typeof jsonProp === "number") {
view = (view as ArrayBasicTreeView<any>).get(jsonProp);
} else if (typeof jsonProp === "string") {
if (type instanceof OptionalType) {
type = type.getPropertyType();
}
if (type instanceof ContainerType) {
// Coerce jsonProp to a fieldName. JSON paths may be in JSON notation or fieldName notation
const fieldName = type["jsonKeyToFieldName"][jsonProp] ?? jsonProp;
Expand Down Expand Up @@ -131,6 +137,10 @@ function getJsonPathValue(type: Type<unknown>, json: unknown, jsonPath: JsonPath
if (typeof jsonProp === "number") {
json = (json as unknown[])[jsonProp];
} else if (typeof jsonProp === "string") {
if (type instanceof OptionalType) {
type = type.getPropertyType();
}

if (type instanceof ContainerType) {
if (type["jsonKeyToFieldName"][jsonProp] === undefined) {
throw Error(`Unknown jsonProp ${jsonProp} for type ${type.typeName}`);
Expand Down

0 comments on commit 5ac9b83

Please sign in to comment.