From 2dd496bff80b3dc932d6f341600f3a95ad8d18be Mon Sep 17 00:00:00 2001 From: KtorZ Date: Wed, 22 May 2024 19:48:19 +0200 Subject: [PATCH] Include key suffix in leaf's hash. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes quite a few things and make the proof verification slightly more complicated, but overall remains manageable. This change is however necessary to prevent one from including entire sub-trees in one go by making look like a _value_; thus resulting in multiple trees with the same root hash. So for example, one could construct the following tree: ``` ╔═══════════════════════════════════════════════════════════════════╗ ║ #f6ee5ad5391e966a7e0f24465d98ab5df2596e5987f189b67479d271b6e938a7 ║ ╚═══════════════════════════════════════════════════════════════════╝ ┌─ 09ad7..[55 digits]..19d9 → apple └─ 177ca..[55 digits]..98a3 → ``` Which is in fact, a way of disguising the following tree: ``` ╔═══════════════════════════════════════════════════════════════════╗ ║ #f6ee5ad5391e966a7e0f24465d98ab5df2596e5987f189b67479d271b6e938a7 ║ ╚═══════════════════════════════════════════════════════════════════╝ ┌─ 09ad7..[55 digits]..19d9 → apple └─ f #177cab65ae5d ├─ 3 #9a4b2591979d │ ├─ 2e49b..[53 digits]..9728 → pomegranate │ └─ 7d2a6..[53 digits]..d578 → kiwi └─ a3c8d..[54 digits]..52ff → plum ``` The value is just chosen to be the pre-image of the 'f' node. However, this completely breaks the logic and guarantee that comes with insert/delete as now, one is able to add or remove entire parts of the tree in one go. The fix: includes the remaining key path in the leaf's hash, all the way up to the root. By including the remaining path (or suffix) in the final leaf, the trie is made completely tampered proof as the root now fully depends on not only the elements present in the tree, but also the structure of the tree itself. So, the root tree effectively depends on the number of branches, and their positions. So a situation like the above is no longer possible; the first tree would yield a different root hash and the proof would be invalid against the second tree. NOTE: The on-chain implementation, as well as the proof serialisation format must be adjusted. --- off-chain/lib/index.js | 160 +++++++++++----- off-chain/tests/index.test.js | 331 +++++++++++++++++----------------- 2 files changed, 280 insertions(+), 211 deletions(-) diff --git a/off-chain/lib/index.js b/off-chain/lib/index.js index acfd16b..6fe42f9 100644 --- a/off-chain/lib/index.js +++ b/off-chain/lib/index.js @@ -133,6 +133,47 @@ export class Tree { return loop('', values.map(value => ({ key: intoKey(value), value }))); } + + /** Set the prefix on a tree, and computes its corresponding root hash. Both steps + * are done in lock-step because the node's hash crucially includes its prefix. + * + * @param {string} prefix A sequence of nibbles. + * @param {Buffer} hash A hash digest of the value. + * @return {Tree} A reference to the underlying tree with its prefix modified. + * @private + */ + setPrefix(prefix, hash) { + // NOTE: + // We append the remaining prefix to the value. However, to make this + // step more efficient on-chain, we append it as a raw bytestring instead of + // an array of nibbles. + // + // If the prefix's length is odd however, we do still add one nibble, and + // then the rest. + const isOdd = prefix.length % 2 > 0; + + const head = isOdd + ? nibbles(prefix.slice(0, 1)) + : Buffer.from([]); + + const tail = Buffer.from(isOdd + ? prefix.slice(1) + : prefix, + 'hex' + ); + + assert( + hash.length === DIGEST_LENGTH, + `hash must be a ${DIGEST_LENGTH}-byte digest in 'setPrefix' but it is` + ); + + this.hash = prefix.length === 0 ? hash : digest(Buffer.concat([head, tail, hash])); + this.prefix = prefix; + + return this; + } + + /** Conveniently access a child in the tree at the given path. A path is * sequence of nibbles, as an hex-encoded string. * @@ -234,7 +275,7 @@ export class Leaf extends Tree { this.size = 1; this.value = value; - this.setPrefix(prefix); + this.setPrefix(prefix, digest(value)); } @@ -246,25 +287,16 @@ export class Leaf extends Tree { * @private */ [inspect.custom](depth, options, _inspect) { + const hash = options.stylize( + `#${this.hash.toString('hex').slice(0, DIGEST_SUMMARY_LENGTH)}`, + 'special' + ); + const prefix = withEllipsis(this.prefix, PREFIX_CUTOFF, options); const value = options.stylize(this.value, 'string'); - return `${prefix} → ${value}`; - } - - - /** Set the prefix on a tree, and computes its corresponding root hash. Both steps - * are done in lock-step because the node's hash crucially includes its prefix. - * - * @param {string} prefix A sequence of nibbles. - * @return {Tree} A reference to the underlying tree with its prefix modified. - * @private - */ - setPrefix(prefix) { - this.hash = digest(this.value); - this.prefix = prefix; - return this; + return `${prefix} ${hash} → ${value}`; } @@ -346,24 +378,12 @@ export class BranchNode extends Tree { this.size = children.reduce((size, child) => size + (child?.size || 0), 0); this.children = children; - this.setPrefix(prefix); - } - - /** Sets the prefix and hash of the node, ensuring that the former is used to - * compute the latter. - * - * @param {string} prefix A sequence of nibbles. - * @return {BranchNode} - * @private - */ - setPrefix(prefix) { - const children = this.children.flatMap(node => node === undefined ? [] : [node.hash || node]); - - this.hash = digest(Buffer.concat([nibbles(prefix), ...children])); - - this.prefix = prefix; - - return this; + this.setPrefix( + prefix, + digest(Buffer.concat(this.children.flatMap( + node => node === undefined ? [] : [node.hash] + ))), + ); } /** @@ -413,7 +433,7 @@ export class BranchNode extends Tree { '│' ); if (depth === 2 && this.prefix.length > 0) { - first = `\n ${this.prefix.toString('hex')}${first}` + first = `\n ${this.prefix}${first}` } // ----- In-between @@ -493,10 +513,33 @@ export class Proof { * @private */ rewind(target, skip, children) { - this.steps.unshift({ - skip, - neighbors: children.flatMap(x => x?.hash.equals(target.hash) ? [] : [x?.hash]), - }); + children = children.filter(x => !x?.hash.equals(target.hash)); + + if (children.filter(x => x !== undefined).length === 1) { + this.steps.unshift({ + skip, + neighbors: children.map(x => { + if (x === undefined) { + return x; + } + + return { + prefix: x.prefix, + value: digest(x instanceof Leaf + ? x.value + : Buffer.concat(x.children.flatMap(node => { + return node === undefined ? [] : [node.hash] + })) + ), + }; + }), + }); + } else { + this.steps.unshift({ + skip, + neighbors: children.map(x => x?.hash), + }); + } return this; } @@ -531,7 +574,7 @@ export class Proof { const suffix = path.slice(cursor); const zero = withElement - ? Leaf.prototype.setPrefix.call({ value: this.value }, suffix).hash + ? Tree.prototype.setPrefix.call({}, suffix, digest(this.value)).hash : undefined; return this.steps.reduceRight((currentHash, step, ix) => { @@ -548,15 +591,43 @@ export class Proof { // // Now, when the last branch node had only two elements (i.e. neighbors.length === 1), // we must treat this node as a leaf instead and take its hash directly. - if (currentHash === undefined && neighbors.length === 1) { - return neighbors[0]; + if (neighbors.length === 1) { + const neighbor = neighbors[0]; + + assert(neighbor.prefix !== undefined); + + const neighborPos = step.neighbors.indexOf(neighbor); + + const neighborNibble = neighborPos + (nibble <= neighborPos ? 1 : 0); + + if (currentHash === undefined) { + return Tree.prototype.setPrefix.call({}, + `${neighborNibble.toString('16')}${neighbor.prefix}`, + neighbor.value + ).hash; + } + + const neighborHash = Tree.prototype.setPrefix + .call({}, neighbor.prefix, neighbor.value) + .hash; + + return Tree.prototype.setPrefix.call({}, + prefix, + nibble <= neighborPos + ? digest(Buffer.concat([currentHash, neighborHash])) + : digest(Buffer.concat([neighborHash, currentHash])) + ).hash; } const children = step.neighbors.slice(0, nibble) .concat(currentHash === undefined ? [] : currentHash) - .concat(step.neighbors.slice(nibble)); + .concat(step.neighbors.slice(nibble)) + .filter(x => x !== undefined); - return BranchNode.prototype.setPrefix.call({ children }, prefix).hash; + return Tree.prototype.setPrefix.call({}, + prefix, + digest(Buffer.concat(children)) + ).hash; }, zero); } @@ -568,6 +639,7 @@ export class Proof { */ serialise() { // TODO: rewrite to CBOR. + throw new Error('TODO: rework proof format with new structure.'); return JSON.stringify(this.steps.map(step => { let nextDefined = 0; const lookup = step.neighbors.map((x, ix) => { diff --git a/off-chain/tests/index.test.js b/off-chain/tests/index.test.js index 954c901..efda259 100644 --- a/off-chain/tests/index.test.js +++ b/off-chain/tests/index.test.js @@ -34,66 +34,6 @@ const FRUITS_LIST = [ 'watermelon', ]; -// ------------------------------------------------------------------------ Leaf - -test('Leaf: a new Leaf is a Leaf', t => { - const tree = new Leaf('00000000', 'value'); - t.true(tree instanceof Leaf); -}); - -test('Leaf: inspect with human-readable key', t => { - const tree = new Leaf('00000000', 'value'); - t.is(inspect(tree), '00000000 → value'); -}); - - -// ------------------------------------------------------------------------ BranchNode - -test('BranchNode: is not empty', t => { - const tree = new BranchNode('', { - 1: new Leaf('01', '14'), - 2: new Leaf('02', '42'), - }); - t.false(tree.isEmpty()); -}); - -test('BranchNode: inspect a simple one-level tree', t => { - const tree = new BranchNode('', { - 1: new Leaf('01', '14'), - 2: new Leaf('02', '42'), - }); - t.is(inspect(tree), unindent` - ╔═══════════════════════════════════════════════════════════════════╗ - ║ #2781af6c16c2da267d64825c0348ade4ab73927d602fd1c9459f317e63841fb8 ║ - ╚═══════════════════════════════════════════════════════════════════╝ - ┌─ 101 → 14 - └─ 202 → 42 - `); -}); - -test('BranchNode: inspect complex trees', t => { - const tree = new BranchNode('0000', { - 1: new BranchNode('01', { - 0: new Leaf('00', '14'), - 'f': new Leaf('ff', '1337'), - }), - 2: new Leaf('02', '42'), - 9: new Leaf('09', '999'), - }); - t.is(inspect(tree), unindent` - ╔═══════════════════════════════════════════════════════════════════╗ - ║ #0e5435b5bde2452c7357a6fb2f1bce9b3c43a648c04a2f0498c07c1146bc1f6b ║ - ╚═══════════════════════════════════════════════════════════════════╝ - 0000 - ├─ 101 #a8048fe39838 - │ ├─ 000 → 14 - │ └─ fff → 1337 - ├─ 202 → 42 - └─ 909 → 999 - `); -}); - - // ------------------------------------------------------------------------- Tree test('Tree: a new Tree is always empty', t => { @@ -158,16 +98,15 @@ test('Tree: cannot create proof for leaf-tree for non-existing elements', t => { test('Tree: can create proof for simple trees', t => { const values = [ 'foo', 'bar' ].map(Buffer.from); - /* - ╔═══════════════════════════════════════════════════════════════════╗ - ║ #6d9a495a9352061d331fd6039e97297aec9591b291b61f4d46d28c6a9b302577 ║ - ╚═══════════════════════════════════════════════════════════════════╝ - ┌─ 84418..[55 digits]..e71d → bar - └─ b8fe9..[55 digits]..49fd → foo - */ const tree = Tree.fromList(values); - t.is(tree.size, 2); + t.is(inspect(tree), unindent` + ╔═══════════════════════════════════════════════════════════════════╗ + ║ #5e9870f1f1d3464cc775d9e81ba36ab815e72642a1b9150bfd476c9d1e508f01 ║ + ╚═══════════════════════════════════════════════════════════════════╝ + ┌─ 84418..[55 digits]..e71d #80980aea9d27 → bar + └─ b8fe9..[55 digits]..49fd #9cadc73321de → foo + `); const proofs = { foo: tree.prove('foo'), @@ -189,50 +128,50 @@ test('Tree: can create proof for complex trees', t => { const values = FRUITS_LIST.map(Buffer.from); /* - ╔═══════════════════════════════════════════════════════════════════╗ - ║ #18c56d4fb717703237c19903ca4c58e94e66dd49ccaad1b20665d8a0fec7e081 ║ - ╚═══════════════════════════════════════════════════════════════════╝ - ┌─ 09ad7..[55 digits]..19d9 → apple - ├─ 12b59..[55 digits]..9386 → durian - ├─ 2 #98cd7528f92a - │ ├─ 36e5e..[54 digits]..97f6 → cranberry - │ └─ 9d848..[54 digits]..e47b → grapefruit - ├─ 3 #6f923a110c88 - │ ├─ 70a5c..[54 digits]..bd36 → orange - │ └─ 9cd47..[54 digits]..9e65 → blueberry - ├─ 4c4ce..[55 digits]..8230 → watermelon - ├─ 5 #4b5ee2d337f1 - │ ├─ 4d444..[54 digits]..9f76 → banana - │ └─ acb40..[54 digits]..7a07 → grape - ├─ 7 #dbfd99280a60 - │ ├─ 0abba..[54 digits]..81c3 → lime - │ └─ 548bb..[54 digits]..f3a9 → lemon - ├─ 8 #6fcc4950bc99 - │ ├─ c0dfd..[54 digits]..ae0e → pineapple - │ ├─ dafdb..[54 digits]..00ca → blackberry - │ └─ f #e1231b489fe6 - │ ├─ 04b7b..[53 digits]..adc6 → cherry - │ └─ 7033a..[53 digits]..6a64 → fig - ├─ a #d9447ec94c48 - │ ├─ 3d7d4..[54 digits]..f988 → kumquat - │ └─ a2fb0..[54 digits]..4587 → coconut - ├─ b8 #e9f8df4e5ca0 - │ ├─ 884aa..[53 digits]..ad80 → raspberry - │ └─ aad88..[53 digits]..fcf0 → pear - ├─ c49a4..[55 digits]..3565 → peach - ├─ d #4a8837353ceb - │ ├─ 4a3ea..[54 digits]..7bd9 → passionfruit - │ └─ 8bf23..[54 digits]..eca7 → strawberry - ├─ e #3b6f7a6c29e1 - │ ├─ 5a02a..[54 digits]..37b6 → papaya - │ └─ 6 #ba86887bf06b - │ ├─ 4d91f..[53 digits]..0cfe → guava - │ └─ 7e298..[53 digits]..d9d1 → mango - └─ f #177cab65ae5d - ├─ 3 #9a4b2591979d - │ ├─ 2e49b..[53 digits]..9728 → pomegranate - │ └─ 7d2a6..[53 digits]..d578 → kiwi - └─ a3c8d..[54 digits]..52ff → plum + ╔═══════════════════════════════════════════════════════════════════╗ + ║ #1010037b1fe7c96b833527ec7968f3b00dd48fc3d8f1d778b54c356e21174e03 ║ + ╚═══════════════════════════════════════════════════════════════════╝ + ┌─ 09ad7..[55 digits]..19d9 #9261850f9a56 → apple + ├─ 12b59..[55 digits]..9386 #dfea1e596dff → durian + ├─ 2 #8a4f388e0661 + │ ├─ 36e5e..[54 digits]..97f6 #b7c699f0347f → cranberry + │ └─ 9d848..[54 digits]..e47b #2498f0adb130 → grapefruit + ├─ 3 #6ea585310006 + │ ├─ 70a5c..[54 digits]..bd36 #40f9ef4577ae → orange + │ └─ 9cd47..[54 digits]..9e65 #654672508f82 → blueberry + ├─ 4c4ce..[55 digits]..8230 #2711855c9d13 → watermelon + ├─ 5 #c880dc733511 + │ ├─ 4d444..[54 digits]..9f76 #574d2d71ad4b → banana + │ └─ acb40..[54 digits]..7a07 #dcaae7b42e37 → grape + ├─ 7 #46f0eec92a88 + │ ├─ 0abba..[54 digits]..81c3 #d31a6a9db0aa → lime + │ └─ 548bb..[54 digits]..f3a9 #6741d5499cc2 → lemon + ├─ 8 #0260cafbd265 + │ ├─ c0dfd..[54 digits]..ae0e #d589f73c5992 → pineapple + │ ├─ dafdb..[54 digits]..00ca #98e27cac47e2 → blackberry + │ └─ f #52883d12a023 + │ ├─ 04b7b..[53 digits]..adc6 #4d4c4a45c0a0 → cherry + │ └─ 7033a..[53 digits]..6a64 #32ac5559d8f6 → fig + ├─ a #6813cef05801 + │ ├─ 3d7d4..[54 digits]..f988 #51ce43f8c8c2 → kumquat + │ └─ a2fb0..[54 digits]..4587 #1c793dfde026 → coconut + ├─ b8 #ff065ee6497e + │ ├─ 884aa..[53 digits]..ad80 #59f0a53949c3 → raspberry + │ └─ aad88..[53 digits]..fcf0 #0a83da727401 → pear + ├─ c49a4..[55 digits]..3565 #8d935cd32d15 → peach + ├─ d #39de29c529fe + │ ├─ 4a3ea..[54 digits]..7bd9 #74d9e59c7370 → passionfruit + │ └─ 8bf23..[54 digits]..eca7 #228660a26f9c → strawberry + ├─ e #f0aa8cc42fee + │ ├─ 5a02a..[54 digits]..37b6 #93144299de64 → papaya + │ └─ 6 #e170d1dfc702 + │ ├─ 4d91f..[53 digits]..0cfe #f5e9e5122ccc → guava + │ └─ 7e298..[53 digits]..d9d1 #e35735c3689b → mango + └─ f #8feafd139ff2 + ├─ 3 #7aee7f871e07 + │ ├─ 2e49b..[53 digits]..9728 #1d20453bad70 → pomegranate + │ └─ 7d2a6..[53 digits]..d578 #73d471559ed4 → kiwi + └─ a3c8d..[54 digits]..52ff #4e7480807124 → plum */ const tree = Tree.fromList(values); @@ -249,7 +188,7 @@ test('Tree: can create proof for complex trees', t => { [ { skip: 0, - neighbors: Array.from('12345678abcdef').map(x => tree.childAt(x)), + neighbors: Array.from('123456789abcdef').map(x => tree.childAt(x)?.hash), } ] ); @@ -262,13 +201,18 @@ test('Tree: can create proof for complex trees', t => { [ { skip: 0, - neighbors: Array.from('01234568abcdef').map(x => tree.childAt(x)), + neighbors: Array.from('012345689abcdef').map(x => tree.childAt(x)?.hash), }, { skip: 0, - neighbors: [ - tree.childAt('75'), - ] + neighbors: helpers.intoVector({ + // NOTE: It's at position '5', but because it's only the neighbors + // and 'lime' is at position '0', it shifts towards 0. + 4: { + prefix: tree.childAt('75').prefix, + value: digest(tree.childAt('75').value) + }, + }, 15) }, ] ); @@ -281,13 +225,18 @@ test('Tree: can create proof for complex trees', t => { [ { skip: 0, - neighbors: Array.from('012345678acdef').map(x => tree.childAt(x)), + neighbors: Array.from('0123456789acdef').map(x => tree.childAt(x)?.hash), }, { skip: 1, - neighbors: [ - tree.childAt('ba'), - ] + neighbors: helpers.intoVector({ + // NOTE: Same reasoning as above, should be 'a', but it is shifted by one + // because we only store *neighbors* and the actual element is before ('8'). + 9: { + prefix: tree.childAt('ba').prefix, + value: digest(tree.childAt('ba').value), + } + }, 15), } ] ); @@ -300,19 +249,25 @@ test('Tree: can create proof for complex trees', t => { [ { skip: 0, - neighbors: Array.from('012345678abcde').map(x => tree.childAt(x)), + neighbors: Array.from('0123456789abcde').map(x => tree.childAt(x)?.hash), }, { skip: 0, - neighbors: [ - tree.childAt('fa'), - ] + neighbors: helpers.intoVector({ + 9: { + prefix: tree.childAt('fa').prefix, + value: digest(tree.childAt('fa').value), + }, + }, 15), }, { skip: 0, - neighbors: [ - tree.childAt('f32'), - ] + neighbors: helpers.intoVector({ + 2: { + prefix: tree.childAt('f32').prefix, + value: digest(tree.childAt('f32').value), + }, + }, 15) } ] ); @@ -322,38 +277,38 @@ test('Tree: can create proof for complex trees', t => { }); test('Tree: checking for insertion', t => { - /* - ╔═══════════════════════════════════════════════════════════════════╗ - ║ #28264c9e300e2ec8433c07d839042b7681fe85df6ab6a83b39d5f4dbf51b3ae7 ║ - ╚═══════════════════════════════════════════════════════════════════╝ - ┌─ 09ad7..[55 digits]..19d9 → apple - └─ fa3c8..[55 digits]..52ff → plum - */ const st0 = Tree.fromList([ 'apple', 'plum' ]); + t.is(inspect(st0), unindent` + ╔═══════════════════════════════════════════════════════════════════╗ + ║ #d15c97eac41f304f939e820c821a7bdc72562d5613179b66e9b51950487ec790 ║ + ╚═══════════════════════════════════════════════════════════════════╝ + ┌─ 09ad7..[55 digits]..19d9 #9261850f9a56 → apple + └─ fa3c8..[55 digits]..52ff #603120a89780 → plum + `); - /* - ╔═══════════════════════════════════════════════════════════════════╗ - ║ #a694e4b261e7650bdc9f99279ea092b3be9e6e7eb5d1bb4836ca64c9784c8259 ║ - ╚═══════════════════════════════════════════════════════════════════╝ - ┌─ 09ad7..[55 digits]..19d9 → apple - └─ f #9c0875c21f33 - ├─ 37d2a..[54 digits]..d578 → kiwi - └─ a3c8d..[54 digits]..52ff → plum - */ const st1 = Tree.fromList([ 'apple', 'kiwi', 'plum' ]); + t.is(inspect(st1), unindent` + ╔═══════════════════════════════════════════════════════════════════╗ + ║ #e16e4c64efd32469d535590fdd702ee73e1f73d585e5cb370134bc8059f72b27 ║ + ╚═══════════════════════════════════════════════════════════════════╝ + ┌─ 09ad7..[55 digits]..19d9 #9261850f9a56 → apple + └─ f #095584676d31 + ├─ 37d2a..[54 digits]..d578 #5fcb97ee97a9 → kiwi + └─ a3c8d..[54 digits]..52ff #4e7480807124 → plum + `); - /* - ╔═══════════════════════════════════════════════════════════════════╗ - ║ #b3ea3161299926db3326f74edaea9f5bc4879f2ff46eea251eeb11e60010b4c5 ║ - ╚═══════════════════════════════════════════════════════════════════╝ - ┌─ 09ad7..[55 digits]..19d9 → apple - └─ f #eb2355dcead1 - ├─ 3 #843cadeff6b7 - │ ├─ 2e49b..[53 digits]..9728 → pomegranate - │ └─ 7d2a6..[53 digits]..d578 → kiwi - └─ a3c8d..[54 digits]..52ff → plum - */ const st2 = Tree.fromList([ 'apple', 'pomegranate', 'kiwi', 'plum' ]); + t.is(inspect(st2), unindent` + ╔═══════════════════════════════════════════════════════════════════╗ + ║ #94ca4d707f679d96b8fe71fa383e712f7960f63c3366047c1f60fb84ab4663db ║ + ╚═══════════════════════════════════════════════════════════════════╝ + ┌─ 09ad7..[55 digits]..19d9 #9261850f9a56 → apple + └─ f #8feafd139ff2 + ├─ 3 #7aee7f871e07 + │ ├─ 2e49b..[53 digits]..9728 #1d20453bad70 → pomegranate + │ └─ 7d2a6..[53 digits]..d578 #73d471559ed4 → kiwi + └─ a3c8d..[54 digits]..52ff #4e7480807124 → plum + `); // Insert 'kiwi' into st0 t.true(st1.prove('kiwi').verify(false).equals(st0.hash)); @@ -366,7 +321,45 @@ test('Tree: checking for insertion', t => { // Insert 'pomegranate' into st1 t.true(st2.prove('pomegranate').verify(false).equals(st1.hash)); -}) +}); + +test('tree extension from top', t => { + const st0 = Tree.fromList([ 'apple' ]); + + /* + ╔═══════════════════════════════════════════════════════════════════╗ + ║ #94ca4d707f679d96b8fe71fa383e712f7960f63c3366047c1f60fb84ab4663db ║ + ╚═══════════════════════════════════════════════════════════════════╝ + ┌─ 09ad7..[55 digits]..19d9 #9261850f9a56 → apple + └─ f #8feafd139ff2 + ├─ 3 #7aee7f871e07 + │ ├─ 2e49b..[53 digits]..9728 #1d20453bad70 → pomegranate + │ └─ 7d2a6..[53 digits]..d578 #73d471559ed4 → kiwi + └─ a3c8d..[54 digits]..52ff #4e7480807124 → plum + */ + const st1 = Tree.fromList([ 'apple', 'pomegranate', 'kiwi', 'plum' ]); + + const value = Buffer.concat(st1.children[15].children + .flatMap(x => x === undefined ? [] : [x.hash]) + ); + + const st2 = Tree.fromList([ 'apple', value ]); + + const proof = new Proof(value); + proof.steps = [{ + skip: 0, + neighbors: helpers.intoVector({ + 0: { + prefix: st0.prefix, + value: digest(st0.value), + } + }) + }]; + + t.true(proof.verify().toString('hex') === st2.hash.toString('hex')); + t.true(proof.verify().toString('hex') !== st1.hash.toString('hex')); + t.true(st1.hash.toString('hex') !== st2.hash.toString('hex')); +}); // ---------------------------------------------------------------------- Helpers @@ -438,21 +431,25 @@ function unindent(str) { return lines.map(s => s.slice(n)).join('\n').trimEnd(); } +function stringifyNeighbor(x) { + if (x == undefined || x instanceof Buffer) { + return x?.toString('hex'); + } + + return { ...x, value: x.value.toString('hex') }; +} + + function assertProof(t, root, proof, expected) { - t.is(proof.verify().toString('hex'), root.toString('hex')); proof.steps.forEach((step, k) => { t.is(step.skip, expected[k].skip); t.deepEqual( - step.neighbors.flatMap(x => { - return x === undefined - ? [] - : [x.toString('hex')] - }), - expected[k].neighbors.flatMap(x => { - return x === undefined - ? [] - : [x.hash.toString('hex')]; - }), + step.neighbors.map(stringifyNeighbor), + expected[k].neighbors.map(stringifyNeighbor), ); }); + + t.is(proof.steps.length, expected.length); + + t.is(proof.verify().toString('hex'), root.toString('hex')); }