All notable changes to @xochi/sdk are documented here. Format follows Keep a Changelog, versions follow SemVer.
-
EIP-712 domain rename:
XochiZKPOracle→ERC8262Oracle-- the EIP-712 domain separator that providers sign over for credential-root publications now usesname = "ERC8262Oracle"(was"XochiZKPOracle"). Any signed payloads minted under the old domain will fail to recover to the registered signer on-chain. Providers must re-sign all in-flight credential-root publications. Mirrors the contract-side rename inERC-8262(project renamed to drop the project name from the ERC reviewer's surface). -
TS class renames --
XochiOracle→ERC8262Oracle,XochiVerifier→ERC8262Verifier,XochiProver→ERC8262Prover,XochiContractError→ERC8262ContractError. Callers must update imports andinstanceofchecks. The 18 typed-error subclasses (SubmitterMismatchError,ProofAlreadyUsedError, etc.) keep their names; only the base class renames. Package name@xochi/sdkis unchanged. -
Forge artifact paths -- integration tests now load bytecode from
../../ERC-8262/out/ERC8262Oracle.sol/...(was../../erc-xochi-zkp/out/XochiZKPOracle.sol/...). The CI workflow clonesxochi-fi/ERC-8262instead ofxochi-fi/erc-xochi-zkp. -
buildPatternInputs/PatternInput-- now requiressettlementRoot: string(audit H-1). Pre-this-change PATTERN proofs are unsubmittable: the on-chainProofTypes.expectedPublicInputCount(PATTERN)was bumped to 7 withsettlement_rootas input[6], but the SDK was still producing 6-input witnesses. 0.2.0 absorbed the H-2 piece (patternPublicInputsarg onfinalizeTrade) but missed H-1. Callers that intend to finalize a trade MUST first callSettlementRegistryClient.computeSettlementRoot(tradeId)and pass the result assettlementRoot; callers that don't intend to finalize pass"0x" + "0".repeat(64). The on-chain Oracle is transparent to this value, butSettlementRegistry.finalizeTradeenforces equality and reverts withSettlementRootMismatchon mismatch. -
PUBLIC_INPUT_COUNTS[0x03]-- bumped 6 → 7. The bundledcircuits/pattern.jsonwas re-synced fromERC-8262/circuits/target/to pick up the post-H-1 ABI.
SettlementRegistryClient.computeSettlementRoot(tradeId)-- view that returns thebytes32value a PATTERN proof must commit to in order to bind totradeId. Mirrors_computeSettlementRooton-chain:bytes32(uint256(keccak256(abi.encode(subTradeCount, proofHashes))) % BN254_FR_MODULUS). Provers MUST call this before generating the proof.- Typed contract errors --
InvalidPublicInputLengthError,UnalignedPublicInputsError,SettlementRootMismatchError. The first two surface ABI-shape mismatches that were previously opaque selectors (the H-1 absorption gap was originally diagnosed via raw0xf0b9e463); the third decodes the H-1 binding-check revert. Total typed wrappers now 21. COMPLIANCE_MULTI_SIGNED(proof type0x09) -- M-of-N multi-provider signed compliance. Bundles up toMAX_PROVIDERS_MULTI = 5parallel signer slots; M of them must each produce a valid secp256k1 signature over a slot-specific Pedersen digest AND each must individually attest the subject is below the jurisdiction's high-risk floor. Trust upgrade over0x07(one signer "compliant" vs. M independent signers "compliant", AND-aggregated). Mirrors the on-chain validator and verifier shipped inerc-xochi-zkp2026-05-14.XochiProver.proveComplianceMultiSigned(opts)-- new entry point.buildComplianceMultiSignedInputs-- input builder. Takesslots: (MultiSignedSlot | null)[]with length exactly 5;nullslots get the inactive-slot witness padding (weight_sum = 1,weights = [1, 0..0],signals = [0; 8], zero pubkey/sig) automatically.signSlotPayload(api, key, req)-- mints one slot's secp256k1 signature over the slot-specific Pedersen digest. Orchestration across M daemons is the caller's responsibility; the signer only signs its own slot.signSlotPayloadWithReplayProtection-- same with the existingReplayDbintegration. Replay key is(submitter, slot_payload_hash);slot_indexis embedded in the digest so a single daemon signing different slots for the same subject produces distinct keys.computeSlotPayloadHash-- bb.js mirror ofxochi_shared::multi_sig::compute_slot_payload_hash. New domain tagDOMAIN_MULTI_SIGNED_SIGNALS = 0x4d554c54495f5349(ASCII "MULTI_SI"); 25-field layout:[tag, slot_index, chain_id, oracle_address, jurisdiction_id, provider_set_hash, config_hash, signals[0..8], weights[0..8], timestamp, submitter]. Parity vector locked end-to-end (test_parity_with_sdk_slot_payload_hashon the circuit side).MAX_PROVIDERS_MULTIandMIN_MULTI_PROVIDER_THRESHOLDSconstants (EU=1, US=2, UK=1, SG=2, mirrorsJurisdictionConfig.minMultiProviderThresholdon the Oracle).- Daemon
POST /sign-multiroute -- bearer-/mTLS-authed, replay-protected, audit-logged. Signs ONE slot per call. - Typed contract errors:
InsufficientSignersError,BelowJurisdictionMinProvidersError,DuplicateSignerError,InvalidThresholdMError. Decoded via existingdecodeContractError/withDecodedErrors. Total typed wrappers now 18. PUBLIC_INPUT_COUNTS[0x09] = 14(jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, threshold_m, 5x signer_pubkey_hash, chain_id, oracle_address, submitter). The Oracle requires all non-zero signer hashes to be in_validSignerPubkeyHashes(the same registry0x07uses).circuits/compliance_multi_signed.jsonsynced fromerc-xochi-zkp/circuits/target/.scripts/sync-circuits.shnow includes the new circuit.
- Proof type
0x0ais reserved for a futurecompliance_multi_signed_largevariant (N > 5). BumpingMAX_PROVIDERS_MULTIpast 5 doubles per-proof gas for everyone using 2-of-3; a parallel large-N circuit is the right shape when demand emerges. - M-of-N for
risk_score_signed(would-be0x08analogue) is intentionally out of scope; same shape, different circuit, follow-up if integrators ask for it. - Aggregate-score semantics (mean / weighted) are out of scope. The current circuit AND-aggregates: each active slot must individually be below the floor (regulators want "M independent yeses", not "average is OK").
Breaking -- aligned with the post-audit contracts in erc-xochi-zkp. Every consumer must migrate; old SDK proofs do not verify against the new verifiers and old call signatures fail typecheck. The breaking changes fall in five buckets: input-builder shapes (audit fixes H-3, M-2, C-1), the new ATTESTATION circuit (C-1), the SettlementRegistry finalizeTrade signature (H-2), the signed-variant digest layout (audit F-6), and the updateProviderConfig ABI (audit F-2).
buildMembershipInputs-- removedelement, added optionalsubjectSalt(defaults to"0"for public sets). Per audit fix H-3, the leaf is nowleaf_hash_subject(submitter, set_id, salt)and binds to the submitter; the prover no longer claims membership of an arbitrary value.buildNonMembershipInputs-- removedelement. AddedlowLeafSaltandhighLeafSalt(default"0"). The submitter is the value being proven non-member. New client-side adjacency check (H-4): throws ifhighIndex !== lowIndex + 1to mirror the in-circuit constraint.buildAttestationInputs-- redesigned for the credentials-tree model (C-1). RemovedcredentialHash,credentialSubject,providerMerkleIndex,providerMerklePath. RenamedmerkleRoottocredentialRoot. New required fields:credentialAttribute,merkleIndex,merklePath. The credential hash is computed in-circuit and bound to (provider_id, submitter, type, attribute, expiry); the leaf isleaf_hash_value(credential_hash)in the provider's per-provider credentials Merkle tree.SettlementRegistryClient.finalizeTrade(tradeId, patternProofHash)-- now takes a third argumentpatternPublicInputs: Hex. Per audit fix H-2, the registry verifieskeccak256(patternPublicInputs) == attestation.publicInputsHashand thatanalysis_type == 1(anti-structuring). VELOCITY (2) and ROUND_AMOUNT (3) PATTERN proofs are rejected at the registry. Pass the samepublicInputsHexyou sent tosubmitCompliance.buildRiskScoreInputs-- now rejects trivially-true bounds client-side (audit H-1): throws onbound_lower=0for THRESHOLD/GT,bound_lower>=10000for THRESHOLD/LT, inverted ranges, and the full-domain[0, 10000]range. The Oracle enforces the same rules on-chain via the newInvalidRiskProofType/InvalidRiskDirection/TrivialRiskBound/InvalidRiskBounderrors.buildComplianceSignedInputs/buildRiskScoreSignedInputs-- now requirechainId: bigint | string | numberandoracleAddress: Address. Audit F-6 binds these into the in-circuit Pedersen digest the provider signs over so a single signature cannot mint attestations on multiple Oracle instances or chains. They MUST equal the values the provider used when signing AND the deployment'sblock.chainid/address(this)-- the on-chain Oracle reverts withPublicInputMismatchotherwise. Public input counts grew accordingly:COMPLIANCE_SIGNED7 → 9,RISK_SCORE_SIGNED9 → 11.XochiOracle.updateProviderConfigABI -- gained a third argumentproviderIds: uint256[](audit F-2). The contract now requires the provider denylist and the config metadata to be bound atomically so denied providers can never reference the new config. Existing two-arg callers fail viem ABI encoding.MAX_BATCH_SIZE-- lowered 100 → 10 (audit F-3, mainnet block-gas budget). Batches submitted viaXochiOracle.submitBatchare now capped at 10 sub-trades; oversize batches are rejected client-side and on-chain (BatchTooLarge).
- Per-provider credential-root API on
XochiOracle(audit C-1 + Phase 1 infra):getProviderPublisher(providerId)-- read the publisher EOA.setProviderPublisher(providerId, publisher)-- owner-only authorization.publishCredentialRoot(providerId, root, cid)-- publisher-only; emitsCredentialRootPublished.revokeCredentialRoot(root)-- owner OR provider publisher.isValidCredentialRoot(root)-- view; checks registered + not revoked + within 48h TTL.getCredentialRoot(root)-- returns{providerId, registeredAt, revoked}.isRevokedConfig(configHash)-- view (audit M-3: permanent config revocation).
- Timelocked verifier-version revocation on
XochiVerifier(audit I-3b):proposeVersionRevocation(proofType, version)-- schedules with 6h delay.executeVersionRevocation(proofType, version)-- executes after delay.cancelVersionRevocation(proofType, version)-- aborts.getPendingRevocation(proofType, version)-- read pending readyAt.revocationTimelock()-- read the 6h constant. The existing immediaterevokeVerifierVersionis now documented as emergency-only; routine revocations should use the timelocked path.
- Pattern analysis-type constants exported from
inputs/pattern.ts:PATTERN_STRUCTURING = 1,PATTERN_VELOCITY = 2,PATTERN_ROUND_AMOUNT = 3. Doc note that SettlementRegistry requires STRUCTURING. - New ABI surface in
ORACLE_ABI/VERIFIER_ABI/SETTLEMENT_REGISTRY_ABI:- Oracle events:
ProviderPublisherSet,CredentialRootPublished,CredentialRootRevoked. - Oracle constant getters:
CREDENTIAL_ROOT_TTL,PATTERN_STRUCTURING,PATTERN_VELOCITY,PATTERN_ROUND_AMOUNT,MAX_RISK_SCORE_BPS. - Oracle errors:
InvalidRiskProofType,InvalidRiskDirection,TrivialRiskBound,InvalidRiskBound,InvalidAnalysisType,ConfigPermanentlyRevoked,NotProviderPublisher,CredentialRootAlreadyPublished,CredentialRootNotFound,InvalidProviderId,CredentialRootExpired,CredentialRootProviderMismatch,ProofTypePaused,ProofTypeNotPaused. - Verifier events:
VersionRevocationProposed,VersionRevocationCancelled. - SettlementRegistry errors:
PatternPublicInputsMismatch,PatternAnalysisTypeMismatch. - Oracle errors (signed-variant gating):
SignedSignalsRequired(uint8 jurisdictionId, uint8 proofType),InvalidSignerPubkeyHash(bytes32 signerPubkeyHash).
- Oracle events:
- Signed-variant proof types (closes audit I-1, signal honesty):
PROOF_TYPES.COMPLIANCE_SIGNED(0x07) -- compliance with provider-signed signals.PROOF_TYPES.RISK_SCORE_SIGNED(0x08) -- risk-score claim with provider-signed signals.XochiProver.proveComplianceSigned()/XochiProver.proveRiskScoreSigned().buildComplianceSignedInputs/buildRiskScoreSignedInputsinput builders.
@xochi/sdk/providersubpath export -- server-side signing surface for provider operators. Public exports includesignSignals,loadSignerKey,RawKeyLoader/HexKeyLoader,MemoryReplayDb+signSignalsWithReplayProtection, EIP-712 credential-root signing helpers, and the Pedersen primitives (computeSignedPayloadHash,computeSignerPubkeyHash). Excluded from the browser bundle.- Reference signing daemon in
daemon/-- HTTP server hosting the signer key withPOST /sign(replay-protected, audit-logged),POST /sign-credential-root, andGET /pubkey-hashfor one-time on-chain registration. Not part of the npm package; run from source. - Typed contract errors -- new wrappers
SignedSignalsRequiredError,InvalidSignerPubkeyHashErrordecoded from the matching ORACLE_ABI errors viadecodeContractError/withDecodedErrors. Total typed wrappers now 14.
- Circuit JSON regenerated (
circuits/*.json) fromnargo 1.0.0-beta.20against the post-audit Noir circuits. NewVK_HASHvalues for all six verifiers; proofs generated against 0.1.x circuits do not verify against 0.2.x verifiers. No public-input count changes (still 6, 8, 6, 6, 5, 5 for the six proof types). - MEMBERSHIP / NON_MEMBERSHIP merkle leaves now use
leaf_hash_subject(submitter, set_id, salt)(post-H-3). Tree publishers MUST construct each leaf this way and supply each user'ssubjectSalt(or0for public sets such as sanctions lists). The previoushash2(element, set_id)format is incompatible. - NON_MEMBERSHIP comparison now uses full Field ordering (post-M-2): no u64 ceiling, supports Ethereum addresses (160-bit) and any value < BN254 prime. The previous u64 cast-and-compare path was removed.
- Merkle internal nodes use a domain-separated
internal_hash(post-L-2). Older trees built withhash2(left, right)for internals do not produce the same root. Regenerate your trees off-chain with the publishedleaf_hash_subject/internal_hashhelpers.
// MEMBERSHIP -- before (0.1.x)
buildMembershipInputs({ element: "42", merkleIndex, merklePath, ... });
// MEMBERSHIP -- after (0.2.0)
buildMembershipInputs({ subjectSalt: "0", merkleIndex, merklePath, ... });
// (the leaf is now derived from `submitter` + `set_id` + `subjectSalt` in-circuit)
// NON_MEMBERSHIP -- before
buildNonMembershipInputs({ element: "0xabc...", lowLeaf, highLeaf, lowIndex, highIndex, ... });
// NON_MEMBERSHIP -- after
buildNonMembershipInputs({
lowLeaf, lowLeafSalt: "0", lowIndex,
highLeaf, highLeafSalt: "0", highIndex, // MUST be lowIndex + 1
...
});
// ATTESTATION -- before
buildAttestationInputs({
credentialHash, credentialSubject, credentialAttribute,
providerMerkleIndex, providerMerklePath, merkleRoot, ...
});
// ATTESTATION -- after
buildAttestationInputs({
credentialAttribute, expiryTimestamp,
merkleIndex, merklePath,
providerId, credentialType,
credentialRoot, // from oracle.publishCredentialRoot
currentTimestamp, submitter,
});
// SettlementRegistry.finalizeTrade -- before
await registry.finalizeTrade(tradeId, patternProofHash);
// after (audit H-2)
await registry.finalizeTrade(tradeId, patternProofHash, patternPublicInputs);
// updateProviderConfig -- before
await oracle.updateProviderConfig(newConfigHash, metadataURI);
// after (audit F-2)
await oracle.updateProviderConfig(newConfigHash, metadataURI, providerIds);
// MAX_BATCH_SIZE consumers -- batches > 10 now revert (audit F-3)
const batches = chunk(allSubTrades, MAX_BATCH_SIZE); // MAX_BATCH_SIZE === 10
// Signed-variant proofs -- new since 0.1.x; require chainId + oracleAddress (audit F-6)
import { signSignals } from "@xochi/sdk/provider";
const signed = await signSignals(api, signerKey, {
chainId: 1n,
oracleAddress: BigInt("0x..."), // bound into the in-circuit signed digest
providerSetHash, signals, weights, timestamp, submitter,
});
const proof = await prover.proveComplianceSigned({
...complianceFields,
chainId: 1n, // MUST equal what was signed
oracleAddress: "0x...",
signedBundle: signed,
});For ATTESTATION integrators, the provider must register a publisher EOA via the oracle owner (oracle.setProviderPublisher) and then publish credential roots (oracle.publishCredentialRoot) before any user can prove against that provider. Roots have a 48-hour TTL.
For COMPLIANCE_SIGNED / RISK_SCORE_SIGNED integrators, the provider's signing pubkey must be registered on the Oracle via oracle.registerSignerPubkeyHash(signerPubkeyHash) (read it from the daemon's GET /pubkey-hash or compute it locally via computeSignerPubkeyHash). Unregistered hashes revert with InvalidSignerPubkeyHash.
First public release. Aligned with erc-xochi-zkp@828a41b (security hardening + emergency verifier revocation) and erc-xochi-zkp@9527804.
- Single-tx batch submission:
XochiOracle.submitBatchnow wraps the on-chainsubmitComplianceBatch(one atomic transaction, max 100 proofs per batch). ExposesMAX_BATCH_SIZEconstant. XochiOracle.checkComplianceByType: query attestations filtered by proof type (e.g. require aPATTERNattestation).- Emergency verifier revocation on
XochiVerifier:isVersionRevoked()(read),revokeVerifierVersion()(owner-only write),VerifierVersionRevokedevent inVERIFIER_ABI. - Typed contract errors (
src/errors.ts): Solidity reverts decode into named JS classes --SubmitterMismatchError,ProofAlreadyUsedError,ProofTimestampStaleError,BatchTooLargeError,EmptyBatchError,BatchLengthMismatchError,VersionRevokedError,TimelockNotElapsedError,TradeAlreadyExistsError,TradeNotFoundError,AttestationNotFoundError. Base classXochiContractErrorforinstanceofdiscrimination of any decoded revert. Helpers:decodeContractError,withDecodedErrors. All write methods onXochiOracle,XochiVerifier, andSettlementRegistryClientnow surface typed errors. - Submitter type safety:
submitterfield on all input builders andgenerateTierProofis now typed as viemAddress. RuntimevalidateSubmitterrejects the zero address fail-fast (mirrors circuitassert(submitter != 0)). - Drift test for
DEFAULT_CONFIG_HASH: integration test asserts the hardcoded constant matches what the compliance circuit emits asconfig_hashfor a single-provider proof. Catches upstream provider config changes before silent breakage.
- All 6 circuits now expose
submitteras a public input. Public input counts: pattern 5→6, attestation 5→6, membership 4→5, non_membership 4→5 (compliance and risk_score were already 6 and 8). The "submitter gap" -- where the SDK had to manually append submitter bytes topublicInputsHexfor 4 proof types -- no longer exists.PUBLIC_INPUT_COUNTSinconstants.tsis now the single source of truth and matches both circuit and Oracle expectations. submitteris now required onPatternInput,AttestationInput,MembershipInput, andNonMembershipInput(previously not present; the SDK appended bytes after-the-fact).- Circuit artifacts pinned to Noir
1.0.0-beta.20(circuits.ts+circuits-browser.ts).@noir-lang/noir_jsruntime stays at1.0.0-beta.19(latest stable on npm; forward-compatible with beta.20 circuits). EXPECTED_NOIR_VERSIONconsolidated intosrc/noir-version.ts(was duplicated incircuits.ts+circuits-browser.ts).XochiOracle,XochiVerifier, andSettlementRegistryClientnow requireWalletClient<Transport, Chain | undefined, Account>(exported asConfiguredWalletClient) for write operations. Killedas anycasts onwriteContract; viem-provided generics now check args at compile time.XochiVerifierconstructor gains an optionalwalletClient+chainfor revocation writes.BatchSubmitResultadds top-leveltxHashfield. Per-submissiontxHashis retained for backwards compatibility but is identical across all entries (single tx).
XochiOracle.submitBatchpreviously sent N sequential transactions; failures part-way through left the settlement in an inconsistent state. Now atomic via on-chainsubmitComplianceBatch.NodeCircuitLoadernow tries bothcircuits/<name>/target/<name>.json(pre-beta.20 layout) andcircuits/target/<name>.json(workspace layout, beta.20+) before throwing. Cross-repo consumers compiling with nargo beta.20 no longer see ENOENT.
- Prettier added (
prettier@^3.3.3) with.prettierrc.json(printWidth: 100,trailingComma: "all"). New scripts:npm run formatandnpm run format:check.prepublishOnlynow runsformat:checkbefore typecheck/test/build. - Added
preparescript that runsnpm run build. Required fornpm install github:xochi-fi/xochi-sdk#<ref>consumers, since npm doesn't runprepublishOnlyfor git-installed packages anddist/is gitignored.
Initial scaffold (private). See HANDOVER.md for the full P0-P5 build history.