From 5cefe245affc55312abbf7e4547aba20b62634b6 Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Sun, 31 Jul 2022 23:00:17 -0400 Subject: [PATCH] *Use custom json flatten algorithm to ensure deterministic hashing --- package.json | 1 - src/rpcQuery.ts | 11 ++++++----- src/util.ts | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 src/util.ts diff --git a/package.json b/package.json index 7c32f40..7dccfeb 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ }, "dependencies": { "@hyperswarm/dht": "^6.0.1", - "json-stable-stringify": "^1.0.1", "libskynet": "^0.0.61", "msgpackr": "^1.6.1" } diff --git a/src/rpcQuery.ts b/src/rpcQuery.ts index 97feddd..6f3ca9f 100644 --- a/src/rpcQuery.ts +++ b/src/rpcQuery.ts @@ -4,8 +4,7 @@ import { pack, unpack } from "msgpackr"; import { RPCRequest, RPCResponse } from "./types"; import { Buffer } from "buffer"; import { blake2b } from "libskynet"; - -import jsonStringify from "json-stable-stringify"; +import { flatten } from "./util.js"; export default class RpcQuery { private _network: RpcNetwork; @@ -100,22 +99,24 @@ export default class RpcQuery { private checkResponses() { const responseStore = this._responses; - const responseStoreData = Object.values(responseStore); type ResponseGroup = { [response: string]: number }; const responseObjects = responseStoreData.reduce((output: any, item) => { + const itemFlattened = flatten(item?.data).sort(); + const hash = Buffer.from( - blake2b(Buffer.from(jsonStringify(item?.data))) + blake2b(Buffer.from(JSON.stringify(itemFlattened))) ).toString("hex"); output[hash] = item?.data; return output; }, {}); const responses: ResponseGroup = responseStoreData.reduce( (output: ResponseGroup, item) => { + const itemFlattened = flatten(item?.data).sort(); const hash = Buffer.from( - blake2b(Buffer.from(jsonStringify(item?.data))) + blake2b(Buffer.from(JSON.stringify(itemFlattened))) ).toString("hex"); output[hash] = output[hash] ?? 0; output[hash]++; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..4a10b97 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,51 @@ +function isBuffer(obj: any): boolean { + return ( + obj && + obj.constructor && + typeof obj.constructor.isBuffer === "function" && + obj.constructor.isBuffer(obj) + ); +} +/* +Forked from https://github.com/hughsk/flat + */ +export function flatten(target: any, opts: any = {}): any[] { + opts = opts || {}; + + const delimiter = opts.delimiter || "."; + const maxDepth = opts.maxDepth; + const transformKey = + opts.transformKey || ((key: any) => (isNaN(parseInt(key)) ? key : "")); + const output: any[] = []; + + function step(object: any, prev?: any, currentDepth?: any) { + currentDepth = currentDepth || 1; + Object.keys(object).forEach(function (key) { + const value = object[key]; + const isarray = opts.safe && Array.isArray(value); + const type = Object.prototype.toString.call(value); + const isbuffer = isBuffer(value); + const isobject = type === "[object Object]" || type === "[object Array]"; + + const newKey = prev + ? prev + delimiter + transformKey(key) + : transformKey(key); + + if ( + !isarray && + !isbuffer && + isobject && + Object.keys(value).length && + (!opts.maxDepth || currentDepth < maxDepth) + ) { + return step(value, newKey, currentDepth + 1); + } + + output.push(`${newKey}=${value}`); + }); + } + + step(target); + + return output; +}