@jayanth-kumar-morem/indexed-merkle-tree
Version:
Indexed Merkle Tree implemented in Typescript and circom circuits using Poseidon hashing to perform non membership proofs with serialising and deserialising to off chain storage
486 lines (343 loc) โข 13.6 kB
Markdown
# Indexed Merkle Tree
[](https://badge.fury.io/js/@jayanth-kumar-morem%2Findexed-merkle-tree)
[](https://opensource.org/licenses/MIT)
A TypeScript implementation of Indexed Merkle Trees with Poseidon hash function support for generating and verifying non-membership proofs. This library provides an efficient way to prove that a value does not exist in a set using cryptographic proofs.
## Features
- ๐ณ **Indexed Merkle Tree**: Efficient tree structure for membership/non-membership proofs
- ๐ **Poseidon Hash**: Uses Poseidon hash function for cryptographic security
- โก **Circom Circuits**: Included zk-SNARK circuits for zero-knowledge proofs
- ๐ฆ **Serialization**: Complete state serialization/deserialization support
- ๐พ **File I/O**: Save and load tree states to/from files
- โ
**Type Safe**: Full TypeScript support with comprehensive type definitions
- ๐งช **Well Tested**: Comprehensive test suite with 100% functionality coverage
- ๐ **Zero Dependencies**: Only requires circomlibjs for Poseidon hashing
## Installation
```bash
npm install @jayanth-kumar-morem/indexed-merkle-tree circomlibjs
```
## Quick Start
```javascript
import { IndexedMerkleTree } from '@jayanth-kumar-morem/indexed-merkle-tree';
import { buildPoseidon } from 'circomlibjs';
async function example() {
// Initialize Poseidon hash function
const poseidon = await buildPoseidon();
// Create a new Indexed Merkle Tree
const tree = new IndexedMerkleTree(poseidon);
// Insert values
await tree.insert(100n);
await tree.insert(200n);
await tree.insert(150n);
// Generate a non-membership proof for value 175
const proof = tree.createNonMembershipProof(175n);
// Verify the proof
const isValid = tree.verifyNonMembershipProof(proof);
console.log('Proof is valid:', isValid); // true
// Save tree state
await tree.saveToFile('./tree-state.json');
// Load tree state
const loadedTree = await IndexedMerkleTree.loadFromFile('./tree-state.json');
}
example().catch(console.error);
```
## API Reference
### Class: IndexedMerkleTree
#### Constructor
```typescript
constructor(poseidon: any)
```
Creates a new Indexed Merkle Tree instance.
**Parameters:**
- `poseidon`: Poseidon hash function instance from circomlibjs
#### Properties
- `depth: number` - Tree depth (readonly, always 32)
- `size: number` - Number of leaves in the tree
- `root: bigint` - Current root hash of the tree
#### Methods
##### `async insert(value: bigint): Promise<void>`
Inserts a new value into the tree.
**Parameters:**
- `value`: The bigint value to insert
**Throws:**
- Error if the value already exists in the tree
##### `createNonMembershipProof(query: bigint): NonMembershipProof`
Creates a non-membership proof for a given value.
**Parameters:**
- `query`: The bigint value to prove non-membership for
**Returns:**
- `NonMembershipProof` object containing proof data
**Throws:**
- Error if no predecessor is found (empty tree)
##### `verifyNonMembershipProof(proof: NonMembershipProof): boolean`
Verifies a non-membership proof.
**Parameters:**
- `proof`: The proof object to verify
**Returns:**
- `true` if proof is valid, `false` otherwise
##### `serialize(): SerializedIMT`
Serializes the tree state to a JSON-compatible object.
**Returns:**
- `SerializedIMT` object with string representations of bigint values
##### `static deserialize(data: SerializedIMT, poseidon: any): IndexedMerkleTree`
Deserializes a tree state from a JSON-compatible object.
**Parameters:**
- `data`: Serialized tree data
- `poseidon`: Poseidon hash function instance
**Returns:**
- New `IndexedMerkleTree` instance with restored state
##### `async saveToFile(filePath: string): Promise<void>`
Saves the tree state to a JSON file.
**Parameters:**
- `filePath`: Path where to save the file (directories will be created if needed)
##### `static async loadFromFile(filePath: string, poseidon?: any): Promise<IndexedMerkleTree>`
Loads a tree state from a JSON file.
**Parameters:**
- `filePath`: Path to the file to load
- `poseidon`: Optional Poseidon instance (will be built if not provided)
**Returns:**
- New `IndexedMerkleTree` instance with loaded state
##### `getLeaves(): Leaf[]`
Returns a copy of all leaves in the tree.
**Returns:**
- Array of `Leaf` objects
##### `getNodesAtLevel(level: number): bigint[]`
Returns all nodes at a specific tree level.
**Parameters:**
- `level`: Tree level (0 = leaves, increasing towards root)
**Returns:**
- Array of bigint node values
**Throws:**
- Error if level is invalid
### Interfaces
#### Leaf
```typescript
interface Leaf {
val: bigint; // The leaf value
nextVal: bigint; // Next value in sorted order (0n if last)
nextIdx: number; // Index of next leaf
}
```
#### NonMembershipProof
```typescript
interface NonMembershipProof {
query: bigint; // The value being proved absent
preLeaf: Leaf; // The predecessor leaf
path: bigint[]; // Merkle path (sibling hashes)
directions: number[]; // Path directions (0 = left, 1 = right)
root: bigint; // Tree root at time of proof
}
```
#### SerializedIMT
```typescript
interface SerializedIMT {
depth: number;
nodes: string[][]; // String representations of bigint values
leaves: {
val: string; // String representation of bigint
nextVal: string; // String representation of bigint
nextIdx: number;
}[];
}
```
## Use Cases
### Privacy-Preserving Applications
- Prove you're not in a blocklist without revealing your identity
- Demonstrate absence from a database without exposing the data
### Zero-Knowledge Proofs
- Integration with zk-SNARK circuits for scalable privacy
- Batch verification of multiple non-membership claims
### Blockchain Applications
- Efficient state exclusion proofs
- Privacy-preserving transaction validation
## Circom Circuits for Zero-Knowledge Proofs
This package includes pre-built circom circuits for generating zero-knowledge proofs of non-membership and insertion operations. These circuits allow you to prove statements about the tree without revealing the actual tree structure or values.
### Accessing Circom Files
The circom files are included in the npm package and can be accessed like this:
```javascript
const path = require('path');
const fs = require('fs');
// Get the package installation path
const packagePath = path.dirname(require.resolve('@jayanth-kumar-morem/indexed-merkle-tree/package.json'));
// Read the circom circuits
const nonMembershipCircuit = fs.readFileSync(
path.join(packagePath, 'circom', 'IMTNonMembership.circom'),
'utf8'
);
const insertionCircuit = fs.readFileSync(
path.join(packagePath, 'circom', 'IMTInsertion.circom'),
'utf8'
);
```
### Available Circuits
#### 1. IMTNonMembership.circom
Proves that a value does not exist in the indexed merkle tree.
**Template**: `IMTNonMembership(depth)`
**Public Inputs**:
- `root_pub`: The merkle tree root (public)
**Private Inputs**:
- `query`: The value to prove non-membership for
- `pre_val`: The predecessor value (should be 0 for IMT)
- `pre_next`: The next value after predecessor
- `path[depth]`: Merkle path (sibling hashes)
- `dirs[depth]`: Path directions (0 = left, 1 = right)
**Usage Example**:
```javascript
// Generate a non-membership proof using the TypeScript library
const tree = new IndexedMerkleTree(poseidon);
await tree.insert(100n);
await tree.insert(200n);
const proof = tree.createNonMembershipProof(150n);
// Use the proof data as circuit inputs
const circuitInputs = {
query: proof.query.toString(),
pre_val: proof.preLeaf.val.toString(),
pre_next: proof.preLeaf.nextVal.toString(),
path: proof.path.map(p => p.toString()),
dirs: proof.directions,
root_pub: proof.root.toString()
};
// Compile and run the circuit with your preferred circom toolchain
// (snarkjs, circom_tester, etc.)
```
#### 2. IMTInsertion.circom
Proves that a value was correctly inserted into the indexed merkle tree, transforming it from an old state to a new state.
**Template**: `IMTInsertion(depth)`
**Inputs**:
- `query`: The value being inserted
- `pre_val`: The predecessor value (should be 0 for IMT)
- `pre_next`: The next value after predecessor
- `path[depth]`: Merkle path to the predecessor leaf
- `dirs[depth]`: Path directions (0 = left, 1 = right)
- `old_root`: The tree root before insertion
**Outputs**:
- `new_root`: The tree root after insertion
**Usage Example**:
```javascript
// Before insertion
const tree = new IndexedMerkleTree(poseidon);
await tree.insert(100n);
await tree.insert(300n);
const oldRoot = tree.root;
// Generate non-membership proof for the value to be inserted
const proof = tree.createNonMembershipProof(200n);
// Perform the insertion
await tree.insert(200n);
const newRoot = tree.root;
// Use the data as circuit inputs
const circuitInputs = {
query: "200",
pre_val: proof.preLeaf.val.toString(),
pre_next: proof.preLeaf.nextVal.toString(),
path: proof.path.map(p => p.toString()),
dirs: proof.directions,
old_root: oldRoot.toString()
};
// The circuit will output new_root which should match newRoot.toString()
```
### Integration with zk-SNARK Tools
These circuits are designed to work with standard circom toolchains:
#### Using with snarkjs
```bash
# Compile the circuit
circom circom/IMTNonMembership.circom --r1cs --wasm --sym
# Generate witness
node generate_witness.js IMTNonMembership.wasm input.json witness.wtns
# Generate proof (requires trusted setup)
snarkjs groth16 prove circuit.zkey witness.wtns proof.json public.json
```
#### Using with circom_tester (for testing)
```javascript
const circom_tester = require("circom_tester");
const path = require("path");
describe("IMT Circuits", () => {
let circuit;
before(async () => {
const packagePath = path.dirname(require.resolve('@jayanth-kumar-morem/indexed-merkle-tree/package.json'));
const circuitPath = path.join(packagePath, 'circom', 'IMTNonMembership.circom');
circuit = await circom_tester.wasm(circuitPath);
});
it("should verify non-membership proof", async () => {
const input = {
query: "150",
pre_val: "0",
pre_next: "200",
path: [...], // from your proof
dirs: [...], // from your proof
root_pub: "..." // tree root
};
const witness = await circuit.calculateWitness(input);
await circuit.checkConstraints(witness);
});
});
```
## Advanced Usage
### Working with Large Values
```javascript
const largeValue = BigInt('0x' + 'f'.repeat(64)); // 32-byte value
await tree.insert(largeValue);
const proof = tree.createNonMembershipProof(largeValue - 1n);
const isValid = tree.verifyNonMembershipProof(proof);
```
### Batch Operations
```javascript
const values = [100n, 200n, 300n, 150n, 250n];
// Insert multiple values
for (const value of values) {
await tree.insert(value);
}
// Generate multiple proofs
const queries = [125n, 175n, 275n, 400n];
const proofs = queries.map(q => tree.createNonMembershipProof(q));
// Verify all proofs
const results = proofs.map(proof => tree.verifyNonMembershipProof(proof));
console.log('All proofs valid:', results.every(r => r));
```
### State Persistence
```javascript
// Save tree state
await tree.saveToFile('./data/tree-backup.json');
// Load and continue working
const restoredTree = await IndexedMerkleTree.loadFromFile('./data/tree-backup.json');
await restoredTree.insert(999n);
// Verify proofs still work
const proof = restoredTree.createNonMembershipProof(500n);
const isValid = restoredTree.verifyNonMembershipProof(proof);
```
## Performance
- **Tree Depth**: Fixed at 32 levels for optimal balance of security and performance
- **Insertion**: O(log n) time complexity
- **Proof Generation**: O(log n) time complexity
- **Proof Verification**: O(log n) time complexity
- **Memory Usage**: Efficient sparse tree representation
## Security Considerations
- Uses Poseidon hash function designed for zero-knowledge applications
- 32-level tree provides 2^32 capacity with strong security guarantees
- All operations are deterministic and verifiable
- Proofs are cryptographically sound and cannot be forged
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
### Development Setup
```bash
git clone https://github.com/jayanth-kumar-morem/indexed-merkle-tree.git
cd indexed-merkle-tree
npm install
npm run build
npm test
```
### Running Tests
```bash
npm test # Run all tests
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverage
```
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Author
**Jayanth Kumar**
- Email: jayanthkumar1903@gmail.com
- GitHub: [@jayanth-kumar-morem](https://github.com/jayanth-kumar-morem)
## Acknowledgments
- Built with [circomlibjs](https://github.com/iden3/circomlibjs) for Poseidon hashing
- Inspired by the indexed Merkle tree research in zero-knowledge cryptography
---
For more examples and detailed documentation, visit the [GitHub repository](https://github.com/jayanth-kumar-morem/indexed-merkle-tree).