Skip to content
Open
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
2 changes: 1 addition & 1 deletion sdk/src/encoding.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createHash } from 'crypto';
import { createHash } from './hash';

// BN254 scalar field prime
// r = 21888242871839275222246405745257275088548364400416034343698204186575808495617
Expand Down
3 changes: 3 additions & 0 deletions sdk/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ export class WitnessValidationError extends Error {
super(message);
this.name = 'WitnessValidationError';
this.reason = reason ?? 'structure';

// Maintain prototype chain
Object.setPrototypeOf(this, WitnessValidationError.prototype);
}
}
121 changes: 121 additions & 0 deletions sdk/src/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Browser-safe hash functions.
*
* Provides SHA-256 and other common hashes that work across environments:
* - Node.js (native crypto module)
* - Browsers (SubtleCrypto)
*
* NOTE: For production use with ZK circuits, you should use a dedicated
* Poseidon hash implementation compatible with your proving system.
*/

import { detectEnv, RuntimeEnv } from './random';

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/**
* A hash function that takes arbitrary bytes and returns a fixed-size digest.
*/
export interface HashFunction {
/**
* Compute the hash of the input data.
*/
update(data: Buffer): this;

/**
* Finalize and return the digest.
*/
digest(): Buffer;
}

// ---------------------------------------------------------------------------
// Hash implementations
// ---------------------------------------------------------------------------

/**
* Node.js SHA-256 implementation.
*/
export class NodeSha256 implements HashFunction {
private readonly hash: any;

constructor() {
const { createHash } = require('crypto');
this.hash = createHash('sha256');
}

update(data: Buffer): this {
this.hash.update(data);
return this;
}

digest(): Buffer {
return this.hash.digest();
}
}

/**
* Web Crypto SHA-256 implementation.
* Note: This is async - you must await the digest promise.
*/
export class WebCryptoSha256 {
private chunks: Buffer[] = [];

update(data: Buffer): this {
this.chunks.push(data);
return this;
}

/**
* WARNING: This returns a Promise, not a Buffer!
* If you need a sync API, use Node.js or a pure-JS SHA-256 implementation.
*/
async digest(): Promise<Buffer> {
const data = Buffer.concat(this.chunks);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
return Buffer.from(new Uint8Array(hashBuffer));
}
}

// ---------------------------------------------------------------------------
// Convenience API - SHA-256 (Node.js only for now due to async)
// ---------------------------------------------------------------------------

/**
* Create a SHA-256 hash context.
* NOTE: In browsers, this will throw - use a pure JS implementation or SubtleCrypto directly.
*/
export function createHash(algorithm: 'sha256'): HashFunction {
if (algorithm !== 'sha256') {
throw new Error(`Unsupported hash algorithm: ${algorithm}. Only 'sha256' is available.`);
}

const env = detectEnv();

switch (env) {
case 'node':
return new NodeSha256();

default:
throw new Error(
`Synchronous SHA-256 is not available in environment '${env}'. ` +
`In browsers, use crypto.subtle.digest('SHA-256', data) which is async, ` +
`or use a pure-JS SHA-256 implementation.`
);
}
}

/**
* Compute SHA-256 hash of data in one call.
*/
export function sha256(data: Buffer): Buffer {
return createHash('sha256').update(data).digest();
}

export default {
createHash,
sha256,
NodeSha256,
WebCryptoSha256,
};
4 changes: 4 additions & 0 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ export * from './backends';
export * from './benchmark';
export * from './encoding';
export * from './errors';
export * from './hash';
export * from './merkle';
export * from './note';
export * from './proof';
export * from './proofErrors';
export * from './proofCache';
export * from './gas';
export * from './random';
export * from './stealth';
export * from './withdraw';
export {
Expand Down
3 changes: 2 additions & 1 deletion sdk/src/note.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createHash, randomBytes } from 'crypto';
import { createHash } from './hash';
import { randomBytes } from './random';

