Skip to content

Commit

Permalink
feat(core): add full BigInt parser/serializer
Browse files Browse the repository at this point in the history
  • Loading branch information
capt-nemo429 committed Apr 22, 2023
1 parent a8bf221 commit 43540c1
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 368 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"test:prettier": "prettier ./**/*.ts --list-different",
"test:unit": "vitest run",
"cov:check": "vitest run --coverage",
"cov:open": "vitest run --coverage ; open-cli coverage/index.html"
"cov:open": "vitest run --coverage ; open-cli coverage/index.html",
"bench": "vitest bench"
},
"devDependencies": {
"@noble/hashes": "^1.3.0",
Expand Down
37 changes: 36 additions & 1 deletion packages/common/src/utils/bigIntUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { describe, expect, it } from "vitest";
import { decimalize, ensureBigInt, sumBy, undecimalize } from "./bigIntUtils";
import {
bigIntToHex,
decimalize,
ensureBigInt,
hexToBigInt,
sumBy,
undecimalize
} from "./bigIntUtils";

describe("decimalize()", () => {
it("Should decimalize", () => {
Expand Down Expand Up @@ -168,4 +175,32 @@ describe("BigInt sumBy()", () => {
)
).toBe(13158n);
});

it("Should convert a bigint value to a hex string", () => {
expect(bigIntToHex(-54417895017443177n)).toBe("ff3eab367a0f9097");

expect(bigIntToHex(170892133397465074381480318756786823280n)).toBe(
"008090a0b0c0d0e0f00010203040506070"
);
expect(bigIntToHex(-169390233523473389081894288674981388176n)).toBe(
"8090a0b0c0d0e0f00010203040506070"
);

expect(bigIntToHex(1207883114728849269100423775319436127n)).toBe(
"00e8a13c46cdde58d442c8e45b9f2b5f"
);
expect(bigIntToHex(-518499127179672366370132270668500813n)).toBe(
"9c2404f2634ef40afccc320eed30b3"
);
expect(bigIntToHex(4n)).toBe("04");
});

it("Should convert a hex string to a bigint value", () => {
expect(hexToBigInt("0e8a13c46cdde58d442c8e45b9f2b5f")).toBe(
1207883114728849269100423775319436127n
);
expect(hexToBigInt("9c2404f2634ef40afccc320eed30b3")).toBe(
-518499127179672366370132270668500813n
);
});
});
74 changes: 73 additions & 1 deletion packages/common/src/utils/bigIntUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Amount } from "../types";
import { isEmpty } from "./arrayUtils";
import { first, isEmpty } from "./arrayUtils";
import { _0n, _10n } from "./bigIntLiterals";
import { isUndefined } from "./objectUtils";

Expand Down Expand Up @@ -149,3 +149,75 @@ export function sumBy<T>(

return acc;
}

/**
* Converts a hex string to bigint.
* @param hex The hex string to be converted.
* @returns The bigint value represented by the hex string.
*/
export function hexToBigInt(hex: string): bigint {
// https://coolaj86.com/articles/convert-hex-to-decimal-with-js-bigints/
if (hex.length % 2) {
hex = "0" + hex;
}

const value = BigInt("0x" + hex);
const highByte = parseInt(hex.slice(0, 2), 16);

if (0x80 & highByte) {
return -_bitNegate(value); // add two's complement and invert the number to negative
}

return value;
}

/**
* Serializes a `BigInt` to a hex string
* @param value The bigint value to be serialized
* @returns Hex representation for the provided `number`.
*/
export function bigIntToHex(value: bigint): string {
// implementation inspired on
// https://coolaj86.com/articles/convert-decimal-to-hex-with-js-bigints/
const positive = value >= _0n;
if (!positive) {
value = _bitNegate(value);
}

let hex = value.toString(16);
if (hex.length % 2) {
hex = "0" + hex;
}

if (positive && 0x80 & parseInt(hex.slice(0, 2), 16)) {
hex = "00" + hex;
}

return hex;
}

/**
* Returns the two’s complement of a bigint value.
* @param value The bigint value to negate.
* @returns The two’s complement of `number` as a bigint.
*/
export function _bitNegate(value: bigint): bigint {
const negative = value < _0n;
if (negative) {
value = -value; // turn into a positive number
}

const bits = value.toString(2);
let bitLen = bits.length; // convert to binary

const mod = bitLen % 8;
if (mod > 0) {
bitLen += 8 - mod;
} else if (negative && first(bits) === "1" && bits.indexOf("1", 1) !== -1) {
bitLen += 8;
}

const mask = (1n << BigInt(bitLen)) - 1n; // create a mask

return (~value & mask) + 1n; // invert bits, mask it, and add one
}
49 changes: 37 additions & 12 deletions packages/core/src/serializer/sigma/constantSerializer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import {
collIntTestVectors,
collLongTestVectors,
collShortTestVectors,
sBigIntTestVectors,
sGroupElementTestVectors,
sIntTestVectors,
sLongTestVectors,
sNegativeBigIntTestVectors,
sPositiveBigIntTestVectors,
sSigmaPropTestVectors
} from "../../tests/testVectors/constantsTestVectors";
import { SConstant, SParse } from "./constantSerializer";
Expand Down Expand Up @@ -59,20 +58,12 @@ describe("Primary types serialization", () => {
}
});

