-
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
2929e8b
commit 3b714a2
Showing
6 changed files
with
367 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({})}, | ||
], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}, | ||
], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters