From 270e2fc5c72949f88c80d2c1af17b94befba25e6 Mon Sep 17 00:00:00 2001 From: hatef Date: Thu, 6 Jul 2023 16:07:19 +0300 Subject: [PATCH] :seedling: Introduce two new interfaces for verification - Also introduce a new function `isInclusion` to check inclusion & non-inclusion - Update JS unit test to check two new interfaces --- sparse_merkle_tree.js | 37 ++++++++++++++++++++++----- state_db.js | 35 +++++++++++++++++++++++--- test/sparse_merkle_tree.spec.js | 44 +++++++++++++++++++++++++++------ test/statedb.spec.js | 24 +++++++++--------- test/utils.js | 2 +- types.d.ts | 4 +++ utils.js | 21 ++++++++++++++++ 7 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 utils.js diff --git a/sparse_merkle_tree.js b/sparse_merkle_tree.js index 8e9a263..2284e96 100644 --- a/sparse_merkle_tree.js +++ b/sparse_merkle_tree.js @@ -19,6 +19,7 @@ const { in_memory_smt_verify, in_memory_smt_calculate_root, } = require("./bin-package/index.node"); +const { isInclusion } = require('./utils'); const DEFAULT_KEY_LENGTH = 38; @@ -68,12 +69,36 @@ class SparseMerkleTree { } async verify(root, queries, proof) { - return new Promise((resolve, reject) => { - in_memory_smt_verify.call(null, root, queries, proof, this._keyLength, (err, result) => { - if (err) { - reject(err); - return; + return new Promise((resolve) => { + in_memory_smt_verify.call(null, root, queries, proof, this._keyLength, (result) => { + resolve(result); + }); + }); + } + + async verifyInclusionProof(root, queries, proof) { + return new Promise((resolve) => { + for (let i = 0; i < queries.length; i++) { + if (!isInclusion(queries[i], proof.queries[i])) { + resolve(false); } + } + + in_memory_smt_verify.call(null, root, queries, proof, this._keyLength, (_, result) => { + resolve(result); + }); + }); + } + + async verifyNonInclusionProof(root, queries, proof) { + return new Promise((resolve) => { + for (let i = 0; i < queries.length; i++) { + if (isInclusion(queries[i], proof.queries[i])) { + resolve(false); + } + } + + in_memory_smt_verify.call(null, root, queries, proof, this._keyLength, (result) => { resolve(result); }); }); @@ -94,4 +119,4 @@ class SparseMerkleTree { module.exports = { SparseMerkleTree, -}; \ No newline at end of file +}; diff --git a/state_db.js b/state_db.js index 6a81a58..eab7b75 100644 --- a/state_db.js +++ b/state_db.js @@ -47,6 +47,7 @@ const { const { NotFoundError } = require('./error'); const { Iterator } = require("./iterator"); const { getOptionsWithDefault } = require('./options'); +const { isInclusion } = require('./utils'); class StateReader { constructor(db) { @@ -303,11 +304,37 @@ class StateDB { } async verify(root, queries, proof) { - return new Promise((resolve, reject) => { - state_db_verify.call(this._db, root, queries, proof, (err, result) => { - if (err) { - return reject(err); + return new Promise((resolve) => { + state_db_verify.call(this._db, root, queries, proof, (_, result) => { + resolve(result); + }); + }); + } + + + async verifyInclusionProof(root, queries, proof) { + return new Promise((resolve) => { + for (let i = 0; i < queries.length; i++) { + if (!isInclusion(queries[i], proof.queries[i])) { + resolve(false); } + } + + state_db_verify.call(this._db, root, queries, proof, (_, result) => { + resolve(result); + }); + }); + } + + async verifyNonInclusionProof(root, queries, proof) { + return new Promise((resolve) => { + for (let i = 0; i < queries.length; i++) { + if (isInclusion(queries[i], proof.queries[i])) { + resolve(false); + } + } + + state_db_verify.call(this._db, root, queries, proof, (_, result) => { resolve(result); }); }); diff --git a/test/sparse_merkle_tree.spec.js b/test/sparse_merkle_tree.spec.js index 7983d15..b631ad0 100644 --- a/test/sparse_merkle_tree.spec.js +++ b/test/sparse_merkle_tree.spec.js @@ -12,10 +12,9 @@ * Removal or modification of this copyright notice is prohibited. */ const os = require('os'); -const path = require('path'); -const fs = require('fs'); -const { StateDB, SparseMerkleTree } = require('../main'); +const { SparseMerkleTree } = require('../main'); const { getRandomBytes } = require('./utils'); +const { isInclusion } = require('../utils'); const SMTFixtures = require('./fixtures/smt_fixtures.json'); const FixturesInclusionProof = require('./fixtures/fixtures_no_delete_inclusion_proof.json'); @@ -89,6 +88,37 @@ describe('SparseMerkleTree', () => { }); } + /* + * We do not know which testcase is inclusion or non-inclusion so define the two + * following functions to call correct verification function based on the result + * of these functions. + */ + const isNonInclusionProof = (queriesKeys, proofQueries) => { + for (let i = 0; i < queriesKeys.length; i++) { + if (isInclusion(queriesKeys[i], proofQueries[i])) { + return false; + } + } + + return true; + } + const isInclusionProof = (queriesKeys, proofQueries) => { + for (let i = 0; i < queriesKeys.length; i++) { + if (!isInclusion(queriesKeys[i], proofQueries[i])) { + return false; + } + } + + return true; + } + if (isNonInclusionProof(queryKeys, proof.queries)) { + await expect(smt.verifyNonInclusionProof(Buffer.from(outputMerkleRoot, 'hex'), queryKeys, proof)).resolves.toEqual(true); + await expect(smt.verifyInclusionProof(Buffer.from(outputMerkleRoot, 'hex'), queryKeys, proof)).resolves.toEqual(false); + } else if (isInclusionProof(queryKeys, proof.queries)) { + await expect(smt.verifyInclusionProof(Buffer.from(outputMerkleRoot, 'hex'), queryKeys, proof)).resolves.toEqual(true); + await expect(smt.verifyNonInclusionProof(Buffer.from(outputMerkleRoot, 'hex'), queryKeys, proof)).resolves.toEqual(false); + } + expect(siblingHashesString).toEqual(outputProof.siblingHashes); expect(queriesString).toEqual(outputProof.queries); await expect(smt.verify(Buffer.from(outputMerkleRoot, 'hex'), queryKeys, proof)).resolves.toEqual(true); @@ -115,6 +145,8 @@ describe('SparseMerkleTree', () => { const randomSiblingPrependedProof = { ...proof }; randomSiblingPrependedProof.siblingHashes = [getRandomBytes(), ...randomSiblingPrependedProof.siblingHashes]; await expect(smt.verify(Buffer.from(outputMerkleRoot, 'hex'), queryKeys, randomSiblingPrependedProof)).resolves.toEqual(false); + await expect(smt.verifyInclusionProof(Buffer.from(outputMerkleRoot, 'hex'), queryKeys, randomSiblingPrependedProof)).resolves.toEqual(false); + await expect(smt.verifyNonInclusionProof(Buffer.from(outputMerkleRoot, 'hex'), queryKeys, randomSiblingPrependedProof)).resolves.toEqual(false); const randomSiblingAppendedProof = { ...proof }; randomSiblingAppendedProof.siblingHashes = [...randomSiblingAppendedProof.siblingHashes, getRandomBytes()]; @@ -182,17 +214,15 @@ describe('SparseMerkleTree', () => { const rootAfterDelete = await smt.update(rootHash, deletingKVPair); const deletedKeyProof = await smt.prove(rootAfterDelete, queryKeys); - const isInclusion = (proofQuery, queryKey) => - queryKey.equals(proofQuery.key) && !proofQuery.value.equals(Buffer.alloc(0)); await expect(smt.verify(rootAfterDelete, queryKeys, deletedKeyProof)).resolves.toEqual(true); for (let i = 0; i < queryKeys.length; i += 1) { const query = queryKeys[i]; const proofQuery = deletedKeyProof.queries[i]; if (inputKeys.find(k => Buffer.from(k, 'hex').equals(query)) !== undefined && [...deleteQueryKeys, ...deletedKeys.map(keyHex => Buffer.from(keyHex, 'hex'))].find(k => k.equals(query)) === undefined) { - expect(isInclusion(proofQuery, query)).toEqual(true); + expect(isInclusion(query, proofQuery)).toEqual(true); } else { - expect(isInclusion(proofQuery, query)).toEqual(false); + expect(isInclusion(query, proofQuery)).toEqual(false); } } }); diff --git a/test/statedb.spec.js b/test/statedb.spec.js index 52e88b5..f78249e 100644 --- a/test/statedb.spec.js +++ b/test/statedb.spec.js @@ -587,9 +587,9 @@ describe('statedb', () => { const queries = [getRandomBytes(38), getRandomBytes(38)]; const proof = await db.prove(root, queries); - const result = await db.verify(root, queries, proof); - - expect(result).toEqual(true); + await expect(db.verify(root, queries, proof)).resolves.toEqual(true); + await expect(db.verifyNonInclusionProof(root, queries, proof)).resolves.toEqual(true); + await expect(db.verifyInclusionProof(root, queries, proof)).resolves.toEqual(false); }); it('should generate wrong non-inclusion proof and verify that a result is not correct', async () => { @@ -599,9 +599,9 @@ describe('statedb', () => { // change sibling hash in proof to make it wrong proof.siblingHashes[0] = getRandomBytes(32); - const result = await db.verify(root, queries, proof); - - expect(result).toEqual(false); + await expect(db.verify(root, queries, proof)).resolves.toEqual(false); + await expect(db.verifyNonInclusionProof(root, queries, proof)).resolves.toEqual(false); + await expect(db.verifyInclusionProof(root, queries, proof)).resolves.toEqual(false); }); it('should generate inclusion proof and verify that a result is correct', async () => { @@ -612,9 +612,9 @@ describe('statedb', () => { ]; const proof = await db.prove(root, queries); - const result = await db.verify(root, queries, proof); - - expect(result).toEqual(true); + await expect(db.verify(root, queries, proof)).resolves.toEqual(true); + await expect(db.verifyNonInclusionProof(root, queries, proof)).resolves.toEqual(false); + await expect(db.verifyInclusionProof(root, queries, proof)).resolves.toEqual(true); }); it('should generate wrong inclusion proof and verify that a result is not correct', async () => { @@ -628,9 +628,9 @@ describe('statedb', () => { // change sibling hash in proof to make it wrong proof.siblingHashes[0] = getRandomBytes(32); - const result = await db.verify(root, queries, proof); - - expect(result).toEqual(false); + await expect(db.verify(root, queries, proof)).resolves.toEqual(false); + await expect(db.verifyNonInclusionProof(root, queries, proof)).resolves.toEqual(false); + await expect(db.verifyInclusionProof(root, queries, proof)).resolves.toEqual(false); }); }); diff --git a/test/utils.js b/test/utils.js index 1987009..a2d2bc1 100644 --- a/test/utils.js +++ b/test/utils.js @@ -18,4 +18,4 @@ const getRandomBytes = (size = 32) => crypto.randomBytes(size); module.exports = { getRandomBytes, -}; \ No newline at end of file +}; diff --git a/types.d.ts b/types.d.ts index 49971d1..b3121fc 100644 --- a/types.d.ts +++ b/types.d.ts @@ -120,6 +120,8 @@ export class StateDB { commit(readWriter: StateReadWriter, height: number, prevRoot: Buffer, options?: StateCommitOption): Promise; prove(root: Buffer, queries: Buffer[]): Promise; verify(root: Buffer, queries: Buffer[], proof: Proof): Promise; + verifyInclusionProof(root: Buffer, queries: Buffer[], proof: Proof): Promise; + verifyNonInclusionProof(root: Buffer, queries: Buffer[], proof: Proof): Promise; finalize(height: number): Promise; newReader(): StateReader; newReadWriter(): StateReadWriter; @@ -134,5 +136,7 @@ export class SparseMerkleTree { update(root: Buffer, kvpair: { key: Buffer, value: Buffer }[]): Promise; prove(root: Buffer, queries: Buffer[]): Promise; verify(root: Buffer, queries: Buffer[], proof: Proof): Promise; + verifyInclusionProof(root: Buffer, queries: Buffer[], proof: Proof): Promise; + verifyNonInclusionProof(root: Buffer, queries: Buffer[], proof: Proof): Promise; calculateRoot(proof: Proof): Promise; } diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..8e79b8a --- /dev/null +++ b/utils.js @@ -0,0 +1,21 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +const isInclusion = (queryKey, proofQuery) => + queryKey.equals(proofQuery.key) && !proofQuery.value.equals(Buffer.alloc(0)); + +module.exports = { + isInclusion, +}; +