it("Should serialize positive SBigInt", () => {
for (const tv of sPositiveBigIntTestVectors) {
it("Should serialize SBigInt", () => {
for (const tv of sBigIntTestVectors) {
expect(SConstant(SBigInt(tv.value))).toBe(tv.hex);
}
});

it("Should fail for negative SBigInt", () => {
for (const tv of sNegativeBigIntTestVectors) {
expect(() => {
SConstant(SBigInt(tv.value));
}).toThrow();
}
});

it("Should serialize SUnit", () => {
expect(SConstant(SUnit())).toBe("62");
});
Expand Down Expand Up @@ -220,11 +211,38 @@ describe("SColl deserialization", () => {
}
});

it("Should deserialize SBigInt", () => {
for (const tv of sBigIntTestVectors) {
expect(SParse<bigint>(tv.hex).toString()).toBe(tv.value);
}
});

it("Should deserialize 'Coll[SLong]'", () => {
for (const tv of collLongTestVectors) {
expect(SParse(tv.hex)).toEqual(tv.coll);
}
});

it("Should deserialize 'Coll[Coll[Byte]]'", () => {
const hex =
"1a0c4065653430323366366564303963313332326234363630376538633163663737653733653030353039613334343838306232663339616332643430623433376463046572676f0763617264616e6f3339666965744263636a48774c774b4c5339573131453641766d565a6e4e6938347042487854317a3946723978314b6b79424a686761646472317179733577356d76796665783572646c75683039393273766d3074747834643439346336767979346a3933336573707a6d776e6c343277633833763837736b6c71773979387266766a6366743973616433376c61747778677170637132747a36347a08000000003b9aca00080000000002faf080080000000000013880036572672c6173736574316a7935713561307670737475747135713664386367646d72643471753579656663646e6a677a40366331346131353637363364613936303962383065386638326363613436663630363330346630613864306363363665356565323234306336333165666166640800000000000ee48c";
const expected = [
"65653430323366366564303963313332326234363630376538633163663737653733653030353039613334343838306232663339616332643430623433376463",
"6572676f",
"63617264616e6f",
"39666965744263636a48774c774b4c5339573131453641766d565a6e4e6938347042487854317a3946723978314b6b79424a68",
"61646472317179733577356d76796665783572646c75683039393273766d3074747834643439346336767979346a3933336573707a6d776e6c343277633833763837736b6c71773979387266766a6366743973616433376c61747778677170637132747a36347a",
"000000003b9aca00",
"0000000002faf080",
"0000000000013880",
"657267",
"6173736574316a7935713561307670737475747135713664386367646d72643471753579656663646e6a677a",
"36633134613135363736336461393630396238306538663832636361343666363036333034663061386430636336366535656532323430633633316566616664",
"00000000000ee48c"
].map((x) => hexToBytes(x));

expect(SParse(hex)).toStrictEqual(expected);
});
});

describe("Serialize -> Parse roundtrip", () => {
Expand Down Expand Up @@ -281,6 +299,13 @@ describe("Serialize -> Parse roundtrip", () => {
}
});

it("Should roundtrip SBigInt", () => {
for (let i = 0; i < 1000; i++) {
const value = randomBigInt(-9_223_372_036_854_775_808_000n, 9_223_372_036_854_775_807_000n);
expect(SParse(SConstant(SBigInt(value)))).toBe(value);
}
});

it("Should roundtrip SGroupElement", () => {
for (const tv of sGroupElementTestVectors) {
expect(SParse(SConstant(SGroupElement(hexToBytes(tv.value))))).toEqual(hexToBytes(tv.value));
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/serializer/sigma/dataSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ export class DataSerializer {
return reader.readInt();
case SigmaTypeCode.Long:
return reader.readLong();
// case SigmaTypeCode.BigInt:
// break;
case SigmaTypeCode.BigInt:
return reader.readBigInt();
case SigmaTypeCode.GroupElement:
return reader.readBytes(GROUP_ELEMENT_LENGTH);
case SigmaTypeCode.SigmaProp: {
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/serializer/sigma/sigmaReader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HexString, isEmpty } from "@fleet-sdk/common";
import { hexToBytes } from "@noble/hashes/utils";
import { HexString, hexToBigInt, isEmpty } from "@fleet-sdk/common";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { readBigVLQ, readVLQ } from "../vlq";
import { zigZagDecode, zigZagDecodeBigInt } from "../zigZag";
import { SigmaTypeCode } from "./sigmaTypeCode";
Expand Down Expand Up @@ -76,4 +76,11 @@ export class SigmaReader {
public readLong(): bigint {
return zigZagDecodeBigInt(readBigVLQ(this));
}

public readBigInt(): bigint {
const len = readVLQ(this);
const hex = bytesToHex(this.readBytes(len));

return hexToBigInt(hex);
}
}
19 changes: 3 additions & 16 deletions packages/core/src/serializer/sigma/sigmaWriter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { _0n } from "@fleet-sdk/common";
import { bigIntToHex } from "@fleet-sdk/common";
import { bytesToHex } from "@noble/hashes/utils";
import { writeBigVLQ, writeVLQ } from "../vlq";
import { zigZagEncode, zigZagEncodeBigInt } from "../zigZag";
Expand Down Expand Up @@ -103,21 +103,8 @@ export class SigmaWriter {
return this;
}

public writeBigInt(number: bigint): SigmaWriter {
// todo: take a look at https://coolaj86.com/articles/convert-decimal-to-hex-with-js-bigints/
// and https://coolaj86.com/articles/convert-hex-to-decimal-with-js-bigints/
if (number < _0n) {
throw new Error("Negative BigInt values are not supported Fleet serializer.");
}

let hex = number.toString(16);
if (hex.length % 2) {
hex = "0" + hex;
} else if (Number.parseInt(hex.substring(0, 1), 16) >= 8) {
// maximum positive need to prepend 0 otherwise results in negative number
hex = "00" + hex;
}

public writeBigInt(value: bigint): SigmaWriter {
const hex = bigIntToHex(value);
this.writeVLQ(hex.length / 2);
this.writeHex(hex);

Expand Down
Loading

0 comments on commit 43540c1

Please sign in to comment.