Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`generateDisclosureProof()`** (`src/disclosure.ts`) — new proof generation function for selective disclosure circuit:
- Accepts `value`, `ownerPubkey`, `blinding`, `assetId`, `commitment` as `bigint` + `DisclosureMask` flags
- Computes `viewing_key = Poseidon(owner_pubkey)` via circomlibjs for ZK-friendly key derivation
- Generates Groth16 proof via `disclosure.wasm` + `disclosure_pk.zkey` artifacts
- Returns `DisclosureProofOutput` with 128-byte compressed proof, 4 public signals (LE hex), and `revealedData` decoded to human-readable values
- Validates mask: throws if all flags are `false`
- **New types exported** from `src/index.ts`:
- `DisclosureMask` — three boolean disclosure flags (`discloseValue`, `discloseAssetId`, `discloseOwner`)
- `DisclosureProofOutput` — proof + publicSignals + revealedData
- `bigIntToBytes32()` / `bytes32ToBigInt()` — big-endian field element helpers
- `hexSignalToBigInt()` — decode LE hex public signals back to BigInt
- `bigIntToHex()` — BigInt to 0x-prefixed 64-char big-endian hex
- **`CircuitType.Disclosure`** added to `src/circuits.ts` with `expectedPublicSignals: 4`
- **30 unit tests** in `tests/unit/disclosure.test.ts` across 5 describe blocks — all pass in 1.4s
- **Integration test** `tests/integration/disclosure.test.ts` with real circuit artifacts (graceful skip if artifacts unavailable)

## [3.1.1] - 2026-02-18

### Fixed
Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
{
useESM: true,
tsconfig: {
target: 'ES2020',
module: 'esnext',
},
},
Expand Down
6 changes: 6 additions & 0 deletions lint-staged.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
'*.ts': ['prettier --write'],
'*.{json,md}': ['prettier --write'],
// Run full project lint after all formatters complete
'*': () => 'npm run lint'
};
13 changes: 2 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@orbinum/proof-generator",
"version": "3.1.1",
"version": "3.2.0",
"description": "ZK-SNARK proof generator for Orbinum. Combines snarkjs (witness) with arkworks WASM (proof generation) to produce 128-byte Groth16 proofs.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -26,7 +26,7 @@
"test:disclosure": "jest disclosure.test.ts",
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
"lint": "tsc --noEmit",
"lint": "tsc --project tsconfig.test.json --noEmit",
"prepare": "husky",
"prepublishOnly": "npm run build && npm run lint && npm test",
"clean": "rm -rf dist node_modules package-lock.json"
Expand Down Expand Up @@ -59,15 +59,6 @@
"ts-jest": "29.2.5",
"typescript": "^5.7.3"
},
"lint-staged": {
"*.ts": [
"prettier --write",
"tsc --noEmit"
],
"*.{json,md}": [
"prettier --write"
]
},
"engines": {
"node": ">=22.0.0"
}
Expand Down
117 changes: 117 additions & 0 deletions src/disclosure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Selective Disclosure – Proof Orchestrator
*
* Generates Groth16 proofs for the `disclosure.circom` circuit.
*
* ## Circuit Public Inputs (in order)
* 0. commitment – Note commitment (always revealed)
* 1. revealed_value – Note value, or 0 if not disclosed
* 2. revealed_asset_id – Asset ID, or 0 if not disclosed
* 3. revealed_owner_hash – Poseidon(owner_pubkey), or 0 if not disclosed
*
* @module @orbinum/proof-generator/disclosure
*/

// @ts-ignore - circomlibjs has no type declarations
import { buildPoseidon } from 'circomlibjs';
import { generateProof, CircuitType } from './index';
import { DisclosureMask, DisclosureProofOutput, ProofResult } from './types';
import {
bigIntToHex,
bigIntToBytes32,
bytes32ToBigInt,
hexSignalToBigInt,
u64ToFieldStr,
} from './utils';
import { ArtifactProvider } from './provider';

// ============================================================================
// Internal: circuit input builder
// ============================================================================

/**
* Build the snarkjs-compatible inputs object for the disclosure circuit.
*
* All values are decimal BigInt strings — the native format that snarkjs
* expects for scalar field elements.
*/
async function buildCircuitInputs(
value: bigint,
ownerPubkey: bigint,
blinding: bigint,
assetId: bigint,
commitment: bigint,
mask: DisclosureMask
): Promise<Record<string, string>> {
const poseidon = await buildPoseidon();
const F = poseidon.F;

// viewing_key = Poseidon(owner_pubkey) — matches the circom constraint
const viewingKey: bigint = F.toObject(poseidon([ownerPubkey]));

return {
// Public inputs
commitment: commitment.toString(),
revealed_value: (mask.discloseValue ? value : 0n).toString(),
revealed_asset_id: (mask.discloseAssetId ? assetId : 0n).toString(),
revealed_owner_hash: (mask.discloseOwner ? viewingKey : 0n).toString(),
// Private inputs
value: u64ToFieldStr(value),
asset_id: u64ToFieldStr(assetId),
owner_pubkey: ownerPubkey.toString(),
blinding: blinding.toString(),
viewing_key: viewingKey.toString(),
disclose_value: mask.discloseValue ? '1' : '0',
disclose_asset_id: mask.discloseAssetId ? '1' : '0',
disclose_owner: mask.discloseOwner ? '1' : '0',
};
}

// ============================================================================
// Public API
// ============================================================================

