1029 lines
No EOL
41 KiB
HTML
1029 lines
No EOL
41 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>License Verification ZKP System - Real Implementation</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/snarkjs@0.7.5/build/snarkjs.min.js"></script>
|
|
<script src="/js/zkp-bundle.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
background: #f0ead6;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
color: white;
|
|
text-align: center;
|
|
margin-bottom: 10px;
|
|
font-size: 2.5rem;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.subtitle {
|
|
color: rgba(255,255,255,0.9);
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.mode-indicator {
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.mode-badge {
|
|
display: inline-block;
|
|
padding: 8px 20px;
|
|
background: white;
|
|
border-radius: 20px;
|
|
font-weight: bold;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.mode-badge.real {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
|
|
.mode-badge.mock {
|
|
background: #f59e0b;
|
|
color: white;
|
|
}
|
|
|
|
.dashboard {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
padding: 25px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.card:hover {
|
|
transform: translateY(0px);
|
|
}
|
|
|
|
.card h2 {
|
|
color: #333;
|
|
margin-bottom: 20px;
|
|
font-size: 1.5rem;
|
|
border-bottom: 2px solid #667eea;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
color: #555;
|
|
font-weight: 500;
|
|
}
|
|
|
|
input, select {
|
|
width: 100%;
|
|
padding: 10px 15px;
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
transition: border-color 0.3s ease;
|
|
}
|
|
|
|
input:focus, select:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
button {
|
|
width: 100%;
|
|
padding: 12px 20px;
|
|
background: #77a3bd;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
|
}
|
|
|
|
button:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.status {
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
background: #f5f5f5;
|
|
min-height: 60px;
|
|
}
|
|
|
|
.status.success {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
|
|
.status.error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
|
|
.status.loading {
|
|
background: #d1ecf1;
|
|
color: #0c5460;
|
|
border: 1px solid #bee5eb;
|
|
}
|
|
|
|
.metrics {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.metric {
|
|
background: #f8f9fa;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #667eea;
|
|
}
|
|
|
|
.metric-label {
|
|
font-size: 12px;
|
|
color: #666;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.proof-details {
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.timeline {
|
|
margin-top: 20px;
|
|
padding: 20px;
|
|
background: white;
|
|
border-radius: 8px;
|
|
border: 2px dashed #667eea;
|
|
}
|
|
|
|
.timeline-item {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.timeline-icon {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 15px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.timeline-icon.pending {
|
|
background: #e0e0e0;
|
|
color: #666;
|
|
}
|
|
|
|
.timeline-icon.active {
|
|
background: #f59e0b;
|
|
color: white;
|
|
animation: pulse 1s infinite;
|
|
}
|
|
|
|
.timeline-icon.complete {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { transform: scale(1); }
|
|
50% { transform: scale(1.1); }
|
|
100% { transform: scale(1); }
|
|
}
|
|
|
|
.loader {
|
|
border: 3px solid #f3f3f3;
|
|
border-radius: 50%;
|
|
border-top: 3px solid #667eea;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 20px auto;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.warning-box {
|
|
background: #fef3c7;
|
|
border: 1px solid #fbbf24;
|
|
color: #92400e;
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
margin-bottom: 15px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.warning-box svg {
|
|
width: 20px;
|
|
height: 20px;
|
|
margin-right: 10px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1> License Verification ZKP</h1>
|
|
|
|
<div class="dashboard">
|
|
<!-- System Status Card -->
|
|
<div class="card">
|
|
<h2>System Status</h2>
|
|
<div id="systemStatus">
|
|
<div class="metrics">
|
|
<div class="metric">
|
|
<div class="metric-value" id="zkpStatus">-</div>
|
|
<div class="metric-label">ZKP Engine</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="merkleStatus">-</div>
|
|
<div class="metric-label">Merkle Tree</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="circuitStatus">-</div>
|
|
<div class="metric-label">Circuit</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="treeLeaves">-</div>
|
|
<div class="metric-label">Licenses</div>
|
|
</div>
|
|
</div>
|
|
<button onclick="checkSystemStatus()" style="margin-top: 15px;">Refresh Status</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- License Verification Card -->
|
|
<div class="card">
|
|
<h2>Verify License</h2>
|
|
|
|
<div class="warning-box" id="mockWarning" style="display: none;">
|
|
<svg fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
<span>Running in mock mode - compile circuits for real proofs</span>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="licenseNumber">License Number:</label>
|
|
<input type="text" id="licenseNumber" placeholder="LIC-00000001" value="LIC-00000001">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="practitionerName">Practitioner Name:</label>
|
|
<input type="text" id="practitionerName" placeholder="Dr. Jane Smith" value="Test Practitioner 1">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="jurisdiction">Jurisdiction:</label>
|
|
<select id="jurisdiction">
|
|
<option value="CA">California</option>
|
|
<option value="NY">New York</option>
|
|
<option value="TX">Texas</option>
|
|
<option value="FL">Florida</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="issuedDate">Issued Date:</label>
|
|
<input type="number" id="issuedDate" value="1737590400" min="1" max="100000000000">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="expiryDate">Expirey Date:</label>
|
|
<input type="number" id="expiryDate" value="1832198400" min="1" max="100000000000">
|
|
</div>
|
|
<!-- <div class="form-group">
|
|
<label for="validityDays">Minimum Validity (days):</label>
|
|
<input type="number" id="validityDays" value="30" min="1" max="365">
|
|
</div> -->
|
|
|
|
<button onclick="generateAndVerifyProof()" id="verifyBtn">Generate & Verify Proof</button>
|
|
|
|
<div id="verificationStatus" class="status"></div>
|
|
|
|
<div id="proofTimeline" class="timeline" style="display: none;">
|
|
<div class="timeline-item" id="step1">
|
|
<div class="timeline-icon pending">1</div>
|
|
<div>Fetching Merkle proof...</div>
|
|
</div>
|
|
<div class="timeline-item" id="step2">
|
|
<div class="timeline-icon pending">2</div>
|
|
<div>Generating ZK proof...</div>
|
|
</div>
|
|
<div class="timeline-item" id="step3">
|
|
<div class="timeline-icon pending">3</div>
|
|
<div>Verifying proof...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Proof Details Card -->
|
|
<div class="card">
|
|
<h2>Proof Details</h2>
|
|
<div id="proofDetails" class="proof-details">
|
|
<p style="color: #999;">No proof generated yet</p>
|
|
</div>
|
|
</div>
|
|
<br>
|
|
<!-- Performance Metrics Card -->
|
|
<!-- <div class="card">
|
|
<h2>Performance Metrics</h2>
|
|
<div class="metrics">
|
|
<div class="metric">
|
|
<div class="metric-value" id="lastGenTime">-</div>
|
|
<div class="metric-label">Last Generation (ms)</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="lastVerifyTime">-</div>
|
|
<div class="metric-label">Last Verification (ms)</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="proofSize">-</div>
|
|
<div class="metric-label">Proof Size (bytes)</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="totalProofs">0</div>
|
|
<div class="metric-label">Total Proofs</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button onclick="runBenchmark()" style="margin-top: 20px;">Run Performance Benchmark</button>
|
|
<div id="benchmarkResults" style="margin-top: 15px;"></div>
|
|
</div> -->
|
|
</div>
|
|
|
|
<script>
|
|
// Configuration
|
|
const ZKP_API = 'http://localhost:8080';
|
|
const MERKLE_API = 'http://localhost:8082';
|
|
const CIRCUIT_BASE_URL = 'http://localhost:3000';
|
|
|
|
// State
|
|
let proofCount = 0;
|
|
let poseidon = null;
|
|
let poseidonF = null;
|
|
let circuitFiles = null;
|
|
let vKey = null;
|
|
|
|
async function initPoseidon() {
|
|
if (!poseidon) {
|
|
try {
|
|
poseidon = await window.zkpLibs.buildPoseidon();
|
|
poseidonF = poseidon.F;
|
|
console.log('Poseidon initialized successfully');
|
|
} catch (error) {
|
|
console.error('Failed to initialize Poseidon:', error);
|
|
}
|
|
}
|
|
return poseidon;
|
|
}
|
|
async function computeLeafHash(licenseData) {
|
|
await initPoseidon();
|
|
|
|
const inputs = [
|
|
stringToFieldElement(licenseData.licenseNumber),
|
|
stringToFieldElement(licenseData.practitionerName),
|
|
licenseData.issuedDate.toString(),
|
|
licenseData.expiryDate.toString(),
|
|
stringToFieldElement(licenseData.jurisdiction)
|
|
];
|
|
|
|
const leafHash = poseidon.F.toString(poseidon(inputs));
|
|
return leafHash;
|
|
}
|
|
|
|
async function loadCircuitFiles() {
|
|
if (circuitFiles) return circuitFiles;
|
|
|
|
try {
|
|
console.log('Loading circuit files...');
|
|
|
|
// Load WASM and zkey files
|
|
const files = await window.zkpLibs.loadCircuitFiles(
|
|
`${CIRCUIT_BASE_URL}/circuits/license_verification_js/license_verification.wasm`,
|
|
`${CIRCUIT_BASE_URL}/keys/license_verification.zkey`
|
|
);
|
|
|
|
// Load verification key
|
|
const vKeyResponse = await axios.get(`${CIRCUIT_BASE_URL}/api/circuit/vkey`);
|
|
vKey = vKeyResponse.data;
|
|
|
|
circuitFiles = files;
|
|
console.log('Circuit files loaded successfully');
|
|
return files;
|
|
} catch (error) {
|
|
console.error('Failed to load circuit files:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function generateRealProof(input) {
|
|
const circuit = await loadCircuitFiles();
|
|
if (!circuit) return null;
|
|
|
|
try {
|
|
const circuitInputs = formatCircuitInputs(input);
|
|
console.log('[ZKP] ===== LEAF HASH VERIFICATION =====');
|
|
|
|
// Compute what the leaf hash SHOULD be
|
|
await initPoseidon();
|
|
const expectedLeafHash = poseidon.F.toString(poseidon([
|
|
circuitInputs.licenseNumber,
|
|
circuitInputs.practitionerName,
|
|
circuitInputs.issuedDate,
|
|
circuitInputs.expiryDate,
|
|
circuitInputs.jurisdiction
|
|
]));
|
|
|
|
console.log('[ZKP] Expected leaf hash (from Poseidon in JS):', expectedLeafHash);
|
|
console.log('[ZKP] Leaf from Merkle proof:', input.merkleProof?.leaf || 'not provided');
|
|
console.log('[ZKP] Do they match?', expectedLeafHash === input.merkleProof?.leaf);
|
|
console.log('[ZKP] ===== END VERIFICATION =====');
|
|
// In generateRealProof, before fullProve:
|
|
console.log('[ZKP DEBUG] Merkle Path Verification:');
|
|
console.log('[ZKP DEBUG] Leaf would be hash of:', {
|
|
licenseNumber: circuitInputs.licenseNumber,
|
|
practitionerName: circuitInputs.practitionerName,
|
|
issuedDate: circuitInputs.issuedDate,
|
|
expiryDate: circuitInputs.expiryDate,
|
|
jurisdiction: circuitInputs.jurisdiction
|
|
});
|
|
console.log('[ZKP DEBUG] Path has', circuitInputs.pathElements.length, 'levels');
|
|
console.log('[ZKP DEBUG] All path indices:', circuitInputs.pathIndices);
|
|
console.log('[ZKP DEBUG] Note: ALL indices are "1" - is this correct?');
|
|
console.log('[ZKP] Circuit inputs prepared:', circuitInputs);
|
|
const response = await snarkjs.groth16.fullProve(
|
|
circuitInputs,
|
|
circuit.wasm,
|
|
circuit.zkey
|
|
);
|
|
|
|
console.log('[ZKP] Circuit outputs:', response);
|
|
const { proof, publicSignals } = response;
|
|
|
|
// Correct indices based on your circuit output
|
|
const isValid = publicSignals[0];
|
|
const debugExpectedRoot = publicSignals[1];
|
|
const debugComputedRoot = publicSignals[2];
|
|
const merkleRoot = publicSignals[3];
|
|
const currentTimestamp = publicSignals[4];
|
|
const minExpiryTimestamp = publicSignals[5];
|
|
|
|
console.log('[ZKP DEBUG] =====================================');
|
|
console.log('[ZKP DEBUG] isValid:', isValid);
|
|
console.log('[ZKP DEBUG] Expected Root:', debugExpectedRoot);
|
|
console.log('[ZKP DEBUG] Computed Root:', debugComputedRoot);
|
|
console.log('[ZKP DEBUG] Roots Match:', debugExpectedRoot === debugComputedRoot);
|
|
console.log('[ZKP DEBUG] Input merkleRoot:', circuitInputs.merkleRoot);
|
|
console.log('[ZKP DEBUG] =====================================');
|
|
|
|
return { proof, publicSignals };
|
|
} catch (error) {
|
|
console.error('[ZKP] Real proof generation failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
// Format and validate circuit inputs
|
|
function formatCircuitInputs(input) {
|
|
// Ensure all inputs are properly formatted as strings (field elements)
|
|
const formatted = {
|
|
// Private inputs - ensure they're strings
|
|
licenseNumber: ensureFieldElement(input.licenseNumber),
|
|
practitionerName: ensureFieldElement(input.practitionerName),
|
|
issuedDate: ensureFieldElement(input.issuedDate),
|
|
expiryDate: ensureFieldElement(input.expiryDate),
|
|
jurisdiction: ensureFieldElement(input.jurisdiction),
|
|
|
|
// Merkle proof - ensure exactly 17 elements
|
|
pathElements: formatMerklePathElements(input.pathElements),
|
|
pathIndices: formatMerklePathIndices(input.pathIndices),
|
|
|
|
// Public inputs
|
|
merkleRoot: ensureFieldElement(input.merkleRoot),
|
|
currentTimestamp: ensureFieldElement(input.currentTimestamp),
|
|
minExpiryTimestamp: ensureFieldElement(input.minExpiryTimestamp)
|
|
};
|
|
|
|
return formatted;
|
|
}
|
|
|
|
// Ensure a value is formatted as a field element string
|
|
function ensureFieldElement(value) {
|
|
if (value === null || value === undefined) {
|
|
return "0";
|
|
}
|
|
|
|
// If it's already a string representation of a number, return it
|
|
if (typeof value === 'string' && /^\d+$/.test(value)) {
|
|
return value;
|
|
}
|
|
|
|
// If it's a hex string, convert to decimal
|
|
if (typeof value === 'string' && value.startsWith('0x')) {
|
|
console.log(`[ZKP] ensureFieldElement ${value}`);
|
|
return BigInt(value).toString();
|
|
}
|
|
|
|
// If it's a number, convert to string
|
|
if (typeof value === 'number') {
|
|
return Math.floor(value).toString();
|
|
}
|
|
|
|
// If it's a BigInt, convert to string
|
|
if (typeof value === 'bigint') {
|
|
return value.toString();
|
|
}
|
|
|
|
// Default: convert to string
|
|
return value.toString();
|
|
}
|
|
|
|
// Format Merkle path elements to ensure exactly 20 elements
|
|
function formatMerklePathElements(pathElements) {
|
|
const MERKLE_DEPTH = 17;
|
|
const formatted = [];
|
|
|
|
if (!pathElements || !Array.isArray(pathElements)) {
|
|
console.warn('[ZKP] No path elements provided, using zeros');
|
|
return Array(MERKLE_DEPTH).fill("0");
|
|
}
|
|
|
|
for (let i = 0; i < MERKLE_DEPTH; i++) {
|
|
if (i < pathElements.length && pathElements[i] !== undefined) {
|
|
// Convert hex strings to decimal field elements
|
|
let element = pathElements[i];
|
|
|
|
if (typeof element === 'string') {
|
|
// Remove any 0x prefix
|
|
element = element.replace(/^0x/i, '');
|
|
|
|
// If it looks like hex (contains non-decimal chars), convert it
|
|
if (/[a-fA-F]/.test(element)) {
|
|
// Convert hex to decimal string
|
|
console.log(`[ZKP] formatMerklePathElements ${element}`);
|
|
element = BigInt('0x' + element).toString();
|
|
} else if (element === '') {
|
|
element = "0";
|
|
}
|
|
} else if (typeof element === 'number') {
|
|
element = Math.floor(element).toString();
|
|
} else {
|
|
element = "0";
|
|
}
|
|
|
|
formatted.push(element);
|
|
} else {
|
|
// Pad with zeros if not enough elements
|
|
formatted.push("0");
|
|
}
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
|
|
// Format Merkle path indices to ensure exactly 20 elements
|
|
function formatMerklePathIndices(pathIndices) {
|
|
const MERKLE_DEPTH = 17;
|
|
const formatted = [];
|
|
|
|
if (!pathIndices || !Array.isArray(pathIndices)) {
|
|
console.warn('[ZKP] No path indices provided, using zeros');
|
|
return Array(MERKLE_DEPTH).fill("0");
|
|
}
|
|
|
|
for (let i = 0; i < MERKLE_DEPTH; i++) {
|
|
if (i < pathIndices.length && pathIndices[i] !== undefined) {
|
|
let index = pathIndices[i];
|
|
|
|
if (index === 0 || index === "0" || index === false) {
|
|
formatted.push("0");
|
|
} else if (index === 1 || index === "1" || index === true) {
|
|
formatted.push("1");
|
|
} else {
|
|
console.warn(`[ZKP] Invalid path index at position ${i}: ${index}, defaulting to 0`);
|
|
formatted.push("0");
|
|
}
|
|
} else {
|
|
// Default to 0 if not enough indices
|
|
formatted.push("0");
|
|
}
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
|
|
function stringToFieldElement(str) {
|
|
// Convert string to BigInt representation
|
|
let result = BigInt(0);
|
|
console.log(`[ZKP] stringToFieldElement ${str}`);
|
|
for (let i = 0; i < Math.min(str.length, 31); i++) { // Limit to fit in field
|
|
result = result * BigInt(256) + BigInt(str.charCodeAt(i));
|
|
}
|
|
return result.toString();
|
|
}
|
|
|
|
// Check system status
|
|
async function checkSystemStatus() {
|
|
try {
|
|
const [zkpHealth, merkleInfo, circuitStatus, merkleStats] = await Promise.all([
|
|
axios.get(`${ZKP_API}/health`).catch(() => ({data: {status: 'error'}})),
|
|
axios.get(`${MERKLE_API}/api/tree-info`).catch(() => ({data: {}})),
|
|
axios.get(`${ZKP_API}/api/circuit-status`).catch(() => ({data: {}})),
|
|
axios.get(`${MERKLE_API}/api/stats`).catch(() => ({data: {}}))
|
|
]);
|
|
|
|
// Update status indicators
|
|
document.getElementById('zkpStatus').textContent = zkpHealth.data.status === 'healthy' ? 'Y' : 'X';
|
|
document.getElementById('merkleStatus').textContent = merkleInfo.data.isBuilt ? 'Y' : 'X';
|
|
document.getElementById('circuitStatus').textContent = circuitStatus.data.hasCircuit ? 'Y' : 'X';
|
|
document.getElementById('treeLeaves').textContent = merkleInfo.data.leafCount || '0';
|
|
|
|
|
|
// Log details
|
|
console.log('System Status:', {
|
|
zkp: zkpHealth.data,
|
|
merkle: merkleInfo.data,
|
|
circuit: circuitStatus.data,
|
|
stats: merkleStats.data
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Status check failed:', error);
|
|
document.getElementById('zkpStatus').textContent = 'X';
|
|
document.getElementById('merkleStatus').textContent = 'X';
|
|
}
|
|
}
|
|
|
|
// Update timeline step
|
|
function updateTimelineStep(stepId, status) {
|
|
const step = document.getElementById(stepId);
|
|
const icon = step.querySelector('.timeline-icon');
|
|
icon.className = `timeline-icon ${status}`;
|
|
|
|
if (status === 'complete') {
|
|
icon.textContent = '✓';
|
|
}
|
|
}
|
|
|
|
|
|
async function generateProof(req){
|
|
const startTime = performance.now();
|
|
|
|
const {
|
|
licenseData,
|
|
merkleProof,
|
|
merkleRoot,
|
|
currentTimestamp,
|
|
minExpiryTimestamp
|
|
} = req;
|
|
|
|
// Dont validate allow to generate
|
|
// if (!licenseData || !licenseData.licenseNumber) {
|
|
// return res.status(400).json({ error: 'License data required' });
|
|
// }
|
|
|
|
// Get or generate merkle root
|
|
let actualMerkleRoot = merkleRoot;
|
|
|
|
// Ensure pathElements and pathIndices are properly formatted
|
|
let pathElements = merkleProof?.pathElements || [];
|
|
let pathIndices = merkleProof?.pathIndices || [];
|
|
|
|
// Prepare circuit inputs with proper formatting
|
|
const circuitInputs = {
|
|
// Private inputs
|
|
licenseNumber: stringToFieldElement(licenseData?.licenseNumber || ''),
|
|
practitionerName: stringToFieldElement(licenseData?.practitionerName || ''),
|
|
issuedDate: (licenseData?.issuedDate || Math.floor(Date.now() / 1000) - 31536000).toString(),
|
|
expiryDate: (licenseData?.expiryDate || Math.floor(Date.now() / 1000) + 31536000).toString(),
|
|
jurisdiction: stringToFieldElement(licenseData?.jurisdiction || 'Unknown'),
|
|
pathElements: pathElements, // Already formatted by the helper functions
|
|
pathIndices: pathIndices,
|
|
|
|
// Public inputs
|
|
merkleRoot: actualMerkleRoot,
|
|
currentTimestamp: (currentTimestamp || Math.floor(Date.now() / 1000)).toString(),
|
|
minExpiryTimestamp: (minExpiryTimestamp || Math.floor(Date.now() / 1000) + 86400).toString()
|
|
};
|
|
|
|
// Log the formatted inputs for debugging
|
|
console.log('[ZKP] Formatted circuit inputs:', {
|
|
merkleRoot: actualMerkleRoot,
|
|
pathElementsCount: circuitInputs.pathElements.length,
|
|
pathIndicesCount: circuitInputs.pathIndices.length,
|
|
pathElementsSample: circuitInputs.pathElements.slice(0, 2),
|
|
pathIndicesSample: circuitInputs.pathIndices.slice(0, 2)
|
|
});
|
|
|
|
// Try real proof generation first, fall back to mock if needed
|
|
let proofData;
|
|
let isRealProof = false;
|
|
|
|
try {
|
|
proofData = await generateRealProof(circuitInputs);
|
|
if (proofData) {
|
|
isRealProof = true;
|
|
console.log('[ZKP] Generated real proof');
|
|
}
|
|
} catch (err) {
|
|
console.log('[ZKP] Real proof failed:', err.message);
|
|
proofData = { proof: '', };
|
|
}
|
|
const generationTime = performance.now() - startTime;
|
|
|
|
console.log(`[ZKP] Proof generated in ${generationTime.toFixed(0)}ms (${isRealProof ? 'real' : 'mock'})`);
|
|
|
|
return {
|
|
data: {
|
|
proof: proofData.proof,
|
|
publicSignals: proofData.publicSignals,
|
|
generationTimeMs: generationTime,
|
|
proofSize: JSON.stringify(proofData.proof).length,
|
|
isRealProof,
|
|
merkleRoot: actualMerkleRoot
|
|
}
|
|
}
|
|
|
|
}
|
|
// Generate and verify proof
|
|
async function generateAndVerifyProof() {
|
|
const statusDiv = document.getElementById('verificationStatus');
|
|
const timeline = document.getElementById('proofTimeline');
|
|
const detailsDiv = document.getElementById('proofDetails');
|
|
const btn = document.getElementById('verifyBtn');
|
|
|
|
// Reset timeline
|
|
['step1', 'step2', 'step3'].forEach(id => updateTimelineStep(id, 'pending'));
|
|
|
|
btn.disabled = true;
|
|
timeline.style.display = 'block';
|
|
statusDiv.className = 'status loading';
|
|
statusDiv.innerHTML = '<div class="loader"></div><p>Starting verification process...</p>';
|
|
|
|
try {
|
|
const licenseNumber = document.getElementById('licenseNumber').value;
|
|
const practitionerName = document.getElementById('practitionerName').value;
|
|
const jurisdiction = document.getElementById('jurisdiction').value;
|
|
const issuedDate = parseInt(document.getElementById('issuedDate').value);
|
|
const expiryDate = parseInt(document.getElementById('expiryDate').value);
|
|
// const validityDays = parseInt(document.getElementById('validityDays').value);
|
|
let licenseData = {
|
|
"licenseNumber": licenseNumber,
|
|
"practitionerName": practitionerName,
|
|
"issuedDate": issuedDate,
|
|
"expiryDate": expiryDate,
|
|
"jurisdiction": jurisdiction
|
|
}
|
|
const leafHash = await computeLeafHash(licenseData);
|
|
|
|
// Step 1: Get Merkle proof
|
|
updateTimelineStep('step1', 'active');
|
|
const merkleStartTime = performance.now();
|
|
|
|
// const merkleResponse = await axios.get(
|
|
// `${MERKLE_API}/api/merkle-proof/${licenseNumber}`
|
|
// );
|
|
const merkleResponse = await axios.get(
|
|
`${MERKLE_API}/api/merkle-proof-by-hash/${leafHash}`
|
|
);
|
|
console.log('[Frontend] Merkle response:', merkleResponse.data);
|
|
|
|
// console.log(merkleResponse)
|
|
const merkleTime = performance.now() - merkleStartTime;
|
|
updateTimelineStep('step1', 'complete');
|
|
|
|
// Step 2: Generate ZK proof
|
|
updateTimelineStep('step2', 'active');
|
|
statusDiv.innerHTML = '<div class="loader"></div><p>Generating zero-knowledge proof...</p>';
|
|
|
|
const currentTimestamp = Math.floor(Date.now() / 1000);
|
|
const minExpiryTimestamp = currentTimestamp + (30 * 86400);
|
|
|
|
console.log('[API] Merkle response:', {
|
|
root: merkleResponse.data.root,
|
|
leaf: merkleResponse.data.leaf,
|
|
leafIndex: merkleResponse.data.leafIndex
|
|
});
|
|
const proofStartTime = performance.now();
|
|
const proofResponse = await generateProof({
|
|
licenseData,
|
|
//merkleResponse.data.licenseData,
|
|
merkleProof: {
|
|
pathElements: merkleResponse.data.pathElements,
|
|
pathIndices: merkleResponse.data.pathIndices,
|
|
},
|
|
merkleRoot: merkleResponse.data.root,
|
|
currentTimestamp: currentTimestamp,
|
|
minExpiryTimestamp: minExpiryTimestamp
|
|
})
|
|
// const proofResponse = await axios.post(`${ZKP_API}/api/generate-proof`, {
|
|
// licenseData: {
|
|
// "licenseNumber": licenseNumber,
|
|
// "practitionerName": practitionerName,
|
|
// "issuedDate": issuedDate,
|
|
// "expiryDate": expiryDate,
|
|
// "jurisdiction": jurisdiction
|
|
// },
|
|
|
|
// //merkleResponse.data.licenseData,
|
|
// merkleProof: {
|
|
// pathElements: merkleResponse.data.pathElements,
|
|
// pathIndices: merkleResponse.data.pathIndices,
|
|
// leaf: merkleResponse.data.leaf
|
|
// },
|
|
// merkleRoot: merkleResponse.data.root,
|
|
// currentTimestamp: currentTimestamp,
|
|
// minExpiryTimestamp: minExpiryTimestamp
|
|
// });
|
|
|
|
const proofTime = performance.now() - proofStartTime;
|
|
updateTimelineStep('step2', 'complete');
|
|
|
|
// Step 3: Verify proof
|
|
updateTimelineStep('step3', 'active');
|
|
statusDiv.innerHTML = '<div class="loader"></div><p>Verifying proof...</p>';
|
|
|
|
const verifyStartTime = performance.now();
|
|
const verifyResponse = await axios.post(`${ZKP_API}/api/verify-proof`, {
|
|
proof: proofResponse.data.proof,
|
|
publicSignals: proofResponse.data.publicSignals
|
|
});
|
|
const verifyTime = performance.now() - verifyStartTime;
|
|
updateTimelineStep('step3', 'complete');
|
|
|
|
// Update metrics
|
|
// document.getElementById('lastGenTime').textContent = Math.round(proofTime);
|
|
// document.getElementById('lastVerifyTime').textContent = Math.round(verifyTime);
|
|
// document.getElementById('proofSize').textContent = proofResponse.data.proofSize;
|
|
// document.getElementById('totalProofs').textContent = ++proofCount;
|
|
|
|
// Show results
|
|
if (verifyResponse.data.valid) {
|
|
statusDiv.className = 'status success';
|
|
statusDiv.innerHTML = `
|
|
<strong> License Verified!</strong><br>
|
|
<small>
|
|
Mode: ${proofResponse.data.isRealProof ? 'Real ZK Proof' : 'Mock Proof'}<br>
|
|
Generation: ${Math.round(proofTime)}ms |
|
|
Verification: ${Math.round(verifyTime)}ms<br>
|
|
</small>
|
|
`;
|
|
// statusDiv.innerHTML = `
|
|
// <strong> License Verified!</strong><br>
|
|
// <small>
|
|
// Mode: ${proofResponse.data.isRealProof ? 'Real ZK Proof' : 'Mock Proof'}<br>
|
|
// Generation: ${Math.round(proofTime)}ms |
|
|
// Verification: ${Math.round(verifyTime)}ms<br>
|
|
// Valid until: ${new Date(verifyResponse.data.validUntil).toLocaleDateString()}
|
|
// </small>
|
|
// `;
|
|
} else {
|
|
statusDiv.className = 'status error';
|
|
statusDiv.innerHTML = '<strong> Verification Failed</strong>';
|
|
}
|
|
|
|
// Show proof details
|
|
detailsDiv.innerHTML = `
|
|
<strong>Public Signals:</strong><br>
|
|
Merkle Root: ${verifyResponse.data.merkleRoot?.substring(0, 20)}...<br><br>
|
|
<strong>Proof (truncated):</strong><br>
|
|
${JSON.stringify(proofResponse.data.proof, null, 2).substring(0, 500)}...
|
|
`;
|
|
// detailsDiv.innerHTML = `
|
|
// <strong>Public Signals:</strong><br>
|
|
// Merkle Root: ${verifyResponse.data.merkleRoot?.substring(0, 20)}...<br>
|
|
// Timestamp: ${verifyResponse.data.verifiedAt}<br>
|
|
// Valid Until: ${verifyResponse.data.validUntil}<br><br>
|
|
// <strong>Proof (truncated):</strong><br>
|
|
// ${JSON.stringify(proofResponse.data.proof, null, 2).substring(0, 500)}...
|
|
// `;
|
|
|
|
} catch (error) {
|
|
console.error('Verification failed:', error);
|
|
statusDiv.className = 'status error';
|
|
statusDiv.innerHTML = `<strong> Error:</strong> ${error.response?.data?.message || error.message}`;
|
|
['step1', 'step2', 'step3'].forEach(id => {
|
|
const icon = document.getElementById(id).querySelector('.timeline-icon');
|
|
if (icon.className.includes('active')) {
|
|
icon.className = 'timeline-icon error';
|
|
icon.style.background = '#ef4444';
|
|
}
|
|
});
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Run benchmark
|
|
async function runBenchmark() {
|
|
const resultsDiv = document.getElementById('benchmarkResults');
|
|
resultsDiv.innerHTML = '<div class="loader"></div><p>Running benchmark...</p>';
|
|
|
|
try {
|
|
const response = await axios.get(`${ZKP_API}/api/benchmark`);
|
|
|
|
resultsDiv.innerHTML = `
|
|
<table style="width: 100%; margin-top: 10px;">
|
|
<thead>
|
|
<tr style="border-bottom: 2px solid #667eea;">
|
|
<th style="text-align: left; padding: 8px;">Operation</th>
|
|
<th style="text-align: right; padding: 8px;">Avg (ms)</th>
|
|
<th style="text-align: right; padding: 8px;">P95 (ms)</th>
|
|
<th style="text-align: right; padding: 8px;">Count</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${response.data.results.map(r => `
|
|
<tr>
|
|
<td style="padding: 8px;">${r.operation_type}</td>
|
|
<td style="text-align: right; padding: 8px;">${parseFloat(r.avg_ms).toFixed(0)}</td>
|
|
<td style="text-align: right; padding: 8px;">${parseFloat(r.p95_ms).toFixed(0)}</td>
|
|
<td style="text-align: right; padding: 8px;">${r.count}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
} catch (error) {
|
|
resultsDiv.innerHTML = `<div class="status error">Benchmark failed: ${error.message}</div>`;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize on load
|
|
window.onload = function() {
|
|
checkSystemStatus();
|
|
initPoseidon();
|
|
setInterval(checkSystemStatus, 30000);
|
|
};
|
|
</script>
|
|
</body>
|
|
</html> |