ZKP-License-System/merkle-service/src/index.js

956 lines
No EOL
34 KiB
JavaScript

// Real Merkle Service for License Verification
const express = require('express');
const { Pool } = require('pg');
const cors = require('cors');
const crypto = require('crypto');
const { performance } = require('perf_hooks');
const circomlibjs = require('circomlibjs');
// Initialize Express
const app = express();
app.use(cors());
app.use(express.json());
// Database connection
const db = new Pool({
connectionString: process.env.DATABASE_URL ||
'postgresql://license_admin:secure_license_pass_123@postgres:5432/license_verification',
max: 5
});
// Configuration
const TREE_DEPTH = parseInt(process.env.TREE_DEPTH || '17');
const MAX_LEAVES = Math.pow(2, TREE_DEPTH);
// Poseidon hash instance
let poseidon = null;
let poseidonF = null;
// Current tree in memory
let currentTree = {
version: 0,
root: null,
leaves: [],
leafMap: new Map(), // licenseHash -> leafIndex
layers: [],
isBuilt: false,
treeId: null
};
// Initialize Poseidon
async function initPoseidon() {
if (!poseidon) {
const poseidonJs = await circomlibjs.buildPoseidon();
poseidon = poseidonJs;
poseidonF = poseidonJs.F;
console.log('[Merkle] Poseidon hash initialized');
}
return poseidon;
}
// Convert string to field element
function stringToFieldElement(str) {
let result = BigInt(0);
for (let i = 0; i < Math.min(str.length, 31); i++) {
result = result * BigInt(256) + BigInt(str.charCodeAt(i));
}
return result.toString();
}
// Hash two elements with Poseidon
function poseidonHash2(left, right) {
if (!poseidon) throw new Error('Poseidon not initialized');
return poseidonF.toString(poseidon([left, right]));
}
// Hash license data
function hashLicenseData(licenseData) {
if (!poseidon) throw new Error('Poseidon not initialized');
const inputs = [
stringToFieldElement(licenseData.licenseNumber),
stringToFieldElement(licenseData.practitionerName || 'Anonymous'),
BigInt(licenseData.issuedDate || 0).toString(),
BigInt(licenseData.expiryDate || 0).toString(),
stringToFieldElement(licenseData.jurisdiction || 'Unknown')
];
return poseidonF.toString(poseidon(inputs));
}
// Build Merkle tree with Poseidon
function buildMerkleTree(leaves) {
if (leaves.length === 0) throw new Error('No leaves provided');
// Ensure we have a power of 2 number of leaves
const targetSize = Math.pow(2, Math.ceil(Math.log2(leaves.length)));
const paddedLeaves = [...leaves];
// Pad with zeros
const zeroHash = "0";
while (paddedLeaves.length < targetSize) {
paddedLeaves.push(zeroHash);
}
// Build tree layers
const layers = [paddedLeaves];
let currentLayer = paddedLeaves;
while (currentLayer.length > 1) {
const nextLayer = [];
for (let i = 0; i < currentLayer.length; i += 2) {
const left = currentLayer[i];
const right = currentLayer[i + 1] || left;
nextLayer.push(poseidonHash2(left, right));
}
layers.push(nextLayer);
currentLayer = nextLayer;
}
return {
root: currentLayer[0],
layers: layers,
depth: layers.length - 1
};
}
// Generate Merkle proof for a leaf
function generateMerkleProof(tree, leafIndex) {
if (!tree || !tree.layers) throw new Error('Invalid tree');
if (leafIndex >= tree.layers[0].length) throw new Error('Leaf index out of bounds');
const pathElements = [];
const pathIndices = [];
let currentIndex = leafIndex;
for (let level = 0; level < tree.depth; level++) {
const isRightNode = currentIndex % 2 === 1;
const siblingIndex = isRightNode ? currentIndex - 1 : currentIndex + 1;
if (siblingIndex < tree.layers[level].length) {
pathElements.push(tree.layers[level][siblingIndex]);
pathIndices.push(isRightNode ? "1" : "0");
} else {
// Sibling doesn't exist, use the same node (shouldn't happen with proper padding)
pathElements.push(tree.layers[level][currentIndex]);
pathIndices.push("0");
}
currentIndex = Math.floor(currentIndex / 2);
}
return { pathElements, pathIndices };
}
// Build tree from database licenses
// Build tree from database licenses (with auto-population)
async function buildTreeFromDatabase() {
const startTime = performance.now();
console.log('[Merkle] Building tree from database...', currentTree);
if(currentTree.isBuilt || currentTree.isBuilding){
return null
}
currentTree.isBuilding = true;
try {
// Get active licenses
let result = await db.query(`
SELECT
id,
license_number,
practitioner_name,
EXTRACT(EPOCH FROM issued_date)::INTEGER as issued_date,
EXTRACT(EPOCH FROM expiry_date)::INTEGER as expiry_date,
jurisdiction
FROM licenses
WHERE status = 'active'
ORDER BY id
LIMIT $1
`, [MAX_LEAVES]);
let licenses = result.rows;
console.log(`[Merkle] Found ${licenses.length} active licenses`);
// Auto-generate licenses if none exist
if (licenses.length === 0) {
console.log('[Merkle] No licenses found. Auto-generating 100,000 licenses...');
const BATCH_SIZE = 1000;
const TOTAL_LICENSES = 100000;
const jurisdictions = ['CA', 'NY', 'TX', 'FL', 'IL', 'PA', 'OH', 'GA', 'NC', 'MI'];
await db.query('BEGIN');
try {
for (let batch = 0; batch < TOTAL_LICENSES / BATCH_SIZE; batch++) {
const values = [];
const placeholders = [];
for (let i = 0; i < BATCH_SIZE; i++) {
let licenseIndex = batch * BATCH_SIZE + i + 1;
let licenseNumber = `LIC-${String(licenseIndex).padStart(8, '0')}`;
let practitionerName = `Dr. ${generateRandomName()} ${generateRandomSurname()}`;
let issuedDate = new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000); // Random date in past year
let expiryDate = new Date(Date.now() + (365 + Math.random() * 730) * 24 * 60 * 60 * 1000); // 1-3 years from now
let jurisdiction = jurisdictions[Math.floor(Math.random() * jurisdictions.length)];
let status = Math.random() > 0.02 ? 'active' : 'suspended'; // 98% active
if(i === 0){
status = 'active';
jurisdiction = jurisdictions[0];
practitionerName = 'Test Practitioner 1';
issuedDate = new Date('2025-01-23');
expiryDate = new Date('2028-01-23');
}
// Calculate hash using SHA256
const hash = crypto.createHash('sha256').update(licenseNumber).digest();
values.push(
licenseNumber,
hash,
practitionerName,
issuedDate,
expiryDate,
status,
jurisdiction
);
const offset = i * 7;
placeholders.push(
`($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, $${offset + 7})`
);
}
const insertQuery = `
INSERT INTO licenses (
license_number,
license_hash,
practitioner_name,
issued_date,
expiry_date,
status,
jurisdiction
) VALUES ${placeholders.join(', ')}
ON CONFLICT (license_number) DO NOTHING
`;
await db.query(insertQuery, values);
if ((batch + 1) % 10 === 0) {
console.log(`[Merkle] Inserted ${(batch + 1) * BATCH_SIZE} licenses...`);
}
}
await db.query('COMMIT');
console.log(`[Merkle] Successfully generated ${TOTAL_LICENSES} licenses`);
// Rerun the query to get the newly inserted licenses
result = await db.query(`
SELECT
id,
license_number,
practitioner_name,
EXTRACT(EPOCH FROM issued_date)::INTEGER as issued_date,
EXTRACT(EPOCH FROM expiry_date)::INTEGER as expiry_date,
jurisdiction
FROM licenses
WHERE status = 'active'
ORDER BY id
LIMIT $1
`, [MAX_LEAVES]);
licenses = result.rows;
console.log(`[Merkle] Now have ${licenses.length} active licenses`);
} catch (insertError) {
await db.query('ROLLBACK');
console.error('[Merkle] Failed to generate licenses:', insertError);
throw new Error('Failed to auto-generate licenses: ' + insertError.message);
}
}
// Hash each license to create leaves
const leaves = [];
const leafMap = new Map();
const leafHashToIndex = new Map();
for (let i = 0; i < licenses.length; i++) {
const license = licenses[i];
const leaf = hashLicenseData({
licenseNumber: license.license_number,
practitionerName: license.practitioner_name,
issuedDate: license.issued_date,
expiryDate: license.expiry_date,
jurisdiction: license.jurisdiction
});
leaves.push(leaf);
leafHashToIndex.set(leaf, i);
leafMap.set(license.license_number, i);
// Also store by simple hash for testing
const simpleHash = crypto.createHash('sha256')
.update(license.license_number)
.digest('hex');
leafMap.set(simpleHash, i);
}
// Build the Merkle tree
const tree = buildMerkleTree(leaves);
let treeId;
// Store in database
await db.query('BEGIN');
// Lock the table to prevent concurrent modifications
await db.query('LOCK TABLE merkle_trees IN EXCLUSIVE MODE');
// Deactivate all currently active trees
const deactivateResult = await db.query(
'UPDATE merkle_trees SET is_active = false WHERE is_active = true RETURNING id'
);
if (deactivateResult.rows.length > 0) {
console.log(`[Merkle] Deactivated ${deactivateResult.rows.length} previous tree(s)`);
}
let treeRoot = Buffer.from(tree.root.padStart(64, '0'), 'hex');
// Insert the new active tree
const insertResult = await db.query(`
INSERT INTO merkle_trees (
tree_version,
root_hash,
tree_depth,
leaf_count,
is_active,
finalized_at
)
VALUES ($1, $2, $3, $4, true, NOW())
RETURNING *
`, [
currentTree.version + 1,
tree.root,
tree.depth,
licenses.length
]);
treeId = insertResult.rows[0].id;
// Store leaf mappings (limit for performance)
const leafBatchSize = Math.min(licenses.length, 10000);
console.log(`[Merkle] Storing ${leafBatchSize} leaf mappings...`);
for (let i = 0; i < leafBatchSize; i += 100) {
const batch = [];
for (let j = i; j < Math.min(i + 100, leafBatchSize); j++) {
batch.push([
treeId,
licenses[j].id,
j,
leaves[j]
]);
}
if (batch.length > 0) {
const placeholders = batch.map((_, idx) =>
`($${idx * 4 + 1}, $${idx * 4 + 2}, $${idx * 4 + 3}, $${idx * 4 + 4})`
).join(', ');
const flatValues = batch.flat();
await db.query(`
INSERT INTO merkle_leaves (tree_id, license_id, leaf_index, leaf_hash)
VALUES ${placeholders}
ON CONFLICT DO NOTHING
`, flatValues);
}
// Log progress for large insertions
if ((i + 100) % 1000 === 0) {
console.log(`[Merkle] Stored ${Math.min(i + 100, leafBatchSize)} leaf mappings...`);
}
}
// Commit the transaction
await db.query('COMMIT');
// Update in-memory tree
currentTree = {
version: currentTree.version + 1,
root: tree.root,
leaves: leaves,
leafMap: leafMap,
leafHashToIndex: leafHashToIndex,
layers: tree.layers,
isBuilt: true,
treeId: treeId,
depth: tree.depth,
leafCount: licenses.length,
isBuilding: false,
};
const buildTime = performance.now() - startTime;
console.log(`[Merkle] Tree built in ${buildTime.toFixed(0)}ms`);
console.log(`[Merkle] Root: ${tree.root}`);
console.log(`[Merkle] Total leaves: ${licenses.length}`);
return {
treeId,
root: tree.root,
leafCount: licenses.length,
depth: tree.depth,
buildTimeMs: buildTime
};
} catch (error) {
await db.query('ROLLBACK');
console.error('[Merkle] Build failed:', error);
throw error;
}
}
// Helper functions for generating random names
function generateRandomName() {
const firstNames = [
'James', 'Mary', 'John', 'Patricia', 'Robert', 'Jennifer', 'Michael', 'Linda',
'William', 'Elizabeth', 'David', 'Barbara', 'Richard', 'Susan', 'Joseph', 'Jessica',
'Thomas', 'Sarah', 'Charles', 'Karen', 'Christopher', 'Nancy', 'Daniel', 'Lisa',
'Matthew', 'Betty', 'Anthony', 'Helen', 'Mark', 'Sandra', 'Donald', 'Donna',
'Steven', 'Carol', 'Bill', 'Ruth', 'Paul', 'Sharon', 'Joshua', 'Michelle',
'Kenneth', 'Laura', 'Kevin', 'Sarah', 'Brian', 'Kimberly', 'George', 'Deborah'
];
return firstNames[Math.floor(Math.random() * firstNames.length)];
}
function generateRandomSurname() {
const surnames = [
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis',
'Rodriguez', 'Martinez', 'Hernandez', 'Lopez', 'Gonzalez', 'Wilson', 'Anderson',
'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin', 'Lee', 'Perez', 'Thompson',
'White', 'Harris', 'Sanchez', 'Clark', 'Ramirez', 'Lewis', 'Robinson', 'Walker',
'Young', 'Allen', 'King', 'Wright', 'Scott', 'Torres', 'Nguyen', 'Hill',
'Flores', 'Green', 'Adams', 'Nelson', 'Baker', 'Hall', 'Rivera', 'Campbell',
'Mitchell', 'Carter', 'Roberts', 'Gomez', 'Phillips', 'Evans', 'Turner', 'Diaz'
];
return surnames[Math.floor(Math.random() * surnames.length)];
}
// Generate mock tree for testing
async function buildMockTree() {
console.log('[Merkle] Building mock tree...');
const mockLicenses = [];
const leafMap = new Map();
const leaves = [];
// Generate mock licenses
for (let i = 0; i < 1000; i++) {
const licenseNumber = `LIC-${String(i + 1).padStart(8, '0')}`;
const licenseData = {
licenseNumber: licenseNumber,
practitionerName: `Test Practitioner ${i + 1}`,
issuedDate: Math.floor(Date.now() / 1000) - 31536000, // 1 year ago
expiryDate: Math.floor(Date.now() / 1000) + 31536000 * 2, // 2 years from now
jurisdiction: ['CA', 'NY', 'TX', 'FL'][i % 4]
};
const leaf = hashLicenseData(licenseData);
leaves.push(leaf);
leafMap.set(licenseNumber, i);
// Also map simple hash
const simpleHash = crypto.createHash('sha256').update(licenseNumber).digest('hex');
leafMap.set(simpleHash, i);
}
const tree = buildMerkleTree(leaves);
currentTree = {
version: currentTree.version + 1,
root: tree.root,
leaves: leaves,
leafMap: leafMap,
layers: tree.layers,
isBuilt: true,
treeId: 'mock-' + Date.now(),
depth: tree.depth,
leafCount: leaves.length
};
console.log(`[Merkle] Mock tree built with ${leaves.length} leaves`);
console.log(`[Merkle] Root: ${tree.root}`);
return currentTree;
}
app.get('/api/merkle-proof-by-hash/:leafHash', async (req, res) => {
const startTime = performance.now();
const { leafHash } = req.params;
console.log(`[Merkle] ===== PRIVACY-PRESERVING PROOF REQUEST =====`);
console.log(`[Merkle] Leaf hash: ${leafHash.substring(0, 20)}...`);
console.log(`[Merkle] Server does NOT see license details!`);
try {
// Ensure tree is built
if (!currentTree.isBuilt) {
await buildTreeFromDatabase();
}
// Find this leaf hash in the tree
let leafIndex = currentTree.leaves.findIndex(leaf => leaf === leafHash);
if (leafIndex === -1) {
console.log(`[Merkle] Generating proof anyway - will fail verification`);
leafIndex = 0;
}
console.log(`[Merkle] Found leaf at index: ${leafIndex}`);
// Generate proof
const proof = generateMerkleProof(
{ layers: currentTree.layers, depth: currentTree.depth },
leafIndex
);
const generationTime = performance.now() - startTime;
console.log(`[Merkle] Proof generated in ${generationTime.toFixed(0)}ms`);
console.log(`[Merkle] Privacy preserved - no PII exposed!`);
console.log(`[Merkle] ===== END PRIVACY-PRESERVING REQUEST =====`);
res.json({
pathElements: proof.pathElements,
pathIndices: proof.pathIndices,
root: currentTree.root,
leafIndex: leafIndex,
leaf: leafHash,
generationTimeMs: generationTime,
treeVersion: currentTree.version,
});
} catch (error) {
console.error('[Merkle] Proof generation failed:', error);
res.status(500).json({
error: 'Failed to generate proof',
details: error.message
});
}
});
// API: Get Merkle proof for a license
app.get('/api/merkle-proof/:identifier', async (req, res) => {
const startTime = performance.now();
const { identifier } = req.params;
console.log(`[Merkle] ===== PROOF REQUEST =====`);
console.log(`[Merkle] Identifier: ${identifier}`);
try {
// Ensure tree is built
if (!currentTree.isBuilt) {
try {
await buildTreeFromDatabase();
} catch (err) {
console.log('[Merkle] Database build failed, using mock:', err.message);
await buildMockTree();
}
}
// Try to find leaf index in the tree
let leafIndex = currentTree.leafMap.get(identifier);
let licenseData = null;
let foundInTree = leafIndex !== undefined;
console.log(`[Merkle] Leaf index from map: ${leafIndex}`);
// // Always try to fetch license data from database
// try {
// const result = await db.query(`
// SELECT
// id,
// license_number,
// practitioner_name,
// EXTRACT(EPOCH FROM issued_date)::INTEGER as issued_date,
// EXTRACT(EPOCH FROM expiry_date)::INTEGER as expiry_date,
// jurisdiction,
// status
// FROM licenses
// WHERE license_number = $1
// LIMIT 1
// `, [identifier]);
// if (result.rows.length > 0) {
// licenseData = result.rows[0];
// console.log(`[Merkle] Found license data in DB:`, {
// licenseNumber: licenseData.license_number,
// practitionerName: licenseData.practitioner_name,
// issuedDate: licenseData.issued_date,
// expiryDate: licenseData.expiry_date,
// jurisdiction: licenseData.jurisdiction
// });
// }
// } catch (err) {
// console.log('[Merkle] Database lookup failed:', err.message);
// }
// If not found in tree, use index 0 (proof will fail validation)
if (!foundInTree) {
console.log(`[Merkle] License NOT found in tree, using index 0 (proof will fail validation)`);
leafIndex = 0;
}
console.log(`[Merkle] Using leaf index: ${leafIndex}`);
console.log(`[Merkle] Leaf hash at this index: ${currentTree.leaves[leafIndex]}`);
// Generate proof (even if license not in tree)
const proof = generateMerkleProof(
{
layers: currentTree.layers,
depth: currentTree.depth
},
leafIndex
);
console.log(`[Merkle] Proof generated:`);
console.log(`[Merkle] - Root: ${currentTree.root}`);
console.log(`[Merkle] - Leaf: ${currentTree.leaves[leafIndex]}`);
console.log(`[Merkle] - Found in tree: ${foundInTree}`);
console.log(`[Merkle] - Path indices: [${proof.pathIndices.join(', ')}]`);
console.log(`[Merkle] - First 3 path elements:`, proof.pathElements.slice(0, 3));
console.log(`[Merkle] ===== END PROOF REQUEST =====`);
const generationTime = performance.now() - startTime;
const response = {
pathElements: proof.pathElements,
pathIndices: proof.pathIndices,
root: currentTree.root,
leafIndex: leafIndex,
leaf: currentTree.leaves[leafIndex],
foundInTree: foundInTree, // Flag to indicate if license was in tree
licenseData: null,
generationTimeMs: generationTime,
treeVersion: currentTree.version
};
console.log(`[Merkle] Proof generated in ${generationTime.toFixed(0)}ms`);
res.json(response);
} catch (error) {
console.error('[Merkle] Proof generation failed:', error);
res.status(500).json({
error: 'Failed to generate proof',
details: error.message
});
}
});
// In merkle-service, update the endpoint:
// app.get('/api/merkle-proof/:identifier', async (req, res) => {
// const startTime = performance.now();
// const { identifier } = req.params;
// console.log(`[Merkle] ===== PROOF REQUEST =====`);
// console.log(`[Merkle] Identifier: ${identifier}`);
// try {
// if (!currentTree.isBuilt) {
// await buildTreeFromDatabase();
// }
// // First, find the leaf index in memory
// let leafIndex = currentTree.leafMap.get(identifier);
// if (leafIndex === undefined) {
// return res.status(404).json({
// error: 'License not found in Merkle tree',
// identifier: identifier
// });
// }
// // Get the actual license data from database (without the corrupted leaf_hash)
// let licenseData = null;
// try {
// const result = await db.query(`
// SELECT
// license_number,
// practitioner_name,
// EXTRACT(EPOCH FROM issued_date)::INTEGER as issued_date,
// EXTRACT(EPOCH FROM expiry_date)::INTEGER as expiry_date,
// jurisdiction
// FROM licenses
// WHERE license_number = $1
// LIMIT 1
// `, [identifier]);
// if (result.rows.length > 0) {
// licenseData = result.rows[0];
// console.log(`[Merkle] Found license data:`, licenseData);
// }
// } catch (err) {
// console.error('[Merkle] Database lookup failed:', err.message);
// }
// if (!licenseData) {
// return res.status(404).json({ error: 'License data not found' });
// }
// // Get leaf hash from in-memory tree (this is correct)
// const leafHash = currentTree.leaves[leafIndex];
// console.log(`[Merkle] Using leaf index: ${leafIndex}`);
// console.log(`[Merkle] Leaf hash: ${leafHash}`);
// // Generate proof
// const proof = generateMerkleProof(
// { layers: currentTree.layers, depth: currentTree.depth },
// leafIndex
// );
// console.log(`[Merkle] Proof generated:`);
// console.log(`[Merkle] - Root: ${currentTree.root}`);
// console.log(`[Merkle] - Path indices: [${proof.pathIndices.join(', ')}]`);
// console.log(`[Merkle] ===== END PROOF REQUEST =====`);
// const generationTime = performance.now() - startTime;
// res.json({
// pathElements: proof.pathElements,
// pathIndices: proof.pathIndices,
// root: currentTree.root,
// leafIndex: leafIndex,
// leaf: leafHash, // From in-memory tree
// licenseData: {
// licenseNumber: licenseData.license_number,
// practitionerName: licenseData.practitioner_name,
// issuedDate: licenseData.issued_date,
// expiryDate: licenseData.expiry_date,
// jurisdiction: licenseData.jurisdiction
// },
// generationTimeMs: generationTime,
// treeVersion: currentTree.version
// });
// } catch (error) {
// console.error('[Merkle] Proof generation failed:', error);
// res.status(500).json({
// error: 'Failed to generate proof',
// details: error.message
// });
// }
// });
// API: Build/rebuild tree
app.post('/api/rebuild-tree', async (req, res) => {
try {
console.log('[Merkle] Api Rebuild...');
const result = await buildTreeFromDatabase();
res.json({
success: true,
...result
});
} catch (error) {
console.error('[Merkle] Rebuild failed:', error);
// Try mock tree as fallback
try {
const mockResult = await buildMockTree();
res.json({
success: true,
mode: 'mock',
root: mockResult.root,
leafCount: mockResult.leafCount,
treeId: mockResult.treeId
});
} catch (mockError) {
res.status(500).json({
success: false,
error: error.message
});
}
}
});
// API: Get tree info
app.get('/api/tree-info', async (req, res) => {
try {
if (!currentTree.isBuilt && !currentTree.isBuilding) {
try {
console.log('[Merkle] Tree Info build...');
await buildTreeFromDatabase();
} catch (err) {
await buildMockTree();
}
}
console.log("TreeId", JSON.stringify(currentTree?.treeId, null, 4));
res.json({
id: currentTree.treeId,
version: currentTree.version,
root: currentTree.root,
leafCount: currentTree.leafCount,
depth: currentTree.depth || TREE_DEPTH,
maxCapacity: MAX_LEAVES,
isBuilt: currentTree.isBuilt,
mode: `${currentTree?.treeId}`?.includes('mock') ? 'mock' : 'real',
createdAt: new Date().toISOString()
});
} catch (error) {
console.error('[Merkle] Failed to get tree info:', error);
res.status(500).json({ error: 'Failed to get tree info' });
}
});
// API: Verify a proof
app.post('/api/verify-proof', async (req, res) => {
try {
const { leaf, root, pathElements, pathIndices } = req.body;
// Reconstruct the root from the proof
let computedHash = leaf;
for (let i = 0; i < pathElements.length; i++) {
const sibling = pathElements[i];
const isLeft = pathIndices[i] === "0";
if (isLeft) {
computedHash = poseidonHash2(sibling, computedHash);
} else {
computedHash = poseidonHash2(computedHash, sibling);
}
}
const isValid = computedHash === root;
res.json({
valid: isValid,
computedRoot: computedHash,
expectedRoot: root
});
} catch (error) {
console.error('[Merkle] Proof verification failed:', error);
res.status(500).json({ error: 'Failed to verify proof' });
}
});
// Health check
app.get('/health', async (req, res) => {
try {
let dbHealthy = false;
try {
await db.query('SELECT 1');
dbHealthy = true;
} catch (err) {
console.log('[Merkle] Database unhealthy:', err.message);
}
res.json({
status: 'healthy',
service: 'merkle-service',
timestamp: new Date().toISOString(),
treeVersion: currentTree.version,
hasActiveTree: currentTree.isBuilt,
root: currentTree.root,
leafCount: currentTree.leafCount,
databaseConnected: dbHealthy,
poseidonReady: poseidon !== null
});
} catch (error) {
res.status(503).json({
status: 'unhealthy',
service: 'merkle-service',
error: error.message
});
}
});
// Statistics
app.get('/api/stats', async (req, res) => {
try {
const stats = {
currentTreeVersion: currentTree.version,
currentTreeLeaves: currentTree.leafCount || 0,
currentTreeDepth: currentTree.depth || TREE_DEPTH,
treeBuilt: currentTree.isBuilt,
maxCapacity: MAX_LEAVES,
poseidonInitialized: poseidon !== null,
mode: `${currentTree?.treeId}`?.includes('mock') ? 'mock' : 'real',
};
// Try to get database stats
try {
const result = await db.query(`
SELECT
COUNT(*) as total_licenses,
COUNT(*) FILTER (WHERE status = 'active') as active_licenses,
COUNT(*) FILTER (WHERE expiry_date > CURRENT_DATE) as valid_licenses
FROM licenses
`);
if (result.rows.length > 0) {
stats.totalLicenses = parseInt(result.rows[0].total_licenses);
stats.activeLicenses = parseInt(result.rows[0].active_licenses);
stats.validLicenses = parseInt(result.rows[0].valid_licenses);
}
} catch (err) {
console.log('[Merkle] Could not get database stats');
}
res.json(stats);
} catch (error) {
console.error('[Merkle] Failed to get stats:', error);
res.status(500).json({ error: 'Failed to get statistics' });
}
});
// Initialize service
async function initialize() {
try {
// Initialize Poseidon
await initPoseidon();
// Try to build tree from database, fall back to mock
try {
console.log('[Merkle] Initial build...');
await buildTreeFromDatabase();
console.log('[Merkle] Initial tree built from database');
} catch (err) {
console.log('[Merkle] Database build failed:', err.message);
await buildMockTree();
console.log('[Merkle] Using mock tree for testing');
}
} catch (error) {
console.error('[Merkle] Initialization failed:', error);
}
}
// Start the server
const PORT = process.env.MERKLE_PORT || 8082;
app.listen(PORT, async () => {
console.log(`[Merkle] Service listening on port ${PORT}`);
console.log(`[Merkle] Health check: http://localhost:${PORT}/health`);
// Initialize after startup
setTimeout(initialize, 2000);
});
// Periodic rebuild
if (process.env.UPDATE_INTERVAL) {
const interval = parseInt(process.env.UPDATE_INTERVAL) * 1000;
setInterval(async () => {
console.log('[Merkle] Scheduled rebuild...');
try {
await buildTreeFromDatabase();
} catch (error) {
console.error('[Merkle] Scheduled rebuild failed:', error);
}
}, interval);
}
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('[Merkle] SIGTERM received, shutting down...');
db.end().then(() => process.exit(0));
});