@xochi/sdk
v0.2.0
Published
TypeScript SDK for generating and verifying [Xochi ZKP](https://github.com/xochi-fi/erc-xochi-zkp) compliance proofs. Produce EVM-compatible zero-knowledge proofs client-side using Noir circuits and Barretenberg UltraHonk.
Readme
@xochi/sdk
TypeScript SDK for generating and verifying Xochi ZKP compliance proofs. Produce EVM-compatible zero-knowledge proofs client-side using Noir circuits and Barretenberg UltraHonk.
Also provides trust tier system, privacy level modeling, attestation scoring, settlement splitting (XIP-1), and execution planning (XIP-2).
Install
npm install @xochi/sdkLatest published on npm: 0.1.1. Current source: 0.2.0 (unpublished -- adds the F-1..F-9 audit fixes, signed-variant proofs, the @xochi/sdk/provider signing module, and additional typed contract errors). Peer dependency: viem@^2.0.0 (required for Oracle/Verifier/SettlementRegistry clients).
Quick start
import { XochiProver } from "@xochi/sdk";
import { BundledCircuitLoader } from "@xochi/sdk/node";
const prover = new XochiProver(new BundledCircuitLoader());
// Generate a compliance proof (EU jurisdiction, single provider)
const result = await prover.proveCompliance({
score: 25,
jurisdictionId: 0, // EU
providerSetHash: "0x14b6becf...",
submitter: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
timestamp: String(Math.floor(Date.now() / 1000)),
});
// result.proofHex and result.publicInputsHex are ready for on-chain submission
const valid = await prover.verify("compliance", result.proof, result.publicInputs);
await prover.destroy();Proof types
| Type | Method | Use case |
| ----------------- | ------------------------- | ------------------------------------------------------- |
| Compliance | proveCompliance() | Risk score below jurisdiction threshold |
| Risk Score | proveRiskScore() | Custom threshold (GT/LT) or range proofs |
| Pattern | provePattern() | Anti-structuring, velocity, round amounts |
| Attestation | proveAttestation() | KYC/credential verification |
| Membership | proveMembership() | Merkle inclusion (whitelist) |
| Non-Membership | proveNonMembership() | Sorted Merkle adjacency (sanctions exclusion) |
| Compliance Signed | proveComplianceSigned() | Compliance with provider-signed signals (anti-grinding) |
| Risk Score Signed | proveRiskScoreSigned() | Risk score claim with provider-signed signals |
Multi-provider support
Both compliance and risk score accept multiple screening providers:
const result = await prover.proveCompliance({
signals: [25, 30, 20], // risk scores from 3 providers (0-100)
weights: [50, 30, 20], // importance weights
providerIds: ["1", "2", "3"],
jurisdictionId: 0,
providerSetHash: "0x...",
submitter: account.address, // binds proof to this address (anti-frontrun)
});Single-provider shorthand:
const result = await prover.proveCompliance({
score: 25,
jurisdictionId: 0,
providerSetHash: "0x...",
submitter: account.address,
});Trust tiers
Five tiers with fee rates and MEV rebates:
import { getTierFromScore, getFeeRate, getMevRebate } from "@xochi/sdk";
getTierFromScore(60); // { name: "Verified", min: 50, max: 74, rate: 0.2 }
getFeeRate(60); // 0.2 (0.20%)
getMevRebate(60); // 0.2 (20%)| Tier | Score | Fee | MEV Rebate | | ------------- | ----- | ----- | ---------- | | Standard | 0-24 | 0.30% | 10% | | Trusted | 25-49 | 0.25% | 15% | | Verified | 50-74 | 0.20% | 20% | | Premium | 75-99 | 0.15% | 25% | | Institutional | 100+ | 0.10% | 30% |
Privacy levels
Six levels gated by trust score:
import { getMaxPrivacyLevel, isPrivacyLevelAllowed } from "@xochi/sdk";
getMaxPrivacyLevel(60); // "private"
isPrivacyLevelAllowed("sovereign", 60); // false (needs 75+)
isPrivacyLevelAllowed("stealth", 60); // true| Level | Min Score | Settlement | | ------------------------ | --------- | ---------- | | open / public / standard | 0 | Public L1 | | stealth | 25 | ERC-5564 | | private | 50 | Aztec L2 | | sovereign | 75 | Aztec L2 |
Attestation scoring
Calculate trust scores from attestations with diminishing returns:
import { calculateScoreFromAttestations } from "@xochi/sdk";
const result = calculateScoreFromAttestations([
{ category: "humanity", points: 20 },
{ category: "identity", points: 30 },
{ category: "reputation", points: 8 },
{ category: "compliance", points: 25 },
]);
// { total: 83, byCategory: { humanity: 20, identity: 30, reputation: 8, compliance: 25 } }Within each category, the 1st provider contributes at 100%, 2nd at 25%, 3rd+ at 10%. Category caps: humanity (25), identity (35), reputation (20), compliance (40).
Tier proofs
Prove "score >= threshold" without revealing exact score:
import { generateTierProof, verifyTierProof } from "@xochi/sdk";
import { BundledCircuitLoader } from "@xochi/sdk/node";
const loader = new BundledCircuitLoader();
const proof = await generateTierProof(loader, 60, 25, account.address);
const result = await verifyTierProof(loader, proof);
// { valid: true, threshold: 25, tierName: "Trusted", feeRate: 0.25 }generateHighestTierProof picks the best tier automatically:
import { generateHighestTierProof } from "@xochi/sdk";
const highest = await generateHighestTierProof(loader, 60, account.address);
// Proves score >= 50 (Verified tier)Provider-signed proofs
The signed-variant proofs (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) cryptographically anchor the screening signals to a registered provider's secp256k1 signature, closing audit finding I-1 (signal honesty). The Oracle authenticates the signer via its on-chain _validSignerPubkeyHashes registry -- only proofs whose signer_pubkey_hash public input was previously registered are accepted.
Direct (server-side) via signSignals
import { Barretenberg } from "@aztec/bb.js";
import { XochiProver } from "@xochi/sdk";
import { BundledCircuitLoader } from "@xochi/sdk/node";
import { RawKeyLoader, loadSignerKey, signSignals } from "@xochi/sdk/provider";
const api = await Barretenberg.new();
const signerKey = await loadSignerKey(new RawKeyLoader(privateKeyBytes, "provider-1"));
// 1. Provider signs the screening bundle. chainId + oracleAddress (audit F-6)
// are committed in the in-circuit Pedersen digest the signature is over.
const signed = await signSignals(api, signerKey, {
chainId: 1n, // EVM chain ID of the consuming Oracle
oracleAddress: BigInt("0x..."), // address of the consuming Oracle (uint160 Field)
providerSetHash: BigInt("0x..."),
signals: [25n, 0n, 0n, 0n, 0n, 0n, 0n, 0n], // length 8, zero-pad inactive
weights: [100n, 0n, 0n, 0n, 0n, 0n, 0n, 0n],
timestamp: BigInt(Math.floor(Date.now() / 1000)),
submitter: BigInt(account.address),
});
// 2. Generate the signed-variant proof. chainId + oracleAddress MUST match
// what was signed -- the in-circuit ECDSA verify recomputes the digest.
const prover = new XochiProver(new BundledCircuitLoader());
const result = await prover.proveComplianceSigned({
score: 25,
jurisdictionId: 0,
providerSetHash: "0x...",
submitter: account.address,
timestamp: String(Math.floor(Date.now() / 1000)),
chainId: 1n,
oracleAddress: "0x...",
signedBundle: signed,
});Via the signing daemon
The repo also ships a daemon (daemon/src/server.ts) that holds the signing key and exposes POST /sign. Useful when the signing key shouldn't live in the proof-generating process:
const res = await fetch(`${daemonUrl}/sign`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({
chainId: 1,
oracleAddress: "0x...",
providerSetHash: "0x...",
signals: [25, 0, 0, 0, 0, 0, 0, 0],
weights: [100, 0, 0, 0, 0, 0, 0, 0],
timestamp: Math.floor(Date.now() / 1000),
submitter: account.address,
}),
});
// 200: { signature, pubkeyX, pubkeyY, signerPubkeyHash, payloadHash } as 0x-hex
// 409: replay detected (MemoryReplayDb / persistent backing store)
// 400: validation errorGET /pubkey-hash returns the daemon's signerPubkeyHash for one-time on-chain registration via oracle.registerSignerPubkeyHash(...). The daemon enforces replay protection per request.
Binding (audit F-6): the
chainId+oracleAddressyou pass to the signer and the prover MUST be the values you submit against. The on-chain Oracle asserts they matchblock.chainidandaddress(this); mismatches revert withPublicInputMismatch. A mismatch between signer-side and prover-side fails witness generation withinvalid provider signature on signals.
On-chain submission
XochiOracle submits proofs and queries attestations:
import { XochiOracle, PROOF_TYPES } from "@xochi/sdk";
import { createPublicClient, createWalletClient, http } from "viem";
import { mainnet } from "viem/chains";
const oracle = new XochiOracle(
"0x...", // oracle contract address
createPublicClient({ chain: mainnet, transport: http() }),
createWalletClient({ chain: mainnet, transport: http(), account }),
mainnet,
);
// Submit a single proof (timestamp in publicInputs must be within 1 hour of block.timestamp)
const txHash = await oracle.submitCompliance({
jurisdictionId: 0,
proofType: PROOF_TYPES.COMPLIANCE,
proof: result.proofHex,
publicInputs: result.publicInputsHex,
providerSetHash: "0x...",
});
// Check compliance status
const { valid, attestation } = await oracle.checkCompliance("0x...", 0);
// attestation: { subject, jurisdictionId, proofType, meetsThreshold, timestamp,
// expiresAt, proofHash, providerSetHash, publicInputsHash, verifierUsed }
// Filter by proof type (e.g., require an attestation backed by a PATTERN proof)
const patternStatus = await oracle.checkComplianceByType("0x...", 0, PROOF_TYPES.PATTERN);
// Retrieve historical proofs
const history = await oracle.getAttestationHistory("0x...", 0);
const proof = await oracle.getHistoricalProof(history[0]);Batch submission
Submit all proofs from a proveBatch or provePlan result atomically in a single transaction via the on-chain submitComplianceBatch. Reverts atomically if any sub-trade fails. Max 100 proofs per batch (MAX_BATCH_SIZE).
const batchResult = await oracle.submitBatch({
batch, // from proveBatch() or provePlan()
jurisdictionId: 0,
proofType: PROOF_TYPES.COMPLIANCE,
providerSetHash: "0x...",
});
// batchResult.submissions[i].proofHash -> pass to SettlementRegistryClientOn-chain verification
XochiVerifier verifies proofs directly against the on-chain verifier contracts:
import { XochiVerifier, PROOF_TYPES } from "@xochi/sdk";
const verifier = new XochiVerifier("0x...", publicClient);
// Single proof
const valid = await verifier.verifyProof(PROOF_TYPES.COMPLIANCE, proofHex, publicInputsHex);
// Batch (atomic all-or-nothing)
const batchValid = await verifier.verifyProofBatch(
[PROOF_TYPES.COMPLIANCE, PROOF_TYPES.MEMBERSHIP],
[proof1Hex, proof2Hex],
[pi1Hex, pi2Hex],
);
// Historical verification at a specific verifier version
const historicalValid = await verifier.verifyProofAtVersion(
PROOF_TYPES.COMPLIANCE,
1n,
proofHex,
publicInputsHex,
);
// Emergency revocation (owner-only, requires WalletClient)
const adminVerifier = new XochiVerifier("0x...", publicClient, walletClient, mainnet);
const revoked = await adminVerifier.isVersionRevoked(PROOF_TYPES.COMPLIANCE, 1n);
await adminVerifier.revokeVerifierVersion(PROOF_TYPES.COMPLIANCE, 1n);Lightweight oracle client
For environments without viem (Cloudflare Workers, edge functions):
import { OracleLite, PROOF_TYPES } from "@xochi/sdk";
const oracle = new OracleLite({
address: "0x...",
rpcUrl: "https://rpc.example.com",
});
const status = await oracle.checkCompliance("0x...", 0);
const result = await oracle.verifyProof(
"0x...", // wallet (used as msg.sender in simulation)
PROOF_TYPES.RISK_SCORE,
proofHex,
publicInputsHex,
);Settlement splitting (XIP-1)
Split large trades into sub-trades, generate compliance proofs for each, submit them, and settle on-chain. The full pipeline:
import {
planSplit,
proveBatch,
planExecution,
provePlan,
SettlementRegistryClient,
PROOF_TYPES,
} from "@xochi/sdk";
import { BundledCircuitLoader } from "@xochi/sdk/node";
const loader = new BundledCircuitLoader();
const prover = new XochiProver(loader);
// 1. Plan the split
const splitPlan = planSplit(500n * 10n ** 18n, 0, account.address, {
splitThreshold: 100n * 10n ** 18n, // split above 100 ETH
maxSubTrades: 10,
minSubTradeSize: 1n * 10n ** 18n,
});
// splitPlan.subTrades: [{ index: 0, amount: 100e18 }, ..., { index: 4, amount: 100e18 }]
// 2. Generate compliance proofs for all sub-trades
const batch = await proveBatch(prover, splitPlan, {
score: 25,
jurisdictionId: 0,
providerSetHash: "0x...",
submitter: account.address,
});
// 3. Submit all proofs to oracle
const batchResult = await oracle.submitBatch({
batch,
jurisdictionId: 0,
proofType: PROOF_TYPES.COMPLIANCE,
providerSetHash: "0x...",
});
// 4. Register trade and record sub-settlements
const registry = new SettlementRegistryClient(registryAddr, publicClient, walletClient, chain);
await registry.registerTrade(splitPlan.tradeId, 0, splitPlan.subTrades.length);
for (const sub of batchResult.submissions) {
await registry.recordSubSettlement(splitPlan.tradeId, sub.index, sub.proofHash);
}
// 5. Finalize with a pattern proof (anti-structuring)
await registry.finalizeTrade(splitPlan.tradeId, patternProofHash);Execution planning (XIP-2)
planExecution composes split planning, venue routing, and diffusion scheduling into a single call:
import { planExecution, provePlan } from "@xochi/sdk";
const plan = planExecution(
500n * 10n ** 18n, // total amount
0, // jurisdiction (EU)
account.address,
{ trustScore: 60, gasEstimates: { public: 65_000n, stealth: 150_000n, shielded: 400_000n } },
{ diffusionWindow: 300 }, // spread submissions over 5 minutes
);
// plan.subTrades includes venue assignment and target timestamps
// plan.subTrades[i].venue: "public" | "stealth" | "shielded"
// plan.subTrades[i].targetTimestamp: seconds relative to T0
// Generate proofs for the execution plan
const batch = await provePlan(prover, plan, complianceInput);Venue assignment respects trust score thresholds: public (0+), stealth (25+), shielded (50+). The diffusion scheduler enforces a minimum 12-second gap between consecutive submissions.
Circuit loaders
Three loaders for different environments:
// Node.js: bundled circuit artifacts
import { BundledCircuitLoader } from "@xochi/sdk/node";
// Node.js: load from erc-xochi-zkp repo path (development)
import { NodeCircuitLoader } from "@xochi/sdk/node";
const loader = new NodeCircuitLoader("/path/to/erc-xochi-zkp");
// Browser: load via fetch
import { BrowserCircuitLoader } from "@xochi/sdk/browser";
const loader = new BrowserCircuitLoader("https://cdn.example.com/circuits");Input builders
If you need to construct circuit inputs manually (outside of XochiProver):
import {
buildComplianceInputs,
buildRiskScoreInputs,
buildPatternInputs,
buildAttestationInputs,
buildMembershipInputs,
buildNonMembershipInputs,
buildComplianceSignedInputs,
buildRiskScoreSignedInputs,
} from "@xochi/sdk";Each builder validates constraints (signal range, weight bounds, timestamp limits, Merkle depth) and throws before you waste time on an invalid proof.
Submitter binding: All 8 circuits include
submitteras a public input. The Oracle contract enforcessubmitter == msg.senderfor every proof type, so the SDK no longer post-processespublicInputsHex-- pass the submitter address to the input builder and the prover handles the rest.Signed-variant binding (audit F-6):
buildComplianceSignedInputsandbuildRiskScoreSignedInputsadditionally requirechainIdandoracleAddress. These MUST equal the values the provider used when signing -- they're committed in the in-circuit Pedersen digest the ECDSA signature is checked against. The on-chain Oracle asserts they also matchblock.chainidandaddress(this), so a single provider signature cannot mint attestations on multiple Oracle instances or chains.
Proof type mappings
import {
PROOF_TYPES,
proofTypeToCircuit,
circuitToProofType,
PUBLIC_INPUT_COUNTS,
} from "@xochi/sdk";
proofTypeToCircuit(0x01); // "compliance"
circuitToProofType("risk_score"); // 0x02
PUBLIC_INPUT_COUNTS[0x01]; // 6 -- compliance: 6, risk_score: 8, pattern: 6, attestation: 6,
// membership: 5, non_membership: 5,
// compliance_signed: 9, risk_score_signed: 11
// (signed variants include signer_pubkey_hash + chain_id + oracle_address)Typed contract errors
Solidity reverts from XochiZKPOracle, XochiZKPVerifier, and SettlementRegistry are decoded into named JS error classes so you can branch on them in try/catch instead of regex-matching messages.
import {
SubmitterMismatchError,
ProofAlreadyUsedError,
BatchTooLargeError,
VersionRevokedError,
XochiContractError,
} from "@xochi/sdk";
try {
await oracle.submitCompliance(params);
} catch (err) {
if (err instanceof SubmitterMismatchError) {
// proof was bound to a different address -- regenerate with the right submitter
} else if (err instanceof ProofAlreadyUsedError) {
console.log(`Replay rejected, proof already used: ${err.proofHash}`);
} else if (err instanceof XochiContractError) {
// any other decoded contract revert -- err.errorName + err.args available
console.error(`Contract reverted with ${err.errorName}`, err.args);
} else {
throw err; // network error, gas estimation failure, etc.
}
}Available error classes: SubmitterMismatchError, ProofAlreadyUsedError, ProofTimestampStaleError, TimeWindowTooSmallError, EmptyBatchError, BatchTooLargeError, BatchLengthMismatchError, VersionRevokedError, TimelockNotElapsedError, TradeAlreadyExistsError, TradeNotFoundError, AttestationNotFoundError, SignedSignalsRequiredError, InvalidSignerPubkeyHashError. Any other Solidity custom error decodes to a base XochiContractError with errorName + args populated.
For lower-level use, decodeContractError(err, abi) returns the typed error or null, and withDecodedErrors(abi, fn) wraps any async call.
Development
npm install
npm test # unit tests only (219 tests; integration excluded via vitest.config.ts)
npm run test:integration # proof generation + anvil tests (50 tests, ~30s; uses vitest.integration.config.ts)
npm run typecheck # tsc --noEmit
npm run format # prettier --write
npm run format:check # prettier --check (run in CI / prepublishOnly)
npm run build # compile to dist/
# Sync circuit artifacts from erc-xochi-zkp
./scripts/sync-circuits.sh ../erc-xochi-zkpIntegration tests deploy the full contract stack (XochiZKPVerifier, XochiZKPOracle, SettlementRegistry) on a local anvil node. Requires foundry and a local clone of erc-xochi-zkp with compiled artifacts.
Related
- erc-xochi-zkp -- On-chain contracts and Noir circuit source
- XIPs -- Protocol improvement proposals (XIP-1: settlement splitting, XIP-2: adaptive settlement)
- xochi -- Protocol frontend
License
MIT
