Railway deploy — api-billing-v2 Phase-2 binding
End-to-end runbook to push the verifier-api to Railway with Phase-2 in-circuit commitment binding (apiBillingV2) live. Two-call customer surface stays unchanged.
0. Pre-flight (local repo)
Verify artifacts and tests are green:
# Phase-2 unit + integration tests
cd /Users/ayitsommar/Desktop/zkcaptcha
( cd protocol && node --test --test-force-exit test/billing-commitment-binding.test.js test/billing-incircuit-binding.test.js )
( cd verifier-api && \
DATABASE_URL="postgresql://zkcaptcha:development@localhost:5432/zkcaptcha" \
REDIS_ENABLED="false" REDIS_DISABLED="true" NODE_ENV=test DB_SSL=false \
npm run test:billing-pilot )
# Artifact manifest hashes (boot-time check the API will rerun on Railway)
( cd verifier-api && node -e "import('./src/utils/artifactValidator.js').then(m => m.validateArtifacts().then(r => { console.log(r); process.exit(r.valid ? 0 : 1); }))" )Expected: 15/15 pilot tests, 8/8 protocol tests, validator prints ✅ All 12 circuit artifacts validated against manifests.
After an MPC artifact rotation (new zkey/vkey under verifier-api/circuits/apiBillingV2/):
- Regenerate locally:
bash zk-cp/scripts/mpc-api-billing-v2.sh(or merge the MPC artifacts PR). - Re-run the pre-flight block above — Phase-2 pilot cases must pass with new hashes.
- Redeploy Railway so production loads the updated
ARTIFACT_MANIFEST.jsonand proving key. - Historical attestations keep verifying offline with their embedded vkey; new attestations use the rotated key.
See api-billing-v2 trusted setup for ceremony + rotation semantics.
1. Database migrations (one-time)
Migrations auto-apply on boot when NODE_ENV=production (see verifier-api/src/config/database.js::runMigrations). Confirm migrations 030 and 031 exist locally:
ls verifier-api/src/infrastructure/database/migrations/03*.sql
# 030_vdi_billing_v2.sql
# 031_vdi_billing_replay_crash_safe.sqlIf you'd rather pre-apply them out-of-band (e.g. to inspect):
# Use Railway's psql proxy or your own bastion
railway connect Postgres
\i verifier-api/src/infrastructure/database/migrations/030_vdi_billing_v2.sql
\i verifier-api/src/infrastructure/database/migrations/031_vdi_billing_replay_crash_safe.sql2. Set Railway environment variables
From production/secrets/.env.production, copy only the api-billing-v2 block and Phase-2 block into the Railway service:
# 1. Make sure you're authenticated and linked to the right service
railway login
railway link # pick the verifier-api project + service
# 2. Set the api-billing-v2 secrets (one-shot)
railway variables set \
VDI_ATTEST_ENABLED=true \
DEMO_MODE=false \
VDI_VERIFIER_ID=quantzk-billing-verifier-prod \
VDI_BILLING_VERIFY_PROFILE=VDI_VERIFY_STRICT_V1 \
VDI_BILLING_REPLAY_RESERVE_TTL_SEC=900 \
VDI_BILLING_REVOCATION_CACHE_SEC=60 \
VDI_BILLING_REVOCATION_URL=https://api.quantzk.com/.well-known/vdi-billing-revocation.json \
VDI_BILLING_INCIRCUIT_BINDING=true
# 3. Set the long secrets (use --secret to avoid logging)
railway variables set VDI_SIGNING_KEY="$(grep ^VDI_SIGNING_KEY production/secrets/.env.production | cut -d= -f2-)"
railway variables set VDI_ATTEST_SECRET="$(grep ^VDI_ATTEST_SECRET production/secrets/.env.production | cut -d= -f2-)"
railway variables set VDI_MANIFEST_AUTHORITY_SIGNING_KEY="$(grep ^VDI_MANIFEST_AUTHORITY_SIGNING_KEY production/secrets/.env.production | cut -d= -f2-)"
railway variables set VDI_BILLING_METER_KID="$(grep ^VDI_BILLING_METER_KID production/secrets/.env.production | cut -d= -f2-)"
railway variables set VDI_BILLING_LOG_OPERATOR_KID="$(grep ^VDI_BILLING_LOG_OPERATOR_KID production/secrets/.env.production | cut -d= -f2-)"
# PEM-style multiline secrets — set via the Railway dashboard or use --from-file:
# VDI_BILLING_METER_PRIVATE_KEY_PEM
# VDI_BILLING_METER_TRUST_JSON
# VDI_BILLING_LOG_OPERATOR_PRIVATE_KEY_PEM
# VDI_BILLING_REVOCATION_JSON
# (Railway escapes \n correctly when pasted into the dashboard field.)
# 4. Confirm
railway variables | grep -E "^VDI_|^DEMO_MODE"Why
VDI_BILLING_INCIRCUIT_BINDING=true: this turns on Phase-2 binding globally. The customer surface (/attest,/verify) does NOT change — they just see an additionalbilling_prooffield on the response (additive).
3. Deploy
# From repo root (railway.json points to verifier-api/Dockerfile)
railway up --detach
railway logs --deployment # tail until "✅ All N circuit artifacts validated"Build-time check: the Dockerfile runs validateArtifacts() after npm install. If you see ARTIFACT VALIDATION FAILED in the build log, the artifact bundle is corrupt — do not redeploy until you fix it locally.
4. Post-deploy smoke (live URL)
Replace $API with your production host (https://api.quantzk.com):
API=https://api.quantzk.com bash scripts/smoke-billing-phase2.shThis script lives in scripts/smoke-billing-phase2.sh. It:
- Hits
/healthand the revocation feed. - Issues an attest with a fresh
event_id. - Confirms
attestation.billing_proof.binding_version === 'bn128-poseidon8-v1'. - Asserts
attestation.billing_proof.vkey_hashmatchesartifacts.verification_key.canonical_sha256inARTIFACT_MANIFEST.json(RFC 8785 JSON digest of the vkey; distinct from raw filesha256). - Verifies the bundle and asserts
incircuit_binding_verification.valid === true. - Tampers the expected commitments and asserts verification flips to
false.
If all assertions pass, the Phase-2 binding is live in production with the expected circuit pin.
5. Rollback (if needed)
Phase-2 is opt-in and additive. To turn off without redeploying:
railway variables set VDI_BILLING_INCIRCUIT_BINDING=false
railway redeployCustomer impact: zero — Phase-1 binding (bn128-field-split-v1) keeps working.
6. Operator notes
Trusted setup: the shipped
apiBillingV2_0001.zkeywas generated by a single-operator Phase-2 contribution with dev entropy. Before relying on it for adversarial production trust, re-runzk-cp/scripts/compile-api-billing-v2.shon a controlled host with proper entropy, then mirror the regenerated artifacts to all four locations (zk-cp/build/apiBillingV2/,verifier-api/circuits/apiBillingV2/,protocol/packages/vdi-prover/circuits/apiBillingV2/,public/circuits/apiBillingV2/) and updateARTIFACT_MANIFEST.jsonhashes. Bump the manifestcircuit.versionand document the ceremony.Key separation: the Phase-2 binding does NOT use
VDI_SIGNING_KEY. It's pure-snark — the prover knows the witness or it doesn't, regardless of who holds the Ed25519 issuer key. This is the whole point of moving from Phase 1.Latency: expect ~360ms added to
/attestp50 in production for Phase-2 proof generation./verifyadds ~30ms. If either creeps above target, cache the Poseidon module + zkey in memory (already done; just confirm warm-up).
7. Two-call surface (preserved)
The customer-facing flow remains:
POST /api/vdi/billing/attest → { attestation, receipt, billing }
POST /api/vdi/billing/verify → { verification.valid, billing_check.valid }Phase-2 attaches attestation.billing_proof and emits incircuit_binding_verification on the verify response — customers who don't care about it can ignore those fields entirely. Documentation, SDK, and the DeveloperPortal stay at "two calls. done."
