diff --git a/.travis.yml b/.travis.yml index 984811d..6f6afc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ language: node_js node_js: - - "6" - "8" - "9" - "10" + - "11" + - "12" env: - CXX=g++-4.8 services: diff --git a/package.json b/package.json index 63d7ace..c268448 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "level-ws": "^1.0.0", "readable-stream": "^3.0.6", "rlp": "^2.2.3", - "semaphore": ">=1.0.1" + "semaphore": ">=1.0.1", + "util.promisify": "^1.0.0" }, "devDependencies": { "@ethereumjs/config-nyc": "^1.1.1", diff --git a/src/multiproof.ts b/src/multiproof.ts new file mode 100644 index 0000000..0b25fd7 --- /dev/null +++ b/src/multiproof.ts @@ -0,0 +1,405 @@ +import * as assert from 'assert' +import { decode, encode } from 'rlp' +import { keccak256 } from 'ethereumjs-util' +import { Trie } from './baseTrie' +import { BranchNode, ExtensionNode, LeafNode, EmbeddedNode, decodeRawNode } from './trieNode' +import { stringToNibbles, nibblesToBuffer, matchingNibbleLength } from './util/nibbles' +import { addHexPrefix, removeHexPrefix } from './util/hex' +const promisify = require('util.promisify') + +export enum Opcode { + Branch = 0, + Hasher = 1, + Leaf = 2, + Extension = 3, + Add = 4, +} + +export enum NodeType { + Branch = 0, + Leaf = 1, + Extension = 2, + Hash = 3, +} + +export interface Instruction { + kind: Opcode + value?: number | number[] +} + +export interface Multiproof { + hashes: Buffer[] + keyvals: Buffer[] + instructions: Instruction[] +} + +export function verifyMultiproof(root: Buffer, proof: Multiproof, keys: Buffer[]): boolean { + const stack: any[] = [] + + const leaves = proof.keyvals.map((l: Buffer) => decode(l)) + assert(leaves.length === keys.length) + let leafIdx = 0 + let hashIdx = 0 + const paths = new Array(leaves.length).fill(undefined) + + for (const instr of proof.instructions) { + if (instr.kind === Opcode.Hasher) { + const h = proof.hashes[hashIdx++] + if (!h) { + throw new Error('Not enough hashes in multiproof') + } + stack.push([NodeType.Hash, [h], []]) + } else if (instr.kind === Opcode.Leaf) { + const l = leaves[leafIdx++] + if (!l) { + throw new Error('Expected leaf in multiproof') + } + // TODO: Nibble from prefix `digit` + // @ts-ignore + //stack.push([NodeType.Leaf, [l[0].slice(l[0].length - instr.value), l[1]]]) + // Disregard leaf operand + stack.push([NodeType.Leaf, [l[0], l[1]], [leafIdx - 1]]) + // @ts-ignore + paths[leafIdx - 1] = removeHexPrefix(stringToNibbles(l[0])) + } else if (instr.kind === Opcode.Branch) { + const n = stack.pop() + if (!n) { + throw new Error('Stack underflow') + } + const children = new Array(16).fill(null) + children[instr.value as number] = n + stack.push([NodeType.Branch, children, n[2].slice()]) + for (let i = 0; i < n[2].length; i++) { + paths[n[2][i]] = [instr.value as number, ...paths[n[2][i]]] + } + } else if (instr.kind === Opcode.Extension) { + const n = stack.pop() + if (!n) { + throw new Error('Stack underflow') + } + stack.push([NodeType.Extension, [instr.value, n], n[2].slice()]) + for (let i = 0; i < n[2].length; i++) { + paths[n[2][i]] = [...(instr.value as number[]), ...paths[n[2][i]]] + } + } else if (instr.kind === Opcode.Add) { + const n1 = stack.pop() + const n2 = stack.pop() + if (!n1 || !n2) { + throw new Error('Stack underflow') + } + assert(n2[0] === NodeType.Branch, 'expected branch node on stack') + assert((instr.value as number) < 17) + n2[1][instr.value as number] = n1 + n2[2] = Array.from(new Set([...n1[2], ...n2[2]])) + stack.push(n2) + for (let i = 0; i < n1[2].length; i++) { + paths[n1[2][i]] = [instr.value as number, ...paths[n1[2][i]]] + } + } else { + throw new Error('Invalid opcode') + } + } + + const r = stack.pop() + if (!r) { + throw new Error('Expected root node on top of stack') + } + let h = hashTrie(r) + // Special case, if trie contains only one leaf + // and that leaf has length < 32 + if (h.length < 32) { + h = keccak256(encode(h)) + } + + // Assuming sorted keys + for (let i = 0; i < paths.length; i++) { + const addr = nibblesToBuffer(paths[i]) + assert(addr.equals(keys[i])) + } + return h.equals(root) +} + +function hashTrie(node: any): Buffer { + const typ = node[0] + node = node[1] + if (typ === NodeType.Branch) { + const res = new Array(17).fill(Buffer.alloc(0)) + for (let i = 0; i < 16; i++) { + if (node[i] === null) { + continue + } + res[i] = hashTrie(node[i]) + } + const e = encode(res) + if (e.length > 32) { + return keccak256(e) + } else { + return e + } + } else if (typ === NodeType.Leaf) { + const e = encode(node) + if (e.length > 32) { + return keccak256(e) + } else { + return node + } + } else if (typ === NodeType.Hash) { + // TODO: What if it's an embedded node with length === 32? + // Maybe try decoding and if it fails assume it's a hash + if (node[0].length < 32) { + // Embedded node, decode to get correct serialization for parent node + return decode(node[0]) + } + return node[0] + } else if (typ === NodeType.Extension) { + const hashedNode = hashTrie(node[1]) + node = [nibblesToBuffer(addHexPrefix(node[0], false)), hashedNode] + const e = encode(node) + if (e.length > 32) { + return keccak256(e) + } else { + return e + } + } else { + throw new Error('Invalid node') + } +} + +export async function makeMultiproof(trie: Trie, keys: Buffer[]): Promise { + if (keys.length === 0) { + return { + hashes: [trie.root], + keyvals: [], + instructions: [{ kind: Opcode.Hasher }], + } + } + + const keysNibbles = [] + for (const k of keys) { + keysNibbles.push(stringToNibbles(k)) + } + + return _makeMultiproof(trie, trie.root, keysNibbles) +} + +async function _makeMultiproof( + trie: Trie, + rootHash: EmbeddedNode, + keys: number[][], +): Promise { + let proof: Multiproof = { + hashes: [], + keyvals: [], + instructions: [], + } + + let root + if (Buffer.isBuffer(rootHash)) { + root = await promisify(trie._lookupNode.bind(trie))(rootHash) + } else if (Array.isArray(rootHash)) { + // Embedded node + root = decodeRawNode(rootHash) + } else { + throw new Error('Unexpected root') + } + + if (root instanceof BranchNode) { + // Truncate first nibble of keys + const table = new Array(16).fill(undefined) + // Group target keys based by their first nibbles. + // Also implicitly sorts the keys. + for (const k of keys) { + const idx = k[0] + if (!table[idx]) table[idx] = [] + table[idx].push(k.slice(1)) + } + + let addBranchOp = true + for (let i = 0; i < 16; i++) { + if (table[i] === undefined) { + // Empty subtree, hash it and add a HASHER op + const child = root.getBranch(i) + if (child) { + proof.instructions.push({ kind: Opcode.Hasher }) + // TODO: Make sure child is a hash + // what to do if embedded? + if (Buffer.isBuffer(child)) { + proof.hashes.push(child) + } else if (Array.isArray(child)) { + proof.hashes.push(encode(child)) + } else { + throw new Error('Invalid branch child') + } + if (addBranchOp) { + proof.instructions.push({ kind: Opcode.Branch, value: i }) + addBranchOp = false + } else { + proof.instructions.push({ kind: Opcode.Add, value: i }) + } + } + } else { + const child = root.getBranch(i) as Buffer + if (!child) { + throw new Error('Key not in trie') + } + const p = await _makeMultiproof(trie, child, table[i]) + proof.hashes.push(...p.hashes) + proof.keyvals.push(...p.keyvals) + proof.instructions.push(...p.instructions) + + if (addBranchOp) { + proof.instructions.push({ kind: Opcode.Branch, value: i }) + addBranchOp = false + } else { + proof.instructions.push({ kind: Opcode.Add, value: i }) + } + } + } + } else if (root instanceof ExtensionNode) { + const extkey = root.key + // Make sure all keys follow the extension node + // and truncate them. + for (let i = 0; i < keys.length; i++) { + const k = keys[i] + if (matchingNibbleLength(k, extkey) !== extkey.length) { + // TODO: Maybe allow proving non-existent keys + throw new Error('Key not in trie') + } + keys[i] = k.slice(extkey.length) + } + const p = await _makeMultiproof(trie, root.value, keys) + proof.hashes.push(...p.hashes) + proof.keyvals.push(...p.keyvals) + proof.instructions.push(...p.instructions) + proof.instructions.push({ kind: Opcode.Extension, value: extkey }) + } else if (root instanceof LeafNode) { + if (keys.length !== 1) { + throw new Error('Expected 1 remaining key') + } + if (matchingNibbleLength(keys[0], root.key) !== root.key.length) { + throw new Error("Leaf key doesn't match target key") + } + // TODO: Check key matches leaf's key + proof = { + hashes: [], + keyvals: [root.serialize()], + instructions: [{ kind: Opcode.Leaf }], + } + } else { + throw new Error('Unexpected node type') + } + + return proof +} + +export function decodeMultiproof(raw: Buffer): Multiproof { + const dec = decode(raw) + assert(dec.length === 3) + + return { + // @ts-ignore + hashes: dec[0], + // @ts-ignore + keyvals: dec[1], + // @ts-ignore + instructions: decodeInstructions(dec[2]), + } +} + +export function encodeMultiproof(proof: Multiproof, flatInstructions: boolean = false): Buffer { + return encode(rawMultiproof(proof, flatInstructions)) +} + +export function rawMultiproof(proof: Multiproof, flatInstructions: boolean = false): any { + if (flatInstructions) { + return [proof.hashes, proof.keyvals, flatEncodeInstructions(proof.instructions)] + } else { + return [ + proof.hashes, + proof.keyvals, + proof.instructions.map(i => { + if (i.value !== undefined) return [i.kind, i.value] + return [i.kind] + }), + ] + } +} + +export function flatEncodeInstructions(instructions: Instruction[]): Buffer { + const res: number[] = [] + for (const instr of instructions) { + res.push(instr.kind) + if (instr.kind === Opcode.Branch || instr.kind === Opcode.Add) { + res.push(instr.value as number) + } else if (instr.kind === Opcode.Extension) { + const nibbles = instr.value as number[] + res.push(nibbles.length) + res.push(...nibbles) + } + } + return Buffer.from(new Uint8Array(res)) +} + +export function flatDecodeInstructions(raw: Buffer): Instruction[] { + const res = [] + let i = 0 + while (i < raw.length) { + const op = raw[i++] + switch (op) { + case Opcode.Branch: + res.push({ kind: Opcode.Branch, value: raw[i++] }) + break + case Opcode.Hasher: + res.push({ kind: Opcode.Hasher }) + break + case Opcode.Leaf: + res.push({ kind: Opcode.Leaf }) + break + case Opcode.Extension: + const length = raw.readUInt8(i++) + const nibbles = [] + for (let j = 0; j < length; j++) { + nibbles.push(raw[i++]) + } + res.push({ kind: Opcode.Extension, value: nibbles }) + break + case Opcode.Add: + res.push({ kind: Opcode.Add, value: raw[i++] }) + break + } + } + return res +} + +export function decodeInstructions(instructions: Buffer[][]) { + const res = [] + for (const op of instructions) { + switch (bufToU8(op[0])) { + case Opcode.Branch: + res.push({ kind: Opcode.Branch, value: bufToU8(op[1]) }) + break + case Opcode.Hasher: + res.push({ kind: Opcode.Hasher }) + break + case Opcode.Leaf: + res.push({ kind: Opcode.Leaf }) + break + case Opcode.Extension: + // @ts-ignore + res.push({ kind: Opcode.Extension, value: op[1].map(v => bufToU8(v)) }) + break + case Opcode.Add: + res.push({ kind: Opcode.Add, value: bufToU8(op[1]) }) + break + } + } + return res +} + +function bufToU8(b: Buffer): number { + // RLP decoding of 0 is empty buffer + if (b.length === 0) { + return 0 + } + return b.readUInt8(0) +} diff --git a/src/util/hex.ts b/src/util/hex.ts index 17aa25a..c594db0 100644 --- a/src/util/hex.ts +++ b/src/util/hex.ts @@ -5,20 +5,21 @@ * @returns {Array} - returns buffer of encoded data **/ export function addHexPrefix(key: number[], terminator: boolean): number[] { + const res = key.slice() // odd - if (key.length % 2) { - key.unshift(1) + if (res.length % 2) { + res.unshift(1) } else { // even - key.unshift(0) - key.unshift(0) + res.unshift(0) + res.unshift(0) } if (terminator) { - key[0] += 2 + res[0] += 2 } - return key + return res } /** @@ -28,13 +29,15 @@ export function addHexPrefix(key: number[], terminator: boolean): number[] { * @private */ export function removeHexPrefix(val: number[]): number[] { - if (val[0] % 2) { - val = val.slice(1) + let res = val.slice() + + if (res[0] % 2) { + res = val.slice(1) } else { - val = val.slice(2) + res = val.slice(2) } - return val + return res } /** diff --git a/test/multiproof.js b/test/multiproof.js new file mode 100644 index 0000000..095202d --- /dev/null +++ b/test/multiproof.js @@ -0,0 +1,304 @@ +const tape = require('tape') +const promisify = require('util.promisify') +const rlp = require('rlp') +const { keccak256 } = require('ethereumjs-util') +const { Trie } = require('../dist/baseTrie') +const { SecureTrie } = require('../dist/secure') +const { LeafNode } = require('../dist/trieNode') +const { stringToNibbles } = require('../dist/util/nibbles') +const { + decodeMultiproof, rawMultiproof, encodeMultiproof, + decodeInstructions, flatEncodeInstructions, flatDecodeInstructions, + verifyMultiproof, makeMultiproof, Instruction, Opcode +} = require('../dist/multiproof') + +tape('decode and encode instructions', (t) => { + t.test('rlp encoding', (st) => { + const raw = Buffer.from('d0c20201c20405c603c403030303c28006', 'hex') + const expected = [ + { kind: Opcode.Leaf }, + { kind: Opcode.Add, value: 5 }, + { kind: Opcode.Extension, value: [3, 3, 3, 3] }, + { kind: Opcode.Branch, value: 6 } + ] + const res = decodeInstructions(rlp.decode(raw)) + st.deepEqual(expected, res) + st.end() + }) + + t.test('flat encoding', (st) => { + const raw = Buffer.from('0204050304030303030006', 'hex') + const instructions = [ + { kind: Opcode.Leaf }, + { kind: Opcode.Add, value: 5 }, + { kind: Opcode.Extension, value: [3, 3, 3, 3] }, + { kind: Opcode.Branch, value: 6 } + ] + const encoded = flatEncodeInstructions(instructions) + st.assert(raw.equals(encoded)) + const decoded = flatDecodeInstructions(raw) + st.deepEqual(instructions, decoded) + st.end() + }) +}) + +tape('decode and encode multiproof', (t) => { + t.test('decode and encode one leaf', (st) => { + const raw = Buffer.from('eae1a00101010101010101010101010101010101010101010101010101010101010101c483c20102c2c102', 'hex') + const expected = { + hashes: [Buffer.alloc(32, 1)], + instructions: [{ kind: Opcode.Leaf }], + keyvals: [Buffer.from('c20102', 'hex')] + } + const proof = decodeMultiproof(raw) + st.deepEqual(expected, proof) + + const encoded = encodeMultiproof(expected) + st.assert(raw.equals(encoded)) + + st.end() + }) + + t.test('decode and encode two out of three leaves with extension', async (st) => { + const t = new Trie() + const put = promisify(t.put.bind(t)) + const lookupNode = promisify(t._lookupNode.bind(t)) + const key1 = Buffer.from('1'.repeat(40), 'hex') + const key2 = Buffer.from('2'.repeat(40), 'hex') + const key3 = Buffer.from('1'.repeat(10).concat('3'.repeat(30)), 'hex') + await put(key1, Buffer.from('f'.repeat(64), 'hex')) + await put(key2, Buffer.from('e'.repeat(64), 'hex')) + await put(key3, Buffer.from('d'.repeat(64), 'hex')) + + const keys = [key3, key1] + const proof = await makeMultiproof(t, keys) + const encoded = encodeMultiproof(proof) + const decoded = decodeMultiproof(encoded) + st.deepEqual(proof, decoded) + st.end() + }) +}) + +tape('multiproof tests', (t) => { + t.skip('hash before nested nodes in branch', (st) => { + // TODO: Replace with valid multiproof + const raw = Buffer.from('f876e1a01bbb8445ba6497d9a4642a114cb06b3a61ea8e49ca3853991b4f07b7e1e04892f845b843f8419f02020202020202020202020202020202020202020202020202020202020202a00000000000000000000000000000000000000000000000000000000000000000ccc20180c28001c2021fc20402', 'hex') + const expectedRoot = Buffer.from('0d76455583723bb10c56d34cfad1fb218e692299ae2edb5dd56a950f7062a6e0', 'hex') + const expectedInstructions = [ + { kind: Opcode.Hasher }, + { kind: Opcode.Branch, value: 1 }, + { kind: Opcode.Leaf }, + { kind: Opcode.Add, value: 2 }, + ] + const proof = decodeMultiproof(raw) + st.deepEqual(proof.instructions, expectedInstructions) + st.assert(verifyMultiproof(expectedRoot, proof)) + st.end() + }) + + t.skip('two values', (st) => { + // TODO: Replace with valid multiproof + const raw = Buffer.from('f8c1e1a09afbad9ae00ded5a066bd6f0ec67a45d51f31c258066b997e9bb8336bc13eba8f88ab843f8419f01010101010101010101010101010101010101010101010101010101010101a00101010101010101010101010101010101010101010101010101010101010101b843f8419f02020202020202020202020202020202020202020202020202020202020202a00000000000000000000000000000000000000000000000000000000000000000d2c2021fc28001c2021fc20402c20180c20408', 'hex') + const expectedRoot = Buffer.from('32291409ceb27a3b68b6beff58cfc41c084c0bde9e6aca03a20ce9aa795bb248', 'hex') + const expectedInstructions = [ + { kind: Opcode.Leaf }, + { kind: Opcode.Branch, value: 1 }, + { kind: Opcode.Leaf }, + { kind: Opcode.Add, value: 2 }, + { kind: Opcode.Hasher }, + { kind: Opcode.Add, value: 8 } + ] + const proof = decodeMultiproof(raw) + st.deepEqual(proof.instructions, expectedInstructions) + st.assert(verifyMultiproof(expectedRoot, proof)) + st.end() + }) + + t.end() +}) + +tape('make multiproof', (t) => { + t.test('trie with one leaf', async (st) => { + const t = new Trie() + const put = promisify(t.put.bind(t)) + const lookupNode = promisify(t._lookupNode.bind(t)) + const key = Buffer.from('1'.repeat(40), 'hex') + await put(key, Buffer.from('ffff', 'hex')) + const leaf = await lookupNode(t.root) + + const proof = await makeMultiproof(t, [key]) + st.deepEqual(proof, { + hashes: [], + keyvals: [leaf.serialize()], + instructions: [{ kind: Opcode.Leaf }] + }) + st.assert(verifyMultiproof(t.root, proof, [key])) + st.end() + }) + + t.test('prove one of two leaves in trie', async (st) => { + const t = new Trie() + const put = promisify(t.put.bind(t)) + const lookupNode = promisify(t._lookupNode.bind(t)) + const key1 = Buffer.from('1'.repeat(40), 'hex') + const key2 = Buffer.from('2'.repeat(40), 'hex') + await put(key1, Buffer.from('f'.repeat(64), 'hex')) + await put(key2, Buffer.from('e'.repeat(64), 'hex')) + + const proof = await makeMultiproof(t, [key1]) + st.equal(proof.hashes.length, 1) + st.equal(proof.keyvals.length, 1) + st.equal(proof.instructions.length, 4) + st.assert(verifyMultiproof(t.root, proof, [key1])) + st.end() + }) + + t.test('prove two of three leaves in trie', async (st) => { + const t = new Trie() + const put = promisify(t.put.bind(t)) + const lookupNode = promisify(t._lookupNode.bind(t)) + const key1 = Buffer.from('1'.repeat(40), 'hex') + const key2 = Buffer.from('2'.repeat(40), 'hex') + const key3 = Buffer.from('3'.repeat(40), 'hex') + await put(key1, Buffer.from('f'.repeat(64), 'hex')) + await put(key2, Buffer.from('e'.repeat(64), 'hex')) + await put(key3, Buffer.from('d'.repeat(64), 'hex')) + + const proof = await makeMultiproof(t, [key3, key1]) + st.assert(verifyMultiproof(t.root, proof, [key1, key3])) + st.end() + }) + + t.test('prove two of three leaves (with extension) in trie', async (st) => { + const t = new Trie() + const put = promisify(t.put.bind(t)) + const lookupNode = promisify(t._lookupNode.bind(t)) + const key1 = Buffer.from('1'.repeat(40), 'hex') + const key2 = Buffer.from('2'.repeat(40), 'hex') + const key3 = Buffer.from('1'.repeat(10).concat('3'.repeat(30)), 'hex') + await put(key1, Buffer.from('f'.repeat(64), 'hex')) + await put(key2, Buffer.from('e'.repeat(64), 'hex')) + await put(key3, Buffer.from('d'.repeat(64), 'hex')) + + const keys = [key3, key1] + const proof = await makeMultiproof(t, keys) + keys.sort(Buffer.compare) + st.assert(verifyMultiproof(t.root, proof, keys)) + st.end() + }) + + t.test('two embedded leaves in branch', async (st) => { + const t = new Trie() + const put = promisify(t.put.bind(t)) + const lookupNode = promisify(t._lookupNode.bind(t)) + const key1 = Buffer.from('1'.repeat(40), 'hex') + const key2 = Buffer.from('2'.repeat(40), 'hex') + await put(key1, Buffer.from('f'.repeat(4), 'hex')) + await put(key2, Buffer.from('e'.repeat(4), 'hex')) + + const proof = await makeMultiproof(t, [key1]) + st.assert(verifyMultiproof(t.root, proof, [key1])) + st.end() + }) +}) + +tape('fuzz multiproof generation/verification with official tests', async (t) => { + const trietest = Object.assign({}, require('./fixture/trietest.json').tests) + const trietestSecure = Object.assign({}, require('./fixture/trietest_secureTrie.json').tests) + const hexEncodedTests = Object.assign({}, require('./fixture/hex_encoded_securetrie_test.json').tests) + // Inputs of hex encoded tests are objects instead of arrays + Object.keys(hexEncodedTests).map((k) => { + hexEncodedTests[k].in = Object.keys(hexEncodedTests[k].in).map((key) => [key, hexEncodedTests[k].in[key]]) + }) + const testCases = [ + { name: 'jeff', secure: false, input: trietest.jeff.in, root: trietest.jeff.root }, + { name: 'jeffSecure', secure: true, input: trietestSecure.jeff.in, root: trietestSecure.jeff.root }, + { name: 'emptyValuesSecure', secure: true, input: trietestSecure.emptyValues.in, root: trietestSecure.emptyValues.root }, + { name: 'test1', secure: true, input: hexEncodedTests.test1.in, root: hexEncodedTests.test1.root }, + { name: 'test2', secure: true, input: hexEncodedTests.test2.in, root: hexEncodedTests.test2.root }, + { name: 'test3', secure: true, input: hexEncodedTests.test3.in, root: hexEncodedTests.test3.root } + ] + for (const testCase of testCases) { + const testName = testCase.name + t.comment(testName) + const expect = Buffer.from(testCase.root.slice(2), 'hex') + const removedKeys = {} + // Clean inputs + let inputs = testCase.input.map((input) => { + const res = [null, null] + for (i = 0; i < 2; i++) { + if (!input[i]) continue + if (input[i].slice(0, 2) === '0x') { + res[i] = Buffer.from(input[i].slice(2), 'hex') + } else { + res[i] = Buffer.from(input[i]) + } + } + if (res[1] === null) { + removedKeys[res[0].toString('hex')] = true + } + return res + }) + + let trie + if (testCase.secure) { + trie = new SecureTrie() + } else { + trie = new Trie() + } + for (let input of inputs) { + await promisify(trie.put.bind(trie))(input[0], input[1]) + } + t.assert(trie.root.equals(expect)) + + // TODO: include keys that have been removed from trie + const keyCombinations = getCombinations( + inputs.map((i) => i[0]).filter((i) => removedKeys[i.toString('hex')] !== true) + ) + for (let combination of keyCombinations) { + // If using secure make sure to hash keys + if (testCase.secure) { + combination = combination.map((k) => keccak256(k)) + } + try { + const proof = await makeMultiproof(trie, combination) + // Verification expects a sorted array of keys + combination.sort(Buffer.compare) + t.assert(verifyMultiproof(trie.root, proof, combination)) + } catch (e) { + if (e.message !== 'Key not in trie') { + t.fail(e) + } + t.comment('skipped combination because key is not in trie') + } + } + } + t.end() +}) + +// Given array [a, b, c], produce combinations +// with all lengths [1, arr.length]: +// [[a], [b], [c], [a, b], [a, c], [b, c], [a, b, c]] +function getCombinations(arr) { + // Make sure there are no duplicates + for (let i = 0; i < arr.length; i++) { + for (let j = i + 1; j < arr.length; j++) { + if (arr[i].equals(arr[j])) { + arr.splice(j, 1) + } + } + } + + const res = [] + const numCombinations = Math.pow(2, arr.length) + for (let i = 0; i < numCombinations; i++) { + const tmp = [] + for (let j = 0; j < arr.length; j++) { + if ((i & Math.pow(2, j))) { + tmp.push(arr[j]) + } + } + res.push(tmp) + } + return res +}