Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Commit

Permalink
Introduce two new interfaces for SMT verification (#118)
Browse files Browse the repository at this point in the history
* 🌱 Introduce two new interfaces for verification

- Also introduce a new function `isInclusionProofForQueryKey` to check inclusion & non-inclusion
- Update JS unit test to check two new interfaces

* ♻️ Edit to return an error in verify function

* ♻️ Remove the new Promise from new functions
  • Loading branch information
hrmhatef authored Aug 2, 2023
1 parent a5e8a7a commit 101a43c
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 24 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ jobs:
with:
node-version: ${{ matrix.node_version }}
architecture: ${{ matrix.architecture }}
cache: yarn
- name: Install yarn
run: npm install --global yarn
- uses: actions-rs/toolchain@v1
with:
profile: minimal
Expand Down
24 changes: 21 additions & 3 deletions sparse_merkle_tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
in_memory_smt_verify,
in_memory_smt_calculate_root,
} = require("./bin-package/index.node");
const { isInclusionProofForQueryKey } = require('./utils');

const DEFAULT_KEY_LENGTH = 38;

Expand Down Expand Up @@ -71,14 +72,31 @@ class SparseMerkleTree {
return new Promise((resolve, reject) => {
in_memory_smt_verify.call(null, root, queries, proof, this._keyLength, (err, result) => {
if (err) {
reject(err);
return;
return reject(err);
}
resolve(result);
});
});
}

async verifyInclusionProof(root, queries, proof) {
for (let i = 0; i < queries.length; i++) {
if (!isInclusionProofForQueryKey(queries[i], proof.queries[i])) {
return false;
}
}
return this.verify(root, queries, proof);
}

async verifyNonInclusionProof(root, queries, proof) {
for (let i = 0; i < queries.length; i++) {
if (isInclusionProofForQueryKey(queries[i], proof.queries[i])) {
return false;
}
}
return this.verify(root, queries, proof);
}

async calculateRoot(proof) {
return new Promise((resolve, reject) => {
in_memory_smt_calculate_root.call(null, proof, (err, result) => {
Expand All @@ -94,4 +112,4 @@ class SparseMerkleTree {

module.exports = {
SparseMerkleTree,
};
};
21 changes: 21 additions & 0 deletions state_db.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
'use strict';

const { resolveObjectURL } = require("buffer");
const {
state_db_new,
state_db_close,
Expand Down Expand Up @@ -47,6 +48,7 @@ const {
const { NotFoundError } = require('./error');
const { Iterator } = require("./iterator");
const { getOptionsWithDefault } = require('./options');
const { isInclusionProofForQueryKey } = require('./utils');

class StateReader {
constructor(db) {
Expand Down Expand Up @@ -313,6 +315,25 @@ class StateDB {
});
}


async verifyInclusionProof(root, queries, proof) {
for (let i = 0; i < queries.length; i++) {
if (!isInclusionProofForQueryKey(queries[i], proof.queries[i])) {
return false;
}
}
return this.verify(root, queries, proof);
}

async verifyNonInclusionProof(root, queries, proof) {
for (let i = 0; i < queries.length; i++) {
if (isInclusionProofForQueryKey(queries[i], proof.queries[i])) {
return false;
}
}
return this.verify(root, queries, proof);
}

async finalize(height) {
return new Promise((resolve, reject) => {
state_db_clean_diff_until.call(this._db, height, (err) => {
Expand Down
44 changes: 37 additions & 7 deletions test/sparse_merkle_tree.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 { isInclusionProofForQueryKey } = require('../utils');

const FixturesInclusionProof = require('./fixtures/fixtures_no_delete_inclusion_proof.json');
const FixturesNonInclusionProof = require('./fixtures/fixtures_delete_non_inclusion_proof.json');
Expand Down Expand Up @@ -88,6 +87,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 (isInclusionProofForQueryKey(queriesKeys[i], proofQueries[i])) {
return false;
}
}

return true;
}
const isInclusionProof = (queriesKeys, proofQueries) => {
for (let i = 0; i < queriesKeys.length; i++) {
if (!isInclusionProofForQueryKey(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);
Expand All @@ -114,6 +144,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()];
Expand Down Expand Up @@ -181,17 +213,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(isInclusionProofForQueryKey(query, proofQuery)).toEqual(true);
} else {
expect(isInclusion(proofQuery, query)).toEqual(false);
expect(isInclusionProofForQueryKey(query, proofQuery)).toEqual(false);
}
}
});
Expand Down
24 changes: 12 additions & 12 deletions test/statedb.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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);
});
});

Expand Down
2 changes: 1 addition & 1 deletion test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ const getRandomBytes = (size = 32) => crypto.randomBytes(size);

module.exports = {
getRandomBytes,
};
};
4 changes: 4 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export class StateDB {
commit(readWriter: StateReadWriter, height: number, prevRoot: Buffer, options?: StateCommitOption): Promise<Buffer>;
prove(root: Buffer, queries: Buffer[]): Promise<Proof>;
verify(root: Buffer, queries: Buffer[], proof: Proof): Promise<boolean>;
verifyInclusionProof(root: Buffer, queries: Buffer[], proof: Proof): Promise<boolean>;
verifyNonInclusionProof(root: Buffer, queries: Buffer[], proof: Proof): Promise<boolean>;
finalize(height: number): Promise<void>;
newReader(): StateReader;
newReadWriter(): StateReadWriter;
Expand All @@ -134,5 +136,7 @@ export class SparseMerkleTree {
update(root: Buffer, kvpair: { key: Buffer, value: Buffer }[]): Promise<Buffer>;
prove(root: Buffer, queries: Buffer[]): Promise<Proof>;
verify(root: Buffer, queries: Buffer[], proof: Proof): Promise<boolean>;
verifyInclusionProof(root: Buffer, queries: Buffer[], proof: Proof): Promise<boolean>;
verifyNonInclusionProof(root: Buffer, queries: Buffer[], proof: Proof): Promise<boolean>;
calculateRoot(proof: Proof): Promise<Buffer>;
}
21 changes: 21 additions & 0 deletions utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright © 2023 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 isInclusionProofForQueryKey = (queryKey, proofQuery) =>
queryKey.equals(proofQuery.key) && !proofQuery.value.equals(Buffer.alloc(0));

module.exports = {
isInclusionProofForQueryKey,
};

0 comments on commit 101a43c

Please sign in to comment.