Skip to content

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:

bash
# 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/):

  1. Regenerate locally: bash zk-cp/scripts/mpc-api-billing-v2.sh (or merge the MPC artifacts PR).
  2. Re-run the pre-flight block above — Phase-2 pilot cases must pass with new hashes.
  3. Redeploy Railway so production loads the updated ARTIFACT_MANIFEST.json and proving key.
  4. 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:

bash
ls verifier-api/src/infrastructure/database/migrations/03*.sql
# 030_vdi_billing_v2.sql
# 031_vdi_billing_replay_crash_safe.sql

If you'd rather pre-apply them out-of-band (e.g. to inspect):

bash
# 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.sql

2. Set Railway environment variables ​

From production/secrets/.env.production, copy only the api-billing-v2 block and Phase-2 block into the Railway service:

bash
# 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 additional billing_proof field on the response (additive).


3. Deploy ​

bash
# 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):

bash
API=https://api.quantzk.com bash scripts/smoke-billing-phase2.sh

This script lives in scripts/smoke-billing-phase2.sh. It:

  1. Hits /health and the revocation feed.
  2. Issues an attest with a fresh event_id.
  3. Confirms attestation.billing_proof.binding_version === 'bn128-poseidon8-v1'.
  4. Asserts attestation.billing_proof.vkey_hash matches artifacts.verification_key.canonical_sha256 in ARTIFACT_MANIFEST.json (RFC 8785 JSON digest of the vkey; distinct from raw file sha256).
  5. Verifies the bundle and asserts incircuit_binding_verification.valid === true.
  6. 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:

bash
railway variables set VDI_BILLING_INCIRCUIT_BINDING=false
railway redeploy

Customer impact: zero — Phase-1 binding (bn128-field-split-v1) keeps working.


6. Operator notes ​

  • Trusted setup: the shipped apiBillingV2_0001.zkey was generated by a single-operator Phase-2 contribution with dev entropy. Before relying on it for adversarial production trust, re-run zk-cp/scripts/compile-api-billing-v2.sh on 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 update ARTIFACT_MANIFEST.json hashes. Bump the manifest circuit.version and 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 /attest p50 in production for Phase-2 proof generation. /verify adds ~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."

QuantZK Control docs: gateway, reconciliation, and optional portable evidence.