fix: run Venice E2EE attestation before spending tokens#2
Conversation
Previously attestation happened AFTER _spendToken(), meaning if attestation failed the user lost their tokens with no refund. Now attestation runs first — if it throws, no tokens are spent. - Added _getAttestationAuth() helper that gets auth headers without the main spend (key lookup for apikeys, minimal spend for xcashu) - xcashu mode reuses the attestation spend token for the actual request to avoid a double spend - Both fetchAIResponse() and _prepareRoutedRequest() restructured to do E2EE attestation before _spendToken() - Updated VeniceImpl.md to document the new ordering guarantee
…enice-integration
…enice-integration
Verification: two fund-loss bugs + a credential leak — reproduced with a running harnessI built this PR ( 1. Double-spend (1 sat + the full request amount)One
Observed: 2. The attestation token is spent before attestation is verifiedEvent order: 3.
|
|
As promised, here's the reworked version: #14 (draft). It removes the |
Route requests for models whose id starts with "tinfoil-" through Tinfoil's SecureClient. SecureClient performs enclave attestation and encrypts request bodies via EHBP (HPKE) so only the attested enclave can decrypt them; the response stream is already decrypted before the SDK's SSE processing sees it. Flow mirrors the existing Venice E2EE pattern (attest BEFORE spending tokens) for both the initial request and the failover path, preserving the attest-before-spend invariant so Cashu tokens are never minted for providers whose enclaves cannot be attested. New module client/TinfoilSecure.ts: - isTinfoilModel() detects the tinfoil- prefix - prepareTinfoilClient() lazily imports tinfoil, attests, and caches the SecureClient per resolved option set; failed attestations are evicted so the next call re-attests - The tinfoil- prefix is NOT stripped; the caller-facing id is sent upstream Review cleanups applied: - Remove dead try/catch around getVerificationDocument(); the getter is a bare field return and cannot throw post-ready(), so the swallow-catch only masked impossible conditions and confused the type/log. Tightened TinfoilClientContext.verification to non-nullable VerificationDocument and dropped the optional-chaining and "unknown" fallbacks from the attestation log line. - Drop redundant normalizeBaseUrl() call from cacheKey(); resolveOptions() already normalizes baseUrl, so the key was double-normalizing. Normalization now lives in one place. - Remove imprecise "encrypted at fetch time" comment and reword the redacted body placeholder to "encrypted inside SecureClient.fetch" to accurately describe that HPKE sealing happens within the fetch() call. Adds tinfoil@^1.1.6 as a runtime dependency.
The Routstr proxy's /tee/attestation endpoint is an unauthenticated free GET that bypasses all auth/payment logic and is forwarded directly upstream. No credential is needed to call it. - Delete _getAttestationAuth() entirely - Remove authHeaders param from fetchVeniceAttestation() and prepareE2EERequest() - Call prepareE2EERequest() directly without any pre-attestation spend - Add reqs/, *.sqlite, audit.log, scripts/events.db to .gitignore Fixes double-spend in xcashu mode where a 1-sat token was burned just to authenticate an endpoint that never checked it.
…odel header The attested Tinfoil enclave expects the bare model id (e.g. "kimi-k2-6"), not the caller-facing routstr id (e.g. "tinfoil-kimi-k2-6"). The SDK now strips the tinfoil- prefix from the model id inside the EHBP-encrypted request body via getTinfoilUpstreamModelId(). Since the body is HPKE-encrypted and opaque to the proxy, the full caller-facing model id is sent in the X-Routstr-Model header so the proxy can do model lookup, cost calculation, and routing without parsing the encrypted body. End-to-end model id flow: SDK → proxy (header): X-Routstr-Model: tinfoil-kimi-k2-6 Proxy → PPQ (header): X-Private-Model: private/kimi-k2-6 (server-side) Enclave (body): body.model = kimi-k2-6 (stripped)
…enice-integration
…tream error logging, add custom EHBP fetch wrapper
f174123 to
0e706e8
Compare
Previously attestation happened AFTER
_spendToken(), meaning if attestation failed (e.g. unverified key, nonce mismatch, server error), the user had already lost their tokens with no refund path.This moves E2EE attestation before any spending in both
fetchAIResponse()and_prepareRoutedRequest():_getAttestationAuth()helper that gets auth headers without the main spend:_spendToken()VeniceImpl.mdto document the new ordering guarantee