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: 2 additions & 0 deletions browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<head>
<script src="browser-rpc/resources/snarkjs/snarkjs.min.js">
</script>
<script src="browser-rpc/resources/stwo/s2circuits.js">
</script>
<script type="module">
import * as ReclaimAttestorCore from './browser-rpc/resources/attestor-browser.min.mjs'
window.ReclaimAttestorCore = ReclaimAttestorCore
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"@peculiar/webcrypto": "^1.5.0",
"@peculiar/x509": "^1.14.0",
"@reclaimprotocol/tls": "github:reclaimprotocol/tls",
"@reclaimprotocol/zk-symmetric-crypto": "^5.0.4",
"@reclaimprotocol/zk-symmetric-crypto": "^5.0.9",
"ajv": "^8.17.1",
"bs58": "^6.0.0",
"canonicalize": "^2.0.0",
Expand Down
1 change: 1 addition & 0 deletions proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ enum ErrorCode {
enum ZKProofEngine {
ZK_ENGINE_SNARKJS = 0;
ZK_ENGINE_GNARK = 1;
ZK_ENGINE_STWO = 2;
}

message ClaimContext {
Expand Down
6 changes: 2 additions & 4 deletions src/client/create-claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { asciiToUint8Array } from '@reclaimprotocol/tls'
import { makeRpcTlsTunnel } from '#src/client/tunnels/make-rpc-tls-tunnel.ts'
import { getAttestorClientFromPool } from '#src/client/utils/attestor-pool.ts'
import { DEFAULT_HTTPS_PORT, PROVIDER_CTX, TOPRF_DOMAIN_SEPARATOR } from '#src/config/index.ts'
import { ClaimTunnelRequest, ZKProofEngine } from '#src/proto/api.ts'
import { ClaimTunnelRequest } from '#src/proto/api.ts'
import { providers } from '#src/providers/index.ts'
import type {
CreateClaimOnAttestorOpts,
Expand Down Expand Up @@ -304,9 +304,7 @@ async function _createClaimOnAttestor<N extends ProviderName>(
owner: getAddress(),
},
transcript:transcript,
zkEngine: zkEngine === 'gnark'
? ZKProofEngine.ZK_ENGINE_GNARK
: ZKProofEngine.ZK_ENGINE_SNARKJS,
zkEngine: getEngineProto(zkEngine),
fixedServerIV: serverIV!,
fixedClientIV: clientIV!,
})
Expand Down
8 changes: 7 additions & 1 deletion src/proto/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions src/scripts/build-browser.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ set -e
cp -r node_modules/@reclaimprotocol/zk-symmetric-crypto/resources/ ./browser/resources
cp node_modules/snarkjs/build/snarkjs.min.js ./browser/resources/snarkjs/snarkjs.min.js
# remove r1cs files, we don't need them in prod
rm -r ./browser/resources/snarkjs/*/*.r1cs
rm -rf ./browser/resources/snarkjs/*/*.r1cs 2>/dev/null || true
# remove gnark libs, they are only for nodejs
rm -r ./browser/resources/gnark
rm -rf ./browser/resources/gnark 2>/dev/null || true
# ensure stwo resources exist (s2circuits.js + s2circuits_bg.wasm)
if [ ! -f ./browser/resources/stwo/s2circuits.js ]; then
echo "Warning: stwo/s2circuits.js not found in resources"
fi
if [ ! -f ./browser/resources/stwo/s2circuits_bg.wasm ]; then
echo "Warning: stwo/s2circuits_bg.wasm not found in resources"
fi
npm run run:tsc -- src/scripts/build-browser.ts
npm run run:tsc -- src/scripts/build-jsc.ts
1 change: 1 addition & 0 deletions src/scripts/build-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const rslt = await esbuild.build({
'ip-cidr': '#src/scripts/fallbacks/empty.ts',
'snarkjs': '#src/scripts/fallbacks/snarkjs.ts',
're2': '#src/scripts/fallbacks/re2.ts',
'@reclaimprotocol/zk-symmetric-crypto/stwo': '#src/scripts/fallbacks/stwo.ts',
},
external: [
'dotenv',
Expand Down
1 change: 1 addition & 0 deletions src/scripts/build-jsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const rslt = await esbuild.build({
'ip-cidr': '#src/scripts/fallbacks/empty.ts',
'snarkjs': '#src/scripts/fallbacks/empty.ts',
're2': '#src/scripts/fallbacks/re2.ts',
'@reclaimprotocol/zk-symmetric-crypto/stwo': '#src/scripts/fallbacks/stwo.ts',
},
external: [
'dotenv',
Expand Down
227 changes: 227 additions & 0 deletions src/scripts/fallbacks/stwo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/**
* Browser fallback for stwo - loads from window.s2circuits
* The s2circuits.js script must be loaded before this runs
*/

import type {
EncryptionAlgorithm,
FileFetch,
Logger,
MakeZKOperatorOpts,
ZKOperator,
ZKProofInput
} from '@reclaimprotocol/zk-symmetric-crypto'

// Browser-native base64 utilities
const Base64 = {
fromUint8Array(arr: Uint8Array): string {
let binary = ''
for(const element of arr) {
binary += String.fromCharCode(element)
}

return btoa(binary)
},
toUint8Array(str: string): Uint8Array {
const binary = atob(str)
const arr = new Uint8Array(binary.length)
for(let i = 0; i < binary.length; i++) {
arr[i] = binary.charCodeAt(i)
}

return arr
}
}

type StwoWitnessData = {
algorithm: EncryptionAlgorithm
key: string // base64
nonce: string // base64
counter: number
plaintext: string // base64
ciphertext: string // base64
}

type ProveResult = {
success?: boolean
error?: string
proof?: string
blocks?: number
algorithm?: string
proof_size_bytes?: number
}

type VerifyResult = {
valid?: boolean
error?: string
algorithm?: string
}

// Get s2circuits from window (loaded via script tag)
function getS2Circuits() {
const s2 = (window as unknown as { s2circuits?: unknown })['s2circuits'] as {
initSync?: (options: { module: Uint8Array }) => void
generate_chacha20_proof?: (key: Uint8Array, nonce: Uint8Array, counter: number, plaintext: Uint8Array, ciphertext: Uint8Array) => string
generate_aes128_ctr_proof?: (key: Uint8Array, nonce: Uint8Array, counter: number, plaintext: Uint8Array, ciphertext: Uint8Array) => string
generate_aes256_ctr_proof?: (key: Uint8Array, nonce: Uint8Array, counter: number, plaintext: Uint8Array, ciphertext: Uint8Array) => string
verify_chacha20_proof?: (proof: string, nonce: Uint8Array, counter: number, plaintext: Uint8Array, ciphertext: Uint8Array) => string
verify_aes_ctr_proof?: (proof: string, nonce: Uint8Array, counter: number, plaintext: Uint8Array, ciphertext: Uint8Array) => string
} | undefined

if(!s2) {
throw new Error('s2circuits not loaded. Make sure s2circuits.js is loaded before using stwo.')
}

return s2
}

function assertU32Counter(counter: number): void {
if(!Number.isInteger(counter) || counter < 0 || counter > 0xFFFFFFFF) {
throw new RangeError('counter must be a uint32 integer (0 to 4294967295)')
}
}

let wasmInitialized = false
let initPromise: Promise<void> | undefined

async function ensureWasmInitialized(fetcher: FileFetch, logger?: Logger): Promise<void> {
if(wasmInitialized) {
return
}

if(initPromise) {
return initPromise
}

initPromise = (async() => {
try {
const s2 = getS2Circuits()
const wasmBytes = await fetcher.fetch('stwo', 's2circuits_bg.wasm', logger)
s2.initSync!({ module: wasmBytes })
wasmInitialized = true
} catch(err) {
initPromise = undefined
throw err
}
})()
Comment thread
Scratch-net marked this conversation as resolved.

return initPromise
}
Comment thread
Scratch-net marked this conversation as resolved.

function serializeWitness(algorithm: EncryptionAlgorithm, input: ZKProofInput): Uint8Array {
if(!input.noncesAndCounters?.length) {
throw new Error('noncesAndCounters must be a non-empty array')
}

const { noncesAndCounters: [{ nonce, counter }] } = input
assertU32Counter(counter)
// Note: In the JS library, 'in' is ciphertext and 'out' is plaintext
// Stwo expects (key, nonce, counter, plaintext, ciphertext)
const data: StwoWitnessData = {
algorithm,
key: Base64.fromUint8Array(input.key),
nonce: Base64.fromUint8Array(nonce),
counter,
plaintext: Base64.fromUint8Array(input.out), // out = decrypted plaintext
ciphertext: Base64.fromUint8Array(input.in), // in = encrypted ciphertext
}
return new TextEncoder().encode(JSON.stringify(data))
}

function deserializeWitness(witness: Uint8Array): StwoWitnessData {
const json = new TextDecoder().decode(witness)
return JSON.parse(json)
}

export function makeStwoZkOperator({
algorithm,
fetcher,
}: MakeZKOperatorOpts<object>): ZKOperator {
return {
generateWitness(input) {
return serializeWitness(algorithm, input)
},

async groth16Prove(witness, logger) {
await ensureWasmInitialized(fetcher, logger)
const s2 = getS2Circuits()
const data = deserializeWitness(witness)

const key = Base64.toUint8Array(data.key)
const nonce = Base64.toUint8Array(data.nonce)
const plaintext = Base64.toUint8Array(data.plaintext)
const ciphertext = Base64.toUint8Array(data.ciphertext)

let resultJson: string
switch (data.algorithm) {
case 'chacha20':
resultJson = s2.generate_chacha20_proof!(key, nonce, data.counter, plaintext, ciphertext)
break
case 'aes-128-ctr':
resultJson = s2.generate_aes128_ctr_proof!(key, nonce, data.counter, plaintext, ciphertext)
break
case 'aes-256-ctr':
resultJson = s2.generate_aes256_ctr_proof!(key, nonce, data.counter, plaintext, ciphertext)
break
default:
throw new Error(`Unsupported algorithm: ${data.algorithm}`)
}

const result: ProveResult = JSON.parse(resultJson)
if(result.error) {
throw new Error(`Stwo proof generation failed: ${result.error}`)
}

if(!result.proof) {
throw new Error('Stwo proof generation failed: no proof returned')
}

return { proof: result.proof }
},
Comment thread
Scratch-net marked this conversation as resolved.

async groth16Verify(publicSignals, proof, logger) {
await ensureWasmInitialized(fetcher, logger)
const s2 = getS2Circuits()

const expectedNonce = publicSignals.noncesAndCounters[0]?.nonce
const expectedCounter = publicSignals.noncesAndCounters[0]?.counter
const expectedCiphertext = publicSignals.in
const expectedPlaintext = publicSignals.out

if(!expectedNonce || expectedCounter === undefined) {
logger?.warn('Invalid publicSignals: missing nonce or counter')
return false
}

assertU32Counter(expectedCounter)

const proofStr = typeof proof === 'string'
? proof
: new TextDecoder().decode(proof)

let resultJson: string
if(algorithm === 'chacha20') {
resultJson = s2.verify_chacha20_proof!(
proofStr, expectedNonce, expectedCounter, expectedPlaintext, expectedCiphertext
)
} else {
resultJson = s2.verify_aes_ctr_proof!(
proofStr, expectedNonce, expectedCounter, expectedPlaintext, expectedCiphertext
)
}

const result: VerifyResult = JSON.parse(resultJson)
if(result.error) {
logger?.warn({ error: result.error }, 'Stwo STARK verification failed')
return false
}

return result.valid === true
},
Comment thread
Scratch-net marked this conversation as resolved.

release() {
wasmInitialized = false
initPromise = undefined
}
}
}
2 changes: 1 addition & 1 deletion src/scripts/generate-receipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function main<T extends ProviderName>(
.replace('ws://', 'http://')
.replace('wss://', 'https://')

const zkEngine = getCliArgument('zk') === 'gnark' ? 'gnark' : 'snarkjs'
const zkEngine = getCliArgument('zk') === 'gnark' ? 'gnark' : 'stwo'
Comment thread
Scratch-net marked this conversation as resolved.
const { request, error, claim } = await createClaimOnAttestor({
name: paramsJson.name,
secretParams: paramsJson.secretParams,
Expand Down
Loading
Loading