// ---------------------------------------------------------------------------
// Backup format constants
Expand Down
56 changes: 51 additions & 5 deletions sdk/src/proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ import {
} from './encoding';
import { validateMerkleProof } from './merkle';
import { assertValidGroth16ProofBytes, assertValidPreparedWithdrawalWitness, assertValidStellarAccountId } from './witness';
import { WitnessValidationError } from './errors';
import {
ProofError,
errNoProvingBackend,
wrapProofError,
ProofErrorCode,
} from './proofErrors';
import {
ProofCache,
InMemoryProofCache,
cacheKeyFromWitness,
defaultProofCache,
} from './proofCache';

export interface MerkleProof {
root: Buffer;
Expand Down Expand Up @@ -83,9 +96,11 @@ export interface PreparedWitness {
*/
export class ProofGenerator {
private backend?: ProvingBackend;
private cache?: ProofCache;

constructor(backend?: ProvingBackend) {
constructor(backend?: ProvingBackend, cache?: ProofCache) {
this.backend = backend;
this.cache = cache;
}

/**
Expand All @@ -97,15 +112,40 @@ export class ProofGenerator {

/**
* Generates a proof using the configured backend.
*
* @throws {ProofError} With stable error code on failure.
* @see ProofErrorCode for all possible error codes.
*/
async generate(witness: any): Promise<Uint8Array> {
if (!this.backend) {
throw new Error(
'Proving backend not configured. Please provide a backend to the ProofGenerator.'
);
throw errNoProvingBackend();
}

// assertValidPreparedWithdrawalWitness throws WitnessValidationError (backwards compatible)
assertValidPreparedWithdrawalWitness(witness);
return this.backend.generateProof(witness);

try {
return await this.backend.generateProof(witness);
} catch (e) {
// Wrap backend errors with stable classification
const message = e instanceof Error ? e.message : 'Unknown backend error';
const lower = message.toLowerCase();

if (lower.includes('constraint') || lower.includes('unsatis')) {
throw wrapProofError(e, ProofErrorCode.CONSTRAINT_VIOLATION);
}
if (lower.includes('timeout') || lower.includes('abort')) {
throw wrapProofError(e, ProofErrorCode.PROOF_GENERATION_TIMEOUT);
}
if (lower.includes('memory') || lower.includes('oom') || lower.includes('out of memory')) {
throw wrapProofError(e, ProofErrorCode.PROOF_GENERATION_OOM);
}
if (lower.includes('wasm') || lower.includes('webassembly')) {
throw wrapProofError(e, ProofErrorCode.WASM_RUNTIME_ERROR);
}

throw wrapProofError(e, ProofErrorCode.PROVING_BACKEND_FAILURE);
}
}

/**
Expand All @@ -119,6 +159,8 @@ export class ProofGenerator {
*
* The returned shape exactly matches the circuit parameter list in
* circuits/withdraw/src/main.nr.
*
* @throws {ProofError} With WITNESS_VALIDATION_FAILED or MERKLE_PROOF_INVALID on failure.
*/
static async prepareWitness(
note: Note,
Expand All @@ -127,6 +169,7 @@ export class ProofGenerator {
relayer: string = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF',
fee: bigint = 0n
): Promise<PreparedWitness> {
// Validation asserts throw WitnessValidationError directly (backwards compatible)
validateMerkleProof(merkleProof);
assertValidStellarAccountId(recipient, 'recipient');
if (fee > 0n) {
Expand Down Expand Up @@ -160,9 +203,12 @@ export class ProofGenerator {
/**
* Formats a raw proof from Noir/Barretenberg into the format
* expected by the Soroban contract.
*
* @throws {WitnessValidationError} With PROOF_FORMAT code if validation fails.
*/
static formatProof(rawProof: Uint8Array): Buffer {
// Soroban contract expects Proof struct: { a: BytesN<64>, b: BytesN<128>, c: BytesN<64> }
// Note: assertValidGroth16ProofBytes throws WitnessValidationError
assertValidGroth16ProofBytes(rawProof, 'rawProof');
return Buffer.from(rawProof);
}
Expand Down
Loading