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."

Verification keys are embedded in attestations. The verifier is open source.