/**
* Generate a selective disclosure Groth16 proof.
*
* @param value – Note value as BigInt (u64 field element)
* @param ownerPubkey – Owner public key as BigInt (BN254 scalar)
* @param blinding – Blinding factor as BigInt
* @param assetId – Asset ID as BigInt (u32)
* @param commitment – Note commitment as BigInt
* @param mask – Which fields to disclose to the auditor
* @param options – Optional artifact provider override
*/
export async function generateDisclosureProof(
value: bigint,
ownerPubkey: bigint,
blinding: bigint,
assetId: bigint,
commitment: bigint,
mask: DisclosureMask,
options: { provider?: ArtifactProvider; verbose?: boolean } = {}
): Promise<DisclosureProofOutput> {
if (!mask.discloseValue && !mask.discloseAssetId && !mask.discloseOwner) {
throw new Error(
'DisclosureMask: at least one field (discloseValue, discloseAssetId, discloseOwner) must be true'
);
}

const inputs = await buildCircuitInputs(value, ownerPubkey, blinding, assetId, commitment, mask);

// Public signal order from disclosure.circom:
// [0] commitment [1] revealed_value [2] revealed_asset_id [3] revealed_owner_hash
const result: ProofResult = await generateProof(CircuitType.Disclosure, inputs, {
provider: options.provider,
verbose: options.verbose,
});

const [sigCommitment, sigValue, sigAssetId, sigOwnerHash] = result.publicSignals;

const revealedData: DisclosureProofOutput['revealedData'] = { commitment: sigCommitment };
if (mask.discloseValue) revealedData.value = hexSignalToBigInt(sigValue).toString(10);
if (mask.discloseAssetId) revealedData.assetId = Number(hexSignalToBigInt(sigAssetId));
if (mask.discloseOwner) revealedData.ownerHash = bigIntToHex(hexSignalToBigInt(sigOwnerHash));

return { proof: result.proof, publicSignals: result.publicSignals, revealedData };
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,4 @@ export * from './types';
export * from './utils';
export * from './provider';
export { NodeArtifactProvider, WebArtifactProvider } from './circuits';
export { generateDisclosureProof } from './disclosure';
41 changes: 41 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,44 @@ export class InvalidInputsError extends ProofGeneratorError {
super(message, 'INVALID_INPUTS');
}
}

// ============================================================================
// Disclosure types
// ============================================================================

/**
* Which fields to reveal to the auditor.
*
* At least one of `discloseValue`, `discloseAssetId`, or `discloseOwner`
* must be `true`.
*/
export interface DisclosureMask {
/** Reveal the note value (u64) */
discloseValue: boolean;
/** Reveal the asset ID (u32) */
discloseAssetId: boolean;
/** Reveal the owner identity hash (Poseidon(owner_pubkey)) */
discloseOwner: boolean;
}

/** Proof output returned by `generateDisclosureProof`. */
export interface DisclosureProofOutput {
/** 128-byte compressed Groth16 proof as 0x-prefixed hex string */
proof: string;
/**
* Raw public signals in hex (0x-prefixed, 32 bytes each).
* Order: [commitment, revealed_value, revealed_asset_id, revealed_owner_hash]
*/
publicSignals: string[];
/** Human-readable revealed data */
revealedData: {
/** Revealed note value as decimal string, or undefined if not disclosed */
value?: string;
/** Revealed asset ID as number, or undefined if not disclosed */
assetId?: number;
/** Revealed owner hash as 0x-prefixed hex, or undefined if not disclosed */
ownerHash?: string;
/** Note commitment (always present) */
commitment: string;
};
}
56 changes: 56 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* - Circuit inputs
* - Proof hex strings
* - Public signals
* - BigInt / byte-array conversions
*/

/** BN254 scalar field modulus */
Expand Down Expand Up @@ -127,3 +128,58 @@ export function formatPublicSignalsArray(signals: (bigint | number | string)[]):
return '0x' + bytes.join('');
});
}

// ============================================================================
// BigInt / byte-array conversions
// ============================================================================

/**
* Convert a BigInt to a 32-byte Uint8Array (big-endian).
*/
export function bigIntToBytes32(n: bigint): Uint8Array {
const buf = new Uint8Array(32);
let remaining = n;
for (let i = 31; i >= 0; i--) {
buf[i] = Number(remaining & 0xffn);
remaining >>= 8n;
}
return buf;
}

/**
* Convert a 32-byte Uint8Array (big-endian) to a BigInt.
*/
export function bytes32ToBigInt(buf: Uint8Array): bigint {
let result = 0n;
for (let i = 0; i < 32; i++) {
result = (result << 8n) | BigInt(buf[i]);
}
return result;
}

/**
* Encode a u64 value as a BN254 field element decimal string for snarkjs.
*/
export function u64ToFieldStr(value: bigint): string {
return value.toString(10);
}

/**
* Parse a little-endian hex public signal (0x-prefixed, 32 bytes) back to BigInt.
*
* Public signals returned by the proof generator are little-endian; this
* function reverses the bytes before interpreting as a field element.
*/
export function hexSignalToBigInt(hex: string): bigint {
const clean = hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
const buf = Buffer.from(clean.padStart(64, '0'), 'hex');
const reversed = Buffer.from(buf).reverse();
return bytes32ToBigInt(new Uint8Array(reversed));
}

/**
* Convert a BigInt to a 0x-prefixed 64-hex-char big-endian string.
*/
export function bigIntToHex(n: bigint): string {
return '0x' + n.toString(16).padStart(64, '0');
}
Loading