diff --git a/.changeset/thirty-pears-listen.md b/.changeset/thirty-pears-listen.md new file mode 100644 index 00000000..ad0134cc --- /dev/null +++ b/.changeset/thirty-pears-listen.md @@ -0,0 +1,5 @@ +--- +"@fleet-sdk/serializer": minor +--- + +Add `SAvlTree` serialization and deserialization diff --git a/biome.json b/biome.json index 3684b6fd..ddbe6fc1 100644 --- a/biome.json +++ b/biome.json @@ -40,7 +40,8 @@ "suspicious": { "recommended": true, "noConsoleLog": "error", - "noAssignInExpressions": "off" + "noAssignInExpressions": "off", + "noConstEnum": "off" } } } diff --git a/packages/serializer/src/_test-vectors/constantVectors.ts b/packages/serializer/src/_test-vectors/constantVectors.ts index 9818353e..c034f6c3 100644 --- a/packages/serializer/src/_test-vectors/constantVectors.ts +++ b/packages/serializer/src/_test-vectors/constantVectors.ts @@ -584,3 +584,30 @@ export const sBoxVectors = [ hex: "63c0843d0008cd02200a1c1b8fa17ec82de54bcaef96f23d7b34196c0410f6f578abdbf163b14b258abd33010cd8c9f416e5b1ca9f986a7f10a84191dfb85941619e49e53c0dc30ebf83324b0100b66aab1e43874ad8c5583f685a7d6d947238c373f615aee1d04ee604ba2c934000" } ]; + +export const avlTreeVectors = [ + { + name: "AVL Tree without valueLengthOpt", + hex: "643100d2e101ff01fc047c7f6f00ff80129df69a5090012f01ffca99f5bfff0c803601800100", + data: { + digest: "3100d2e101ff01fc047c7f6f00ff80129df69a5090012f01ffca99f5bfff0c8036", + insertAllowed: true, + keyLength: 128, + removeAllowed: false, + updateAllowed: false, + valueLengthOpt: undefined + } + }, + { + name: "AVL Tree with valueLengthOpt", + hex: "643100d2e101ff01fc047c7f6f00ff80129df69a5090012f01ffca99f5bfff0c80360180010115", + data: { + digest: "3100d2e101ff01fc047c7f6f00ff80129df69a5090012f01ffca99f5bfff0c8036", + insertAllowed: true, + keyLength: 128, + removeAllowed: false, + updateAllowed: false, + valueLengthOpt: 21 + } + } +]; diff --git a/packages/serializer/src/coders/sigmaByteReader.ts b/packages/serializer/src/coders/sigmaByteReader.ts index 525897b1..6795e433 100644 --- a/packages/serializer/src/coders/sigmaByteReader.ts +++ b/packages/serializer/src/coders/sigmaByteReader.ts @@ -102,6 +102,11 @@ export class SigmaByteReader { return this.readBytes(this.#bytes.length - this.#cursor); } + readOption(processor: (r: SigmaByteReader) => T): T | undefined { + if (this.readByte()) return processor(this); + return undefined; + } + /** * Returns bytes without advancing the cursor. */ diff --git a/packages/serializer/src/coders/sigmaByteWriter.ts b/packages/serializer/src/coders/sigmaByteWriter.ts index 96cd500e..2dbd7236 100644 --- a/packages/serializer/src/coders/sigmaByteWriter.ts +++ b/packages/serializer/src/coders/sigmaByteWriter.ts @@ -109,6 +109,18 @@ export class SigmaByteWriter { return this; } + writeOption(value: T | undefined, processor: (w: SigmaByteWriter) => void): SigmaByteWriter { + if (value === undefined) { + this.write(0x00); // write 0x00 for None + return this; + } + + this.write(0x01); // write 0x01 for Some + processor(this); // call the inner writer to write the Option value + + return this; + } + writeChecksum(length = 4, hashFn = blake2b256): SigmaByteWriter { const hash = hashFn(this.toBytes()); return this.writeBytes(length ? hash.subarray(0, length) : hash); diff --git a/packages/serializer/src/serializers/avlTreeSerializer.ts b/packages/serializer/src/serializers/avlTreeSerializer.ts new file mode 100644 index 00000000..b69c21b9 --- /dev/null +++ b/packages/serializer/src/serializers/avlTreeSerializer.ts @@ -0,0 +1,58 @@ +import { hex } from "@fleet-sdk/crypto"; +import { type SigmaByteReader, SigmaByteWriter } from "../coders"; + +export interface AvlTreeFlags { + insertAllowed: boolean; + updateAllowed: boolean; + removeAllowed: boolean; +} + +export interface AvlTreeData extends AvlTreeFlags { + digest: string; + keyLength: number; + valueLengthOpt?: number; +} + +const DIGEST_SIZE = 33; + +const enum AvlTreeFlag { + InsertAllowed = 0x01, + UpdateAllowed = 0x02, + RemoveAllowed = 0x04 +} + +export function serializeAvlTree( + data: AvlTreeData, + writer: SigmaByteWriter = new SigmaByteWriter(4_096) +): SigmaByteWriter { + return writer // (DIGEST_SIZE + 1 + 4 + 1 + 4 /** flags, key len, opt flag, opt value */) + .writeBytes(hex.decode(data.digest)) + .write(serializeFlags(data)) + .writeUInt(data.keyLength) + .writeOption(data.valueLengthOpt, (w) => w.writeUInt(data.valueLengthOpt as number)); +} + +export function deserializeAvlTree(reader: SigmaByteReader): AvlTreeData { + return { + digest: hex.encode(reader.readBytes(DIGEST_SIZE)), + ...parseFlags(reader.readByte()), + keyLength: reader.readUInt(), + valueLengthOpt: reader.readOption((r) => r.readUInt()) + }; +} + +function parseFlags(byte: number): AvlTreeFlags { + return { + insertAllowed: (byte & AvlTreeFlag.InsertAllowed) !== 0, + updateAllowed: (byte & AvlTreeFlag.UpdateAllowed) !== 0, + removeAllowed: (byte & AvlTreeFlag.RemoveAllowed) !== 0 + }; +} + +function serializeFlags(flags: AvlTreeFlags): number { + let byte = 0x0; + if (flags.insertAllowed) byte |= AvlTreeFlag.InsertAllowed; + if (flags.updateAllowed) byte |= AvlTreeFlag.UpdateAllowed; + if (flags.removeAllowed) byte |= AvlTreeFlag.RemoveAllowed; + return byte; +} diff --git a/packages/serializer/src/serializers/dataSerializer.ts b/packages/serializer/src/serializers/dataSerializer.ts index 28348d6c..f263184e 100644 --- a/packages/serializer/src/serializers/dataSerializer.ts +++ b/packages/serializer/src/serializers/dataSerializer.ts @@ -3,6 +3,7 @@ import type { SigmaByteReader, SigmaByteWriter } from "../coders"; import type { SConstant } from "../sigmaConstant"; import { type SCollType, type STupleType, type SType, isColl, isTuple } from "../types"; import { descriptors } from "../types/descriptors"; +import { type AvlTreeData, deserializeAvlTree, serializeAvlTree } from "./avlTreeSerializer"; import { deserializeBox, serializeBox } from "./boxSerializer"; const GROUP_ELEMENT_LENGTH = 33; @@ -82,6 +83,9 @@ export const dataSerializer = { if (type.code === descriptors.unit.code) return writer; if (type.code === descriptors.box.code) return serializeBox(data as Box, writer); + if (type.code === descriptors.avlTree.code) { + return serializeAvlTree(data as AvlTreeData, writer); + } throw Error(`Serialization error: '0x${type.code.toString(16)}' type not implemented.`); }, @@ -139,6 +143,8 @@ export const dataSerializer = { return undefined; case descriptors.box.code: return deserializeBox(reader); + case descriptors.avlTree.code: + return deserializeAvlTree(reader); } } diff --git a/packages/serializer/src/serializers/typeSerializer.ts b/packages/serializer/src/serializers/typeSerializer.ts index 6dfe3a7f..00694710 100644 --- a/packages/serializer/src/serializers/typeSerializer.ts +++ b/packages/serializer/src/serializers/typeSerializer.ts @@ -16,6 +16,8 @@ export const typeSerializer = { writer.write(type.code); } else if (type.code === descriptors.box.code) { writer.write(type.code); + } else if (type.code === descriptors.avlTree.code) { + writer.write(type.code); } else if (isColl(type)) { if (type.elementsType.embeddable) { writer.write(descriptors.coll.simpleCollTypeCode + type.elementsType.code); @@ -144,6 +146,8 @@ export const typeSerializer = { return descriptors.unit; case descriptors.box.code: return descriptors.box; + case descriptors.avlTree.code: + return descriptors.avlTree; } throw new Error("Not implemented."); diff --git a/packages/serializer/src/sigmaConstant.spec.ts b/packages/serializer/src/sigmaConstant.spec.ts index 6d24e432..0d2fcf6d 100644 --- a/packages/serializer/src/sigmaConstant.spec.ts +++ b/packages/serializer/src/sigmaConstant.spec.ts @@ -5,6 +5,7 @@ import { Value$ } from "sigmastate-js/main"; import { describe, expect, it, test, vitest } from "vitest"; import { SPair } from "../dist"; import { + avlTreeVectors, bigintVectors, boolVectors, byteVectors, @@ -31,6 +32,7 @@ import { MIN_I256 } from "./coders/numRanges"; import { dataSerializer } from "./serializers"; +import type { AvlTreeData } from "./serializers/avlTreeSerializer"; import { SConstant, decode, parse, stypeof } from "./sigmaConstant"; import type { SGroupElementType } from "./types"; import { @@ -46,7 +48,7 @@ import { STupleType, SUnit } from "./types/"; -import { SBox, STuple } from "./types/constructors"; +import { SAvlTree, SBox, STuple } from "./types/constructors"; describe("Primitive types serialization and parsing", () => { it.each(boolVectors)("Should road-trip SBool($value)", (tv) => { @@ -135,6 +137,39 @@ describe("Monomorphic types serialization and parsing", () => { }); }); +describe("AVL Tree serialization and parsing", () => { + test.each(avlTreeVectors)("AVL Tree serialization", (tv) => { + const decoded = SConstant.from(tv.hex); + expect(decoded.type.toString()).to.be.equal("SAvlTree"); + expect(decoded.data).to.deep.equal(tv.data); + expect(Value$.fromHex(tv.hex).data).to.deep.equal(tv.data); // confirm with sigmastate + }); + + it.each(avlTreeVectors)("AVL Tree roundtrip", (tv) => { + const avlTree = SConstant.from(tv.hex).data; + expect(SAvlTree(avlTree).toHex()).to.be.equal(tv.hex); + }); + + it("Should serialize different flags", () => { + const tree = avlTreeVectors[0].data; + + let obj = { ...tree, insertAllowed: true }; + let hex = SAvlTree(obj).toHex(); + expect(SConstant.from(hex).data).to.deep.equal(obj); + expect(Value$.fromHex(hex).data).to.deep.equal(obj); // confirm with sigmastate + + obj = { ...tree, insertAllowed: false, updateAllowed: true }; + hex = SAvlTree(obj).toHex(); + expect(SConstant.from(hex).data).to.deep.equal(obj); + expect(Value$.fromHex(hex).data).to.deep.equal(obj); // confirm with sigmastate + + obj = { ...tree, removeAllowed: true }; + hex = SAvlTree(obj).toHex(); + expect(SConstant.from(hex).data).to.deep.equal(obj); + expect(Value$.fromHex(hex).data).to.deep.equal(obj); // confirm with sigmastate + }); +}); + describe("SColl serialization and parsing", () => { it.each(collVectors)("Should serialize $name", (tv) => { expect(tv.sconst.toHex()).to.be.equal(tv.hex); @@ -267,7 +302,7 @@ describe("Data only decoding", () => { describe("Not implemented types", () => { it("Should fail while trying to serialize a not implemented type", () => { const unimplementedType = { - code: 0x64, // AvlTree type code + code: 0x66, // SString type code embeddable: false, coerce: (val: unknown) => val } as unknown as SGroupElementType; @@ -284,7 +319,7 @@ describe("Not implemented types", () => { // not implemented SSigmaProp expression expect(() => { dataSerializer.serialize("", unimplementedType, new SigmaByteWriter(1)); - }).to.throw("Serialization error: '0x64' type not implemented."); + }).to.throw("Serialization error: '0x66' type not implemented."); }); it("Should fail when trying to deserialize a not implemented SigmaProp expression", () => { diff --git a/packages/serializer/src/types/constructors.ts b/packages/serializer/src/types/constructors.ts index 3780a859..c6a533a7 100644 --- a/packages/serializer/src/types/constructors.ts +++ b/packages/serializer/src/types/constructors.ts @@ -1,9 +1,10 @@ import { type Box, isEmpty } from "@fleet-sdk/common"; +import type { AvlTreeData } from "../serializers/avlTreeSerializer"; import { SConstant } from "../sigmaConstant"; import type { SType } from "./base"; import { descriptors } from "./descriptors"; import { SCollType, STupleType } from "./generics"; -import { SBoxType, SUnitType } from "./monomorphics"; +import { SAvlTreeType, SBoxType, SUnitType } from "./monomorphics"; import { SBigIntType, SBoolType, @@ -102,6 +103,9 @@ export const SUnit = monoProxy(SUnitType, undefined, true) as unknown as SUnit; type SBox = (value?: Box) => SConstant, SBoxType>; export const SBox = monoProxy(SBoxType, undefined, true) as unknown as SBox; +type SAvlTree = (value?: AvlTreeData) => SConstant; +export const SAvlTree = monoProxy(SAvlTreeType, undefined, true) as unknown as SAvlTree; + type SColl = { ( type: SConstructor, diff --git a/packages/serializer/src/types/descriptors.ts b/packages/serializer/src/types/descriptors.ts index 02638943..ad57e91b 100644 --- a/packages/serializer/src/types/descriptors.ts +++ b/packages/serializer/src/types/descriptors.ts @@ -1,6 +1,6 @@ import type { SType } from "./base"; import type { SCollType, STupleType } from "./generics"; -import { SBoxType, SUnitType } from "./monomorphics"; +import { SAvlTreeType, SBoxType, SUnitType } from "./monomorphics"; import { SBigIntType, SBoolType, @@ -62,6 +62,7 @@ export const descriptors = { sigmaProp: new SSigmaPropType(), unit: new SUnitType(), box: new SBoxType(), + avlTree: new SAvlTreeType(), coll: collDescriptor, tuple: tupleDescriptor } satisfies { [key: string]: Descriptor }; diff --git a/packages/serializer/src/types/index.ts b/packages/serializer/src/types/index.ts index f19a6ab7..58cddf48 100644 --- a/packages/serializer/src/types/index.ts +++ b/packages/serializer/src/types/index.ts @@ -15,5 +15,6 @@ export { SUnit, SColl, SPair, - SBox + SBox, + SAvlTree } from "./constructors"; diff --git a/packages/serializer/src/types/monomorphics.ts b/packages/serializer/src/types/monomorphics.ts index f71b82ff..1383480e 100644 --- a/packages/serializer/src/types/monomorphics.ts +++ b/packages/serializer/src/types/monomorphics.ts @@ -1,4 +1,5 @@ import type { Box } from "@fleet-sdk/common"; +import type { AvlTreeData } from "../serializers/avlTreeSerializer"; import type { SConstant } from "../sigmaConstant"; import { SMonomorphicType } from "./base"; @@ -21,3 +22,13 @@ export class SBoxType extends SMonomorphicType>> { return "SBox"; } } + +export class SAvlTreeType extends SMonomorphicType { + get code(): 0x64 { + return 0x64; + } + + toString(): string { + return "SAvlTree"; + } +}