diff --git a/L2PS_PHASES.md b/L2PS_PHASES.md deleted file mode 100644 index f6fbf45c9..000000000 --- a/L2PS_PHASES.md +++ /dev/null @@ -1,731 +0,0 @@ -# L2PS Implementation Phases - -This document provides actionable implementation steps for completing the L2PS (Layer 2 Privacy Subnets) system in the Demos Network node software. - -**Branch**: l2ps_simplified -**Status**: ALL PHASES COMPLETE (100%) - Implementation finished, awaiting testing -**Context**: See Serena memories: l2ps_overview, l2ps_architecture, l2ps_implementation_status, l2ps_code_patterns, l2ps_remaining_work - ---- - -## ✅ Phase 1: Core Infrastructure (COMPLETE) -- L2PSMempool entity, manager, transaction handler -- All components fully implemented and tested - -## ✅ Phase 2: Hash Generation Service (COMPLETE) -- L2PSHashService with reentrancy protection -- 5-second interval hash generation -- Integration with src/index.ts - -## ✅ Phase 3a: DTR Integration (COMPLETE) -- Validator relay implementation -- Hash update transaction handler -- getL2PSParticipationById NodeCall endpoint - -## ✅ Phase 3b: Validator Hash Storage (COMPLETE - Commit 51b93f1a) - -**Goal**: Enable validators to store L2PS UID → hash mappings for consensus - -### Step 3b.1: Create L2PSHashes Entity -**File**: `src/model/entities/L2PSHashes.ts` (create new) - -**Action**: Create TypeORM entity for L2PS hash storage - -**Implementation**: -```typescript -import { Entity, PrimaryColumn, Column } from "typeorm" - -@Entity("l2ps_hashes") -export class L2PSHash { - @PrimaryColumn() - l2ps_uid: string - - @Column() - hash: string - - @Column() - transaction_count: number - - @Column({ type: "bigint", default: 0 }) - block_number: bigint - - @Column({ type: "bigint" }) - timestamp: bigint -} -``` - -**Validation**: -- Run `bun run lint:fix` to check syntax -- Verify entity follows TypeORM conventions -- Check that @/ import alias is used if needed - ---- - -### Step 3b.2: Create L2PSHashes Manager -**File**: `src/libs/blockchain/l2ps_hashes.ts` (create new) - -**Action**: Create manager class following l2ps_mempool.ts pattern - -**Required Methods**: -- `init()`: Initialize TypeORM repository -- `updateHash(l2psUid, hash, txCount, blockNumber)`: Store/update hash mapping -- `getHash(l2psUid)`: Retrieve hash for specific L2PS UID -- `getAll()`: Get all hash mappings -- `getStats()`: Return statistics (total UIDs, last update times) - -**Pattern to Follow**: -```typescript -import { Repository } from "typeorm" -import { L2PSHash } from "@/model/entities/L2PSHashes" -import Datasource from "@/model/datasource" -import log from "@/utilities/logger" - -export default class L2PSHashes { - public static repo: Repository = null - - public static async init(): Promise { - const db = await Datasource.getInstance() - this.repo = db.getDataSource().getRepository(L2PSHash) - } - - public static async updateHash( - l2psUid: string, - hash: string, - txCount: number, - blockNumber: bigint - ): Promise { - // Implementation - } - - public static async getHash(l2psUid: string): Promise { - // Implementation - } - - public static async getStats(): Promise { - // Implementation - } -} -``` - -**Validation**: -- Run `bun run lint:fix` to check code quality -- Ensure proper error handling -- Add JSDoc comments -- Use @/ import aliases - ---- - -### Step 3b.3: Initialize L2PSHashes Manager -**File**: `src/index.ts` - -**Action**: Add L2PSHashes.init() alongside existing entity initializations - -**Find**: Section where entities are initialized (search for "L2PSMempool.init()") - -**Add**: -```typescript -import L2PSHashes from "@/libs/blockchain/l2ps_hashes" - -// In initialization section: -await L2PSHashes.init() -log.info("[L2PSHashes] Initialized") -``` - -**Validation**: -- Verify initialization order (after database connection) -- Check that error handling is consistent with other inits -- Run `bun run lint:fix` - ---- - -### Step 3b.4: Complete handleL2PSHashUpdate Storage Logic -**File**: `src/libs/network/endpointHandlers.ts` (handleL2PSHashUpdate method) - -**Action**: Replace TODO comment with actual hash storage - -**Find**: Line ~751 with comment "// TODO: Store hash update for validator consensus" - -**Replace with**: -```typescript -// Store hash update for validator consensus -// Validators store only UID → hash mappings (content blind) -try { - await L2PSHashes.updateHash( - l2psHashPayload.l2ps_uid, - l2psHashPayload.consolidated_hash, - l2psHashPayload.transaction_count, - BigInt(tx.block_number || 0) - ) - - log.info(`[L2PSHashUpdate] Stored hash for L2PS UID: ${l2psHashPayload.l2ps_uid}`) - - response.result = 200 - response.response = "L2PS hash update stored successfully" -} catch (error) { - log.error("[L2PSHashUpdate] Failed to store hash:", error) - response.result = 500 - response.response = "Failed to store L2PS hash update" - response.extra = error.message -} -``` - -**Validation**: -- Run `bun run lint:fix` -- Verify error handling is comprehensive -- Check that logging follows conventions -- Ensure @/ import alias for L2PSHashes - ---- - -### Step 3b.5: Test Phase 3b Completion -**Actions**: -1. Run `bun run lint:fix` - must pass -2. Check TypeORM entity is recognized -3. Verify L2PSHashes manager methods are accessible -4. Confirm handleL2PSHashUpdate has no TODOs - -**Success Criteria**: -- No linting errors -- L2PSHashes entity created with proper schema -- Manager methods implemented and initialized -- handleL2PSHashUpdate stores hashes successfully -- All code uses @/ import aliases -- Comprehensive error handling and logging - -**Report Back**: Confirm Phase 3b completion before proceeding - ---- - -## ✅ Phase 3c-1: Complete NodeCall Endpoints (COMPLETE - Commit 42d42eea) - -**Goal**: Enable L2PS participants to query mempool info and sync transactions - -### Step 3c1.1: Implement getL2PSMempoolInfo -**File**: `src/libs/network/manageNodeCall.ts` - -**Action**: Replace placeholder (lines ~345-354) with actual implementation - -**Replace**: -```typescript -case "getL2PSMempoolInfo": - console.log("[L2PS] Received L2PS mempool info request") - if (!data.l2psUid) { - response.result = 400 - response.response = "No L2PS UID specified" - break - } - response.result = 501 - response.response = "UNIMPLEMENTED - L2PS mempool info endpoint" - break -``` - -**With**: -```typescript -case "getL2PSMempoolInfo": { - console.log("[L2PS] Received L2PS mempool info request") - if (!data.l2psUid) { - response.result = 400 - response.response = "No L2PS UID specified" - break - } - - try { - // Get all processed transactions for this L2PS UID - const transactions = await L2PSMempool.getByUID(data.l2psUid, "processed") - - response.result = 200 - response.response = { - l2psUid: data.l2psUid, - transactionCount: transactions.length, - lastTimestamp: transactions.length > 0 - ? transactions[transactions.length - 1].timestamp - : 0, - oldestTimestamp: transactions.length > 0 - ? transactions[0].timestamp - : 0 - } - } catch (error) { - log.error("[L2PS] Failed to get mempool info:", error) - response.result = 500 - response.response = "Failed to get L2PS mempool info" - response.extra = error.message - } - break -} -``` - -**Validation**: -- Run `bun run lint:fix` -- Verify L2PSMempool import exists -- Check error handling is comprehensive - ---- - -### Step 3c1.2: Implement getL2PSTransactions -**File**: `src/libs/network/manageNodeCall.ts` - -**Action**: Replace placeholder (lines ~356-365) with actual implementation - -**Replace**: -```typescript -case "getL2PSTransactions": - console.log("[L2PS] Received L2PS transactions sync request") - if (!data.l2psUid) { - response.result = 400 - response.response = "No L2PS UID specified" - break - } - response.result = 501 - response.response = "UNIMPLEMENTED - L2PS transactions sync endpoint" - break -``` - -**With**: -```typescript -case "getL2PSTransactions": { - console.log("[L2PS] Received L2PS transactions sync request") - if (!data.l2psUid) { - response.result = 400 - response.response = "No L2PS UID specified" - break - } - - try { - // Optional timestamp filter for incremental sync - const sinceTimestamp = data.since_timestamp || 0 - - // Get all processed transactions for this L2PS UID - let transactions = await L2PSMempool.getByUID(data.l2psUid, "processed") - - // Filter by timestamp if provided - if (sinceTimestamp > 0) { - transactions = transactions.filter(tx => tx.timestamp > sinceTimestamp) - } - - // Return encrypted transactions (validators never see this) - response.result = 200 - response.response = { - l2psUid: data.l2psUid, - transactions: transactions.map(tx => ({ - hash: tx.hash, - l2ps_uid: tx.l2ps_uid, - original_hash: tx.original_hash, - encrypted_tx: tx.encrypted_tx, - timestamp: tx.timestamp, - block_number: tx.block_number - })), - count: transactions.length - } - } catch (error) { - log.error("[L2PS] Failed to get transactions:", error) - response.result = 500 - response.response = "Failed to get L2PS transactions" - response.extra = error.message - } - break -} -``` - -**Validation**: -- Run `bun run lint:fix` -- Verify response structure is correct -- Check filtering logic works properly - ---- - -### Step 3c1.3: Test Phase 3c-1 Completion -**Actions**: -1. Run `bun run lint:fix` - must pass -2. Verify both endpoints return proper responses -3. Check error handling covers all cases - -**Success Criteria**: -- No linting errors -- getL2PSMempoolInfo returns transaction count and timestamps -- getL2PSTransactions returns encrypted transactions with optional filtering -- All code uses proper error handling and logging - -**Report Back**: Confirm Phase 3c-1 completion before proceeding - ---- - -## ✅ Phase 3c-2: Create L2PS Concurrent Sync Service (COMPLETE - Commit a54044dc) - -**Goal**: Enable L2PS participants to discover peers and sync mempools - -### Step 3c2.1: Create L2PSConcurrentSync.ts -**File**: `src/libs/l2ps/L2PSConcurrentSync.ts` (create new) - -**Action**: Create utility functions for L2PS mempool synchronization - -**Implementation Template**: -```typescript -import PeerManager from "@/libs/peer/PeerManager" -import { Peer } from "@/libs/peer/Peer" -import L2PSMempool from "@/libs/blockchain/l2ps_mempool" -import log from "@/utilities/logger" -import type { RPCResponse } from "@/types/types" - -/** - * Discover which peers participate in specific L2PS UIDs - * @param peers List of peers to query - * @param l2psUids L2PS network UIDs to check - * @returns Map of L2PS UID to participating peers - */ -export async function discoverL2PSParticipants( - peers: Peer[], - l2psUids: string[] -): Promise> { - // Implementation: parallel queries to peers - // Use getL2PSParticipationById NodeCall -} - -/** - * Sync L2PS mempool with a specific peer - * @param peer Peer to sync with - * @param l2psUid L2PS network UID - */ -export async function syncL2PSWithPeer( - peer: Peer, - l2psUid: string -): Promise { - // Implementation: - // 1. Get peer's mempool info via getL2PSMempoolInfo - // 2. Compare with local mempool - // 3. Request missing transactions via getL2PSTransactions - // 4. Validate and insert into local mempool -} - -/** - * Exchange L2PS participation info with peers - * @param peers List of peers to exchange with - */ -export async function exchangeL2PSParticipation( - peers: Peer[] -): Promise { - // Implementation: inform peers of local L2PS participation -} -``` - -**Detailed Implementation Requirements**: - -**discoverL2PSParticipants**: -- Use parallel peer.call() for efficiency -- Handle peer failures gracefully -- Return only successful responses -- Log discovery statistics - -**syncL2PSWithPeer**: -- Get peer's mempool info first -- Calculate missing transactions -- Request only what's needed (since_timestamp) -- Validate signatures before inserting -- Handle duplicate transactions gracefully - -**exchangeL2PSParticipation**: -- Broadcast local L2PS UIDs to peers -- No response needed (fire and forget) -- Log exchange completion - -**Validation**: -- Run `bun run lint:fix` -- Ensure all functions have JSDoc comments -- Check error handling is comprehensive -- Verify parallel execution patterns - ---- - -### Step 3c2.2: Test Phase 3c-2 Completion -**Actions**: -1. Run `bun run lint:fix` - must pass -2. Verify functions are properly typed -3. Check parallel execution patterns - -**Success Criteria**: -- No linting errors -- All functions implemented with proper error handling -- Parallel peer communication where applicable -- Comprehensive logging - -**Report Back**: Confirm Phase 3c-2 completion before proceeding - ---- - -## ✅ Phase 3c-3: Integrate L2PS Sync with Blockchain Sync (COMPLETE - Commit 80bc0d62) - -**Goal**: Enable automatic L2PS mempool synchronization during blockchain sync - -### Step 3c3.1: Add L2PS Sync to mergePeerlist() -**File**: `src/libs/blockchain/routines/Sync.ts` - -**Action**: Add L2PS participant exchange after peer merging - -**Find**: `mergePeerlist(block: Block)` function - -**Add** (after peer merging logic): -```typescript -// Exchange L2PS participation info with newly discovered peers -if (getSharedState.l2psJoinedUids.length > 0) { - try { - const newPeers = /* extract new peers from merge result */ - await exchangeL2PSParticipation(newPeers) - log.debug("[Sync] L2PS participation exchanged with new peers") - } catch (error) { - log.error("[Sync] L2PS participation exchange failed:", error) - // Don't break blockchain sync on L2PS errors - } -} -``` - -**Validation**: -- Run `bun run lint:fix` -- Verify import for exchangeL2PSParticipation -- Check that blockchain sync is NOT blocked by L2PS errors - ---- - -### Step 3c3.2: Add L2PS Discovery to getHigestBlockPeerData() -**File**: `src/libs/blockchain/routines/Sync.ts` - -**Action**: Add concurrent L2PS participant discovery - -**Find**: `getHigestBlockPeerData(peers: Peer[])` function - -**Add** (concurrently with highest block discovery): -```typescript -// Discover L2PS participants concurrently with block discovery -if (getSharedState.l2psJoinedUids.length > 0) { - // Run in background, don't await - discoverL2PSParticipants(peers, getSharedState.l2psJoinedUids) - .then(participantMap => { - log.debug(`[Sync] Discovered L2PS participants: ${participantMap.size} networks`) - // Store participant map for later sync operations - }) - .catch(error => { - log.error("[Sync] L2PS participant discovery failed:", error) - }) -} -``` - -**Validation**: -- Run `bun run lint:fix` -- Verify discovery runs concurrently (NOT blocking) -- Check error handling doesn't break blockchain sync - ---- - -### Step 3c3.3: Add L2PS Mempool Sync to requestBlocks() -**File**: `src/libs/blockchain/routines/Sync.ts` - -**Action**: Add L2PS mempool sync alongside block sync - -**Find**: `requestBlocks()` function (main sync loop) - -**Add** (concurrent with block syncing): -```typescript -// Sync L2PS mempools concurrently with blockchain sync -if (getSharedState.l2psJoinedUids.length > 0 && peer) { - for (const l2psUid of getSharedState.l2psJoinedUids) { - // Run in background, don't block blockchain sync - syncL2PSWithPeer(peer, l2psUid) - .then(() => { - log.debug(`[Sync] L2PS mempool synced: ${l2psUid}`) - }) - .catch(error => { - log.error(`[Sync] L2PS sync failed for ${l2psUid}:`, error) - // Don't break blockchain sync on L2PS errors - }) - } -} -``` - -**Validation**: -- Run `bun run lint:fix` -- Verify L2PS sync is concurrent (NOT sequential) -- Check that blockchain sync continues even if L2PS sync fails - ---- - -### Step 3c3.4: Add Required Imports -**File**: `src/libs/blockchain/routines/Sync.ts` - -**Action**: Add imports for L2PS sync functions - -**Add at top of file**: -```typescript -import { - discoverL2PSParticipants, - syncL2PSWithPeer, - exchangeL2PSParticipation -} from "@/libs/l2ps/L2PSConcurrentSync" -import { getSharedState } from "@/utilities/sharedState" -``` - -**Validation**: -- Run `bun run lint:fix` -- Verify @/ import aliases are used - ---- - -### Step 3c3.5: Test Phase 3c-3 Completion -**Actions**: -1. Run `bun run lint:fix` - must pass -2. Verify blockchain sync still works without L2PS -3. Check that L2PS sync runs concurrently -4. Confirm errors don't break blockchain sync - -**Success Criteria**: -- No linting errors -- Blockchain sync unaffected by L2PS code -- L2PS sync runs concurrently (not blocking) -- Comprehensive error handling -- All imports use @/ aliases - -**Report Back**: Confirm Phase 3c-3 completion before proceeding - ---- - -## 🎯 Final Validation - -### Complete System Test -1. **Linting**: `bun run lint:fix` must pass with zero errors -2. **Entity Check**: Verify L2PSHashes entity is recognized by TypeORM -3. **Service Check**: Confirm all services initialize successfully -4. **NodeCall Check**: Verify all L2PS NodeCall endpoints return proper responses -5. **Sync Check**: Confirm blockchain sync continues working without issues - -### Documentation Check -- All new code has JSDoc comments -- Complex logic has inline comments -- REVIEW markers added for new features -- No TODO comments remain in production code - -### Code Quality Check -- All imports use @/ path aliases -- Error handling is comprehensive -- Logging follows conventions ([ServiceName] format) -- Follows existing code patterns - ---- - -## 📝 Implementation Notes - -### Important Constraints -- **Do NOT overengineer**: Follow existing patterns, keep it simple -- **Do NOT break existing sync**: L2PS sync must be additive, not disruptive -- **Privacy first**: Never expose decrypted L2PS transaction content to validators -- **Reuse infrastructure**: No new dependencies, use existing peer/network code -- **Concurrent execution**: L2PS sync must NOT block blockchain sync - -### Testing Strategy -- NEVER start the node during development (./run) -- Use `bun run lint:fix` for validation -- Test with multiple L2PS participants -- Verify validators never receive transaction content -- Test graceful error handling and recovery - -### Dependency Order -- Phase 3b (Hash Storage) - can start immediately -- Phase 3c-1 (NodeCall Endpoints) - can start immediately -- Phase 3c-2 (Concurrent Sync) - requires Phase 3c-1 -- Phase 3c-3 (Sync Integration) - requires Phase 3c-2 - -**Optimal**: Start 3b and 3c-1 in parallel → 3c-2 → 3c-3 - ---- - -## ✅ Completion Criteria - -L2PS implementation is complete when: -1. All validator hash storage works (Phase 3b) -2. All NodeCall endpoints return proper data (Phase 3c-1) -3. L2PS sync service exists and works (Phase 3c-2) -4. Blockchain sync includes L2PS hooks (Phase 3c-3) -5. Zero linting errors -6. All code documented with JSDoc -7. Comprehensive error handling throughout -8. Privacy guarantees maintained (validators content-blind) - ---- - -## 🎉 IMPLEMENTATION COMPLETE - -**Date Completed**: 2025-01-31 -**Branch**: l2ps_simplified -**Total Commits**: 4 (51b93f1a, 42d42eea, a54044dc, 80bc0d62) - -### Files Created/Modified - -**New Files** (3): -1. `src/model/entities/L2PSHashes.ts` - 62 lines - - TypeORM entity for validator hash storage -2. `src/libs/blockchain/l2ps_hashes.ts` - 217 lines - - L2PSHashes manager with CRUD operations -3. `src/libs/l2ps/L2PSConcurrentSync.ts` - 254 lines - - Peer discovery, mempool sync, participation exchange - -**Modified Files** (3): -1. `src/libs/network/endpointHandlers.ts` - - Completed handleL2PSHashUpdate storage logic -2. `src/libs/network/manageNodeCall.ts` - 64 lines added - - Implemented getL2PSMempoolInfo endpoint - - Implemented getL2PSTransactions endpoint -3. `src/libs/blockchain/routines/Sync.ts` - 53 lines added - - L2PS participation exchange in mergePeerlist() - - L2PS participant discovery in getHigestBlockPeerData() - - L2PS mempool sync in requestBlocks() -4. `package.json` - - Added local_tests ignore pattern to lint:fix - -**Total Lines Added**: ~650 lines of production code - -### Key Features Implemented - -**Phase 3b - Validator Hash Storage**: -- Validators store ONLY hash mappings (content-blind consensus) -- Auto-initialization on import -- Complete CRUD operations with statistics - -**Phase 3c-1 - NodeCall Endpoints**: -- Mempool info queries (transaction count, timestamps) -- Transaction sync with incremental updates -- Privacy preserved (only encrypted data returned) - -**Phase 3c-2 - Concurrent Sync Service**: -- Parallel peer discovery for L2PS networks -- Incremental mempool sync (fetch only missing transactions) -- Fire-and-forget participation broadcast - -**Phase 3c-3 - Blockchain Integration**: -- Non-blocking L2PS operations (never block blockchain sync) -- Error isolation (L2PS failures don't break blockchain) -- Concurrent execution throughout - -### Code Quality Metrics - -✅ Zero linting errors -✅ All code documented with JSDoc + examples -✅ Comprehensive error handling throughout -✅ REVIEW markers on all new code -✅ @/ import aliases used consistently -✅ Privacy guarantees maintained (validators content-blind) - -### Testing Status - -⚠️ **NOT TESTED** - Implementation complete but runtime validation pending -📋 See L2PS_TESTING.md for validation checklist when node can be safely started - -### Known Limitations - -1. **No Runtime Validation**: Code has not been tested with running node -2. **Database Schema**: Assuming TypeORM auto-creates l2ps_hashes table -3. **Edge Cases**: Some edge cases may need adjustment after testing -4. **Performance**: Concurrent sync performance not benchmarked - -### Future Improvements - -1. **Retry Logic**: Add exponential backoff for failed sync attempts -2. **Metrics**: Add Prometheus metrics for L2PS operations -3. **Rate Limiting**: Add rate limits to prevent peer spam -4. **Batch Operations**: Optimize bulk transaction insertions -5. **Compression**: Add optional compression for large mempools diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index d960b6b26..000000000 Binary files a/bun.lockb and /dev/null differ diff --git a/documentation/tlsn-oauth-flow.md b/documentation/tlsn-oauth-flow.md new file mode 100644 index 000000000..1b26495e1 --- /dev/null +++ b/documentation/tlsn-oauth-flow.md @@ -0,0 +1,147 @@ +# TLSN + OAuth Web2 Identity Flow + +This document describes the current integrated flow for adding Web2 identities (`github`, `discord`, `telegram`) via TLSNotary. + +It reflects the behavior implemented in: +- `src/libs/tlsnotary/verifier.ts` +- `src/libs/network/routines/transactions/handleIdentityRequest.ts` +- `src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts` +- SDK payload contracts from `@kynesyslabs/demosdk` (`InferFromTLSNPayload`, `addWeb2IdentityViaTLSN`) + +## 1. End-to-End Flow + +```text +CLIENT (Incentives / Wallet / SDK) +OAuth token acquired + -> TLSN attest target API + -> get transcript (sent/recv) + -> select disclosed recv bytes (revealedRecv) + -> compute recvHash = sha256(revealedRecv) + -> build tlsn_identity_assign payload + -> send tx via addWeb2IdentityViaTLSN + +NODE +handleIdentityRequest + -> verifyTLSNProof + -> validate proof structure + if fail: REJECT + -> validate recvHash format (64 hex chars) + if fail: REJECT + -> validate sha256(revealedRecv) == recvHash + if fail: REJECT + -> parse HTTP body from revealedRecv + -> extract username/userId by context + -> compare extracted fields with claimed fields + if mismatch: REJECT + -> applyTLSNIdentityAdd + -> store identity + proofHash + incentives +``` + +## 2. Payload Contract (`tlsn_identity_assign`) + +Current payload expected by node verification (`TLSNIdentityPayload`): + +```ts +{ + context: "github" | "discord" | "telegram", + proof: { + version: string, + data: string, // hex proof blob + meta: { + notaryUrl?: string, + websocketProxyUrl?: string + } + }, + recvHash: string, // 64-char hex sha256 of revealedRecv bytes + proofRanges: { + recv: Array<{ start: number; end: number }>, + sent: Array<{ start: number; end: number }> + }, + revealedRecv: number[], // disclosed recv bytes used for extraction and hash check + username: string, + userId: string, + referralCode?: string +} +``` + +Notes: +- `recvHash` must be lowercase/uppercase hex without `0x` prefix (exactly 64 hex chars). +- `revealedRecv` is currently required for strict verification on node. +- `proofRanges` is carried in payload for compatibility/audit metadata, but strict extraction uses `revealedRecv`. + +## 3. What Node Verifies + +Node-side verification (`verifyTLSNProof`) performs: + +1. Context validation (`github` / `discord` / `telegram`). +2. `recvHash` format validation (`^[0-9a-fA-F]{64}$`). +3. TLSN presentation structure validation (`proof.version`, `proof.data` hex, min length). +4. `revealedRecv` byte-array validation (`0..255`, non-empty). +5. Integrity check: `sha256(revealedRecv)` must equal payload `recvHash`. +6. HTTP/body parsing from `revealedRecv`. +7. Context-specific identity extraction: + - GitHub: `login`, `id` + - Discord: `username`, `id` + - Telegram: `user.username` or `first_name`, and `id` +8. Equality check vs claimed payload fields: + - extracted `username` === claimed `username` + - extracted `userId` === claimed `userId` + +If any step fails, transaction is rejected. + +## 4. What Happens After Verification + +If verification succeeds: + +1. `GCRIdentityRoutines.applyTLSNIdentityAdd` ensures identity does not already exist for same `userId` in context. +2. Identity is persisted under `identities.web2[context]` with: + - `userId` + - `username` + - `proof` (presentation) + - `proofHash = sha256(JSON.stringify(proof))` + - `proofType = "tlsn"` + - `timestamp` +3. Incentive awarding path runs for first-time link per context. + +## 5. Current Trust Model and Limits + +Important: +- Node currently does **structure validation** for TLSN proof objects. +- Node does **consistency validation** between claimed identity and disclosed transcript bytes. +- Node does **not** perform full TLSN cryptographic attestation verification of canonical transcript in this path. + +So the current model is: +- Strong binding between `recvHash`, `revealedRecv`, and extracted identity fields. +- Not equivalent to full backend cryptographic verification of proof internals. + +## 6. Integration Checklist (Incentives / Wallet / SDK) + +To avoid common failures: + +1. Use the same byte source for both values: + - `revealedRecv` + - `recvHash = sha256(revealedRecv)` +2. Send `recvHash` as plain 64-char hex (no `0x`). +3. Send `revealedRecv` in payload and ensure wallet-extension forwards it unchanged. +4. Keep `username`/`userId` from the same revealed response that produced `recvHash`. +5. Keep reveal ranges large enough to include required JSON fields (`id`, `login`/`username`). + +## 7. Security Notes + +1. Avoid revealing request bytes containing OAuth secrets. +2. Do not reveal fixed `sent[0..N]` if that can include `Authorization: Bearer ...`. +3. Prefer revealing only minimal `recv` bytes needed for extraction. +4. Treat downloaded/on-chain proofs as public disclosure artifacts. + +## 8. Typical Failure Messages + +Common errors and meaning: + +- `Invalid TLSN recvHash: expected 64-char hex sha256` + - `recvHash` format is wrong (often `0x` prefix or wrong length). +- `recvHash mismatch: provided hash does not match disclosed recv bytes` + - Hash computed from node-received `revealedRecv` differs from payload `recvHash`. +- `Failed to extract user from revealedRecv payload` + - Disclosed bytes do not contain parseable fields for that context. +- `Username mismatch` / `UserId mismatch` + - Claimed values do not match extracted values from disclosed bytes. diff --git a/install-deps.sh b/install-deps.sh index 794e98a6f..e93ad2d6c 100755 --- a/install-deps.sh +++ b/install-deps.sh @@ -3,6 +3,9 @@ set -e set -u set -o pipefail +# Add cargo to PATH +export PATH="$HOME/.cargo/bin:$PATH" + # Verify prerequisites command -v bun >/dev/null 2>&1 || { echo "Error: bun is not installed" >&2; exit 1; } command -v cargo >/dev/null 2>&1 || { echo "Error: cargo is not installed" >&2; exit 1; } diff --git a/package.json b/package.json index e0880e154..bffd780e8 100644 --- a/package.json +++ b/package.json @@ -1,139 +1,139 @@ { - "name": "demos-node-software", - "version": "0.9.8", - "description": "Demos Node Software", - "author": "Kynesys Labs", - "license": "none", - "private": true, - "main": "src/index.ts", - "scripts": { - "lint": "eslint . --ignore-pattern 'local_tests' --ignore-pattern 'aptos_tests' --ext .ts", - "lint:fix": "eslint . --ignore-pattern 'local_tests' --ignore-pattern 'aptos_tests' --fix --ext .ts", - "type-check": "bun build src/index.ts --target=bun --no-emit", - "type-check-ts": "tsc --noEmit", - "prettier-format": "prettier --config .prettierrc.json modules/**/*.ts --write", - "format": "prettier --plugin-search-dir . --write .", - "start": "tsx -r tsconfig-paths/register src/index.ts", - "start:up": "bun install && tsx -r tsconfig-paths/register src/index.ts", - "start:bun": "bun -r tsconfig-paths/register src/index.ts", - "start:clean": "rm -rf data/chain.db && tsx -r tsconfig-paths/register src/index.ts", - "start:purge": "rm -rf .demos_identity && rm -rf data/chain.db && tsx -r tsconfig-paths/register src/index.ts", - "dev": "ts-node-dev --import tsx --respawn --transpile-only src/index.ts", - "upgrade_sdk": "bun update @kynesyslabs/demosdk --latest", - "upgrade_deps": "bun update-interactive --latest", - "upgrade_deps:force": "ncu -u && yarn", - "keygen": "tsx -r tsconfig-paths/register src/libs/utils/keyMaker.ts", - "l2ps:zk:setup": "cd src/libs/l2ps/zk/scripts && bash setup_all_batches.sh", - "show:pubkey": "tsx -r tsconfig-paths/register src/libs/utils/showPubkey.ts", - "ceremony:contribute": "bash scripts/ceremony_contribute.sh", - "test:chains": "jest --testMatch '**/tests/**/*.ts' --testPathIgnorePatterns src/* tests/utils/* tests/**/_template* --verbose", - "restore": "bun run src/utilities/backupAndRestore.ts", - "typeorm": "typeorm-ts-node-esm", - "migration:run": "NODE_OPTIONS='--loader ts-node/esm' typeorm-ts-node-esm migration:run -d ./src/model/datasource.ts", - "migration:revert": "NODE_OPTIONS='--loader ts-node/esm' typeorm-ts-node-esm migration:revert -d ./src/model/datasource.ts", - "migration:generate": "NODE_OPTIONS='--loader ts-node/esm' typeorm-ts-node-esm migration:generate -d ./src/model/datasource.ts", - "knip": "knip", - "zk:setup": "tsx -r tsconfig-paths/register scripts/setup-zk-all.ts", - "zk:identity:setup": "tsx -r tsconfig-paths/register src/features/zk/scripts/setup-zk.ts", - "zk:l2ps:setup": "cd src/libs/l2ps/zk/scripts && bash setup_all_batches.sh", - "zk:compile": "circom2 src/features/zk/circuits/identity.circom --r1cs --wasm --sym -o src/features/zk/circuits/ -l node_modules", - "zk:compile:merkle": "circom2 src/features/zk/circuits/identity_with_merkle.circom --r1cs --wasm --sym -o src/features/zk/circuits/ -l node_modules", - "zk:test": "bun test src/features/zk/tests/", - "zk:ceremony": "npx tsx src/features/zk/scripts/ceremony.ts" - }, - "devDependencies": { - "@jest/globals": "^30.2.0", - "@types/bun": "^1.2.10", - "@types/jest": "^29.5.12", - "@types/node": "^25.0.2", - "@types/node-fetch": "^2.6.5", - "@types/ntp-client": "^0.5.0", - "@types/terminal-kit": "^2.5.6", - "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.62.0", - "circom2": "^0.2.22", - "circom_tester": "^0.0.20", - "eslint": "^8.57.1", - "jest": "^29.7.0", - "knip": "^5.74.0", - "prettier": "^2.8.0", - "ts-jest": "^29.3.2", - "ts-node": "^10.9.1", - "ts-node-dev": "^2.0.0", - "typescript": "^5.9.3" - }, - "dependencies": { - "@aptos-labs/ts-sdk": "^5.2.0", - "@coral-xyz/anchor": "^0.32.1", - "@cosmjs/encoding": "^0.33.1", - "@fastify/cors": "^9.0.1", - "@fastify/swagger": "^8.15.0", - "@fastify/swagger-ui": "^4.1.0", - "@kynesyslabs/demosdk": "^2.8.16", - "@metaplex-foundation/js": "^0.20.1", - "@modelcontextprotocol/sdk": "^1.13.3", - "@noble/ed25519": "^3.0.0", - "@noble/hashes": "^2.0.1", - "@octokit/core": "^6.1.5", - "@scure/bip39": "^2.0.1", - "@solana/web3.js": "^1.98.4", - "@types/express": "^4.17.21", - "@types/http-proxy": "^1.17.14", - "@types/lodash": "^4.17.4", - "@types/node-forge": "^1.3.6", - "@unstoppabledomains/resolution": "^9.3.3", - "@zk-kit/incremental-merkle-tree": "^1.1.0", - "alea": "^1.0.1", - "async-mutex": "^0.5.0", - "axios": "^1.6.5", - "big-integer": "^1.6.52", - "bip39": "^3.1.0", - "bs58": "^6.0.0", - "bun": "^1.2.10", - "circomlib": "^2.0.5", - "circomlibjs": "^0.1.7", - "cli-progress": "^3.12.0", - "cors": "^2.8.5", - "crc": "^4.3.2", - "dotenv": "^16.4.5", - "ethers": "^6.16.0", - "express": "^4.19.2", - "fastify": "^4.28.1", - "ffjavascript": "^0.3.1", - "helmet": "^8.1.0", - "http-proxy": "^1.18.1", - "lodash": "^4.17.21", - "node-disk-info": "^1.3.0", - "node-fetch": "2", - "node-forge": "^1.3.3", - "node-seal": "^5.1.3", - "npm-check-updates": "^16.14.18", - "ntp-client": "^0.5.3", - "object-sizeof": "^2.6.3", - "pg": "^8.12.0", - "poseidon-lite": "^0.3.0", - "prom-client": "^15.1.3", - "reflect-metadata": "^0.1.13", - "rijndael-js": "^2.0.0", - "rollup-plugin-polyfill-node": "^0.12.0", - "rubic-sdk": "^5.57.4", - "seedrandom": "^3.0.5", - "snarkjs": "^0.7.5", - "socket.io": "^4.7.1", - "socket.io-client": "^4.7.2", - "sqlite3": "^5.1.6", - "superdilithium": "^2.0.6", - "terminal-kit": "^3.1.1", - "tsconfig-paths": "^4.2.0", - "tsx": "^3.12.8", - "tweetnacl": "^1.0.3", - "typeorm": "^0.3.17", - "web3": "^4.16.0", - "zod": "^3.25.67" - }, - "type": "module", - "optionalDependencies": { - "bufferutil": "^4.0.8", - "utf-8-validate": "^5.0.10" - } -} \ No newline at end of file + "name": "demos-node-software", + "version": "0.9.8", + "description": "Demos Node Software", + "author": "Kynesys Labs", + "license": "none", + "private": true, + "main": "src/index.ts", + "scripts": { + "lint": "eslint . --ignore-pattern 'local_tests' --ignore-pattern 'aptos_tests' --ext .ts", + "lint:fix": "eslint . --ignore-pattern 'local_tests' --ignore-pattern 'aptos_tests' --fix --ext .ts", + "type-check": "bun build src/index.ts --target=bun --no-emit", + "type-check-ts": "tsc --noEmit", + "prettier-format": "prettier --config .prettierrc.json modules/**/*.ts --write", + "format": "prettier --plugin-search-dir . --write .", + "start": "tsx -r tsconfig-paths/register src/index.ts", + "start:up": "bun install && tsx -r tsconfig-paths/register src/index.ts", + "start:bun": "bun -r tsconfig-paths/register src/index.ts", + "start:clean": "rm -rf data/chain.db && tsx -r tsconfig-paths/register src/index.ts", + "start:purge": "rm -rf .demos_identity && rm -rf data/chain.db && tsx -r tsconfig-paths/register src/index.ts", + "dev": "ts-node-dev --import tsx --respawn --transpile-only src/index.ts", + "upgrade_sdk": "bun update @kynesyslabs/demosdk --latest", + "upgrade_deps": "bun update-interactive --latest", + "upgrade_deps:force": "ncu -u && yarn", + "keygen": "tsx -r tsconfig-paths/register src/libs/utils/keyMaker.ts", + "l2ps:zk:setup": "cd src/libs/l2ps/zk/scripts && bash setup_all_batches.sh", + "show:pubkey": "tsx -r tsconfig-paths/register src/libs/utils/showPubkey.ts", + "ceremony:contribute": "bash scripts/ceremony_contribute.sh", + "test:chains": "jest --testMatch '**/tests/**/*.ts' --testPathIgnorePatterns src/* tests/utils/* tests/**/_template* --verbose", + "restore": "bun run src/utilities/backupAndRestore.ts", + "typeorm": "typeorm-ts-node-esm", + "migration:run": "NODE_OPTIONS='--loader ts-node/esm' typeorm-ts-node-esm migration:run -d ./src/model/datasource.ts", + "migration:revert": "NODE_OPTIONS='--loader ts-node/esm' typeorm-ts-node-esm migration:revert -d ./src/model/datasource.ts", + "migration:generate": "NODE_OPTIONS='--loader ts-node/esm' typeorm-ts-node-esm migration:generate -d ./src/model/datasource.ts", + "knip": "knip", + "zk:setup": "tsx -r tsconfig-paths/register scripts/setup-zk-all.ts", + "zk:identity:setup": "tsx -r tsconfig-paths/register src/features/zk/scripts/setup-zk.ts", + "zk:l2ps:setup": "cd src/libs/l2ps/zk/scripts && bash setup_all_batches.sh", + "zk:compile": "circom2 src/features/zk/circuits/identity.circom --r1cs --wasm --sym -o src/features/zk/circuits/ -l node_modules", + "zk:compile:merkle": "circom2 src/features/zk/circuits/identity_with_merkle.circom --r1cs --wasm --sym -o src/features/zk/circuits/ -l node_modules", + "zk:test": "bun test src/features/zk/tests/", + "zk:ceremony": "npx tsx src/features/zk/scripts/ceremony.ts" + }, + "devDependencies": { + "@jest/globals": "^30.2.0", + "@types/bun": "^1.2.10", + "@types/jest": "^29.5.12", + "@types/node": "^25.0.2", + "@types/node-fetch": "^2.6.5", + "@types/ntp-client": "^0.5.0", + "@types/terminal-kit": "^2.5.6", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "circom2": "^0.2.22", + "circom_tester": "^0.0.20", + "eslint": "^8.57.1", + "jest": "^29.7.0", + "knip": "^5.74.0", + "prettier": "^2.8.0", + "ts-jest": "^29.3.2", + "ts-node": "^10.9.1", + "ts-node-dev": "^2.0.0", + "typescript": "^5.9.3" + }, + "dependencies": { + "@aptos-labs/ts-sdk": "^5.2.0", + "@coral-xyz/anchor": "^0.32.1", + "@cosmjs/encoding": "^0.33.1", + "@fastify/cors": "^9.0.1", + "@fastify/swagger": "^8.15.0", + "@fastify/swagger-ui": "^4.1.0", + "@kynesyslabs/demosdk": "^2.10.2", + "@metaplex-foundation/js": "^0.20.1", + "@modelcontextprotocol/sdk": "^1.13.3", + "@noble/ed25519": "^3.0.0", + "@noble/hashes": "^2.0.1", + "@octokit/core": "^6.1.5", + "@scure/bip39": "^2.0.1", + "@solana/web3.js": "^1.98.4", + "@types/express": "^4.17.21", + "@types/http-proxy": "^1.17.14", + "@types/lodash": "^4.17.4", + "@types/node-forge": "^1.3.6", + "@unstoppabledomains/resolution": "^9.3.3", + "@zk-kit/incremental-merkle-tree": "^1.1.0", + "alea": "^1.0.1", + "async-mutex": "^0.5.0", + "axios": "^1.6.5", + "big-integer": "^1.6.52", + "bip39": "^3.1.0", + "bs58": "^6.0.0", + "bun": "^1.2.10", + "circomlib": "^2.0.5", + "circomlibjs": "^0.1.7", + "cli-progress": "^3.12.0", + "cors": "^2.8.5", + "crc": "^4.3.2", + "dotenv": "^16.4.5", + "ethers": "^6.16.0", + "express": "^4.19.2", + "fastify": "^4.28.1", + "ffjavascript": "^0.3.1", + "helmet": "^8.1.0", + "http-proxy": "^1.18.1", + "lodash": "^4.17.21", + "node-disk-info": "^1.3.0", + "node-fetch": "2", + "node-forge": "^1.3.3", + "node-seal": "^5.1.3", + "npm-check-updates": "^16.14.18", + "ntp-client": "^0.5.3", + "object-sizeof": "^2.6.3", + "pg": "^8.12.0", + "poseidon-lite": "^0.3.0", + "prom-client": "^15.1.3", + "reflect-metadata": "^0.1.13", + "rijndael-js": "^2.0.0", + "rollup-plugin-polyfill-node": "^0.12.0", + "rubic-sdk": "^5.57.4", + "seedrandom": "^3.0.5", + "snarkjs": "^0.7.5", + "socket.io": "^4.7.1", + "socket.io-client": "^4.7.2", + "sqlite3": "^5.1.6", + "superdilithium": "^2.0.6", + "terminal-kit": "^3.1.1", + "tsconfig-paths": "^4.2.0", + "tsx": "^3.12.8", + "tweetnacl": "^1.0.3", + "typeorm": "^0.3.17", + "web3": "^4.16.0", + "zod": "^3.25.67" + }, + "type": "module", + "optionalDependencies": { + "bufferutil": "^4.0.8", + "utf-8-validate": "^5.0.10" + } +} diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index 44f765bab..17140c871 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -932,6 +932,95 @@ export class PointSystem { } } + /** + * Award points for linking a Telegram account via TLSN. + * @param userId The user's Demos address + * @param telegramUserId The Telegram user ID + * @param referralCode Optional referral code + * @returns RPCResponse + */ + async awardTelegramTLSNPoints( + userId: string, + telegramUserId: string, + referralCode?: string, + ): Promise { + try { + // Get user's account data from GCR to verify Telegram ownership + const account = await ensureGCRForUser(userId) + + // Verify the Telegram account is actually linked to this user + const telegramIdentities = account.identities.web2?.telegram || [] + const isOwner = telegramIdentities.some( + (tg: any) => tg.userId === telegramUserId, + ) + + if (!isOwner) { + return { + result: 400, + response: { + pointsAwarded: 0, + totalPoints: account.points.totalPoints || 0, + message: + "Error: Telegram account not linked to this user", + }, + require_reply: false, + extra: {}, + } + } + + const userPointsWithIdentities = await this.getUserPointsInternal( + userId, + ) + + // Check if user already has Telegram points specifically + if ( + userPointsWithIdentities.breakdown.socialAccounts.telegram > 0 + ) { + return { + result: 200, + response: { + pointsAwarded: 0, + totalPoints: userPointsWithIdentities.totalPoints, + message: "Telegram points already awarded", + }, + require_reply: false, + extra: {}, + } + } + + await this.addPointsToGCR( + userId, + pointValues.LINK_TELEGRAM, + "socialAccounts", + "telegram", + referralCode, + ) + + const updatedPoints = await this.getUserPointsInternal(userId) + + return { + result: 200, + response: { + pointsAwarded: pointValues.LINK_TELEGRAM, + totalPoints: updatedPoints.totalPoints, + message: "Points awarded for linking Telegram", + }, + require_reply: false, + extra: {}, + } + } catch (error) { + return { + result: 500, + response: "Error awarding points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } + } + /** * Deduct points for unlinking a Telegram account * @param userId The user's Demos address diff --git a/src/features/tlsnotary/proxyManager.ts b/src/features/tlsnotary/proxyManager.ts index 86da4ac15..43cd55b1a 100644 --- a/src/features/tlsnotary/proxyManager.ts +++ b/src/features/tlsnotary/proxyManager.ts @@ -124,13 +124,17 @@ function getTLSNotaryState(): TLSNotaryState { * @throws Error if wstcp cannot be found or installed */ export async function ensureWstcp(): Promise { + const cargoHome = process.env.CARGO_HOME || `${process.env.HOME}/.cargo` + const wstcpPath = `${cargoHome}/bin/wstcp` + const cargoPath = `${cargoHome}/bin/cargo` + try { - await execAsync("which wstcp") - log.debug("[TLSNotary] wstcp binary found") + await execAsync(`test -x "${wstcpPath}"`) + log.debug("[TLSNotary] wstcp binary found at " + wstcpPath) } catch { log.info("[TLSNotary] wstcp not found, installing via cargo...") try { - await execAsync("cargo install wstcp") + await execAsync(`"${cargoPath}" install wstcp`) log.info("[TLSNotary] wstcp installed successfully") } catch (installError: any) { throw new Error(`Failed to install wstcp: ${installError.message}`) @@ -265,7 +269,10 @@ async function spawnProxy( const args = ["--bind-addr", `0.0.0.0:${localPort}`, `${domain}:${targetPort}`] log.info(`[TLSNotary] Spawning wstcp: wstcp ${args.join(" ")}`) - const childProcess = spawn("wstcp", args, { + const cargoHome = process.env.CARGO_HOME || `${process.env.HOME}/.cargo` + const wstcpPath = `${cargoHome}/bin/wstcp` + + const childProcess = spawn(wstcpPath, args, { stdio: ["ignore", "pipe", "pipe"], detached: false, }) diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts index c9bded59b..c38df23a5 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts @@ -20,9 +20,14 @@ import { } from "@/model/entities/types/IdentityTypes" import log from "@/utilities/logger" import { IncentiveManager } from "./IncentiveManager" +import { + verifyTLSNProof, + type TLSNIdentityPayload, + type TLSNProofRanges, + type TLSNotaryPresentation, +} from "@/libs/tlsnotary" import { ProofVerifier } from "@/features/zk/proof/ProofVerifier" import { IdentityCommitment } from "@/model/entities/GCRv2/IdentityCommitment" -import { UsedNullifier } from "@/model/entities/GCRv2/UsedNullifier" import { IdentityCommitmentPayload, IdentityAttestationPayload, @@ -269,9 +274,9 @@ export default class GCRIdentityRoutines { context === "telegram" ? "Telegram attestation validation failed" : "Sha256 proof mismatch: Expected " + - data.proofHash + - " but got " + - Hashing.sha256(data.proof), + data.proofHash + + " but got " + + Hashing.sha256(data.proof), } } @@ -583,8 +588,9 @@ export default class GCRIdentityRoutines { if (!validNetworks.includes(payload.network)) { return { success: false, - message: `Invalid network: ${payload.network - }. Must be one of: ${validNetworks.join(", ")}`, + message: `Invalid network: ${ + payload.network + }. Must be one of: ${validNetworks.join(", ")}`, } } if (!validRegistryTypes.includes(payload.registryType)) { @@ -1161,6 +1167,20 @@ export default class GCRIdentityRoutines { simulate, ) break + case "tlsnadd": + result = await this.applyTLSNIdentityAdd( + identityEdit, + gcrMainRepository, + simulate, + ) + break + case "tlsnremove": + result = await this.applyTLSNIdentityRemove( + identityEdit, + gcrMainRepository, + simulate, + ) + break case "zk_attestationadd": result = await this.applyZkAttestationAdd( identityEdit, @@ -1421,4 +1441,320 @@ export default class GCRIdentityRoutines { return { success: true, message: "Nomis identity removed" } } + + // SECTION TLSNotary Identity Routines + + /** + * Expected API endpoints for TLSN verification per context + */ + private static TLSN_EXPECTED_ENDPOINTS: Record< + string, + { server: string; pathPrefix: string } + > = { + github: { server: "api.github.com", pathPrefix: "/user" }, + discord: { server: "discord.com", pathPrefix: "/api/users/@me" }, + telegram: { + server: "telegram-backend", + pathPrefix: "/api/telegram/user", + }, + } + + /** + * Add an identity via TLSNotary proof verification. + */ + static async applyTLSNIdentityAdd( + editOperation: any, + gcrMainRepository: Repository, + simulate: boolean, + ): Promise { + // Extract context from editOperation.data (top level) + const { context } = editOperation.data + // Extract nested data fields (proof, username, userId are inside data.data) + const { + proof: proofString, + recvHash, + proofRanges, + revealedRecv, + username, + userId, + } = editOperation.data.data || {} + // referralCode is at the editOperation level + const referralCode = editOperation.referralCode + + if (!context) { + return { + success: false, + message: "Missing TLSN context", + } + } + + if (!username) { + return { + success: false, + message: "Missing TLSN username", + } + } + + if (userId === undefined || userId === null) { + return { + success: false, + message: "Missing TLSN userId", + } + } + + if (proofString === undefined || proofString === null) { + return { + success: false, + message: "Missing TLSN proof", + } + } + + if (!recvHash) { + return { + success: false, + message: "Missing TLSN recvHash", + } + } + + if (!proofRanges) { + return { + success: false, + message: "Missing TLSN proofRanges", + } + } + + if (revealedRecv === undefined || revealedRecv === null) { + return { + success: false, + message: "Missing TLSN revealedRecv", + } + } + + // Parse the proof JSON string back to object + let proof: any + try { + proof = + typeof proofString === "string" + ? JSON.parse(proofString) + : proofString + } catch (e) { + return { + success: false, + message: "Invalid proof: failed to parse proof JSON string", + } + } + + // 1. Validate context is supported + if (!this.TLSN_EXPECTED_ENDPOINTS[context]) { + return { + success: false, + message: `Unsupported TLSN context: ${context}`, + } + } + + // 2. Validate proof structure + if (!proof || typeof proof !== "object") { + return { + success: false, + message: + "Invalid proof: expected TLSNotary presentation object", + } + } + + if (!proof.data || !proof.version) { + return { + success: false, + message: "Invalid proof structure: missing data or version", + } + } + + // 3. Verify proof and validate recvHash/proofRanges-derived identity claims + const verification = await verifyTLSNProof({ + context, + proof: proof as TLSNotaryPresentation, + recvHash, + proofRanges: proofRanges as TLSNProofRanges, + revealedRecv, + username: String(username), + userId: String(userId), + referralCode, + } as TLSNIdentityPayload) + + if (!verification.success) { + log.warn( + `[TLSN Identity] Proof verification failed: ${verification.message}`, + ) + return { + success: false, + message: verification.message, + } + } + + // 8. Get/create GCR and check for duplicates + const accountGCR = await ensureGCRForUser(editOperation.account) + + accountGCR.identities.web2 = accountGCR.identities.web2 || {} + accountGCR.identities.web2[context] = + accountGCR.identities.web2[context] || [] + + // Check if identity already exists (by userId to prevent duplicate registrations) + const exists = accountGCR.identities.web2[context].some( + (id: Web2GCRData["data"]) => id.userId === String(userId), + ) + + if (exists) { + return { success: false, message: "Identity already exists" } + } + + // 9. Prepare data for storage + const proofHash = Hashing.sha256(JSON.stringify(proof)) + const data = { + userId: String(userId), + username: username, + proof: proof, + proofHash: proofHash, + proofType: "tlsn", // Mark as TLSNotary-verified + timestamp: Date.now(), + } + + accountGCR.identities.web2[context].push(data) + + // 10. Save and award incentives + if (!simulate) { + await gcrMainRepository.save(accountGCR) + + if (context === "github") { + const isFirst = await this.isFirstConnection( + "github", + { userId: String(userId) }, + gcrMainRepository, + editOperation.account, + ) + + if (isFirst) { + await IncentiveManager.githubLinked( + editOperation.account, + String(userId), + referralCode, + ) + } + } else if (context === "discord") { + const isFirst = await this.isFirstConnection( + "discord", + { userId: String(userId) }, + gcrMainRepository, + editOperation.account, + ) + + if (isFirst) { + await IncentiveManager.discordLinked( + editOperation.account, + referralCode, + ) + } + } else if (context === "telegram") { + const isFirst = await this.isFirstConnection( + "telegram", + { userId: String(userId) }, + gcrMainRepository, + editOperation.account, + ) + + if (isFirst) { + await IncentiveManager.telegramTLSNLinked( + editOperation.account, + String(userId), + referralCode, + ) + } + } + } + + return { success: true, message: "TLSN identity added successfully" } + } + + /** + * Remove an identity that was added via TLSNotary. + * + * Removes only TLSN-proven identities (proofType === "tlsn") from web2 storage. + */ + static async applyTLSNIdentityRemove( + editOperation: any, + gcrMainRepository: Repository, + simulate: boolean, + ): Promise { + const { context, username } = editOperation.data as { + context?: string + username?: string + } + + if (!context || !username) { + return { + success: false, + message: "Invalid payload: missing context or username", + } + } + + if (!this.TLSN_EXPECTED_ENDPOINTS[context]) { + return { + success: false, + message: `Unsupported TLSN context: ${context}`, + } + } + + const accountGCR = await gcrMainRepository.findOneBy({ + pubkey: editOperation.account, + }) + + if (!accountGCR) { + return { success: false, message: "Account not found" } + } + + accountGCR.identities.web2 = accountGCR.identities.web2 || {} + accountGCR.identities.web2[context] = + accountGCR.identities.web2[context] || [] + + const isMatch = (id: Web2GCRData["data"] & { proofType?: string }) => { + // TLSN remove must never affect legacy/non-TLSN web2 identities. + if (id.proofType !== "tlsn") { + return false + } + return id.username === username + } + + // Find the TLSN identity to remove + const identity = accountGCR.identities.web2[context].find( + (id: Web2GCRData["data"]) => isMatch(id as Web2GCRData["data"] & { proofType?: string }), + ) + + if (!identity) { + return { success: false, message: "TLSN identity not found" } + } + + // Filter out only the matching TLSN identity + accountGCR.identities.web2[context] = accountGCR.identities.web2[ + context + ].filter( + (id: Web2GCRData["data"]) => + !isMatch(id as Web2GCRData["data"] & { proofType?: string }), + ) + + if (!simulate) { + await gcrMainRepository.save(accountGCR) + + // Trigger TLSN incentive rollback only for confirmed TLSN provenance. + if (context === "github" && identity.userId) { + await IncentiveManager.githubUnlinked( + editOperation.account, + identity.userId, + ) + } else if (context === "discord") { + await IncentiveManager.discordUnlinked(editOperation.account) + } else if (context === "telegram") { + await IncentiveManager.telegramUnlinked(editOperation.account) + } + } + + return { success: true, message: "TLSN identity removed successfully" } + } } diff --git a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts index 3b48087bb..629ac773f 100644 --- a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts +++ b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts @@ -104,6 +104,21 @@ export class IncentiveManager { ) } + /** + * Hook to be called after TLSN Telegram linking + */ + static async telegramTLSNLinked( + userId: string, + telegramUserId: string, + referralCode?: string, + ): Promise { + return await this.pointSystem.awardTelegramTLSNPoints( + userId, + telegramUserId, + referralCode, + ) + } + /** * Hook to be called after Telegram unlinking */ diff --git a/src/libs/network/routines/transactions/handleIdentityRequest.ts b/src/libs/network/routines/transactions/handleIdentityRequest.ts index df9670888..704ba081f 100644 --- a/src/libs/network/routines/transactions/handleIdentityRequest.ts +++ b/src/libs/network/routines/transactions/handleIdentityRequest.ts @@ -11,8 +11,7 @@ import IdentityManager from "@/libs/blockchain/gcr/gcr_routines/identityManager" import { UDIdentityManager } from "@/libs/blockchain/gcr/gcr_routines/udIdentityManager" import { NomisWalletIdentity } from "@/model/entities/types/IdentityTypes" import { Referrals } from "@/features/incentive/referrals" -import log from "@/utilities/logger" -import ensureGCRForUser from "@/libs/blockchain/gcr/gcr_routines/ensureGCRForUser" +import { verifyTLSNProof, TLSNIdentityPayload } from "@/libs/tlsnotary" interface IdentityResponse { success: boolean @@ -100,11 +99,15 @@ export default async function handleIdentityRequest( return await IdentityManager.verifyNomisPayload( payload.payload as NomisWalletIdentity, ) + case "tlsn_identity_assign": + // TLSNotary identity verification - verify proof structure + return await verifyTLSNProof(payload.payload as TLSNIdentityPayload) case "xm_identity_remove": case "pqc_identity_remove": case "web2_identity_remove": case "nomis_identity_remove": case "ud_identity_remove": + case "tlsn_identity_remove": return { success: true, message: "Identity removed", @@ -112,7 +115,9 @@ export default async function handleIdentityRequest( default: return { success: false, - message: `Unsupported identity method: ${(payload as IdentityPayload).method}`, + message: `Unsupported identity method: ${ + (payload as IdentityPayload).method + }`, } } } diff --git a/src/libs/omniprotocol/protocol/handlers/gcr.ts b/src/libs/omniprotocol/protocol/handlers/gcr.ts index 698cc4d63..d7af0c629 100644 --- a/src/libs/omniprotocol/protocol/handlers/gcr.ts +++ b/src/libs/omniprotocol/protocol/handlers/gcr.ts @@ -34,7 +34,7 @@ interface IdentityAssignRequest { type: "identity" isRollback: boolean account: string - context: "xm" | "web2" | "pqc" | "ud" + context: "xm" | "web2" | "pqc" | "ud" | "nomis" | "tlsn" operation: "add" | "remove" data: any // Varies by context - see GCREditIdentity txhash: string @@ -71,8 +71,8 @@ export const handleIdentityAssign: OmniHandler = async ({ message, conte return encodeResponse(errorResponse(400, "account is required")) } - if (!editOperation.context || !["xm", "web2", "pqc", "ud"].includes(editOperation.context)) { - return encodeResponse(errorResponse(400, "Invalid context, must be xm, web2, pqc, or ud")) + if (!editOperation.context || !["xm", "web2", "pqc", "ud", "nomis", "tlsn"].includes(editOperation.context)) { + return encodeResponse(errorResponse(400, "Invalid context, must be xm, web2, pqc, ud, nomis, or tlsn")) } if (!editOperation.operation || !["add", "remove"].includes(editOperation.operation)) { diff --git a/src/libs/tlsnotary/index.ts b/src/libs/tlsnotary/index.ts new file mode 100644 index 000000000..57bfb2527 --- /dev/null +++ b/src/libs/tlsnotary/index.ts @@ -0,0 +1,19 @@ +/** + * TLSNotary Verification Module + */ +export { + initTLSNotaryVerifier, + isVerifierInitialized, + verifyTLSNotaryPresentation, + parseHttpResponse, + verifyTLSNProof, + extractUser, + type TLSNIdentityContext, + type TLSNIdentityPayload, + type TLSNProofRanges, + type TranscriptRange, + type TLSNotaryPresentation, + type TLSNotaryVerificationResult, + type ParsedHttpResponse, + type ExtractedUser, +} from "./verifier" diff --git a/src/libs/tlsnotary/verifier.ts b/src/libs/tlsnotary/verifier.ts new file mode 100644 index 000000000..3565f7644 --- /dev/null +++ b/src/libs/tlsnotary/verifier.ts @@ -0,0 +1,818 @@ +/** + * TLSNotary Proof Verifier for Node + * + * Validates TLSNotary presentation structure server-side. + * + * NOTE: Full cryptographic verification via WASM is not currently supported + * in Node.js CommonJS environments. This module validates proof structure + * and trusts client-provided claims. The actual cryptographic verification + * happens on the frontend (browser) where WASM works properly. + * + * TODO: Enable full WASM verification when tlsn-js supports Node.js properly. + */ +import log from "@/utilities/logger" +import Hashing from "@/libs/crypto/hashing" + +/** + * TLSNotary presentation format (from tlsn-js attestation) + */ +export interface TLSNotaryPresentation { + /** TLSNotary version (e.g., "0.1.0-alpha.12") */ + version: string + /** Hex-encoded proof data containing request/response and signatures */ + data: string + /** Metadata about the attestation */ + meta: { + notaryUrl?: string + websocketProxyUrl?: string + } +} + +/** + * Result of TLSNotary proof verification + */ +export interface TLSNotaryVerificationResult { + success: boolean + serverName?: string + sent?: Uint8Array | string + recv?: Uint8Array | string + time?: number + verifyingKey?: string + error?: string +} + +/** + * Parsed HTTP response structure + */ +export interface ParsedHttpResponse { + statusLine: string + headers: Record + body: string +} + +/** + * Supported TLSN identity contexts + */ +export type TLSNIdentityContext = "github" | "discord" | "telegram" + +/** + * Extracted user data (generic for all platforms) + */ +export interface ExtractedUser { + username: string + userId: string +} + +/** + * TLSN identity payload structure for verification + */ +export interface TLSNIdentityPayload { + context: TLSNIdentityContext + proof: TLSNotaryPresentation + recvHash: string + proofRanges: TLSNProofRanges + revealedRecv: number[] + username: string + userId: string + referralCode?: string +} + +export type TranscriptRange = { start: number; end: number } + +export type TLSNProofRanges = { + recv: TranscriptRange[] + sent: TranscriptRange[] +} + +function isHex(value: string): boolean { + return /^[0-9a-fA-F]+$/.test(value) +} + +function decodeRevealedRecv(revealedRecv: number[]): Uint8Array | null { + if (Array.isArray(revealedRecv)) { + const isValid = revealedRecv.every( + n => Number.isInteger(n) && n >= 0 && n <= 255, + ) + if (!isValid) return null + return new Uint8Array(revealedRecv) + } + return null +} + +function findBalancedJsonValue(text: string): string | null { + for (let start = 0; start < text.length; start++) { + const first = text[start] + if (first !== "{" && first !== "[") { + continue + } + + const stack: string[] = [first] + let inString = false + let escaped = false + + for (let i = start + 1; i < text.length; i++) { + const ch = text[i] + + if (inString) { + if (escaped) { + escaped = false + continue + } + + if (ch === "\\") { + escaped = true + continue + } + + if (ch === '"') { + inString = false + } + + continue + } + + if (ch === '"') { + inString = true + continue + } + + if (ch === "{" || ch === "[") { + stack.push(ch) + continue + } + + if (ch === "}" || ch === "]") { + const open = stack.pop() + if (!open) { + break + } + + const isMatch = + (open === "{" && ch === "}") || (open === "[" && ch === "]") + if (!isMatch) { + break + } + + if (stack.length === 0) { + return text.slice(start, i + 1) + } + } + } + } + + return null +} + +function findBalancedJsonValueAt( + text: string, + start: number, +): { value: string; end: number } | null { + const first = text[start] + if (first !== "{" && first !== "[") { + return null + } + + const stack: string[] = [first] + let inString = false + let escaped = false + + for (let i = start + 1; i < text.length; i++) { + const ch = text[i] + + if (inString) { + if (escaped) { + escaped = false + continue + } + + if (ch === "\\") { + escaped = true + continue + } + + if (ch === '"') { + inString = false + } + + continue + } + + if (ch === '"') { + inString = true + continue + } + + if (ch === "{" || ch === "[") { + stack.push(ch) + continue + } + + if (ch === "}" || ch === "]") { + const open = stack.pop() + if (!open) { + return null + } + + const isMatch = + (open === "{" && ch === "}") || (open === "[" && ch === "]") + if (!isMatch) { + return null + } + + if (stack.length === 0) { + return { value: text.slice(start, i + 1), end: i } + } + } + } + + return null +} + +function findBalancedJsonCandidates(text: string): string[] { + const candidates: string[] = [] + + for (let i = 0; i < text.length; i++) { + if (text[i] !== "{" && text[i] !== "[") { + continue + } + + const match = findBalancedJsonValueAt(text, i) + if (!match) { + continue + } + + candidates.push(match.value) + i = match.end + } + + return candidates +} + +function maybeParseJsonText(text: string): string | null { + const trimmed = text.trim() + if (!trimmed) { + return null + } + + // Direct JSON body + if (trimmed.startsWith("{") || trimmed.startsWith("[")) { + return trimmed + } + + // Attempt to strip chunked framing for common HTTP chunked payloads. + // Example: "179\r\n{...json...}\r\n0" + const lines = trimmed.split(/\r?\n/) + if (lines.length >= 3) { + const first = lines[0].trim() + const last = lines[lines.length - 1].trim() + if (/^[0-9a-fA-F]+$/.test(first) && last === "0") { + const middle = lines.slice(1, -1).join("\n").trim() + if (middle.startsWith("{") || middle.startsWith("[")) { + return middle + } + } + } + + // Last-resort extraction for mixed text containing JSON: + // return the first balanced JSON object/array substring. + const balancedJson = findBalancedJsonValue(trimmed) + if (balancedJson) { + return balancedJson + } + + return null +} + +function parseDisclosedRecvBody(recvBytes: Uint8Array): string | null { + const httpResponse = parseHttpResponse(recvBytes) + if (httpResponse) { + const jsonBody = maybeParseJsonText(httpResponse.body) + if (jsonBody) { + return jsonBody + } + } + + // Body-only fallback (when no HTTP headers are included in disclosed bytes). + const text = new TextDecoder().decode(recvBytes) + const jsonBody = maybeParseJsonText(text) + if (jsonBody) { + return jsonBody + } + + return null +} + +function extractUserFromRawText( + context: TLSNIdentityContext, + text: string, +): ExtractedUser | null { + try { + const candidates = findBalancedJsonCandidates(text) + + for (const candidate of candidates) { + let parsed: unknown + try { + parsed = JSON.parse(candidate) + } catch { + parsed = null + } + + const objects: unknown[] = parsed + ? Array.isArray(parsed) + ? parsed + : [parsed] + : [] + + for (const obj of objects) { + if (!obj || typeof obj !== "object") { + continue + } + const value = obj as Record + + if ( + context === "github" && + value.login && + value.id !== undefined + ) { + return { + username: String(value.login), + userId: String(value.id), + } + } + + if ( + context === "discord" && + value.username && + value.id !== undefined + ) { + return { + username: String(value.username), + userId: String(value.id), + } + } + + if (context === "telegram") { + const user = + value.user && typeof value.user === "object" + ? (value.user as Record) + : value + const extractedUsername = user.username || user.first_name + if (user.id !== undefined && extractedUsername) { + return { + username: String(extractedUsername), + userId: String(user.id), + } + } + } + } + + // Fallback for partially redacted/non-strict JSON candidates: + // still require both fields to come from the same candidate blob. + if (context === "github") { + const loginMatch = candidate.match(/"login"\s*:\s*"([^"]+)"/) + const idMatch = candidate.match(/"id"\s*:\s*"?(\d+)"?/) + if (loginMatch?.[1] && idMatch?.[1]) { + return { username: loginMatch[1], userId: idMatch[1] } + } + } + + if (context === "discord") { + const usernameMatch = candidate.match( + /"username"\s*:\s*"([^"]+)"/, + ) + const idMatch = candidate.match(/"id"\s*:\s*"?(\d+)"?/) + if (usernameMatch?.[1] && idMatch?.[1]) { + return { username: usernameMatch[1], userId: idMatch[1] } + } + } + + if (context === "telegram") { + const usernameMatch = candidate.match( + /"username"\s*:\s*"([^"]+)"/, + ) + const firstNameMatch = candidate.match( + /"first_name"\s*:\s*"([^"]+)"/, + ) + const idMatch = candidate.match(/"id"\s*:\s*"?(\d+)"?/) + const extractedUsername = + usernameMatch?.[1] || firstNameMatch?.[1] + if (idMatch?.[1] && extractedUsername) { + return { + username: extractedUsername, + userId: idMatch[1], + } + } + } + } + + // Final fallback when no balanced JSON candidate is discoverable + // (e.g. heavily redacted/truncated bodies): require both fields + // within the same bounded text window. + if (context === "github") { + const pairMatch = + text.match( + /"login"\s*:\s*"([^"]+)"[\s\S]{0,2000}"id"\s*:\s*"?(\d+)"?/, + ) || + text.match( + /"id"\s*:\s*"?(\d+)"?[\s\S]{0,2000}"login"\s*:\s*"([^"]+)"/, + ) + + if (pairMatch) { + if (pairMatch[1]?.match(/^\d+$/)) { + return { username: pairMatch[2], userId: pairMatch[1] } + } + return { username: pairMatch[1], userId: pairMatch[2] } + } + } + + if (context === "discord") { + const pairMatch = + text.match( + /"username"\s*:\s*"([^"]+)"[\s\S]{0,2000}"id"\s*:\s*"?(\d+)"?/, + ) || + text.match( + /"id"\s*:\s*"?(\d+)"?[\s\S]{0,2000}"username"\s*:\s*"([^"]+)"/, + ) + + if (pairMatch) { + if (pairMatch[1]?.match(/^\d+$/)) { + return { username: pairMatch[2], userId: pairMatch[1] } + } + return { username: pairMatch[1], userId: pairMatch[2] } + } + } + + if (context === "telegram") { + const usernameAndId = + text.match( + /"username"\s*:\s*"([^"]+)"[\s\S]{0,2000}"id"\s*:\s*"?(\d+)"?/, + ) || + text.match( + /"id"\s*:\s*"?(\d+)"?[\s\S]{0,2000}"username"\s*:\s*"([^"]+)"/, + ) + + if (usernameAndId) { + if (usernameAndId[1]?.match(/^\d+$/)) { + return { + username: usernameAndId[2], + userId: usernameAndId[1], + } + } + return { username: usernameAndId[1], userId: usernameAndId[2] } + } + + const firstNameAndId = + text.match( + /"first_name"\s*:\s*"([^"]+)"[\s\S]{0,2000}"id"\s*:\s*"?(\d+)"?/, + ) || + text.match( + /"id"\s*:\s*"?(\d+)"?[\s\S]{0,2000}"first_name"\s*:\s*"([^"]+)"/, + ) + + if (firstNameAndId) { + if (firstNameAndId[1]?.match(/^\d+$/)) { + return { + username: firstNameAndId[2], + userId: firstNameAndId[1], + } + } + return { + username: firstNameAndId[1], + userId: firstNameAndId[2], + } + } + } + } catch { + return null + } + + return null +} + +/** + * Initialize TLSNotary verifier (no-op in current implementation) + * + * This function exists for API compatibility. Full WASM initialization + * is not supported in Node.js CommonJS environments. + */ +export async function initTLSNotaryVerifier(): Promise { + log.info( + "[TLSNotary Verifier] Structure-only verification mode (WASM not available in Node.js)", + ) +} + +/** + * Check if the verifier is initialized + * + * Always returns true since structure validation doesn't require initialization. + */ +export function isVerifierInitialized(): boolean { + return true +} + +/** + * Verify a TLSNotary presentation structure + * + * Validates that the presentation has the required fields and format. + * Does NOT perform cryptographic verification (that happens on frontend). + * + * @param presentationJSON - The TLSNotary presentation to verify + * @returns Verification result + */ +export async function verifyTLSNotaryPresentation( + presentationJSON: TLSNotaryPresentation, +): Promise { + try { + // Validate presentation structure + if (!presentationJSON || typeof presentationJSON !== "object") { + return { + success: false, + error: "Invalid presentation: expected object", + } + } + + if ( + !presentationJSON.data || + typeof presentationJSON.data !== "string" + ) { + return { + success: false, + error: "Invalid presentation: missing or invalid 'data' field", + } + } + + if ( + !presentationJSON.version || + typeof presentationJSON.version !== "string" + ) { + return { + success: false, + error: "Invalid presentation: missing or invalid 'version' field", + } + } + + // Validate data is hex-encoded (basic check) + if (!isHex(presentationJSON.data)) { + return { + success: false, + error: "Invalid presentation: 'data' field is not valid hex", + } + } + + // Minimum data length check (a valid proof should have substantial data) + if (presentationJSON.data.length < 100) { + return { + success: false, + error: "Invalid presentation: 'data' field is too short", + } + } + + log.info("[TLSNotary Verifier] Proof structure validated successfully") + + return { + success: true, + time: Date.now(), + verifyingKey: "structure-validation-only", + } + } catch (error) { + log.error(`[TLSNotary Verifier] Verification failed: ${error}`) + return { + success: false, + error: error instanceof Error ? error.message : String(error), + } + } +} + +/** + * Parse HTTP response from recv bytes + * + * Extracts the status line, headers, and body from raw HTTP response bytes. + * + * @param recv - Raw HTTP response bytes from TLSNotary verification + * @returns Parsed HTTP response or null if parsing fails + */ +export function parseHttpResponse( + recv: Uint8Array | string, +): ParsedHttpResponse | null { + try { + const text = + typeof recv === "string" ? recv : new TextDecoder().decode(recv) + + // Find the end of headers (double CRLF) + const headerEndIndex = text.indexOf("\r\n\r\n") + if (headerEndIndex === -1) { + log.warn( + "[TLSNotary Verifier] No header/body separator found in response", + ) + return null + } + + const headerSection = text.slice(0, headerEndIndex) + const body = text.slice(headerEndIndex + 4) + + const headerLines = headerSection.split("\r\n") + const statusLine = headerLines[0] || "" + + const headers: Record = {} + for (let i = 1; i < headerLines.length; i++) { + const colonIndex = headerLines[i].indexOf(":") + if (colonIndex !== -1) { + const key = headerLines[i] + .slice(0, colonIndex) + .trim() + .toLowerCase() + const value = headerLines[i].slice(colonIndex + 1).trim() + headers[key] = value + } + } + + return { statusLine, headers, body } + } catch (error) { + log.error( + `[TLSNotary Verifier] Failed to parse HTTP response: ${error}`, + ) + return null + } +} + +/** + * Extract user data from API response body based on context + * + * Parses the JSON response from the platform's API and extracts + * the username and user ID based on the context. + * + * @param context - The platform context (github, discord, telegram) + * @param responseBody - The JSON body from the platform's API endpoint + * @returns Extracted user data or null if extraction fails + */ +export function extractUser( + context: TLSNIdentityContext, + responseBody: string, +): ExtractedUser | null { + try { + const json = JSON.parse(responseBody) + + switch (context) { + case "github": + if (json.login && json.id !== undefined) { + return { + username: json.login, + userId: String(json.id), + } + } + log.warn( + "[TLSNotary Verifier] GitHub response missing 'login' or 'id' fields", + ) + return null + + case "discord": + if (json.username && json.id !== undefined) { + return { + username: json.username, + userId: String(json.id), + } + } + log.warn( + "[TLSNotary Verifier] Discord response missing 'username' or 'id' fields", + ) + return null + + case "telegram": { + // Handle response format: { user: { id, username, first_name, ... } } + const user = json.user || json + if (user.id !== undefined) { + const extractedUsername = user.username || user.first_name + if (!extractedUsername) { + log.warn( + "[TLSNotary Verifier] Telegram response missing 'username' and 'first_name' fields", + ) + return null + } + return { + username: extractedUsername, + userId: String(user.id), + } + } + log.warn( + "[TLSNotary Verifier] Telegram response missing 'id' field", + ) + return null + } + + default: + log.warn(`[TLSNotary Verifier] Unsupported context: ${context}`) + return null + } + } catch (error) { + log.error( + `[TLSNotary Verifier] Failed to parse ${context} response: ${error}`, + ) + return null + } +} + +/** + * Verify a TLSNotary proof for any supported context + * + * Validates proof structure, verifies recv hash against proof ranges, + * parses the extracted HTTP response, and checks extracted identity + * fields against claimed username/userId. + * + * @param payload - The TLSN identity payload containing context, proof, username, and userId + * @returns Verification result + */ +export async function verifyTLSNProof(payload: TLSNIdentityPayload): Promise<{ + success: boolean + message: string + extractedUsername?: string + extractedUserId?: string +}> { + const { context, proof, recvHash, revealedRecv, username, userId } = payload + + // Validate context + if (!["github", "discord", "telegram"].includes(context)) { + return { + success: false, + message: `Unsupported TLSN context: ${context}`, + } + } + + if (typeof recvHash !== "string" || !/^[0-9a-fA-F]{64}$/.test(recvHash)) { + return { + success: false, + message: "Invalid TLSN recvHash: expected 64-char hex sha256", + } + } + + // Verify the proof structure + const verified = await verifyTLSNotaryPresentation(proof) + if (!verified.success) { + return { + success: false, + message: `Proof verification failed: ${verified.error}`, + } + } + + const recvBytes = decodeRevealedRecv(revealedRecv) + if (!recvBytes) { + return { + success: false, + message: "Invalid TLSN revealedRecv: expected byte array (0-255)", + } + } + + if (recvBytes.length === 0) { + return { + success: false, + message: "Invalid TLSN revealedRecv: empty payload", + } + } + + const computedRecvHash = Hashing.sha256Bytes(recvBytes) + if (computedRecvHash.toLowerCase() !== recvHash.toLowerCase()) { + return { + success: false, + message: + "recvHash mismatch: provided hash does not match disclosed recv bytes", + } + } + + const responseBody = parseDisclosedRecvBody(recvBytes) + const rawText = new TextDecoder().decode(recvBytes) + const extractedUser = + (responseBody ? extractUser(context, responseBody) : null) || + extractUserFromRawText(context, rawText) + if (!extractedUser) { + return { + success: false, + message: `Failed to extract user from ${context} revealedRecv payload`, + } + } + + if (extractedUser.username !== username) { + return { + success: false, + message: `Username mismatch: claimed '${username}', proof contains '${extractedUser.username}'`, + } + } + + if (extractedUser.userId !== String(userId)) { + return { + success: false, + message: `UserId mismatch: claimed '${String( + userId, + )}', proof contains '${extractedUser.userId}'`, + } + } + + log.info( + `[TLSNotary Verifier] ${context} proof and recvHash validated for userId=${userId}`, + ) + + return { + success: true, + message: "Proof and recvHash verified", + extractedUsername: extractedUser.username, + extractedUserId: extractedUser.userId, + } +} diff --git a/src/model/entities/GCRv2/GCR_Main.ts b/src/model/entities/GCRv2/GCR_Main.ts index d3154f288..e08c41a8d 100644 --- a/src/model/entities/GCRv2/GCR_Main.ts +++ b/src/model/entities/GCRv2/GCR_Main.ts @@ -41,6 +41,11 @@ export class GCRMain { points: number }> nomisScores: { [chain: string]: number } + zkAttestation?: Array<{ + date: string + points: number + nullifier: string + }> } lastUpdated: Date }