From d69b20689bdc700ad41af72ef2400e76abf75623 Mon Sep 17 00:00:00 2001 From: jeffsmale90 <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 1 Apr 2022 10:19:22 +1300 Subject: [PATCH] fix: correctly pad / truncate in JSON-RPC types, add tests to ethereum-address and json-rpc-data (#2716) fixes #1594 --- .../ethereum/address/tests/index.test.ts | 25 ++- .../things/json-rpc/json-rpc-base-types.ts | 1 + .../src/things/json-rpc/json-rpc-data.ts | 31 ++-- .../utils/tests/json-rpc-data.test.ts | 174 ++++++++++++++++++ 4 files changed, 213 insertions(+), 18 deletions(-) create mode 100644 src/packages/utils/tests/json-rpc-data.test.ts diff --git a/src/chains/ethereum/address/tests/index.test.ts b/src/chains/ethereum/address/tests/index.test.ts index d2b19a28e3..0251283e3a 100644 --- a/src/chains/ethereum/address/tests/index.test.ts +++ b/src/chains/ethereum/address/tests/index.test.ts @@ -1,6 +1,27 @@ import assert from "assert"; -import ethereumAddress from "../"; +import {Address} from "../"; describe("@ganache/ethereum-address", () => { - it("needs tests"); + describe("toString()", () => { + it("should pad an address to 20 bytes", () => { + const address = new Address("0x1"); + const stringifiedAddress = address.toString(); + + assert.equal(stringifiedAddress, "0x0000000000000000000000000000000000000001"); + }); + + it("should truncate an address to the specified length", () => { + const address = new Address("0x1"); + const stringifiedAddress = address.toString(1); + + assert.equal(stringifiedAddress, "0x01"); + }); + + it("should stringify a 20 byte address string", () => { + const address = new Address("0x2104859394604359378433865360947116707876"); + const stringifiedAddress = address.toString(); + + assert.equal(stringifiedAddress, "0x2104859394604359378433865360947116707876"); + }); + }); }); diff --git a/src/packages/utils/src/things/json-rpc/json-rpc-base-types.ts b/src/packages/utils/src/things/json-rpc/json-rpc-base-types.ts index 1e91935e98..33963cf5f1 100644 --- a/src/packages/utils/src/things/json-rpc/json-rpc-base-types.ts +++ b/src/packages/utils/src/things/json-rpc/json-rpc-base-types.ts @@ -51,6 +51,7 @@ export class BaseJsonRpcType< case "string": { // handle hex-encoded string if ((value as string).indexOf("0x") === 0) { + toStrings.set(this, () => (value as string).toLowerCase().slice(2)); strCache.set(this, (value as string).toLowerCase()); toBuffers.set(this, () => { let fixedValue = (value as string).slice(2); diff --git a/src/packages/utils/src/things/json-rpc/json-rpc-data.ts b/src/packages/utils/src/things/json-rpc/json-rpc-data.ts index e0c06a4001..6ff153343f 100644 --- a/src/packages/utils/src/things/json-rpc/json-rpc-data.ts +++ b/src/packages/utils/src/things/json-rpc/json-rpc-data.ts @@ -2,40 +2,39 @@ import { BaseJsonRpcType } from "./json-rpc-base-types"; import { strCache, toStrings } from "./json-rpc-base-types"; function validateByteLength(byteLength?: number) { - if (typeof byteLength !== "number" || byteLength < 0) { - throw new Error(`byteLength must be a number greater than 0`); + if (typeof byteLength !== "number" || !(byteLength >= 0)) { + throw new Error(`byteLength must be a number greater than or equal to 0, provided: ${byteLength}`); } } -const byteLengths = new WeakMap(); + export class Data extends BaseJsonRpcType { - constructor(value: string | Buffer, byteLength?: number) { + + constructor(value: string | Buffer, private _byteLength?: number) { + super(value); if (typeof value === "bigint") { throw new Error(`Cannot create a ${typeof value} as a Data`); } - super(value); - if (byteLength !== void 0) { - validateByteLength(byteLength); - byteLengths.set(this, byteLength | 0); + if (_byteLength !== undefined) { + validateByteLength(_byteLength); } } public toString(byteLength?: number): string { - const str = strCache.get(this) as string; - if (str !== void 0) { - return str; + if (byteLength === undefined) { + byteLength = this._byteLength; + } + if (byteLength === undefined && strCache.has(this)) { + return strCache.get(this) as string; } else { let str = toStrings.get(this)() as string; let length = str.length; + if (length % 2 === 1) { length++; str = `0${str}`; } - if (byteLength !== void 0) { + if (byteLength !== undefined) { validateByteLength(byteLength); - } else { - byteLength = byteLengths.get(this); - } - if (byteLength !== void 0) { const strLength = byteLength * 2; const padBy = strLength - length; if (padBy < 0) { diff --git a/src/packages/utils/tests/json-rpc-data.test.ts b/src/packages/utils/tests/json-rpc-data.test.ts new file mode 100644 index 0000000000..4f50f736c8 --- /dev/null +++ b/src/packages/utils/tests/json-rpc-data.test.ts @@ -0,0 +1,174 @@ +import assert from "assert"; +import {Data} from ".."; + +describe("json-rpc-data", () => { + const inputOf32Bytes = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + const validValues = [ "0x", "0x1", "0x1234", Buffer.from([]), Buffer.from([0x12,0x34]), inputOf32Bytes ]; + const invalidValues: any[] = [ "1234", 1234n, NaN/*, undefined, null, 1234, [], {}, "0x-1234"*/ ]; // todo: this should be addressed in rewrite of json-rpc-data + // See related https://github.com/trufflesuite/ganache/labels/json-rpc%20refactor + const validBytelengths = (() => { let i = 0; return [...new Array(100)].map(_ => i++); })(); // [0...99] + const invalidBytelengths: any[] = [ -1, "1", {}, [], null, NaN ]; + + function getExpectedString(value: Buffer|string, bytelength?: number) { + let expected: string; + + if (typeof value === "string") { + expected = value.slice(2); + } else if (Buffer.isBuffer(value)) { + expected = (value).toString("hex"); + } else { + throw new Error(`Type not supported ${typeof value}`) + } + if (bytelength !== undefined) { + /* + If the value is longer than the specified bytelength, then the result must be left padded. + ie: "0x01" bytelength 2 becomes "0x0001" + + If the value is longer than the specified bytelength, then the result must be truncated. + ie: "0x123456" bytelength 2 becomes "0x1234" + */ + const padCharCount = (bytelength - expected.length / 2) * 2; // (desired byte count - actual byte count) * 2 characters per byte + if (padCharCount > 0) { + expected = "0".repeat(padCharCount) + expected; + } else { + expected = expected.slice(0, bytelength * 2); + } + } + return "0x" + expected; + } + + describe("constructor", () => { + it("should accept different values", () => { + validValues.forEach(value => { + const d = new Data(value); + }); + }); + + it("should fail with invalid values", () => { + invalidValues.forEach(value => { + assert.throws(() => { + const d = new Data(value); + }, undefined, `Should fail to accept value: ${value} of type: ${typeof value}`); + }); + }); + + it("should accept valid bytelengths", () => { + validBytelengths.forEach(bytelength => { + const d = new Data("0x01", bytelength); + }); + }); + + it("should fail with invalid bytelengths", () => { + invalidBytelengths.forEach(bytelength => { + assert.throws(() => { + const d = new Data("0x01", bytelength); + }, undefined, `Should fail to accept bytelength: ${bytelength} of type: ${typeof bytelength}`); + }); + }); + }); + + describe("from()", () => { + it("should accept different representations of value", () => { + validValues.forEach(value => { + const d = Data.from(value); + }); + }); + + it("should fail with invalid values", () => { + invalidValues.forEach(value => { + assert.throws(() => { + const d = Data.from(value); + }, undefined, `Should fail to accept value: ${value} of type: ${typeof value}`); + }); + }); + + it("should accept valid bytelengths", () => { + validBytelengths.forEach(bytelength => { + const d = Data.from("0x01", bytelength); + }); + }); + + it("should fail with invalid bytelengths", () => { + invalidBytelengths.forEach(bytelength => { + assert.throws(() => { + const d = Data.from("0x01", bytelength); + }, undefined, `Should fail to accept bytelength: ${bytelength} of type: ${typeof bytelength}`); + }); + }); + }); + + describe("toString()", () => { + it("should stringify without arguments", () => { + validValues.forEach(value => { + const d = new Data(value); + const s = d.toString(); + const expected = getExpectedString(value); + + assert.equal(s, expected); + }); + }); + + it("should stringify with valid bytelengths", () => { + validValues.forEach(value => { + const d = new Data(value); + validBytelengths.forEach(bytelength => { + const s = d.toString(bytelength); + const expected = getExpectedString(s, bytelength); + + assert.equal(s, expected); + }); + }); + }); + + it("should fail with invalid bytelengths", () => { + validValues.forEach(value => { + const d = new Data(value); + invalidBytelengths.forEach(bytelength => { + assert.throws(() => { + const s = d.toString(bytelength); + }, undefined, `Should fail to accept bytelength: ${bytelength} of type: ${typeof bytelength}`); + }); + }); + }); + + it("should stringify with valid bytelengths provided to constructor", () => { + validValues.forEach(value => { + validBytelengths.forEach(bytelength => { + const d = new Data(value, bytelength); + const s = d.toString(); + const expected = getExpectedString(s, bytelength); + + assert.equal(s, expected); + }); + }); + }); + }); + + describe("toBuffer()", () => { + it("should create a buffer", () => { + const expected = Buffer.from([0x12, 0x34]); + const d = new Data("0x1234"); + const b = d.toBuffer(); + + assert.deepEqual(b, expected); + }); + + // todo: this should be addressed in rewrite of json-rpc-data https://github.com/trufflesuite/ganache/labels/json-rpc%20refactor + it.skip("should create a buffer with a smaller bytelength", () => { + const expected = Buffer.from([0x12, 0x34]); + const d = new Data("0x123456789abcdef", 2); + const b = d.toBuffer(); + + assert.deepEqual(b, expected); + }); + + // todo: this should be addressed in rewrite of json-rpc-data https://github.com/trufflesuite/ganache/labels/json-rpc%20refactor + it.skip("should create a buffer with a larger bytelength", () => { + const expected = Buffer.from([0x00, 0x00, 0x12, 0x34]); + const d = new Data("0x1234", 4); + const b = d.toBuffer(); + + assert.deepEqual(b, expected); + }); + }); +});