Skip to content

Latest commit

 

History

History
404 lines (343 loc) · 73.1 KB

File metadata and controls

404 lines (343 loc) · 73.1 KB

Sentinel SDK Failure Catalog

Every failure pattern discovered across 10 apps, 2200+ mainnet node tests, 25 project findings files, 161 suggestion files, and 200+ hours of development. For any AI building on this SDK: read this BEFORE writing code. Every entry cost real tokens and real debugging time.


Quick Rules -- The 35 Most Critical

# Rule Category Consequence of Violation
1 Use v3 LCD paths, not v2 -- v2 returns "Not Implemented" except /sentinel/provider/v2/ chain Zero chain queries work
2 Never trust count_total or next_key on Sentinel LCD pagination -- use limit=5000 single request chain Missing 400+ nodes
2a Same rule for RPC: QueryNodes/QueryNodesForPlan silently truncate at limit, never emit next_key -- use limit=10000 on both endpoints. Old SDK default limit=500 made plan 36 (803 nodes) look like a 500-node plan. chain Plans with >500 nodes return exactly 500; no error, no warning
3 remote_addrs is an array, not remote_url string -- LCD v3 changed the field name and format chain All connections fail silently (undefined)
4 Session data is nested under base_session -- always access session.base_session.id chain Silent undefined propagation
5 WireGuard requires Administrator privileges -- check BEFORE paying for a session tunnel Money wasted, no connection
6 Never install full-tunnel to unreachable endpoint -- AllowedIPs=0.0.0.0/0 kills internet instantly tunnel Total internet death
7 V2Ray must be exactly v5.2.1 -- newer versions have observatory bugs dependencies Silent connection failures
8 MTU must be 1280, not 1420 -- Sentinel nodes are configured for 1280 tunnel TLS handshake failures, service crashes
9 WireGuard DNS must be 10.8.0.1 (node's internal resolver), not external DNS tunnel DNS resolution fails through tunnel
10 grpc/tls has 0% success rate -- filter these transports BEFORE paying protocol Guaranteed failure, tokens wasted
11 QUIC quicSettings must use security: 'none' -- not chacha20-poly1305 protocol 0% QUIC connections
12 VMess AEAD requires clock drift <120s -- but VLess is immune to clock drift protocol VMess-only nodes fail with drift
13 Register MsgEndSession in protobuf Registry -- or sessions never end on-chain chain Orphaned sessions leak resources
14 Account sequence mismatch -- serialize broadcasts through a mutex, retry with backoff chain TX failures cascade
15 Session may be inactive_pending after TX -- poll until active before handshaking timing "Invalid session status" errors
16 Chain lag -- node's RPC may not see session for 10s after broadcast timing "Session does not exist" on handshake
17 Verify-before-capture for WireGuard -- test with split IPs first, then switch to full tunnel tunnel 78s of dead internet on failure
18 autoReconnect() checks status?.connected but that property doesn't exist -- use !!status protocol Entire reconnect feature is broken
19 Never taskkill /F /IM node.exe -- Claude Code runs on Node.js dependencies Kills development environment
20 BigInt cannot be JSON.stringify'd -- convert sessionId to string before serialization protocol TypeError crash
21 Error code strings are a CONTRACT between SDKs -- SESSION_EXISTS must match exactly parity Cross-language apps break
22 Unit tests prove nothing about live chain -- 656 tests passed, zero features worked testing False confidence
23 fullTunnel: true is the default -- routes ALL traffic through VPN. AI agents should explicitly set fullTunnel: false or use protocol: 'v2ray' for split tunnel. Intentionally true since v26c (false caused "IP didn't change" confusion for consumers). configuration AI's chain queries slow down through VPN
24 SOCKS5 auth breaks Windows system proxy -- system proxy cannot pass credentials tunnel "Connected" but zero traffic
25 Fee grant auto-detection should be opt-in -- don't silently use random granters wallet Unexpected behavior, unreliable grants
26 Shared VPN client for testing corrupts state -- always create a DEDICATED VPN client per test integration Main VPN session disconnected, state corrupted
27 CancellationToken in speed test kills measurements -- pass CancellationToken.None to speed/google tests integration Garbage speed numbers, premature cancellation
28 Background refresh starves test connections -- cancel background work before starting test scan integration Test hangs indefinitely waiting for HTTP client
29 Progress counter must increment on EVERY code path -- success, failure, AND exception integration UI freezes, user thinks app is stuck
30 V2Ray SOCKS5 connection reuse silently fails -- create FRESH HttpClient/SocksProxyAgent per request protocol First request works, subsequent hang until timeout
31 WPF cannot render emoji flags -- use PNG images from flagcdn.com, not emoji code points ux Empty boxes or nothing at all on Windows native apps
32 Load previous results on startup -- never show "No results" when results exist on disk ux User loses trust, thinks data was lost
33 Docs describing non-existent code cause more harm than no docs -- label IMPLEMENTED vs SPEC documentation AI spends 10+ hours trying to use classes that don't exist
34 transport_security is 0-indexed in C#, 1-indexed in JS -- always check enum mappings when bridging parity All C# V2Ray tests fail with wrong TLS setting
35 Missing UUID wait in new code paths -- copy ALL waits/sleeps when adding a new code path timing All V2Ray connections on new path fail silently
36 NEVER use native fetch() for V2Ray traffic -- fetch() silently ignores SOCKS5 proxy. You WILL get your real IP, not the VPN IP. Use axios with SocksProxyAgent for ALL V2Ray verification, speed tests, and IP checks. This is the #1 mistake every AI builder makes. protocol IP leak — agent thinks it's on VPN but all traffic goes direct
37 V2Ray split tunnel IS the SOCKS5 proxy -- V2Ray does not change system routing. Only traffic you explicitly send through socks5://127.0.0.1:{port} goes through the VPN. Everything else is direct. There is no fullTunnel for V2Ray — systemProxy: true sets Windows proxy but that's opt-in, not default. protocol Agent assumes all traffic is encrypted when only proxied traffic is
38 WireGuard split tunnel requires exact destination IPs -- splitIPs: ['example.com'] does NOT work. WireGuard routes by IP, not domain. CDN/anycast services (Cloudflare, Google) resolve to hundreds of IPs. Use V2Ray SOCKS5 for per-app split tunnel, use WireGuard splitIPs only for known static IPs. tunnel Agent sets splitIPs for a CDN domain, traffic goes direct because DNS resolved to a different IP
39 WireGuard disconnect MUST restore DNS to DHCP -- WireGuard config sets system DNS (10.8.0.1 or custom). This persists in the OS adapter AFTER the WG interface is removed. Every disconnect path (normal, error, emergency) must call disableDnsLeakPrevention() or netsh interface ipv4 set dnsservers Wi-Fi dhcp. Discovered 2026-03-27: Cloudflare DNS persisted after split tunnel test, broke all V2Ray and node tester connections. tunnel System DNS silently changed, all subsequent networking affected

Failures by Category

PROTOCOL

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
P1 QUIC 0% success rate All 4 QUIC nodes failed despite having active peers Global quicSettings used security: 'chacha20-poly1305' but Sentinel Go SDK server uses security: 'none' Changed to { security: 'none', key: '', header: { type: 'none' } } in both global and per-outbound settings Always match transport security settings to sentinel-go-sdk server defaults
P2 grpc/tls always fails 0 nodes passed grpc/tls out of 14+ tested; nodes have active peers V2Ray gRPC over TLS has incompatible server config; sentinel nodes don't support it Filter grpc/tls (transport_protocol=3, transport_security=2) BEFORE session payment Pre-filter: if all transports are grpc/tls, skip node
P3 VMess clock drift AEAD failure VMess connections to nodes with >120s clock drift fail silently VMess AEAD timestamp auth has +/-120s tolerance; HTTP Date header drift detected but node still tried Skip VMess-only nodes with >120s drift; prefer VLess outbounds when drift detected Check clock drift AND available protocols; VLess ignores drift
P4 grpc missing serviceName gRPC connections fail with "context canceled" after VMess auth Per-outbound streamSettings had no grpcSettings block; V2Ray fell back to wrong global setting Added grpcSettings: { serviceName: '' } for both grpc and gun networks Always include grpcSettings for gun(2) and grpc(3) transports
P5 gun vs grpc documentation contradiction Docs said gun and grpc are "DIFFERENT protocols" but V2Ray treats them identically Sentinel uses different enum values (2=gun, 3=grpc) but V2Ray config is identical for both Updated docs: both use "network": "grpc" with grpcSettings: { serviceName: '' } For V2Ray config, gun and grpc are the same -- both use grpcSettings
P6 v2-format metadata rejection Most popular node (48 peers) rejected because it returned v2 metadata format SDK threw error on old {protocol, tls, ca} fields instead of mapping to v3 {proxy_protocol, transport_protocol, transport_security} Added v2-to-v3 field mapper: v2 protocol:1->v3 proxy_protocol:2, etc. Never reject metadata outright -- map old formats to new
P7 autoReconnect completely broken autoReconnect() never triggers reconnection Checked status?.connected but getStatus() returns { sessionId, serviceType, ... } with no .connected property -- always false Changed to !!status (getStatus returns null when disconnected) Test feature with real connection lifecycle, not just existence
P8 BigInt JSON serialization crash JSON.stringify({ sessionId: 123n }) throws TypeError sessionId is BigInt, JavaScript cannot serialize BigInt to JSON Call .toString() before sending to frontend; SDK's serializeResult() exists but must be called Always convert BigInt to string at API boundaries
P9 broadcast() name collision SSE broadcast and SDK chain broadcast share the same name SDK exports broadcast() for chain TX; apps commonly have local broadcast() for SSE Document collision risk; consider renaming SDK export to broadcastTx() When importing SDK functions, check for local name conflicts
P10 Transport success rates outdated Code cited grpc/none=58%, quic=55% but real rates are grpc/none=87%, quic=0% Numbers from early testing never updated after 780-node scan Updated TRANSPORT_SUCCESS_RATES to match 780-node data Update transport stats after every major test run
P11 Session ID precision time bomb Number(sessionId) silently rounds integers above 2^53 initHandshakeV3() converts BigInt to Number for handshake POST body Added Number.isSafeInteger() bounds check; fails loudly instead of silently Never downcast BigInt to Number without safety check
P12 V2Ray balancer unreliable V2Ray 5.2.1 observatory marks working outbounds as dead V2Ray internal health checker is buggy in 5.2.1 Implemented own fallback: balancer first (8s), then individual outbounds in priority order Don't rely on V2Ray's internal balancer -- implement app-level retry
P13 Port probe false negative kills nodes with peers V2Ray "service dead" error for nodes with active connections Pre-payment port probe scans 12 standard ports, but V2Ray may run on non-standard ports If peers > 0, skip probe failure and proceed to handshake (reveals actual ports in metadata) Never hard-fail port probe when peers > 0; someone IS connecting, V2Ray IS alive
P14 TCP probe fail wastes 20s per V2Ray outbound Node test times out after trying 3-4 unreachable outbounds When TCP SYN fails but V2Ray starts anyway, SOCKS5 wait (12s) + sleep (4s) per outbound = 16-20s each Reduce SOCKS5 wait from 12/8s to 5s when TCP probe already failed; saves ~15s per unreachable port Use TCP probe result to size timeouts: probe OK → normal timeout; probe fail → fast fail
P15 VMess clock drift unfixable for AEAD-only servers VMess nodes with >120s drift fail with both alterId=0 and alterId=64 AEAD (alterId=0) rejects timestamps >120s off. Legacy (alterId=64) on AEAD-only server → auth mismatch → 15s silent drain. No V2Ray config adjusts timestamp. Try both alterId values, accept failure. Only fix is node operator fixing their clock or supporting legacy. VMess with drift >120s on AEAD server = UNFIXABLE from client. Skip these nodes and document why. VLess is immune to drift.
P16 SOCKS5 "connected" but no internet V2Ray opens SOCKS5 port even when remote connection fails V2Ray starts SOCKS5 listener immediately, before establishing remote tunnel. Traffic enters SOCKS5 but can't route. Add 3s google connectivity pre-check before running full speedtest. Detects dead tunnels 10x faster. Never assume SOCKS5 port open = tunnel working. Always pre-check connectivity.
P17 Port scan discovers non-V2Ray ports 7874/7876 Discovered ports tried as V2Ray, waste time, always fail Sentinel-go-sdk nodes have internal control/WireGuard ports (7874, 7876) that accept TCP but don't serve V2Ray Filter discovered ports: if they accept TCP but no TLS and no HTTP response, skip as non-V2Ray Cross-reference discovered ports with known sentinel internal port ranges before attempting V2Ray

CHAIN

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
C1 v2 LCD endpoints return "Not Implemented" All C# SDK chain queries fail -- zero features work C# SDK used v2 paths (/sentinel/nodes?status=STATUS_ACTIVE); chain runs v3 Updated all 9 endpoints to v3 paths (e.g., /sentinel/node/v3/nodes?status=1) Always use v3 paths; only exception: provider remains v2
C2 LCD count_total returns wrong number Plan discovery shows 1 node but plan has 733 LCD returns min(actual_count, limit) as count_total with limit=1 Changed to limit=5000 and count array length directly NEVER trust count_total -- always count the returned array
C3 LCD next_key always null on plan nodes Pagination stops at 200 nodes; misses 533 /sentinel/node/v3/plans/{id}/nodes doesn't implement next_key pagination Single request with limit=5000 Test pagination per endpoint; some are broken
C4 remote_addrs vs remote_url All node connections fail -- node.remote_url is undefined v3 LCD returns remote_addrs: ["IP:PORT"] (array, no protocol) instead of remote_url: "https://IP:PORT" Added resolveRemoteUrl() that handles both formats Always use resolveRemoteUrl(node), never access field directly
C5 base_session nesting undocumented session.id returns undefined; no error, just silent null v3 LCD nests session fields under base_session Always use const bs = session.base_session || session Flatten base_session in all query helpers
C6 MsgEndSession not registered in protobuf Sessions NEVER end on-chain; orphaned sessions accumulate MsgEndSessionRequest type URL not registered in CosmJS Registry; no encoder function exists Registered type in buildRegistry() and created encodeMsgEndSession() Verify ALL message types are registered, not just commonly used ones
C7 status=1 vs status=STATUS_ACTIVE Node status filter returns wrong results v3 uses integer status codes (1=active), v2 used string enum Updated all queries to use status=1 Use integer status codes for v3 chain
C8 acc_address vs address Subscription parser fails v3 uses acc_address field, v2 used address Updated parser to read acc_address Always verify field names against actual LCD responses
C9 Plan discovery endpoint returns 501 DiscoverPlansAsync() returns 0 plans on mainnet /sentinel/plan/v3/plans/{id} is NOT IMPLEMENTED on chain Use subscription+node endpoints to probe plan existence Test endpoints with curl before building against them
C10 Provider endpoint is v2 only Provider queries fail with v3 path /sentinel/provider/v3/ returns 501; must use /sentinel/provider/v2/ Use v2 path for provider queries only Document the v2 exception prominently
C11 Subscription endpoint returns all 143K results GetSubscriptionsAsync fetches entire chain's subscriptions Wrong path /sentinel/subscription/v3/subscriptions?account_address= vs correct /sentinel/subscription/v3/accounts/{addr}/subscriptions Fixed to account-scoped path Always use account-scoped endpoints for per-user queries
C12 Handshake error field type mismatch JsonException on deserializing handshake error response Node returns {"error": {"code": 2, "message": "..."}} (object) but SDK typed Error as string? Changed to JsonElement? with type-safe accessor Use flexible JSON types for node API responses -- formats vary
C13 Plans start inactive AI creates plan, tries to subscribe, gets chain error encodeMsgCreatePlan() creates plan with status=0 (inactive); needs separate encodeMsgUpdatePlanStatus() Added createAndActivatePlan() helper Document two-step plan creation prominently
C14 queryNode() downloaded ALL nodes to find one Single node lookup fetches 900+ nodes then .find() No direct endpoint used; full paginated query used for single lookup Try /sentinel/node/v3/nodes/{address} first; fall back to full list Always use direct endpoints when querying single items
C15 max_price Code 106 "invalid price" in MsgStartSession Session payment fails for nodes with certain price combos (base_value=0.005, quote_value=25M) Chain v3 price validation rejects combinations that were valid at node registration time Catch Code 106 → retry WITHOUT max_price field; chain uses node's registered price directly Always implement retry-without-max_price for MsgStartSession; 14/987 nodes affected
C16 Batch payment fails on ONE bad-price node Entire 5-node batch TX rejected with Code 106 when any node has invalid pricing Batch contains mix of standard (40M quote) and non-standard (25M quote) prices Retry entire batch without max_price; if still fails, fall back to individual per-node payments Batch TX is all-or-nothing; one bad message kills all 5. Always have individual fallback.

TUNNEL

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
T1 WireGuard race condition -- config deleted before service reads it "The system cannot find the file specified" service error SDK deletes config file during cleanup before Windows service has finished starting Never delete config file while service exists; only delete AFTER confirmed uninstall Config must remain on disk as long as service exists
T2 MTU 1420 causes service crash WireGuard service installs but immediately stops Sentinel nodes configured for MTU 1280; using 1420 causes packet fragmentation and TLS failures Changed to MTU = 1280 Always use MTU 1280 for Sentinel WireGuard tunnels
T3 External DNS unreachable in full tunnel DNS queries to OpenDNS (208.67.222.222) fail through tunnel Full tunnel only routes to WireGuard endpoint; external DNS is unreachable Changed DNS to 10.8.0.1 (node's internal resolver) Use node's internal DNS (10.8.0.1) in full-tunnel mode
T4 Full-tunnel verification kills internet for 78s User loses all internet while verification loops fail on a broken node setupWireGuard() installed tunnel with AllowedIPs=0.0.0.0/0 before verification Verify-before-capture: install with split IPs first, verify, then switch to 0.0.0.0/0 Always verify tunnel works with safe split IPs before capturing all traffic
T5 Phantom connected state App shows "connected" when no tunnel exists; IP leak getStatus() trusted state.connection without checking if tunnel was alive Cross-validate tunnel health; auto-clear stale state; emit disconnected event Always verify tunnel liveness (service running, process alive) before reporting connected
T6 Windows service race on tunnel switch Double-uninstall races with Windows Service Manager; tunnel fails to start Manual disconnectWireGuard() + installWgTunnel() internal force-remove created double-uninstall Removed manual pre-disconnect; let installWgTunnel() handle its own cleanup Single uninstall path -- never double-uninstall
T7 V2Ray process leak on outbound loop exit Orphaned V2Ray processes accumulate; SOCKS5 ports consumed Outbound connection loop exits without finally block to kill last spawned process Wrapped loop in try-finally that kills last process on any exception Always use try-finally when spawning child processes in loops
T8 Tunnel not cleaned up on handshake retry failure Dead internet from orphaned WireGuard 0.0.0.0/0 route Retry catch block only marks session as poisoned; doesn't clean up partially installed tunnel Added tunnel cleanup check (state.wgTunnel, state.v2rayProc) in retry catch block Always clean up tunnel state in all error paths
T9 SOCKS5 auth breaks Windows system proxy "Connected" but zero traffic flows V2Ray config uses password auth; Windows system proxy cannot pass SOCKS5 credentials When systemProxy: true, patch SOCKS5 inbound to noauth System proxy mode requires noauth SOCKS5
T10 System proxy stuck after crash All HTTP traffic goes to dead SOCKS5 port; browser shows "no internet" clearSystemProxy() only runs in cleanup handler; crash leaves registry pointing to dead port Write sentinel file on proxy set; check at startup; restore on recovery Always persist proxy state so crash recovery can restore it
T11 Proxy restore overwrites user's previous proxy User had corporate proxy; clearSystemProxy() sets "no proxy" instead of restoring Code force-disables proxy entirely instead of restoring saved state Save proxy state before modifying; restore exact previous state on cleanup Always backup and restore system proxy state
T12 Orphaned WireGuard adapters block new tunnels "Tunnel already installed and running" error on fresh connect Previous crash left Wintun adapter registered but no service managing it Emergency cleanup at startup: detect and remove orphaned adapters Run emergencyCleanupSync() at startup; clean all stale wgsent* services
T13 WireGuard private key file not deleted on failure Private key stays at C:\ProgramData\sentinel-wg\wgsent0.conf indefinitely Bare catch {} on file deletion; locked file or permissions failure silently ignored Retry deletion; overwrite with zeros before delete; log failure Always zero-fill sensitive files before deletion
T14 Config ACL race -- key readable before ACL set Config file (with private key) world-readable in ProgramData between write and ACL set File written FIRST, ACL set SECOND; failure in ACL leaves file exposed Create directory with restrictive ACL first, then write file Set restrictive permissions on directory before writing sensitive files
T15 Full tunnel + Handshake DNS = 0-speed (44 nodes) WireGuard tunnel connects but speed test returns exactly 0 Mbps Full tunnel routes DNS through VPN; Handshake DNS (103.196.38.38) unreachable through many nodes Pre-resolve all speed test hostnames BEFORE tunnel installation; use resolved IPs Always pre-resolve DNS before installing full tunnel
T16 V2Ray port TIME_WAIT kills fallback V2Ray fallback to different outbound fails because SOCKS5 port still in TIME_WAIT All outbound configs used same SOCKS5 port; Windows TIME_WAIT is ~120s Each outbound gets incrementing port: basePort + idx Never reuse same port across V2Ray fallback attempts

WALLET

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
W1 Fee grant auto-detection silently applied Direct-connect app uses random stranger's fee grant without consent SDK auto-detects fee grants and picks grants[0] on every transaction Made fee grant opt-in via explicit FeeGranter option Never auto-apply fee grants; make it explicit opt-in
W2 Insufficient funds with no dry-run option AI builds working code but first real run fails with "insufficient funds" Blockchain requires funded wallet; AI cannot purchase tokens Added dry-run mode that validates everything except payment Provide dryRun: true option to validate without spending tokens
W3 Fast reconnect never sets state._mnemonic Sessions never end on-chain after fast reconnect; session leak connectDirect() called tryFastReconnect() which skips connectInternal() where _mnemonic was set Set state._mnemonic = opts.mnemonic BEFORE calling tryFastReconnect() Set authentication state before any early-return code path

TIMING

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
TM1 Session "does not exist" on handshake 6.7% of handshakes fail immediately after session payment BROADCAST_MODE_SYNC returns after CheckTx, not DeliverTx; block not committed yet (~6-7s) Added retry-on-404: wait 10s and retry handshake once Always retry handshake on "does not exist" with 10s delay
TM2 Session inactive_pending status "Invalid session status inactive_pending, expected active" Session TX confirmed but not yet transitioned to active status Added waitForSessionActive() polling: every 2s for up to 20s Poll session status until active before handshaking
TM3 Sequence mismatch on rapid transactions "Account sequence mismatch" cascades through batch operations CosmJS caches sequence number; concurrent TXs use stale sequence Broadcast mutex + retry with exponential backoff + fresh sequence on retry Serialize broadcasts through mutex; wait 7s between TXs
TM4 Stale session allocation 404 "Resource not found" when reusing previous session Session appears "active" in query but allocation endpoint returns 404 (expired/closed) On 404, skip stale session and create new one automatically Never throw on allocation 404; fall through to new session
TM5 Session indexing race (409 "already exists") Handshake returns 409 even though session just created Node still indexing session after TX confirmation 5s delay after payment; then retry on 409 at 15s and 20s intervals Add post-payment delay and handle 409 with progressive retry
TM6 V2Ray needs post-handshake warmup Speed test returns low/zero speed immediately after V2Ray connect Node needs time to register UUID and stabilize tunnel Added 10s post-handshake delay for V2Ray before speed test Wait 10s after V2Ray handshake before testing connectivity

CONFIGURATION

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
CF1 fullTunnel: true default bricks AI AI's own RPC/LCD/npm calls crawl or die after successful connect Default routes ALL traffic through slow VPN node (median 3 Mbps) Changed defaults to false; explicit opt-in for production apps Default to split tunnel; full tunnel is opt-in
CF2 LCD_ENDPOINTS[0] is an object, not a string "Invalid URL" error on first LCD call LCD_ENDPOINTS exports [{ url, name, verified }] objects; developer assumed string Use LCD_ENDPOINTS[0].url or DEFAULT_LCD string export Make lcd() accept both string and Endpoint objects
CF3 Missing axios.defaults.adapter = 'http' Opaque "fetch failed" errors on self-signed node certs Node.js 18+ uses fetch adapter by default; self-signed certs fail Move axios.defaults.adapter = 'http' to top of index.js Ensure adapter is set on any SDK import path
CF4 Missing registerCleanupHandlers() WireGuard tunnel stays installed with 0.0.0.0/0 after crash; dead internet No cleanup handlers registered; orphaned tunnel captures all traffic Hard-fail if cleanup handlers not registered before connect connect() must refuse to proceed without cleanup handlers
CF5 subscribeToPlan() field name mismatch renewal_price_policy (snake_case) silently ignored Encoder destructures renewalPricePolicy (camelCase); different naming convention Changed to renewalPricePolicy Always match field naming convention between callers and encoders
CF6 PersistentKeepalive too long NAT routers expire UDP mappings at 20-30s; keepalive at 30s causes drops 30s keepalive is at the edge of many NAT timeout windows Changed to PersistentKeepalive = 15 Use 15s keepalive for WireGuard -- safe for all NAT routers
CF7 Node address mismatch wastes tokens Session paid for node A, but handshake endpoint serves node B Node's remote_addrs on chain points to wrong IP; different node at that address Pre-verify node address at remote URL BEFORE creating session Always verify node identity before paying for session
CF8 Node missing moniker/location from LCD node.moniker returns undefined; no error LCD returns only address/prices/remote_addrs; moniker/location requires separate nodeStatusV3() call Document clearly: LCD nodes lack moniker/location; must call node's own API Enrich LCD node data with nodeStatusV3() before displaying
CF9 Daemon loses wallet and connection state on restart User must re-login and reconnect after every daemon restart/crash; auth token survives but wallet doesn't Daemon stores active wallet and connection state only in memory; no disk persistence for wallet or last-connection Persist active-user.json (encrypted mnemonic + address) and last-connection.json (nodeAddress, protocol, sessionId) to ~/.sentinel-daemon/; on startup: load wallet, check session on chain, auto-reconnect if still active Persist ALL daemon state to disk; on restart, restore wallet and attempt reconnection automatically
CF10 connectViaPlan() BigInt(undefined) crash with confusing error TypeError: Cannot convert null to a BigInt or SyntaxError: Cannot convert abc to a BigInt — no context about which parameter BigInt(opts.planId) called without proper type validation; falsy guard !opts.planId rejects 0 (potentially valid) and passes through bad types Added explicit validation: null/empty check first, then try { BigInt(planId) } catch { throw with context } Validate BigInt inputs with try/catch and rethrow with parameter name context
CF11 Handshake timeout 30s too short for distant nodes Functional nodes with active peers time out at exactly 30s; ECONNABORTED: timeout of 30000ms exceeded 30s timeout insufficient for distant nodes (Asia, South America) or nodes under load; TLS handshake + session negotiation round trip exceeds 30s Increased handshake timeout to 45s in both initHandshakeV3() (WireGuard) and initHandshakeV3V2Ray() (V2Ray) Use 45s handshake timeout; builders can override via their own axios config if needed
CF12 C# SentinelVpnOptions missing Hours property Consumer apps can't let users choose hour amounts for time-based sessions PreferHourly = true hardcodes hours = 1 internally; no way to specify 2h, 4h, 8h, etc. Need to add Hours property alongside Gigabytes Expose both Gigabytes and Hours in connection options; never hardcode session duration

DEPENDENCIES

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
D1 V2Ray version mismatch Connections fail with no clear error V2Ray 5.44.1+ has observatory bugs; must be exactly 5.2.1 verifyDependencies() checks version; connect() refuses incompatible V2Ray Check V2Ray version at connect time, not just setup
D2 WireGuard not installed "Failed to start" with no explanation wireguard.exe not found on system Pre-check before session payment; clear error message with install link Check wireguard.exe exists BEFORE paying for session
D3 No admin privileges "Service registered but never reached RUNNING state" WireGuard service installation requires Administrator Pre-check admin before ANY session payment; provide self-elevation helper Check admin at step 0, not step 5
D4 Windows taskkill /F kills Claude Code Development environment dies during debugging taskkill /F /IM node.exe kills ALL Node.js processes including Claude Code Kill only by specific PID: taskkill /F /PID <pid> NEVER use /IM node.exe -- always use /PID
D5 Git Bash mangles /F flag taskkill /F /PID 32516 fails with "Invalid argument" Git Bash converts /F to F:/ (POSIX path conversion) Use //F or execFileSync (bypasses shell) Use execFileSync for all system commands, never string interpolation
D6 Competing VPN applications WireGuard tunnel fails; routing table conflicts NordVPN/ExpressVPN/etc. have active tunnels, route overrides, port conflicts Added VPN conflict detection in pre-connect diagnostic Detect and warn about competing VPNs before connecting
D7 WireGuard Manager Service ghost WireGuard GUI takes over tunnel management; conflicts with programmatic control wireguard.exe /installmanagerservice was called instead of /installtunnelservice Never call /installmanagerservice; only use /installtunnelservice SDK must only use direct tunnel service management

TESTING

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
TS1 656 unit tests, zero working features C# SDK declared "100/100 parity"; every feature broke on first real use Unit tests used mock data; no test hit a real LCD endpoint or real node Mandatory live chain smoke tests before any release Run live chain integration tests, not just unit tests
TS2 Mock data doesn't match real chain Tests used clean integer prices; chain returns 18-decimal sdk.Dec values Hand-crafted mock data instead of real LCD response snapshots Copy-paste actual LCD responses into test fixtures Use real chain response snapshots as test fixtures
TS3 JSON round-trip failure GbPriceUdvpn = "5500000" (string) serialized as integer; deserialized back as null Unit tests never serialize-then-deserialize-then-use Added round-trip serialization tests for all data models Test full lifecycle: create -> serialize -> deserialize -> use
TS4 Second attempt never tested Reconnect to same node fails; first connection always works No tests simulated: connect -> disconnect -> wait -> reconnect Added integration tests for reconnection and crash recovery Always test the SECOND attempt, not just the first
TS5 Production-scale data breaks pagination Plan with 147K subscribers; SDK fetches all (50MB) just to count Test plans had 5 subscribers; nobody tested with large data Use pagination.limit=1&count_total=true for counts (but count_total itself is broken -- see C2) Test with production-scale data; not just toy datasets
TS6 Parallel chain tests kill internet Running JS + C# test suites simultaneously triggers LCD/RPC rate limits Multiple HTTP clients hammering same endpoints in parallel Always sequential: one SDK at a time, 7s between TXs, 60s between suites NEVER run chain tests in parallel
TS7 Same code + same node = same result Retesting without implementing a fix wastes time and tokens Hoping a transient failure will pass on retry; it never does Rule: "What is DIFFERENT this time?" before any retest Never retest without implementing a new solution first

DOCUMENTATION

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
DC1 74% of SDK capability invisible to builders ConnectAsync has 19 steps; docs describe 5 Docs written before features; code improved; docs never updated Comprehensive documentation of all internal steps and edge cases Every code fix MUST include a doc update
DC2 154 suggestion files, none migrated to builder docs Critical edge cases discovered and filed but never reach builders Suggestions treated as internal notes, not source material for docs Migration pipeline from suggestions to feature docs Before saying "done," ask: can an AI reading only docs/ build this correctly?
DC3 Cross-language mapping missing JS connectDirect() = C# ConnectAsync() -- not documented anywhere Each SDK documented independently with different names Added cross-language reference table Maintain JS <-> C# function name mapping document
DC4 Error handling guide is JS-only C# catch patterns not documented; all errors treated identically C# error docs never written; only JS examples exist C# error handling patterns with typed exceptions Every feature doc must have examples in ALL supported languages

PARITY

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
PR1 Error code string mismatch SESSION_EXISTS (JS) vs SESSION_ALREADY_EXISTS (C#) breaks cross-language apps Each SDK chose "reasonable" names independently; no shared contract Error code strings ARE the contract; must be identical Compare error code counts and strings side-by-side before release
PR2 C# missing 11 error codes Apps can't distinguish LCD_ERROR from TX_FAILED in C# Incremental JS feature additions without C# sync Added all 11 missing codes to C# with matching string values When adding ANY error code to JS, mirror to C# in same session
PR3 C# missing severity classifications ErrorSeverity.IsRetryable("TUNNEL_SETUP_FAILED") returns wrong answer Only 11 of 22 codes classified in C#; rest return "unknown" Added severity for all C# error codes Every error code must have severity, user message, and recovery action
PR4 Duplicate UserMessage methods in C# ErrorSeverity.UserMessage and Helpers.UserMessage have different coverage Two developers added user messages independently Merged into single source of truth One canonical location per concern
PR5 Country map: JS 183 vs C# 80 100+ countries fail CountryNameToCode() silently in C# JS built from complete ISO database; C# hand-typed Expanded C# country map to match JS Auto-generate shared data from single source file
PR6 Speed test: JS 6-level fallback, C# 1 target 43 WireGuard 0-speed failures in C# apps C# has no DNS pre-resolve, no fallback targets, no rescue mode Port JS speed test fallback chain to C# When porting features, port the COMPLETE implementation including fallbacks
PR7 Session poisoning: JS has it, C# doesn't C# SDK tries to reuse broken sessions; fails again, wastes time Feature added to JS only; never ported Port markSessionPoisoned() / isSessionPoisoned() to C# Every feature added to one SDK must be tracked for porting
PR8 C# message type URLs wrong 7 message type URLs don't match JS SDK's MSG_TYPES "Describe and generate" instead of line-by-line translation Updated all 7 to match JS SDK exactly NEVER describe-and-generate when porting; translate line by line

SECURITY

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
S1 TLS verification disabled globally All HTTPS node connections accept any certificate; trivial MITM Sentinel nodes use self-signed certs; rejectUnauthorized: false used everywhere Implemented TOFU (Trust-On-First-Use); save cert fingerprint on first connect Use TOFU model; reject changed certificates
S2 TOFU not wired into handshake path TOFU exists in tls-trust.js but handshake still uses insecure agent v3protocol.js imports its own rejectUnauthorized: false agent; TOFU agent bypassed Wired _tofuStore through all 6 call sites (3 handshake, 3 status) Verify security features are connected end-to-end, not just implemented
S2b C# TOFU store not wired into SentinelVpnClient All C# handshake + status calls used "accept all certs" fallback; TOFU was dead code SentinelVpnClient never passed tofuStore/nodeAddress to Handshake.HandshakeAsync() or NodeClient.GetStatusAsync() — all 6 call sites bypassed Added TofuStore to SentinelVpnOptions; wired _tofuStore + nodeAddress through all 6 call sites; added NodeClient missing try/finally for TOFU client disposal Wire security features through ALL call sites; verify with grep, not assumption
S3 27 command injection surfaces execSync with string interpolation allows shell injection via poisoned state state.v2rayPid or state.wgTunnelName could contain shell metacharacters Replace execSync with execFileSync; add input validation on state values ALWAYS use execFileSync (array args); NEVER string interpolation for commands
S4 38 silent empty catches Errors swallowed silently; no trace when things fail catch {} blocks throughout 6 files; zero observability Categorized each catch: expected (comment), unexpected (log), critical (throw) Every catch must document WHY it's safe to swallow, or log the error
S9 Bare catch swallows CancellationToken in C# OperationCanceledException silently swallowed; cancellation never propagates DiscoverPlansAsync and GetProviderByAddressAsync had bare catch blocks that caught ALL exceptions including cancellation Added catch (OperationCanceledException) { throw; } before bare catch blocks ALWAYS rethrow OperationCanceledException before any bare catch; cancellation must propagate
S5 Kill switch state not persisted across crashes After crash, user's internet permanently blocked by orphaned firewall rules _killSwitchEnabled was in-memory only; disableKillSwitch() early-returns Added killSwitchEnabled to state persistence; recoverOrphans() cleans up Persist ALL security-critical state to disk; recover on startup
S6 Kill switch partial failure locks down internet 7 sequential firewall rules; failure after blockoutbound leaves system locked No rollback on partial failure; first rule blocks all, subsequent rules allow exceptions Wrapped in try-catch: if allow rules fail, immediately restore allowoutbound Implement rollback for any multi-step security modification
S7 Fire-and-forget EndSession races with Dispose EndSession TX almost always fails; resources leaked DisconnectAsync() fired EndSession as _ = Task.Run() then disposed HTTP client Added _pendingEndSession field; DisposeAsync awaits it before disposing Never fire-and-forget when subsequent code disposes the resources it needs
S8 Dispose sets _disposed before DisconnectAsync completes ObjectDisposedException thrown during disconnect cleanup _disposed = true was set BEFORE calling DisconnectAsync(), which then checked _disposed and threw Created DisconnectInternalAsync(reason) that skips disposed check; _disposed set AFTER disconnect completes Set disposal flags AFTER cleanup completes, not before; create internal disconnect that skips the guard

INTEGRATION

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
I1 Shared VPN client for testing Test disconnected user's active VPN; connection state leaked to UI Single SentinelVpnClient instance shared between main connection and test Create dedicated SentinelVpnClient per test with ForceNewSession = true; dispose after each test NEVER share the main VPN client with test functions; create+dispose per test
I2 CancellationToken kills speed test Speed test cancelled mid-download; garbage speed numbers; next test also cancelled CancellationToken propagated from scan loop to HttpClient.GetByteArrayAsync() inside speed test Pass CancellationToken.None to speed test and Google check; only check ct.IsCancellationRequested BETWEEN phases Speed test and Google check must run to completion once started; only scan loop is cancellable
I3 Background refresh blocks test connections Clicking "New Test" during background node probe hung indefinitely Single chain client's HTTP connections saturated by 30 parallel status probes; new requests queued Cancel _refreshCts before starting test scan Cancel ALL background chain operations before starting a test scan
I4 Progress counter stuck on errors Progress bar and "X/Y tested" froze during scan _testDone++ was only in the success path; exception handler skipped the increment Added _testDone++ and _testFailed++ in catch-all block EVERY code path (success, error, cancel) must increment progress counter
I5 testVpn null crash in finally block NullReferenceException when connection failed before VPN client assigned testVpn declared before try, assigned inside try, but finally always called testVpn.DisconnectAsync() Null-check testVpn in finally; wrap disconnect AND dispose in separate try/catch Declare VPN client as nullable BEFORE try; null-check in finally BEFORE calling disconnect
I6 NullReferenceException on unrendered dashboard App crashed when "New Test" clicked before dashboard fully rendered Background loop referenced UI TextBlock elements (_testProgressTb) that were null because RenderTestStats() hadn't run yet Null-check ALL UI references in background loops; add global crash handler Null-check every UI element reference in background/async code
I7 Stop button doesn't stop User clicked Stop but test kept running for 15-30 seconds (full ConnectAsync duration) CancellationToken.Cancel() only checked between nodes; SDK's internal async operations don't respond to cancellation mid-flight Added _testStopRequested volatile flag checked at 4 points + force tunnel cleanup on stop Use volatile bool flag checked at explicit points in flow, not just CancellationToken
I8 Lambda factory pattern fails in WPF Button click handlers silently didn't bind WPF click event binding with lambda factory closures doesn't reliably work Explicit button creation with direct Click += async (_, _) => handlers Create WPF buttons explicitly with direct event handlers, not factory patterns
I9 Ternary in C# string interpolation CS8361 compiler error from ternary inside $"" string C# string interpolation doesn't allow ternary without explicit parentheses $"{(a ? b : c)}" Wrapped ternary expressions in parentheses or moved to variable Always parenthesize ternaries inside C# string interpolation
I10 Triple LCD probe on startup Three independent LCD calls (LoadBalance + RefreshAllAsync + preload) serialized; 15s startup Each component initiated its own chain query independently Consolidated into single initialization with shared chain client Coordinate chain queries at startup; never make 3 independent LCD calls for the same data
I11 Page flickering during user interaction UI re-rendered node list while user was browsing Background refresh called RenderNodes() after updating _allNodes Background refresh updates data but does NOT re-render; user clicks Refresh to see updates NEVER re-render during user interaction; update data silently, render on user action
I12 TextChanged fires during init Null-check crash from UI element events firing before initialization complete WPF TextChanged event fires when programmatically setting initial values Null-check UI elements in all event handlers Guard ALL event handlers against null UI elements during initialization
I13 Double node probing on login Nodes probed twice: once on app open, once on login Separate probe triggers for app startup and login without deduplication guard Added _initDone guard + _nodesLoaded cache Use a guard flag to prevent duplicate initialization
I14 Session not visible after disconnect User disconnected but session tab showed no session Session saved to chain but not to local cache on disconnect Save session to local cache instantly on disconnect, before chain confirmation Always update local cache optimistically on state changes
I15 results.json format mismatch C# DiskCache wraps results in {"Data":[...], "SavedAt":"..."} but Node Tester expects raw array Different persistence patterns between JS (raw array) and C# (cache wrapper) Strip wrapper on export; provide both formats Define canonical result format (raw array); never wrap in metadata for cross-tool compatibility
I16 C# bridge was cosmetic for months SDK toggle showed "C#" but all code ran through JS; nobody noticed No logging or verification that the C# code path was actually executing Log [C# SDK] on every status/handshake call; verify code path, not label Verify the code path actually executes, not just that the UI label is correct
I17 transport_security 0-indexed vs 1-indexed C# SDK returns 0=none/1=tls; JS expects 1=none/2=tls; all C# V2Ray tests used wrong TLS Different enum offset conventions between languages; no comparison test Added +1 offset remap in bridge wrapper When bridging between languages, ALWAYS check numeric enum mappings with comparison tests
I18 Session lookup scanning 500+ sessions waitForSessionActive scanned ALL wallet sessions (500+) instead of querying by ID Used broad session list query instead of direct session ID query Pass session ID directly; query by specific ID Always use the most specific query possible; never scan full list when you have the ID
I19 V2Ray port pre-check referenced uninitialized variable useCached referenced before initialization; server crashed on all retests New code inserted in middle of function referenced variable defined later Moved variable declaration before new code; tested locally before deploying When inserting code into middle of function, verify variable scope; run locally before deploying
I20 Aggressive port scanning crashed server Port scan of 1000-65535 in step 100 overwhelmed server with concurrent connections Too many parallel TCP connection attempts Limited to probing 10-15 known common ports first; batch and limit Port scanning must be batched (max 10-15 parallel); never scan full range
I21 Node loading blocks UI for minutes Users see "No nodes found" for 2-8 minutes while 1000+ nodes enriched with live status GetActiveNodesAsync() + Task.WhenAll(statusCalls) blocks UI render until ALL status calls complete Two-phase loading: Phase 1 renders chain data immediately (address + pricing); Phase 2 enriches with live status in background, fires event on complete NEVER block UI on status enrichment; show chain data instantly, enrich in background
I22 No loading state during async data fetch Empty panels during 3-10s chain queries; users think app is broken await called without any visual indicator; panel stays blank until data returns Call ShowLoadingState() BEFORE every await; "No results" only shown after completed query returns zero items Every async data load MUST show a loading indicator before the await

UX

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
UX1 No previous results on restart App showed "No results yet" despite 135 results on disk In-memory array empty on startup; DiskCache loaded but not rendered Load cached results in EnterApp() BEFORE rendering test tab On app startup, load and display cached results immediately; never show empty when data exists
UX2 Binary PASS/FAIL instead of FAST/SLOW/FAIL All passing nodes showed same green badge regardless of speed Only two result categories: connected vs not connected Changed to three-tier: FAST (green, >=10 Mbps), SLOW (amber, <10), FAIL (red) Use three-tier speed badges: FAST/SLOW/FAIL, never binary PASS/FAIL
UX3 GridLength negative crash in WPF MakeSessionRow crashed from negative GridLength value Progress bar calculation used Math.Max(2, ...) which created negative widths for remaining column Clamp progress percentage to 0.01-0.99 range before creating GridLength ALWAYS clamp progress values to 0.01-0.99 before creating proportional WPF grid columns
UX4 Grid overlapping text in WPF Left and right text elements rendered on top of each other Two elements in Grid without explicit ColumnDefinitions Always define explicit ColumnDefinitions for left/right layouts in WPF NEVER put two elements in a WPF Grid without ColumnDefinitions -- they overlap
UX5 No test run history Each new scan's results replaced previous; no way to compare Monday vs Tuesday No run archiving or history mechanism Design: auto-save to runs/YYYY-MM-DD_HH-MM/ with dropdown to load previous Auto-archive every completed scan; provide dropdown to load and compare past runs
UX6 No baseline measurement Cannot distinguish slow node from slow user internet No direct internet speed measurement before tunnel testing Design: measure speedtestDirect() before scanning; store in baselineHistory Always measure baseline (direct speed) before tunnel testing
UX7 No token spend tracking during scan User has no idea how much the test scan cost Balance checked at start but no running delta calculated Design: record balance before scan; show running spend Track and display token spend: "Spent: X P2P (balance: Y -> Z)"
UX8 Country flag rendering failure on native platforms Empty boxes where flags should be on WPF WPF (and Windows generally) cannot render emoji country flags; only browsers can Built three-layer cache: memory -> disk -> download from flagcdn.com PNG Document platform flag rendering: Web=emoji, WPF=PNG images, Swift=emoji works natively
UX9 Test dashboard squeezed into sidebar Full-width dashboard crammed into 360px sidebar panel No layout guidance: "dashboard should take over main area, not sidebar" Moved test dashboard to main content area Node test dashboards are full-width; never squeeze into sidebar
UX10 No click-to-copy on node addresses Users had to manually select and copy node addresses No clipboard integration on table rows Added MouseLeftButtonUp handler for clipboard copy with visual feedback Every node address in tables must be clickable to copy full address
UX11 No expandable row diagnostics Users cannot investigate why a specific node failed No detail view for individual test results Design: click row to expand and show full diagnostics (session ID, connect time breakdown, error details) Provide expandable detail view for each test result
UX12 Dedup results not implemented Retesting a node appended a new row instead of replacing the old one No upsert-by-address logic in results collection Check _testResults.Any(r => r.Address == node.Address) before adding; RemoveAll on retest Results must be deduplicated by node address; replace old result, never show duplicates

SPEED TEST

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
SP1 V2Ray SOCKS5 connection reuse fails First speed download through SOCKS5 worked; subsequent hung until timeout V2Ray SOCKS5 proxy doesn't handle HTTP keep-alive correctly; connection pool stalls Create fresh SocksProxyAgent (JS) or HttpClient+HttpClientHandler (C#) per request NEVER reuse HTTP client for V2Ray SOCKS5 requests; create fresh per request
SP2 V2Ray SOCKS5 connectivity pre-check missing Speed test failed silently because SOCKS5 binding is asynchronous; proxy not ready No verification that SOCKS5 tunnel was actually routing traffic before measuring speed Added 3-attempt connectivity check (6 targets: google, cloudflare, 1.1.1.1, httpbin, ifconfig, ip-api) with 5s pause between ALWAYS verify SOCKS5 connectivity with multi-target check before speed testing
SP3 V2Ray preflight consumed tunnel Preflight 1KB download via separate SocksProxyAgent consumed V2Ray's SOCKS5 connection; speed test's second agent failed with TLS disconnect Multiple SocksProxyAgent instances competing for same V2Ray tunnel Removed separate preflight; speed test probe (2MB) acts as connectivity test; arraybuffer mode instead of stream Never use separate preflight through V2Ray SOCKS5; let speed test probe serve as connectivity check
SP4 DNS failures behind WireGuard tunnels speedtestDirect tried hostname URLs first; DNS failed through tunnel; 56/338 nodes showed ENOTFOUND WireGuard tunnel broke DNS resolution for speed test hostnames Pre-resolve Cloudflare IP before installing tunnel; try IP-based URL FIRST when cached IP available ALWAYS pre-resolve speed test hostnames BEFORE installing WireGuard tunnel
SP5 Speed test fallback chain incomplete in C# 43 WireGuard nodes showed 0 speed in C# apps C# had only 1 speed target, no DNS pre-resolve, no fallback chain, no rescue mode Ported complete 7-level fallback: probe -> multi-request -> OVH -> Tele2 -> rescue -> google-fallback -> connected-no-throughput Port the COMPLETE speed test fallback chain, not just the primary target
SP6 Speed test arraybuffer vs stream mode Speed test returned 0 bytes through V2Ray when using responseType: 'stream' stream mode interacts differently with SocksProxyAgent than arraybuffer Changed speedtestViaSocks5 to arraybuffer mode (matching what works in test-v2ray.js) Use arraybuffer mode for all SOCKS5 speed test downloads
SP7 V2Ray process needs post-handshake warmup Speed test returned 0 immediately after V2Ray handshake Node needs time to register UUID after handshake completes Added 10s post-handshake delay for V2Ray before speed test Wait 10s after V2Ray handshake before testing connectivity or speed
SP8 Native fetch silently ignores SOCKS5 agent Node.js fetch (undici) produced opaque failures through SOCKS5; speed test silently measured nothing Node.js 18+ uses undici fetch adapter which ignores agent option for SOCKS5 proxies Used axios with explicit httpAgent + httpsAgent for all SOCKS5 traffic MUST use axios (not native fetch) for SOCKS5 proxy traffic in Node.js

PRICING

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
PR9 BaseValue shows 18-decimal garbage Price displayed as 52573.099722991367791000000000/GB Used BaseValue (Cosmos sdk.Dec with 18 decimal places) instead of QuoteValue (clean integer) Always use QuoteValue for display; document the difference prominently ALWAYS use quote_value (integer udvpn), NEVER base_value (18-decimal sdk.Dec)
PR10 PreferHourly creates wrong sessions SDK silently creates GB sessions when hourly requested SDK bug: PreferHourly flag was ignored internally; always defaulted to GB Documented as known SDK bug; use explicit Gigabytes = 1 as fallback for hourly Verify SDK actually creates the session type requested; don't trust flags blindly
PR11 Session payment mode not exposed by chain Cannot determine if session is GB-based or hourly from chain data Chain max_bytes is always 1000000000 and max_duration is always "0s" regardless of payment mode Built SessionTracker to persist payment mode locally Consumer apps MUST track payment mode locally; chain data does not distinguish GB vs hourly
PR12 Estimated cost shows insane numbers "Est. Cost: 20,000 P2P" shown for a test scan Sum of ALL viable node prices (500 nodes * 40 P2P) displayed as total Change to show actual spent per scan or per-node average Calculate estimated cost as tested * avgNodePrice, not totalViable * nodePrice

DOCUMENTATION (continued)

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
DC5 Docs describe non-existent code AI spent 10+ hours trying to use NodeTester class, IVpnTestAdapter, createNodeTestAdapter() -- none exist Documentation written as design spec but presented as if documenting existing code; no IMPLEMENTED vs SPEC labels Added status labels; replaced spec references with working code Label EVERY class/function in docs as IMPLEMENTED or SPEC ONLY; grep codebase to verify
DC6 No C# integration guide AI reverse-engineered 6,500 lines of JS to build C# integration; 54% of time wasted Node Tester docs are JS-only; no C# function mapping, no WPF-specific gotchas Created complete C# integration report with working code Every integration guide must include working code in BOTH JS and C#
DC7 Country map only in index.html Country-to-code mapping embedded in client-side HTML; not importable as module _CC lookup table defined at line 688 of index.html; no core/countries.js export Built separate country map with 120+ entries Export shared data (country map, constants, thresholds) as importable modules, not embedded in HTML
DC8 No cross-language function mapping nodeStatusV3() (JS) = NodeClient.GetStatusAsync() (C#) -- no mapping document Each SDK documented independently with different names Created JS-to-C# function mapping table Maintain cross-language function mapping table for every documented function
DC9 Test result schema undocumented AI reverse-engineered 30+ fields from results.json with wrong field names No schema document; result shape varies between WG and V2Ray; optional fields not marked Created complete result schema with real mainnet examples Document result schema with types, optionality, and REAL JSON examples from mainnet tests
DC10 Dashboard layout not specified AI read 700-line HTML and tried to translate to WPF; layout intent lost No layout specification; dashboard exists only as interleaved HTML+CSS+JS Created DASHBOARD-SPEC.md with exact widths, alignments, data sources Provide visual layout specification separate from implementation; not just source code
DC11 Speed test evolved past documentation Docs describe 3-target basic flow; real code has 7-level fallback chain Speed test gained rescue mode, google-fallback, connected-no-throughput through iterative fixes; docs never updated Documented all 7 speed test methods with decision logic Update speed test documentation after EVERY fallback addition
DC12 User journeys not documented Basic features missing (load previous results, export, sort) because nobody walked through user flows Docs describe WHAT the dashboard shows, not WHAT THE USER DOES across sessions Defined 5 user journeys: first use, return visit, share results, investigate failure, compare over time Document user journeys (multi-session workflows), not just features

TESTING (continued)

# Failure Name What Happened Root Cause Fix Applied Prevention Rule
TS8 Blind retesting 5+ times without fixes Same 24 nodes run through same code 5 times; all failed identically Hoping transient failures would pass on retry; they never did Rule: "What is DIFFERENT this time?" -- must name a specific code change before any retest NEVER retest without implementing a specific fix; write down what changed
TS9 "Node-side" dismissal of 8 failures 8 failures dismissed as node problems; all 8 were code bugs Assumed nodes with weird behavior were broken, not our code Iron Rule: peers > 0 = OUR fault. Investigated all 8 and found: stale cache, batch mapping, premature rejection, missing UUID wait NEVER say "node-side" if peers > 0; other clients connect, so a working code path exists
TS10 Stopped running audit to apply code fixes Killed mid-audit server; lost 130 C# results permanently Wanted to deploy fix immediately rather than waiting for natural restart Auto-save results before ANY restart; code fixes wait for next natural restart NEVER stop a running audit to apply fixes; save results first, deploy fix on next natural restart
TS11 TCP port unreachable but peers connected 10 nodes showed "port closed" but had 3-7 active peers TCP probe timeout too short; DNS resolution differs; possible rate limiting from rapid-fire probes Increase probe timeout; try alternate remote_addrs; handle rate limiting Increase TCP probe timeout for distant nodes; try all remote_addrs before declaring unreachable
TS12 SOCKS5 tunnel established but no connectivity 5 V2Ray nodes with handshake success + SOCKS5 bind but zero internet through tunnel; all had 3-15 peers V2Ray 5.2.1 may have grpc/quic bugs; connectivity targets may be blocked by node's egress policy; VMess AEAD may silently fail Try newer V2Ray versions; test with alternate connectivity targets Verify tunnel actually passes traffic before declaring connected; don't trust SOCKS5 port binding alone
TS13 Clock drift nodes skipped but peers connected 4 VMess-only nodes with >120s drift skipped entirely despite having 4-6 active peers Assumed clock drift = permanent failure; peers may use VLess or have different auth Try VLess outbound even on "VMess-only" nodes; verify VLess detection is complete Before skipping for clock drift, verify node truly has NO VLess outbounds; peers may use a protocol we don't detect
TS14 V2 format metadata rejection Most popular node (48 peers) rejected because it returned v2 metadata format SDK threw error on old {protocol, tls, ca} fields instead of mapping to v3 Added v2-to-v3 field mapper NEVER reject metadata outright -- map old formats to new; if 48 peers connect, the data is usable
TS15 Session 500+ scan bottleneck waitForSessionActive took 5+ minutes per node during retests Function scanned ALL wallet sessions (500+) via broad LCD query instead of querying by specific session ID Pass session ID directly; use GET /sentinel/session/v3/sessions/{id} Always use direct session ID lookup, not full wallet session scan

Failure Statistics

Category Count Most Common Root Cause
Protocol 12 Transport config mismatch with sentinel-go-sdk server
Chain 14 v2/v3 field name changes, broken LCD endpoints
Tunnel 16 WireGuard Windows service lifecycle, crash recovery
Wallet 3 Unsafe defaults, missing state in early-return paths
Timing 6 Chain propagation lag, session lifecycle delays
Configuration 8 Wrong defaults for development, field naming
Dependencies 7 Missing admin, wrong binary versions, competing software
Testing 15 Mock data != real chain, blind retesting, "node-side" dismissals
Documentation 12 Docs lag behind code, specs disguised as docs, no cross-language mapping
Parity 8 Incremental additions without cross-language sync
Security 7 Silent swallowing, unconnected security features
Integration 20 Shared state, cancellation propagation, async coordination
UX 12 Missing data persistence across restarts, platform rendering gaps
Speed Test 8 SOCKS5 connection reuse, missing fallback chain, DNS behind tunnel
Pricing 4 BaseValue vs QuoteValue, session mode not on chain
Total 152

The 14 Questions to Ask Before Every Change

From the C# SDK debacle (656 tests, 12 critical bugs) and Handshake dVPN integration (135 nodes, 27 problems):

  1. "Does this work on the second attempt?" -- Test reconnect, retry, recovery
  2. "Does this use real chain data format?" -- Test with actual LCD responses, not mocks
  3. "Does this survive serialization round-trip?" -- Serialize -> deserialize -> use
  4. "Does this have silent side effects?" -- Fee grants, proxy changes, route modifications
  5. "Does this pay tokens before verifying success is possible?" -- Pre-verify everything
  6. "Does this parse ALL available fields?" -- Map every chain field, not just the minimum
  7. "Does this work at production scale?" -- 100K subscribers, 900 nodes, 150K TXs
  8. "Does the test verify the OUTPUT or just the INPUT?" -- Check the broadcast TX bytes, not the flag value
  9. "Does this work on app restart?" -- Data must persist across close/open cycle
  10. "Does the user see their data from last session?" -- Never show empty when data exists on disk
  11. "Can an AI reading only the docs build this correctly?" -- If docs reference non-existent code, they cause more harm than no docs
  12. "Is every code path ported from the reference?" -- Copy ALL waits, sleeps, and fallbacks, not just the happy path
  13. "What is DIFFERENT this time?" -- Before any retest, name the specific code change
  14. "Does this code path actually execute?" -- Verify with logs, not labels; the C# bridge was cosmetic for months

Source Projects

Every finding traces back to a specific project. This section documents the source for traceability.

Handshake dVPN (C# WPF)

Files: handshake-RETROSPECTIVE.md, handshake-STANDARDS.md, handshake-CLAUDE.md, handshake-MANIFESTO.md, handshake-AI-NODE-TEST-INTEGRATION.md Findings: I1-I15, UX1-UX12, SP1-SP8, PR9-PR12, DC5-DC12, TS8-TS10 Summary: 26-hour build, 135 mainnet nodes tested (118 pass, 17 fail). 54% of time wasted on undocumented issues. Discovered that C# integration requires completely reimplementing speed test, flag rendering, disk cache, and session tracking from scratch due to missing SDK components.

Node Tester (JS Express)

Files: node-tester-HANDOFF.md, node-tester-CLAUDE.md, all 12 node-tester-suggestion-*.md files Findings: P1-P12, C1-C14, T1-T16, TM1-TM6, TS1-TS7, TS11-TS15, I16-I20, DC7, DC10, DC11 Summary: 2200+ node tests across JS and C# SDKs. Found 24 protocol bugs. Proved Iron Rule: every node with peers > 0 that failed was our bug, not node-side. Documented complete speed test fallback chain, V2Ray config building, clock drift detection, and WireGuard lifecycle management.

Test2 (JS SDK Proving Ground)

Files: test2-CLAUDE.md Findings: P7-P9, CF1-CF4, W1-W3, D1-D4 Summary: First consumer of JS SDK. Discovered autoReconnect was completely broken, BigInt serialization crash, broadcast name collision, fullTunnel default bricking AI's internet, and missing cleanup handler registration.

ClaudeDVPN / C# SDK (EXE)

Findings: C1-C14, PR1-PR8, S1-S7, TS1-TS3, DC1-DC4 Summary: 656 unit tests passing with zero working features on mainnet. Proved that unit tests are meaningless without live chain integration tests. Found all v2/v3 field name mismatches, MsgEndSession not registered, 27 command injection surfaces, and parity gaps.

One-Shot Buildability Analysis

Files: node-tester-suggestion-one-shot-buildability-analysis.md Findings: Wall 1-7 analysis identifying fundamental barriers to AI building node testers: batch payment not in SDK, session reuse complexity, V2Ray config minefield, clock drift detection, WireGuard admin requirement, fragile speed testing through tunnels, pipeline resilience.

Undiagnosed Failures Report

Files: node-tester-suggestion-undiagnosed-failures.md Findings: TS11-TS14 (22 nodes with active peers: 10 TCP unreachable, 5 SOCKS5 no connectivity, 4 clock drift skips, 1 v2 format metadata, 2 handshake failures). Total ~130 active users connected to these "failing" nodes. All failures are in our code, not nodes.


Pending Integration

[PENDING] fix-registry-backup.md

Category: BUG-FIX Summary: setSystemProxy() overwrites Windows proxy settings with /f (force), no backup/restore of previous state. If user had corporate proxy, clearSystemProxy() sets "no proxy" instead of restoring their previous configuration. Action: Review and integrate into T10/T11 entries above (proxy restore overwrites user's previous proxy).

[PENDING] fix-registry-backup.md

Category: BUG-FIX Summary: Status: SUGGESTION — needs review Date: 2026-03-09 Severity: HIGH — current implementation force-overwrites, no restore to previous state File affected: js-sdk/node-connect.js lines 79-124 setSystemProxy() overwrites Windows proxy settings with /f (force): Action: Review and integrate into main documentation above

[PENDING] fix-registry-backup.md

Category: BUG-FIX Summary: Status: SUGGESTION — needs review Date: 2026-03-09 Severity: HIGH — current implementation force-overwrites, no restore to previous state File affected: js-sdk/node-connect.js lines 79-124 setSystemProxy() overwrites Windows proxy settings with /f (force): Action: Review and integrate into main documentation above