@lazyjackorg/bipcircle-verifier
v0.1.3
Published
Open-source verifier for BIPCircle stablecoin reserve attestations. Re-derives PASS/FAIL from primary sources (XRPL, bank-service JWKS, on-chain token supply) without trusting BIPCircle or Lazy-Jack infrastructure.
Maintainers
Readme
bipcircle-verifier
Open-source verifier for the BIPCircle public-reserve-verifier protocol v1. Anyone can prove a BIPCircle stablecoin reserve attestation PASSes or FAILs without trusting BIPCircle or any Lazy-Jack infrastructure.
The verifier reads primary sources (the XRPL ledger, the bank-service's published JWKS, the witness file in GCS, and the on-chain token contract) and re-derives the same checks BIPCircle's anchor side runs internally. The trust roots are pinned in the verifier source — not user input — so a wrong URL or social-engineered tx hash can't produce a false PASS.
Install
npm install -g @lazyjackorg/bipcircle-verifierRequires Node.js 20+.
Use — pinned-tenant mode (preferred)
bipcircle-verify --xrpl-tx <hash> --tenant <tenantId>The tenant id is looked up in the verifier's pinned src/tenants.json registry. Trust roots — bank-service URL, XRPL issuer account, KMS public-key fingerprint pattern, on-chain token contract — all come from the registry. Output:
VERDICT: PASS
asOfDate: 2026-05-24
tenantId: tvvin-prod
sealCount: 24
signatures: 24/24 OK
merkle root: OK
reserves: 100245700 | supply: 100245700 | match: ✓The "match ✓" line is the on-chain supply comparison — reserves measured in bank-side minor units (pennies/cents) compared to the on-chain token totalSupply().
Use — unsafe-override (ad-hoc verification of an unregistered tenant)
bipcircle-verify \
--xrpl-tx <hash> \
--unsafe-bank-service-url https://bank-service-<tenant>.run.app \
--unsafe-issuer rExampleIssuerAccount...For tenants not yet in the verifier's pinned registry. Operator accepts the trust-anchor responsibility — both the URL and the issuer address need to come from a trusted out-of-band source (DPA, GFSC registry, etc.). On-chain supply check is skipped in this mode (no token-contract config available).
Exit codes: 0 PASS, 1 FAIL, 2 invocation error. --json for machine-readable output.
What the verifier actually checks
- Registry resolution — looks up the pinned trust roots for
--tenantOR validates--unsafe-*overrides. - XRPL transaction — fetches the tx by hash via public JSON-RPC. Validates
tx.Account === pinned issuer(closes Pro F2 cross-account spoofing). Parses Memo 5 (reserve-verifier-v1). - Memo kid binding — verifies the
bankServicePublicKeyIdin Memo 5 matches the tenant's pinnedkidPattern(closes Pro F1 attacker-controlled URL). - Witness file — HTTPS GET. Validates SHA-256 of the bytes against
witnessSha256in Memo 5. Schema check. - Bank-service JWKS — fetches
/.well-known/bank-service-keysfrom the pinned bankServiceUrl. Validates every advertisedkidmatches the tenant'skidPattern. Rejects duplicate kids; pinsalg=ES256. - Seal signatures — for every seal in the witness, decodes the canonical input and ECDSA-verifies the signature against the matching public key.
- Merkle root — re-builds the Merkle root from leaf digests; compares to Memo 5's anchored root.
- On-chain supply — fetches token
totalSupply()from the chain in the tenant registry; compares to the sum of bank-side balances. Reports "match ✓" or "shortfall of N minor units."
Every stage produces a structured failure record on FAIL. --json gives the full diff.
Trust model
The verifier trusts only:
- This verifier's pinned tenant registry (
src/tenants.json, ships in the source release; operator-PR'd as tenants onboard) - The XRPL ledger (public, permissionless)
- The bank-service public keys the operator publishes at the pinned
/.well-known/bank-service-keysendpoint (HSM-backed ECDSA P-256, FIPS 140-2 Level 3) - The witness file's SHA-256 anchored on the immutable XRPL transaction
- Node.js's built-in crypto (no external cryptographic dependencies)
The verifier does not trust BIPCircle, Lazy-Jack, the bank-service operator, the GCS witness host, or this binary's source (you can read it, build it, and check the npm + SLSA provenance on every release).
Protocol
https://github.com/Lazy-Jack-Ltd/bipcircle/blob/main/Documentation/architecture/public-reserve-verifier-protocol.md
Producer-side source-of-truth:
- bank-service
sealSigner.js(seal envelope + canonical input) - BIPCircle
audit.jsbuildBankServiceSealCanonicalInput - BIPCircle
sealMerkle.js(Merkle root + witness file shape) - BIPCircle
publishTenantTreasuryAttestation.js(XRPL Memo 5 emit)
Releases + provenance
Every tagged release ships with:
- npm package signed via npm provenance (build attested by GitHub Actions OIDC on a public runner)
- SHA-256 checksums of the tarball on the GitHub release
- Auto-generated release notes
To verify a downloaded release:
npm audit signatures @lazyjackorg/bipcircle-verifier
sha256sum lazy-jack-bipcircle-verifier-*.tgzAdding a tenant
Tenants are pinned in source: every release embeds the registry available at release time. To onboard a new tenant:
Open a PR to
src/tenants.jsonadding the entry:{ "tenantId": "your-tenant-id", "bankServiceUrl": "https://bank-service-your-tenant.run.app", "xrplIssuerAddress": "rYourTreasuryWalletAddress...", "kidPattern": "^projects/your-gcp-project/locations/europe-west2/keyRings/bank-service-signers/cryptoKeys/your-tenant-signer/cryptoKeyVersions/\\d+$", "token": { "chain": "ethereum", "contract": "0xYourErc20Address...", "decimals": 2, "currency": "GBP" } }Cut a new verifier release (bump the patch / minor). External verifiers upgrade when ready.
Until the new release lands, third parties can verify your tenant using --unsafe-bank-service-url + --unsafe-issuer overrides.
Building from source
git clone https://github.com/Lazy-Jack-Ltd/bipcircle-verifier
cd bipcircle-verifier
npm install
npm test
node bin/bipcircle-verify.js --helpNo build step — pure JavaScript, runs directly on Node.
License
MIT — see LICENSE.
Audit history
- v0.1.0 — initial release
- v0.1.1 — self-audit pass: fetch timeouts on every external call (10s/15s/30s), CLI
--key=valueform, witnesssealCounttype check - v0.1.2 — Gemini Pro adversarial audit + external reviewer feedback:
- F1 (CRITICAL) + F2 (CRITICAL) closed: pinned per-tenant registry binds bank-service URL + XRPL issuer + kid pattern in the verifier source (not user input). Closes both the user-supplied-URL trust gap AND the accept-any-XRPL-account spoofing path.
- F4 (MEDIUM) closed: JWKS now rejects duplicate kids + pins
alg=ES256strictly. - 0.2.0 forward-port: on-chain
totalSupply()comparison wired in via tenant registry's token config. Output now reportsreserves = X | supply = Y | match ✓or shortfall. - F7 (test coverage) closed: 21 tests (up from 14) including F1/F2/F4 regressions + signature-forgery + duplicate-kid + unknown-tenant + wrong-XRPL-account adversarial paths.
- False positives documented in the commit message (nested-key canonicalisation, Merkle reorder, try/catch on crypto.verify — none were real issues; verifier doesn't re-canonicalise, witnessSha256 binds bytes, try/catch was already present).
Reproducible builds (bit-identical output) remain a 0.3.0 target.
