From 55ebc4f0d91e57cb15ca8fa00e14a3e58d85e25b Mon Sep 17 00:00:00 2001 From: Jaco Date: Tue, 22 Aug 2023 20:08:30 +0300 Subject: [PATCH] Check sign flag for i{8, 16, 32, ...} conversions (#1867) * Check sign flag for i{8, 16, 32, ...} conversions * Ensure negative path is tested on i32 perf * CHANGELOG * Comment check on top-most signed bit * toBn test array * toNumber uses exposed toBn tests * Re-use tests in toBn * Re-use expectations inside bnToU8a * Remove explicit BE (swap to non-optimal reverse) * Adjust * Remove defunct "only BE" comment * typo * Use bnToU8a tests in biToU8a * Flatten tests * Additional tests * Additional tests --- CHANGELOG.md | 7 + packages/util/src/bi/toU8a.spec.ts | 35 ++-- packages/util/src/bi/toU8a.ts | 5 +- packages/util/src/bn/toU8a.spec.ts | 118 ++++++----- packages/util/src/u8a/toBigInt.spec.ts | 264 ++---------------------- packages/util/src/u8a/toBigInt.ts | 5 +- packages/util/src/u8a/toBn.spec.ts | 265 ++----------------------- packages/util/src/u8a/toBn.ts | 121 +++++------ packages/util/src/u8a/toNumber.spec.ts | 120 ++--------- packages/util/src/u8a/toNumber.ts | 48 +++-- 10 files changed, 219 insertions(+), 769 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b9b91ac8..189172b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## master + +Changes: + +- Fix `u8aTo{BigInt, Bn, Number}` for non-negative `i{8, 16, 32...}` inputs + + ## 12.4.1 Aug 17, 2023 Changes: diff --git a/packages/util/src/bi/toU8a.spec.ts b/packages/util/src/bi/toU8a.spec.ts index 1d07cc2ca..481de69bc 100644 --- a/packages/util/src/bi/toU8a.spec.ts +++ b/packages/util/src/bi/toU8a.spec.ts @@ -4,12 +4,29 @@ /// import { arrayRange } from '../array/index.js'; +import { TESTS } from '../bn/toU8a.spec.js'; import { perf } from '../test/index.js'; import { nToU8a } from './index.js'; const ptest = arrayRange(65536).map((v) => [v]); describe('nToU8a', (): void => { + describe('conversion tests', (): void => { + for (let i = 0, count = TESTS.length; i < count; i++) { + const [isLe, isNegative, numarr, strval] = TESTS[i]; + const bitLength = numarr.length * 8; + + it(`${i}: converts from ${strval} (bitLength=${bitLength}, isLe=${isLe}, isNegative=${isNegative})`, (): void => { + expect( + nToU8a( + BigInt(strval), + { bitLength, isLe, isNegative } + ) + ).toEqual(new Uint8Array(numarr)); + }); + } + }); + it('converts null values to 0x00', (): void => { expect( nToU8a(null) @@ -40,23 +57,5 @@ describe('nToU8a', (): void => { ).toEqual(new Uint8Array([0x56, 0x34, 0x12, 0x00])); }); - it('converts negative numbers', (): void => { - expect( - nToU8a(-1234n, { isNegative: true }) - ).toEqual(new Uint8Array([46, 251])); - }); - - it('converts negative numbers (BE)', (): void => { - expect( - nToU8a(-1234n, { isLe: false, isNegative: true }) - ).toEqual(new Uint8Array([251, 46])); - }); - - it('converts negative numbers (bitLength)', (): void => { - expect( - nToU8a(-1234n, { bitLength: 32, isNegative: true }) - ).toEqual(new Uint8Array([46, 251, 255, 255])); - }); - perf('nToU8a', 250000, ptest, nToU8a); }); diff --git a/packages/util/src/bi/toU8a.ts b/packages/util/src/bi/toU8a.ts index 6055d37a9..fd5e7173f 100644 --- a/packages/util/src/bi/toU8a.ts +++ b/packages/util/src/bi/toU8a.ts @@ -14,15 +14,16 @@ const NEG_MASK = BigInt(0xff); function toU8a (value: bigint, isLe: boolean, isNegative: boolean): Uint8Array { const arr: number[] = []; + const withSigned = isNegative && (value < _0n); - if (isNegative) { + if (withSigned) { value = (value + _1n) * -_1n; } while (value !== _0n) { const mod = value % DIV; const val = Number( - isNegative + withSigned ? mod ^ NEG_MASK : mod ); diff --git a/packages/util/src/bn/toU8a.spec.ts b/packages/util/src/bn/toU8a.spec.ts index 2d4882a47..63a31fdaf 100644 --- a/packages/util/src/bn/toU8a.spec.ts +++ b/packages/util/src/bn/toU8a.spec.ts @@ -9,7 +9,63 @@ import { BN, bnToU8a } from './index.js'; const ptest = arrayRange(65536).map((v) => [v]); +// eslint-disable-next-line jest/no-export +export const TESTS: [isLe: boolean, isNegative: boolean, numarr: number[], strval: string][] = [ + // LE, positive numbers + [true, false, [0x12], '18'], + [true, false, [0x12, 0x34], '13330'], + [true, false, [0x12, 0x34, 0x56], '5649426'], + [true, false, [0x12, 0x34, 0x56, 0x78], '2018915346'], + [true, false, [0x12, 0x34, 0x56, 0x78, 0x9a], '663443878930'], + [true, false, [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc], '207371629900818'], + [true, false, [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78], '159954953172672629770948536149615195154'], + // LE, positive numbers (w/ signed flag) + [true, true, [12], '12'], + [true, true, [210, 4], '1234'], + [true, true, [64, 226, 1], '123456'], + [true, true, [21, 205, 91, 7], '123456789'], + [true, true, [203, 36, 104, 12, 8], '34567890123'], + [true, true, [255, 159, 114, 78, 24, 9], '9999999999999'], + // LE, negative numbers + [true, true, [244], '-12'], + [true, true, [46, 251], '-1234'], + [true, true, [192, 29, 254], '-123456'], + [true, true, [255, 255, 255, 255], '-1'], + [true, true, [254, 255, 255, 255], '-2'], + [true, true, [235, 50, 164, 248], '-123456789'], + [true, true, [0, 0, 0, 128], '-2147483648'], + [true, true, [0, 0, 0, 240], '-268435456'], + [true, true, [65, 86, 129, 173, 254], '-5678999999'], + [true, true, [1, 96, 141, 177, 231, 246], '-9999999999999'], + [true, true, [1, 0, 156, 88, 76, 73, 31, 242], '-999999999999999999'], + // BE + [false, false, [0x12], '18'], + [false, false, [0x12, 0x34], '4660'], + [false, false, [0x12, 0x34, 0x56], '1193046'], + [false, false, [0x12, 0x34, 0x56, 0x78], '305419896'], + [false, true, [0xf2, 0x34, 0x56, 0x78], '-231451016'], + [false, false, [0x12, 0x34, 0x56, 0x78, 0x9a], '78187493530'], + [false, false, [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc], '20015998343868'], + [false, false, [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78], '24197857161011715162171839636988778104'] +]; + describe('bnToU8a', (): void => { + describe('conversion tests', (): void => { + for (let i = 0, count = TESTS.length; i < count; i++) { + const [isLe, isNegative, numarr, strval] = TESTS[i]; + const bitLength = numarr.length * 8; + + it(`${i}: converts from ${strval} (bitLength=${bitLength}, isLe=${isLe}, isNegative=${isNegative})`, (): void => { + expect( + bnToU8a( + new BN(strval), + { bitLength, isLe, isNegative } + ) + ).toEqual(new Uint8Array(numarr)); + }); + } + }); + it('converts null values to 0x00', (): void => { expect( bnToU8a(null) @@ -40,67 +96,5 @@ describe('bnToU8a', (): void => { ).toEqual(new Uint8Array([0x56, 0x34, 0x12, 0x00])); }); - describe('signed', (): void => { - it('converts negative numbers (BE)', (): void => { - expect( - bnToU8a(new BN(-1234), { isLe: false, isNegative: true }) - ).toEqual(new Uint8Array([251, 46])); - }); - - it('converts negative numbers (LE, i8)', (): void => { - expect( - bnToU8a(new BN(-12), { isNegative: true }) - ).toEqual(new Uint8Array([244])); - }); - - it('converts negative numbers (LE, i16)', (): void => { - expect( - bnToU8a(new BN(-1234), { isNegative: true }) - ).toEqual(new Uint8Array([46, 251])); - }); - - it('converts negative numbers (LE, i24)', (): void => { - expect( - bnToU8a(new BN(-123456), { isNegative: true }) - ).toEqual(new Uint8Array([192, 29, 254])); - }); - - it('converts negative numbers (LE, i32)', (): void => { - expect( - bnToU8a(new BN(-123456789), { isNegative: true }) - ).toEqual(new Uint8Array([235, 50, 164, 248])); - }); - - it('converts negative numbers (LE, i40)', (): void => { - expect( - bnToU8a(new BN(-5678999999), { isNegative: true }) - ).toEqual(new Uint8Array([65, 86, 129, 173, 254])); - }); - - it('converts negative numbers (LE, i48)', (): void => { - expect( - bnToU8a(new BN(-9999999999999), { isNegative: true }) - ).toEqual(new Uint8Array([1, 96, 141, 177, 231, 246])); - }); - - it('converts negative numbers (LE, i64)', (): void => { - expect( - bnToU8a(new BN('-999999999999999999'), { isNegative: true }) - ).toEqual(new Uint8Array([1, 0, 156, 88, 76, 73, 31, 242])); - }); - - it('converts negative numbers (LE, bitLength)', (): void => { - expect( - bnToU8a(new BN(-1234), { bitLength: 32, isNegative: true }) - ).toEqual(new Uint8Array([46, 251, 255, 255])); - }); - - it('converts negative numbers (LE, bitLength, check)', (): void => { - expect( - bnToU8a(new BN(-123456), { bitLength: 32, isNegative: true }) - ).toEqual(new Uint8Array([192, 29, 254, 255])); - }); - }); - perf('bnToU8a', 250000, ptest, bnToU8a); }); diff --git a/packages/util/src/u8a/toBigInt.spec.ts b/packages/util/src/u8a/toBigInt.spec.ts index e82213de6..8249e51a2 100644 --- a/packages/util/src/u8a/toBigInt.spec.ts +++ b/packages/util/src/u8a/toBigInt.spec.ts @@ -3,6 +3,7 @@ /// +import { TESTS } from '../bn/toU8a.spec.js'; import { perf } from '../test/index.js'; import { u8aToBigInt } from './index.js'; @@ -16,238 +17,41 @@ describe('u8aToBigInt', (): void => { ).toBe('3412'); }); - describe('LE', (): void => { - describe('unsigned', (): void => { - it('converts values (u8)', (): void => { + describe('conversion tests', (): void => { + for (let i = 0, count = TESTS.length; i < count; i++) { + const [isLe, isNegative, numarr, strval] = TESTS[i]; + + it(`${i}: creates ${strval} (bitLength=${numarr.length * 8}, isLe=${isLe}, isNegative=${isNegative})`, (): void => { expect( u8aToBigInt( - new Uint8Array([0x12]), - { isLe: true } - ).toString(16) - ).toBe('12'); - }); - - it('converts values (u16)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34]), - { isLe: true } - ).toString(16) - ).toBe('3412'); - }); - - it('converts values (u24)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34, 0x56]), - { isLe: true } - ).toString(16) - ).toBe('563412'); - }); - - it('converts values (u32)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34, 0x56, 0x78]), - { isLe: true } - ).toString(16) - ).toBe('78563412'); - }); - - it('converts values (i32)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0xf2, 0x34, 0x56, 0x78]), - { isLe: false, isNegative: true } - ).toString(16) - ).toBe('-dcba988'); - }); - - it('converts values (u40)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a]), - { isLe: true } - ).toString(16) - ).toBe('9a78563412'); - }); - - it('converts values (u48)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]), - { isLe: true } - ).toString(16) - ).toBe('bc9a78563412'); - }); - - it('converts values (u128)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]), - { isLe: true } - ).toString(16) - ).toBe('78563412785634127856341278563412'); + new Uint8Array(numarr), + { isLe, isNegative } + ).toString() + ).toBe(strval); }); + } + }); + describe('length tests', (): void => { + [true, false].forEach((isLe) => { for (let i = 1; i < 32; i++) { const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]; const tstr = tu8a.map((n) => n.toString(16)); - it(`converts values with length ${i}`, (): void => { + it(`converts values with bitLength=${i * 8}, isLe=${isLe}`, (): void => { expect( u8aToBigInt( new Uint8Array(tu8a.slice(0, i)), - { isLe: true } + { isLe } ).toString(16) - ).toBe(tstr.slice(0, i).reverse().join('')); + ).toBe( + isLe + ? tstr.slice(0, i).reverse().join('') + : tstr.slice(0, i).join('') + ); }); } }); - - describe('signed', (): void => { - it('converts values (i8)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([244]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-12'); - }); - - it('converts values (i16)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([46, 251]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-1234'); - }); - - it('converts values (i24)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([192, 29, 254]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-123456'); - }); - - it('converts values (i32)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([235, 50, 164, 248]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-123456789'); - }); - - it('converts values (i40)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([65, 86, 129, 173, 254]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-5678999999'); - }); - - it('converts values (i48)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([1, 96, 141, 177, 231, 246]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-9999999999999'); - }); - - it('converts values (i64)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([1, 0, 156, 88, 76, 73, 31, 242]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-999999999999999999'); - }); - }); - }); - - describe('BE', (): void => { - it('converts values (u8)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12]), - { isLe: false } - ).toString(16) - ).toBe('12'); - }); - - it('converts values (u16)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34]), - { isLe: false } - ).toString(16) - ).toBe('1234'); - }); - - it('converts values (u24)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34, 0x56]), - { isLe: false } - ).toString(16) - ).toBe('123456'); - }); - - it('converts values (u32)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34, 0x56, 0x78]), - { isLe: false } - ).toString(16) - ).toBe('12345678'); - }); - - it('converts values (u40)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a]), - { isLe: false } - ).toString(16) - ).toBe('123456789a'); - }); - - it('converts values (u48)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]), - { isLe: false } - ).toString(16) - ).toBe('123456789abc'); - }); - - it('converts values (u128)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]), - { isLe: false } - ).toString(16) - ).toBe('12345678123456781234567812345678'); - }); - - for (let i = 1; i < 32; i++) { - const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]; - const tstr = tu8a.map((n) => n.toString(16)); - - it(`converts values with length ${i}`, (): void => { - expect( - u8aToBigInt( - new Uint8Array(tu8a.slice(0, i)), - { isLe: false } - ).toString(16) - ).toBe(tstr.slice(0, i).join('')); - }); - } }); describe('empty creation', (): void => { @@ -288,26 +92,6 @@ describe('u8aToBigInt', (): void => { }); }); - describe('negative', (): void => { - it('handles negative numbers (little-endian)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([46, 251]), - { isLe: true, isNegative: true } - ) - ).toBe(-1234n); - }); - - it('handles negative numbers (big-endian)', (): void => { - expect( - u8aToBigInt( - new Uint8Array([251, 46]), - { isLe: false, isNegative: true } - ) - ).toBe(-1234n); - }); - }); - it('handles overflows correctly (little-endian)', (): void => { expect( u8aToBigInt( @@ -317,12 +101,8 @@ describe('u8aToBigInt', (): void => { ).toBe(256n); }); - perf('u8aToBigInt (i32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToBigInt(v, { isNegative: true })); - + perf('u8aToBigInt (i32)', 750_000, [[new Uint8Array([0x9c, 0x9c, 0x9c, 0x9c])]], (v: Uint8Array) => u8aToBigInt(v, { isNegative: true })); perf('u8aToBigInt (u32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt); perf('u8aToBigInt (u64)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt); perf('u8aToBigInt (u128)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt); - - // perf('BigInt (constructor)', 1_000_000, [[12345678]], (v: number) => BigInt(v).toString()); - // perf('BigInt (constructor -> string)', 1_000_000, [[12345678]], (v: number) => BigInt(v).toString()); }); diff --git a/packages/util/src/u8a/toBigInt.ts b/packages/util/src/u8a/toBigInt.ts index f25ea860e..fcccd5549 100644 --- a/packages/util/src/u8a/toBigInt.ts +++ b/packages/util/src/u8a/toBigInt.ts @@ -16,13 +16,14 @@ const U64_MAX = BigInt('0x10000000000000000'); * @summary Creates a BigInt from a Uint8Array object. */ export function u8aToBigInt (value: Uint8Array, { isLe = true, isNegative = false }: ToBnOptions = {}): bigint { - // BE is not the optimal path - LE is the SCALE default + // slice + reverse is expensive, however SCALE is LE by default so this is the path + // we are most interested in (the BE is added for the sake of being comprehensive) const u8a = isLe ? value : value.slice().reverse(); const count = u8a.length; - if (isNegative) { + if (isNegative && count && (u8a[count - 1] & 0x80)) { switch (count) { case 0: return BigInt(0); diff --git a/packages/util/src/u8a/toBn.spec.ts b/packages/util/src/u8a/toBn.spec.ts index 553d3fe13..36f9f113b 100644 --- a/packages/util/src/u8a/toBn.spec.ts +++ b/packages/util/src/u8a/toBn.spec.ts @@ -3,7 +3,7 @@ /// -// import { BN } from '../bn/index.js'; +import { TESTS } from '../bn/toU8a.spec.js'; import { perf } from '../test/index.js'; import { u8aToBn } from './index.js'; @@ -17,238 +17,41 @@ describe('u8aToBn', (): void => { ).toBe('3412'); }); - describe('LE', (): void => { - describe('unsigned', (): void => { - it('converts values (u8)', (): void => { + describe('conversion tests', (): void => { + for (let i = 0, count = TESTS.length; i < count; i++) { + const [isLe, isNegative, numarr, strval] = TESTS[i]; + + it(`${i}: creates ${strval} (bitLength=${numarr.length * 8}, isLe=${isLe}, isNegative=${isNegative})`, (): void => { expect( u8aToBn( - new Uint8Array([0x12]), - { isLe: true } - ).toString(16) - ).toBe('12'); - }); - - it('converts values (u16)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34]), - { isLe: true } - ).toString(16) - ).toBe('3412'); - }); - - it('converts values (u24)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34, 0x56]), - { isLe: true } - ).toString(16) - ).toBe('563412'); - }); - - it('converts values (u32)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34, 0x56, 0x78]), - { isLe: true } - ).toString(16) - ).toBe('78563412'); - }); - - it('converts values (u40)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a]), - { isLe: true } - ).toString(16) - ).toBe('9a78563412'); - }); - - it('converts values (u48)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]), - { isLe: true } - ).toString(16) - ).toBe('bc9a78563412'); - }); - - it('converts values (u128)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]), - { isLe: true } - ).toString(16) - ).toBe('78563412785634127856341278563412'); + new Uint8Array(numarr), + { isLe, isNegative } + ).toString() + ).toBe(strval); }); + } + }); + describe('length tests', (): void => { + [true, false].forEach((isLe) => { for (let i = 1; i < 32; i++) { const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]; const tstr = tu8a.map((n) => n.toString(16)); - it(`converts values with length ${i}`, (): void => { + it(`converts values with bitLength=${i * 8}, isLe=${isLe}`, (): void => { expect( u8aToBn( new Uint8Array(tu8a.slice(0, i)), - { isLe: true } + { isLe } ).toString(16) - ).toBe(tstr.slice(0, i).reverse().join('')); + ).toBe( + isLe + ? tstr.slice(0, i).reverse().join('') + : tstr.slice(0, i).join('') + ); }); } }); - - describe('signed', (): void => { - it('converts values (i8)', (): void => { - expect( - u8aToBn( - new Uint8Array([244]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-12'); - }); - - it('converts values (i16)', (): void => { - expect( - u8aToBn( - new Uint8Array([46, 251]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-1234'); - }); - - it('converts values (i24)', (): void => { - expect( - u8aToBn( - new Uint8Array([192, 29, 254]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-123456'); - }); - - it('converts values (i32)', (): void => { - expect( - u8aToBn( - new Uint8Array([235, 50, 164, 248]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-123456789'); - }); - - it('converts values (i40)', (): void => { - expect( - u8aToBn( - new Uint8Array([65, 86, 129, 173, 254]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-5678999999'); - }); - - it('converts values (i48)', (): void => { - expect( - u8aToBn( - new Uint8Array([1, 96, 141, 177, 231, 246]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-9999999999999'); - }); - - it('converts values (i64)', (): void => { - expect( - u8aToBn( - new Uint8Array([1, 0, 156, 88, 76, 73, 31, 242]), - { isLe: true, isNegative: true } - ).toString() - ).toBe('-999999999999999999'); - }); - }); - }); - - describe('BE', (): void => { - it('converts values (u8)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12]), - { isLe: false } - ).toString(16) - ).toBe('12'); - }); - - it('converts values (u16)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34]), - { isLe: false } - ).toString(16) - ).toBe('1234'); - }); - - it('converts values (u24)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34, 0x56]), - { isLe: false } - ).toString(16) - ).toBe('123456'); - }); - - it('converts values (u32)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34, 0x56, 0x78]), - { isLe: false } - ).toString(16) - ).toBe('12345678'); - }); - - it('converts values (i32)', (): void => { - expect( - u8aToBn( - new Uint8Array([0xf2, 0x34, 0x56, 0x78]), - { isLe: false, isNegative: true } - ).toString(16) - ).toBe('-dcba988'); - }); - - it('converts values (u40)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a]), - { isLe: false } - ).toString(16) - ).toBe('123456789a'); - }); - - it('converts values (u48)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]), - { isLe: false } - ).toString(16) - ).toBe('123456789abc'); - }); - - it('converts values (u128)', (): void => { - expect( - u8aToBn( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]), - { isLe: false } - ).toString(16) - ).toBe('12345678123456781234567812345678'); - }); - - for (let i = 1; i < 32; i++) { - const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]; - const tstr = tu8a.map((n) => n.toString(16)); - - it(`converts values with length ${i}`, (): void => { - expect( - u8aToBn( - new Uint8Array(tu8a.slice(0, i)), - { isLe: false } - ).toString(16) - ).toBe(tstr.slice(0, i).join('')); - }); - } }); describe('empty creation', (): void => { @@ -289,26 +92,6 @@ describe('u8aToBn', (): void => { }); }); - describe('negative', (): void => { - it('handles negative numbers (little-endian)', (): void => { - expect( - u8aToBn( - new Uint8Array([46, 251]), - { isLe: true, isNegative: true } - ).toNumber() - ).toBe(-1234); - }); - - it('handles negative numbers (big-endian)', (): void => { - expect( - u8aToBn( - new Uint8Array([251, 46]), - { isLe: false, isNegative: true } - ).toNumber() - ).toBe(-1234); - }); - }); - it('handles overflows correctly (little-endian)', (): void => { expect( u8aToBn( @@ -318,12 +101,8 @@ describe('u8aToBn', (): void => { ).toBe(256); }); - perf('u8aToBn (i32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToBn(v, { isNegative: true })); - + perf('u8aToBn (i32)', 750_000, [[new Uint8Array([0x9c, 0x9c, 0x9c, 0x9c])]], (v: Uint8Array) => u8aToBn(v, { isNegative: true })); perf('u8aToBn (u32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToBn); perf('u8aToBn (u64)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBn); perf('u8aToBn (u128)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBn); - - // perf('BN (constructor)', 1_000_000, [[12345678]], (v: number) => new BN(v)); - // perf('BN (constructor -> string)', 1_000_000, [[12345678]], (v: number) => new BN(v).toString()); }); diff --git a/packages/util/src/u8a/toBn.ts b/packages/util/src/u8a/toBn.ts index ed09f239a..518d80fff 100644 --- a/packages/util/src/u8a/toBn.ts +++ b/packages/util/src/u8a/toBn.ts @@ -24,104 +24,77 @@ import { BN } from '../bn/bn.js'; * ``` */ export function u8aToBn (value: Uint8Array, { isLe = true, isNegative = false }: ToBnOptions = {}): BN { - const count = value.length; + // slice + reverse is expensive, however SCALE is LE by default so this is the path + // we are most interested in (the BE is added for the sake of being comprehensive) + const u8a = isLe + ? value + : value.slice().reverse(); + const count = u8a.length; // shortcut for <= u48 values - in this case the manual conversion // here seems to be more efficient than passing the full array - if (isNegative) { - if (isLe) { - // Most common case i{8, 16, 32} default LE SCALE-encoded - // For <= 32, we also optimize the xor to a single op - switch (count) { - case 0: - return new BN(0); - - case 1: - return new BN(((value[0] ^ 0x0000_00ff) * -1) - 1); - - case 2: - return new BN((((value[0] + (value[1] << 8)) ^ 0x0000_ffff) * -1) - 1); - - case 3: - return new BN((((value[0] + (value[1] << 8) + (value[2] << 16)) ^ 0x00ff_ffff) * -1) - 1); - - case 4: - // for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to - // 32-bit, in the case where the top-most bit is set this yields a negative value - return new BN((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1); - - case 5: - return new BN(((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((value[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1); - - case 6: - return new BN(((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((value[4] + (value[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1); - - default: - return new BN(value, 'le').fromTwos(value.length * 8); - } - } - - if (count === 0) { - return new BN(0); - } else if (count > 6) { - return new BN(value, 'be').fromTwos(value.length * 8); - } - - let result = 0; - - for (let i = 0; i < count; i++) { - result = (result * 0x1_00) + (value[i] ^ 0xff); - } - - return new BN((result * -1) - 1); - } - - if (isLe) { - // Most common case - u{8, 16, 32} default LE SCALE-encoded - // - // There are some slight benefits in unrolling this specific loop, - // however it comes with diminishing returns since here the actual - // `new BN` does seem to take up the bulk of the time + if (isNegative && count && (u8a[count - 1] & 0x80)) { + // Most common case i{8, 16, 32} default LE SCALE-encoded + // For <= 32, we also optimize the xor to a single op switch (count) { case 0: return new BN(0); case 1: - return new BN(value[0]); + return new BN(((u8a[0] ^ 0x0000_00ff) * -1) - 1); case 2: - return new BN(value[0] + (value[1] << 8)); + return new BN((((u8a[0] + (u8a[1] << 8)) ^ 0x0000_ffff) * -1) - 1); case 3: - return new BN(value[0] + (value[1] << 8) + (value[2] << 16)); + return new BN((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16)) ^ 0x00ff_ffff) * -1) - 1); case 4: // for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to // 32-bit, in the case where the top-most bit is set this yields a negative value - return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)); + return new BN((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1); case 5: - return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8)) * 0x1_00_00_00)); + return new BN(((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((u8a[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1); case 6: - return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8) + (value[5] << 16)) * 0x1_00_00_00)); + return new BN(((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((u8a[4] + (u8a[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1); default: - return new BN(value, 'le'); + return new BN(u8a, 'le').fromTwos(count * 8); } } - if (count === 0) { - return new BN(0); - } else if (count > 6) { - return new BN(value, 'be'); + // Most common case - u{8, 16, 32} default LE SCALE-encoded + // + // There are some slight benefits in unrolling this specific loop, + // however it comes with diminishing returns since here the actual + // `new BN` does seem to take up the bulk of the time + switch (count) { + case 0: + return new BN(0); + + case 1: + return new BN(u8a[0]); + + case 2: + return new BN(u8a[0] + (u8a[1] << 8)); + + case 3: + return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16)); + + case 4: + // for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to + // 32-bit, in the case where the top-most bit is set this yields a negative value + return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)); + + case 5: + return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8)) * 0x1_00_00_00)); + + case 6: + return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8) + (u8a[5] << 16)) * 0x1_00_00_00)); + + default: + return new BN(u8a, 'le'); } - - let result = 0; - - for (let i = 0; i < count; i++) { - result = (result * 0x1_00) + value[i]; - } - - return new BN(result); } diff --git a/packages/util/src/u8a/toNumber.spec.ts b/packages/util/src/u8a/toNumber.spec.ts index 9946245b4..425f50c41 100644 --- a/packages/util/src/u8a/toNumber.spec.ts +++ b/packages/util/src/u8a/toNumber.spec.ts @@ -3,114 +3,26 @@ /// +import { TESTS } from '../bn/toU8a.spec.js'; import { perf } from '../test/index.js'; import { u8aToNumber } from './index.js'; +const TESTS_NUM = TESTS.filter(([,, numarr]) => numarr.length <= 6); + describe('u8aToNumber', (): void => { - describe('unsigned', (): void => { - it('converts values (u8)', (): void => { - expect( - u8aToNumber( - new Uint8Array([0x12]) - ) - ).toBe(18); - }); + describe('conversion tests', (): void => { + for (let i = 0, count = TESTS_NUM.length; i < count; i++) { + const [isLe, isNegative, numarr, strval] = TESTS_NUM[i]; - it('converts values (u16)', (): void => { - expect( - u8aToNumber( - new Uint8Array([0x12, 0x34]) - ) - ).toBe(13330); - }); - - it('converts values (u24)', (): void => { - expect( - u8aToNumber( - new Uint8Array([0x12, 0x34, 0x56]) - ) - ).toBe(5649426); - }); - - it('converts values (u32)', (): void => { - expect( - u8aToNumber( - new Uint8Array([0x12, 0x34, 0x56, 0x78]) - ) - ).toBe(2018915346); - }); - - it('converts values (u40)', (): void => { - expect( - u8aToNumber( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a]) - ) - ).toBe(663443878930); - }); - - it('converts values (u48)', (): void => { - expect( - u8aToNumber( - new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]) - ) - ).toBe(207371629900818); - }); - }); - - describe('signed', (): void => { - it('converts values (i8)', (): void => { - expect( - u8aToNumber( - new Uint8Array([244]), - { isNegative: true } - ) - ).toBe(-12); - }); - - it('converts values (i16)', (): void => { - expect( - u8aToNumber( - new Uint8Array([46, 251]), - { isNegative: true } - ) - ).toBe(-1234); - }); - - it('converts values (i24)', (): void => { - expect( - u8aToNumber( - new Uint8Array([192, 29, 254]), - { isNegative: true } - ) - ).toBe(-123456); - }); - - it('converts values (i32)', (): void => { - expect( - u8aToNumber( - new Uint8Array([235, 50, 164, 248]), - { isNegative: true } - ) - ).toBe(-123456789); - }); - - it('converts values (i40)', (): void => { - expect( - u8aToNumber( - new Uint8Array([65, 86, 129, 173, 254]), - { isNegative: true } - ) - ).toBe(-5678999999); - }); - - it('converts values (i48)', (): void => { - expect( - u8aToNumber( - new Uint8Array([1, 96, 141, 177, 231, 246]), - { isNegative: true } - ) - ).toBe(-9999999999999); - }); + it(`${i}: creates ${strval} (bitLength=${numarr.length * 8}, isLe=${isLe}, isNegative=${isNegative})`, (): void => { + expect( + u8aToNumber( + new Uint8Array(numarr), + { isLe, isNegative } + ).toString() + ).toBe(strval); + }); + } }); describe('empty creation', (): void => { @@ -132,6 +44,6 @@ describe('u8aToNumber', (): void => { }); }); - perf('u8aToNumber (i32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToNumber(v, { isNegative: true })); + perf('u8aToNumber (i32)', 750_000, [[new Uint8Array([0x9c, 0x9c, 0x9c, 0x9c])]], (v: Uint8Array) => u8aToNumber(v, { isNegative: true })); perf('u8aToNumber (u32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToNumber); }); diff --git a/packages/util/src/u8a/toNumber.ts b/packages/util/src/u8a/toNumber.ts index f8b48fbc3..b70f314b6 100644 --- a/packages/util/src/u8a/toNumber.ts +++ b/packages/util/src/u8a/toNumber.ts @@ -1,44 +1,48 @@ // Copyright 2017-2023 @polkadot/util authors & contributors // SPDX-License-Identifier: Apache-2.0 -interface ToNumberOptions { - /** - * @description Number is signed, apply two's complement - */ - isNegative?: boolean; -} +import type { ToBnOptions } from '../types.js'; /** * @name u8aToNumber - * @summary Creates a number from a Uint8Array object. This only operates on LE values as used in SCALE. + * @summary Creates a number from a Uint8Array object. */ -export function u8aToNumber (value: Uint8Array, { isNegative = false }: ToNumberOptions = {}): number { - const count = value.length; +export function u8aToNumber (value: Uint8Array, { isLe = true, isNegative = false }: ToBnOptions = {}): number { + // slice + reverse is expensive, however SCALE is LE by default so this is the path + // we are most interested in (the BE is added for the sake of being comprehensive) + const u8a = isLe + ? value + : value.slice().reverse(); + const count = u8a.length; - if (isNegative) { + // When the value is a i{8, 16, 24, 32, 40, 40} values and the top-most bit + // indicates a signed value, we use a two's complement conversion. If one of these + // flags are not set, we just do a normal unsigned conversion (the same shortcut + // applies in both the u8aTo{BigInt, Bn} conversions as well) + if (isNegative && count && (u8a[count - 1] & 0x80)) { switch (count) { case 0: return 0; case 1: - return (((value[0] ^ 0x0000_00ff) * -1) - 1); + return (((u8a[0] ^ 0x0000_00ff) * -1) - 1); case 2: - return ((((value[0] + (value[1] << 8)) ^ 0x0000_ffff) * -1) - 1); + return ((((u8a[0] + (u8a[1] << 8)) ^ 0x0000_ffff) * -1) - 1); case 3: - return ((((value[0] + (value[1] << 8) + (value[2] << 16)) ^ 0x00ff_ffff) * -1) - 1); + return ((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16)) ^ 0x00ff_ffff) * -1) - 1); case 4: // for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to // 32-bit, in the case where the top-most bit is set this yields a negative value - return ((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1); + return ((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1); case 5: - return (((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((value[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1); + return (((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((u8a[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1); case 6: - return (((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((value[4] + (value[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1); + return (((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((u8a[4] + (u8a[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1); default: throw new Error('Value more than 48-bits cannot be reliably converted'); @@ -50,24 +54,24 @@ export function u8aToNumber (value: Uint8Array, { isNegative = false }: ToNumber return 0; case 1: - return value[0]; + return u8a[0]; case 2: - return value[0] + (value[1] << 8); + return u8a[0] + (u8a[1] << 8); case 3: - return value[0] + (value[1] << 8) + (value[2] << 16); + return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16); case 4: // for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to // 32-bit, in the case where the top-most bit is set this yields a negative value - return value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00); + return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00); case 5: - return value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8)) * 0x1_00_00_00); + return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8)) * 0x1_00_00_00); case 6: - return value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8) + (value[5] << 16)) * 0x1_00_00_00); + return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8) + (u8a[5] << 16)) * 0x1_00_00_00); default: throw new Error('Value more than 48-bits cannot be reliably converted');