@iam-protocol/pulse-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/index.d.mts +376 -0
- package/dist/index.d.ts +376 -0
- package/dist/index.js +14316 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +14238 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
- package/src/challenge/lissajous.ts +56 -0
- package/src/challenge/phrase.ts +29 -0
- package/src/config.ts +40 -0
- package/src/extraction/kinematic.ts +101 -0
- package/src/extraction/mfcc.ts +93 -0
- package/src/extraction/statistics.ts +59 -0
- package/src/extraction/types.ts +17 -0
- package/src/hashing/poseidon.ts +92 -0
- package/src/hashing/simhash.ts +87 -0
- package/src/hashing/types.ts +16 -0
- package/src/identity/anchor.ts +75 -0
- package/src/identity/types.ts +18 -0
- package/src/index.ts +43 -0
- package/src/proof/prover.ts +87 -0
- package/src/proof/serializer.ts +79 -0
- package/src/proof/types.ts +31 -0
- package/src/pulse.ts +397 -0
- package/src/sensor/audio.ts +94 -0
- package/src/sensor/motion.ts +83 -0
- package/src/sensor/touch.ts +65 -0
- package/src/sensor/types.ts +55 -0
- package/src/submit/relayer.ts +58 -0
- package/src/submit/types.ts +15 -0
- package/src/submit/wallet.ts +167 -0
- package/src/types.d.ts +14 -0
- package/test/integration.test.ts +102 -0
- package/test/poseidon.test.ts +81 -0
- package/test/serializer.test.ts +86 -0
- package/test/simhash.test.ts +57 -0
- package/test/statistics.test.ts +51 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { SolanaProof } from "../proof/types";
|
|
2
|
+
import type { SubmissionResult } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Submit a proof via the IAM relayer API (walletless mode).
|
|
6
|
+
* The relayer submits the on-chain transaction using the integrator's funded account.
|
|
7
|
+
* The user needs no wallet, no SOL, no crypto knowledge.
|
|
8
|
+
*
|
|
9
|
+
* In Phase 3, the relayer endpoint is configurable (stub until executor-node in Phase 4).
|
|
10
|
+
*/
|
|
11
|
+
export async function submitViaRelayer(
|
|
12
|
+
proof: SolanaProof,
|
|
13
|
+
commitment: Uint8Array,
|
|
14
|
+
options: {
|
|
15
|
+
relayerUrl: string;
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
isFirstVerification: boolean;
|
|
18
|
+
}
|
|
19
|
+
): Promise<SubmissionResult> {
|
|
20
|
+
try {
|
|
21
|
+
const body = {
|
|
22
|
+
proof_bytes: Array.from(proof.proofBytes),
|
|
23
|
+
public_inputs: proof.publicInputs.map((pi) => Array.from(pi)),
|
|
24
|
+
commitment: Array.from(commitment),
|
|
25
|
+
is_first_verification: options.isFirstVerification,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const headers: Record<string, string> = {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (options.apiKey) {
|
|
33
|
+
headers["X-API-Key"] = options.apiKey;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const response = await fetch(options.relayerUrl, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers,
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorText = await response.text();
|
|
44
|
+
return { success: false, error: `Relayer error: ${response.status} ${errorText}` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = (await response.json()) as {
|
|
48
|
+
success?: boolean;
|
|
49
|
+
tx_signature?: string;
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
success: result.success ?? true,
|
|
53
|
+
txSignature: result.tx_signature,
|
|
54
|
+
};
|
|
55
|
+
} catch (err: any) {
|
|
56
|
+
return { success: false, error: err.message ?? String(err) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Result of a verification submission */
|
|
2
|
+
export interface SubmissionResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
txSignature?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Result of a full Pulse verification */
|
|
9
|
+
export interface VerificationResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
commitment: Uint8Array;
|
|
12
|
+
txSignature?: string;
|
|
13
|
+
isFirstVerification: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
// Anchor program interactions use runtime IDL fetching, requiring dynamic typing.
|
|
3
|
+
import type { SolanaProof } from "../proof/types";
|
|
4
|
+
import type { SubmissionResult } from "./types";
|
|
5
|
+
import { PROGRAM_IDS } from "../config";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Submit a proof on-chain via a connected wallet (wallet-connected mode).
|
|
9
|
+
* Uses Anchor SDK to construct and send the transaction.
|
|
10
|
+
*
|
|
11
|
+
* Flow: create_challenge → verify_proof → update_anchor (or mint_anchor for first time)
|
|
12
|
+
*/
|
|
13
|
+
export async function submitViaWallet(
|
|
14
|
+
proof: SolanaProof,
|
|
15
|
+
commitment: Uint8Array,
|
|
16
|
+
options: {
|
|
17
|
+
wallet: any; // WalletAdapter
|
|
18
|
+
connection: any; // Connection
|
|
19
|
+
isFirstVerification: boolean;
|
|
20
|
+
trustScore?: number;
|
|
21
|
+
}
|
|
22
|
+
): Promise<SubmissionResult> {
|
|
23
|
+
try {
|
|
24
|
+
const anchor = await import("@coral-xyz/anchor");
|
|
25
|
+
const { PublicKey, SystemProgram } = await import("@solana/web3.js");
|
|
26
|
+
|
|
27
|
+
const provider = new anchor.AnchorProvider(
|
|
28
|
+
options.connection,
|
|
29
|
+
options.wallet,
|
|
30
|
+
{ commitment: "confirmed" }
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const verifierProgramId = new PublicKey(PROGRAM_IDS.iamVerifier);
|
|
34
|
+
const anchorProgramId = new PublicKey(PROGRAM_IDS.iamAnchor);
|
|
35
|
+
|
|
36
|
+
// Generate nonce for challenge
|
|
37
|
+
const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)));
|
|
38
|
+
|
|
39
|
+
// Derive PDAs
|
|
40
|
+
const [challengePda] = PublicKey.findProgramAddressSync(
|
|
41
|
+
[
|
|
42
|
+
Buffer.from("challenge"),
|
|
43
|
+
provider.wallet.publicKey.toBuffer(),
|
|
44
|
+
Buffer.from(nonce),
|
|
45
|
+
],
|
|
46
|
+
verifierProgramId
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const [verificationPda] = PublicKey.findProgramAddressSync(
|
|
50
|
+
[
|
|
51
|
+
Buffer.from("verification"),
|
|
52
|
+
provider.wallet.publicKey.toBuffer(),
|
|
53
|
+
Buffer.from(nonce),
|
|
54
|
+
],
|
|
55
|
+
verifierProgramId
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Build and send create_challenge + verify_proof transactions
|
|
59
|
+
// These use the raw Anchor program interface
|
|
60
|
+
const verifierIdl = await anchor.Program.fetchIdl(
|
|
61
|
+
verifierProgramId,
|
|
62
|
+
provider
|
|
63
|
+
);
|
|
64
|
+
if (!verifierIdl) {
|
|
65
|
+
return { success: false, error: "Failed to fetch verifier IDL" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const verifierProgram: any = new anchor.Program(
|
|
69
|
+
verifierIdl,
|
|
70
|
+
provider
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// 1. Create challenge
|
|
74
|
+
await verifierProgram.methods
|
|
75
|
+
.createChallenge(nonce)
|
|
76
|
+
.accounts({
|
|
77
|
+
challenger: provider.wallet.publicKey,
|
|
78
|
+
challenge: challengePda,
|
|
79
|
+
systemProgram: SystemProgram.programId,
|
|
80
|
+
})
|
|
81
|
+
.rpc();
|
|
82
|
+
|
|
83
|
+
// 2. Verify proof
|
|
84
|
+
const txSig = await verifierProgram.methods
|
|
85
|
+
.verifyProof(
|
|
86
|
+
Buffer.from(proof.proofBytes),
|
|
87
|
+
proof.publicInputs.map((pi) => Array.from(pi)),
|
|
88
|
+
nonce
|
|
89
|
+
)
|
|
90
|
+
.accounts({
|
|
91
|
+
verifier: provider.wallet.publicKey,
|
|
92
|
+
challenge: challengePda,
|
|
93
|
+
verificationResult: verificationPda,
|
|
94
|
+
systemProgram: SystemProgram.programId,
|
|
95
|
+
})
|
|
96
|
+
.rpc();
|
|
97
|
+
|
|
98
|
+
// 3. Mint or update anchor
|
|
99
|
+
const anchorIdl = await anchor.Program.fetchIdl(anchorProgramId, provider);
|
|
100
|
+
if (anchorIdl) {
|
|
101
|
+
const anchorProgram: any = new anchor.Program(anchorIdl, provider);
|
|
102
|
+
|
|
103
|
+
if (options.isFirstVerification) {
|
|
104
|
+
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
105
|
+
[Buffer.from("identity"), provider.wallet.publicKey.toBuffer()],
|
|
106
|
+
anchorProgramId
|
|
107
|
+
);
|
|
108
|
+
const [mintPda] = PublicKey.findProgramAddressSync(
|
|
109
|
+
[Buffer.from("mint"), provider.wallet.publicKey.toBuffer()],
|
|
110
|
+
anchorProgramId
|
|
111
|
+
);
|
|
112
|
+
const [mintAuthority] = PublicKey.findProgramAddressSync(
|
|
113
|
+
[Buffer.from("mint_authority")],
|
|
114
|
+
anchorProgramId
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Token-2022 program ID
|
|
118
|
+
const TOKEN_2022_PROGRAM_ID = new PublicKey(
|
|
119
|
+
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const { getAssociatedTokenAddressSync } = await import(
|
|
123
|
+
"@solana/spl-token"
|
|
124
|
+
);
|
|
125
|
+
const ata = getAssociatedTokenAddressSync(
|
|
126
|
+
mintPda,
|
|
127
|
+
provider.wallet.publicKey,
|
|
128
|
+
false,
|
|
129
|
+
TOKEN_2022_PROGRAM_ID
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await anchorProgram.methods
|
|
133
|
+
.mintAnchor(Array.from(commitment))
|
|
134
|
+
.accounts({
|
|
135
|
+
user: provider.wallet.publicKey,
|
|
136
|
+
identityState: identityPda,
|
|
137
|
+
mint: mintPda,
|
|
138
|
+
mintAuthority,
|
|
139
|
+
tokenAccount: ata,
|
|
140
|
+
associatedTokenProgram: new PublicKey(
|
|
141
|
+
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
|
142
|
+
),
|
|
143
|
+
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
144
|
+
systemProgram: SystemProgram.programId,
|
|
145
|
+
})
|
|
146
|
+
.rpc();
|
|
147
|
+
} else {
|
|
148
|
+
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
149
|
+
[Buffer.from("identity"), provider.wallet.publicKey.toBuffer()],
|
|
150
|
+
anchorProgramId
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
await anchorProgram.methods
|
|
154
|
+
.updateAnchor(Array.from(commitment), options.trustScore ?? 0)
|
|
155
|
+
.accounts({
|
|
156
|
+
authority: provider.wallet.publicKey,
|
|
157
|
+
identityState: identityPda,
|
|
158
|
+
})
|
|
159
|
+
.rpc();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { success: true, txSignature: txSig };
|
|
164
|
+
} catch (err: any) {
|
|
165
|
+
return { success: false, error: err.message ?? String(err) };
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare module "snarkjs" {
|
|
2
|
+
export const groth16: {
|
|
3
|
+
fullProve(
|
|
4
|
+
input: any,
|
|
5
|
+
wasmPath: string,
|
|
6
|
+
zkeyPath: string
|
|
7
|
+
): Promise<{ proof: any; publicSignals: string[] }>;
|
|
8
|
+
verify(vk: any, publicSignals: string[], proof: any): Promise<boolean>;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare module "circomlibjs" {
|
|
13
|
+
export function buildPoseidon(): Promise<any>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import { simhash, hammingDistance } from "../src/hashing/simhash";
|
|
5
|
+
import { generateTBH, computeCommitment } from "../src/hashing/poseidon";
|
|
6
|
+
import { prepareCircuitInput, generateProof } from "../src/proof/prover";
|
|
7
|
+
import { serializeProof } from "../src/proof/serializer";
|
|
8
|
+
import { TOTAL_PROOF_SIZE, NUM_PUBLIC_INPUTS } from "../src/config";
|
|
9
|
+
|
|
10
|
+
// Circuit artifacts from adjacent circuits repo
|
|
11
|
+
const WASM_PATH = path.resolve(
|
|
12
|
+
__dirname,
|
|
13
|
+
"../../circuits/build/iam_hamming_js/iam_hamming.wasm"
|
|
14
|
+
);
|
|
15
|
+
const ZKEY_PATH = path.resolve(
|
|
16
|
+
__dirname,
|
|
17
|
+
"../../circuits/build/iam_hamming_final.zkey"
|
|
18
|
+
);
|
|
19
|
+
const VK_PATH = path.resolve(
|
|
20
|
+
__dirname,
|
|
21
|
+
"../../circuits/keys/verification_key.json"
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Skip tests if circuit artifacts aren't built
|
|
25
|
+
const circuitArtifactsExist =
|
|
26
|
+
fs.existsSync(WASM_PATH) && fs.existsSync(ZKEY_PATH) && fs.existsSync(VK_PATH);
|
|
27
|
+
|
|
28
|
+
describe.skipIf(!circuitArtifactsExist)(
|
|
29
|
+
"integration: full crypto pipeline",
|
|
30
|
+
() => {
|
|
31
|
+
it("generates a valid proof from mock features end-to-end", async () => {
|
|
32
|
+
// 1. Create mock feature vector (~236 random values)
|
|
33
|
+
const features = Array.from({ length: 236 }, (_, i) =>
|
|
34
|
+
Math.sin(i * 0.3) * Math.cos(i * 0.7)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// 2. SimHash → 256-bit fingerprint
|
|
38
|
+
const fpNew = simhash(features);
|
|
39
|
+
expect(fpNew.length).toBe(256);
|
|
40
|
+
|
|
41
|
+
// 3. Create "previous" fingerprint by flipping 10 bits
|
|
42
|
+
const fpPrev = [...fpNew];
|
|
43
|
+
for (let i = 0; i < 10; i++) {
|
|
44
|
+
fpPrev[i * 25] = fpPrev[i * 25] === 1 ? 0 : 1;
|
|
45
|
+
}
|
|
46
|
+
expect(hammingDistance(fpNew, fpPrev)).toBe(10);
|
|
47
|
+
|
|
48
|
+
// 4. Generate TBHs with Poseidon commitments
|
|
49
|
+
const tbhNew = await generateTBH(fpNew);
|
|
50
|
+
const tbhPrev = await generateTBH(fpPrev);
|
|
51
|
+
|
|
52
|
+
expect(tbhNew.commitment).toBeGreaterThan(BigInt(0));
|
|
53
|
+
expect(tbhPrev.commitment).toBeGreaterThan(BigInt(0));
|
|
54
|
+
|
|
55
|
+
// 5. Prepare circuit input
|
|
56
|
+
const input = prepareCircuitInput(tbhNew, tbhPrev, 30);
|
|
57
|
+
expect(input.ft_new.length).toBe(256);
|
|
58
|
+
expect(input.threshold).toBe("30");
|
|
59
|
+
|
|
60
|
+
// 6. Generate Groth16 proof
|
|
61
|
+
const { proof, publicSignals } = await generateProof(
|
|
62
|
+
input,
|
|
63
|
+
WASM_PATH,
|
|
64
|
+
ZKEY_PATH
|
|
65
|
+
);
|
|
66
|
+
expect(publicSignals.length).toBe(NUM_PUBLIC_INPUTS);
|
|
67
|
+
|
|
68
|
+
// 7. Serialize for Solana
|
|
69
|
+
const { proofBytes, publicInputs } = serializeProof(
|
|
70
|
+
proof,
|
|
71
|
+
publicSignals
|
|
72
|
+
);
|
|
73
|
+
expect(proofBytes.length).toBe(TOTAL_PROOF_SIZE);
|
|
74
|
+
expect(publicInputs.length).toBe(NUM_PUBLIC_INPUTS);
|
|
75
|
+
for (const input of publicInputs) {
|
|
76
|
+
expect(input.length).toBe(32);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 8. Verify locally
|
|
80
|
+
const snarkjs = await import("snarkjs");
|
|
81
|
+
const vk = JSON.parse(fs.readFileSync(VK_PATH, "utf-8"));
|
|
82
|
+
const valid = await snarkjs.groth16.verify(vk, publicSignals, proof);
|
|
83
|
+
expect(valid).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("fails proof generation when distance exceeds threshold", async () => {
|
|
87
|
+
// Create fingerprints with ~200 bits different
|
|
88
|
+
const fpNew = Array.from({ length: 256 }, () =>
|
|
89
|
+
Math.random() > 0.5 ? 1 : 0
|
|
90
|
+
);
|
|
91
|
+
const fpPrev = fpNew.map((b) => (Math.random() > 0.2 ? 1 - b : b));
|
|
92
|
+
|
|
93
|
+
const tbhNew = await generateTBH(fpNew);
|
|
94
|
+
const tbhPrev = await generateTBH(fpPrev);
|
|
95
|
+
const input = prepareCircuitInput(tbhNew, tbhPrev, 30);
|
|
96
|
+
|
|
97
|
+
await expect(
|
|
98
|
+
generateProof(input, WASM_PATH, ZKEY_PATH)
|
|
99
|
+
).rejects.toThrow();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
computeCommitment,
|
|
4
|
+
generateSalt,
|
|
5
|
+
packBits,
|
|
6
|
+
bigintToBytes32,
|
|
7
|
+
generateTBH,
|
|
8
|
+
} from "../src/hashing/poseidon";
|
|
9
|
+
import { FINGERPRINT_BITS } from "../src/config";
|
|
10
|
+
|
|
11
|
+
describe("poseidon", () => {
|
|
12
|
+
const testFingerprint = Array.from({ length: FINGERPRINT_BITS }, (_, i) =>
|
|
13
|
+
i % 3 === 0 ? 1 : 0
|
|
14
|
+
);
|
|
15
|
+
const testSalt = BigInt("12345678901234567890");
|
|
16
|
+
|
|
17
|
+
it("packs bits into two 128-bit field elements (little-endian)", () => {
|
|
18
|
+
const bits = new Array(FINGERPRINT_BITS).fill(0);
|
|
19
|
+
bits[0] = 1; // bit 0 → lo = 1
|
|
20
|
+
bits[128] = 1; // bit 128 → hi = 1
|
|
21
|
+
|
|
22
|
+
const { lo, hi } = packBits(bits);
|
|
23
|
+
expect(lo).toBe(BigInt(1));
|
|
24
|
+
expect(hi).toBe(BigInt(1));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("packs complex bit patterns correctly", () => {
|
|
28
|
+
const bits = new Array(FINGERPRINT_BITS).fill(0);
|
|
29
|
+
bits[0] = 1;
|
|
30
|
+
bits[1] = 1;
|
|
31
|
+
bits[7] = 1;
|
|
32
|
+
// lo should be 1 + 2 + 128 = 131
|
|
33
|
+
const { lo } = packBits(bits);
|
|
34
|
+
expect(lo).toBe(BigInt(131));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("computes deterministic commitment", async () => {
|
|
38
|
+
const c1 = await computeCommitment(testFingerprint, testSalt);
|
|
39
|
+
const c2 = await computeCommitment(testFingerprint, testSalt);
|
|
40
|
+
expect(c1).toBe(c2);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("different salts produce different commitments", async () => {
|
|
44
|
+
const c1 = await computeCommitment(testFingerprint, testSalt);
|
|
45
|
+
const c2 = await computeCommitment(testFingerprint, testSalt + BigInt(1));
|
|
46
|
+
expect(c1).not.toBe(c2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("different fingerprints produce different commitments", async () => {
|
|
50
|
+
const fp2 = [...testFingerprint];
|
|
51
|
+
fp2[0] = fp2[0] === 1 ? 0 : 1;
|
|
52
|
+
const c1 = await computeCommitment(testFingerprint, testSalt);
|
|
53
|
+
const c2 = await computeCommitment(fp2, testSalt);
|
|
54
|
+
expect(c1).not.toBe(c2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("generates salt within BN254 scalar field", () => {
|
|
58
|
+
const salt = generateSalt();
|
|
59
|
+
expect(salt).toBeGreaterThan(BigInt(0));
|
|
60
|
+
expect(salt).toBeLessThan(
|
|
61
|
+
BigInt(
|
|
62
|
+
"21888242871839275222246405745257275088548364400416034343698204186575808495617"
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("converts bigint to 32-byte big-endian", () => {
|
|
68
|
+
const bytes = bigintToBytes32(BigInt(256));
|
|
69
|
+
expect(bytes[30]).toBe(1);
|
|
70
|
+
expect(bytes[31]).toBe(0);
|
|
71
|
+
expect(bytes.length).toBe(32);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("generates complete TBH", async () => {
|
|
75
|
+
const tbh = await generateTBH(testFingerprint);
|
|
76
|
+
expect(tbh.fingerprint).toEqual(testFingerprint);
|
|
77
|
+
expect(tbh.salt).toBeGreaterThan(BigInt(0));
|
|
78
|
+
expect(tbh.commitment).toBeGreaterThan(BigInt(0));
|
|
79
|
+
expect(tbh.commitmentBytes.length).toBe(32);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { serializeProof, toBigEndian32 } from "../src/proof/serializer";
|
|
3
|
+
import { TOTAL_PROOF_SIZE, NUM_PUBLIC_INPUTS, BN254_BASE_FIELD } from "../src/config";
|
|
4
|
+
|
|
5
|
+
// Mock proof matching snarkjs output format
|
|
6
|
+
const mockProof = {
|
|
7
|
+
pi_a: [
|
|
8
|
+
"12345678901234567890123456789012345678901234567890",
|
|
9
|
+
"98765432109876543210987654321098765432109876543210",
|
|
10
|
+
"1",
|
|
11
|
+
],
|
|
12
|
+
pi_b: [
|
|
13
|
+
[
|
|
14
|
+
"11111111111111111111111111111111111111111111111111",
|
|
15
|
+
"22222222222222222222222222222222222222222222222222",
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
"33333333333333333333333333333333333333333333333333",
|
|
19
|
+
"44444444444444444444444444444444444444444444444444",
|
|
20
|
+
],
|
|
21
|
+
],
|
|
22
|
+
pi_c: [
|
|
23
|
+
"55555555555555555555555555555555555555555555555555",
|
|
24
|
+
"66666666666666666666666666666666666666666666666666",
|
|
25
|
+
],
|
|
26
|
+
protocol: "groth16",
|
|
27
|
+
curve: "bn128",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const mockPublicSignals = [
|
|
31
|
+
"111111111111111111111",
|
|
32
|
+
"222222222222222222222",
|
|
33
|
+
"30",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
describe("serializer", () => {
|
|
37
|
+
it("toBigEndian32 converts decimal string to 32 bytes", () => {
|
|
38
|
+
const bytes = toBigEndian32("256");
|
|
39
|
+
expect(bytes.length).toBe(32);
|
|
40
|
+
expect(bytes[30]).toBe(1);
|
|
41
|
+
expect(bytes[31]).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("toBigEndian32 handles zero", () => {
|
|
45
|
+
const bytes = toBigEndian32("0");
|
|
46
|
+
expect(bytes.every((b) => b === 0)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("produces 256-byte proof output", () => {
|
|
50
|
+
const { proofBytes } = serializeProof(mockProof as any, mockPublicSignals);
|
|
51
|
+
expect(proofBytes.length).toBe(TOTAL_PROOF_SIZE);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("produces correct number of public inputs", () => {
|
|
55
|
+
const { publicInputs } = serializeProof(mockProof as any, mockPublicSignals);
|
|
56
|
+
expect(publicInputs.length).toBe(NUM_PUBLIC_INPUTS);
|
|
57
|
+
for (const input of publicInputs) {
|
|
58
|
+
expect(input.length).toBe(32);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("negates proof_a y-coordinate", () => {
|
|
63
|
+
const { proofBytes } = serializeProof(mockProof as any, mockPublicSignals);
|
|
64
|
+
|
|
65
|
+
// Extract the y-coordinate from proof_a (bytes 32-63)
|
|
66
|
+
let yFromProof = BigInt(0);
|
|
67
|
+
for (let i = 32; i < 64; i++) {
|
|
68
|
+
yFromProof = (yFromProof << BigInt(8)) + BigInt(proofBytes[i]!);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The original y
|
|
72
|
+
const yOriginal = BigInt(mockProof.pi_a[1]!);
|
|
73
|
+
const yExpected = (BN254_BASE_FIELD - yOriginal) % BN254_BASE_FIELD;
|
|
74
|
+
|
|
75
|
+
expect(yFromProof).toBe(yExpected);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("reverses G2 coordinate ordering in proof_b", () => {
|
|
79
|
+
const { proofBytes } = serializeProof(mockProof as any, mockPublicSignals);
|
|
80
|
+
|
|
81
|
+
// First 32 bytes of proof_b should be pi_b[0][1] (c1), not pi_b[0][0] (c0)
|
|
82
|
+
const expectedFirst = toBigEndian32(mockProof.pi_b[0]![1]!);
|
|
83
|
+
const actualFirst = proofBytes.slice(64, 96);
|
|
84
|
+
expect(Array.from(actualFirst)).toEqual(Array.from(expectedFirst));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { simhash, hammingDistance } from "../src/hashing/simhash";
|
|
3
|
+
import { FINGERPRINT_BITS } from "../src/config";
|
|
4
|
+
|
|
5
|
+
describe("simhash", () => {
|
|
6
|
+
const featureA = Array.from({ length: 100 }, (_, i) => Math.sin(i * 0.1));
|
|
7
|
+
|
|
8
|
+
it("produces a 256-bit binary fingerprint", () => {
|
|
9
|
+
const fp = simhash(featureA);
|
|
10
|
+
expect(fp.length).toBe(FINGERPRINT_BITS);
|
|
11
|
+
for (const bit of fp) {
|
|
12
|
+
expect(bit === 0 || bit === 1).toBe(true);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("is deterministic", () => {
|
|
17
|
+
const fp1 = simhash(featureA);
|
|
18
|
+
const fp2 = simhash(featureA);
|
|
19
|
+
expect(fp1).toEqual(fp2);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("similar vectors produce low Hamming distance", () => {
|
|
23
|
+
// Slightly perturbed version of featureA
|
|
24
|
+
const featureB = featureA.map((v) => v + (Math.random() - 0.5) * 0.01);
|
|
25
|
+
const fpA = simhash(featureA);
|
|
26
|
+
const fpB = simhash(featureB);
|
|
27
|
+
const dist = hammingDistance(fpA, fpB);
|
|
28
|
+
// Small perturbation should produce distance well below 128 (random chance)
|
|
29
|
+
expect(dist).toBeLessThan(64);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("dissimilar vectors produce high Hamming distance", () => {
|
|
33
|
+
const featureC = Array.from({ length: 100 }, (_, i) => -Math.cos(i * 3.7));
|
|
34
|
+
const fpA = simhash(featureA);
|
|
35
|
+
const fpC = simhash(featureC);
|
|
36
|
+
const dist = hammingDistance(fpA, fpC);
|
|
37
|
+
// Different vectors should have distance closer to 128 (random)
|
|
38
|
+
expect(dist).toBeGreaterThan(50);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("empty feature vector returns all zeros", () => {
|
|
42
|
+
const fp = simhash([]);
|
|
43
|
+
expect(fp.length).toBe(FINGERPRINT_BITS);
|
|
44
|
+
expect(fp.every((b) => b === 0)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("hamming distance is symmetric", () => {
|
|
48
|
+
const fpA = simhash(featureA);
|
|
49
|
+
const fpB = simhash(featureA.map((v) => v + 0.5));
|
|
50
|
+
expect(hammingDistance(fpA, fpB)).toBe(hammingDistance(fpB, fpA));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("hamming distance of identical fingerprints is zero", () => {
|
|
54
|
+
const fp = simhash(featureA);
|
|
55
|
+
expect(hammingDistance(fp, fp)).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mean, variance, skewness, kurtosis, condense } from "../src/extraction/statistics";
|
|
3
|
+
|
|
4
|
+
describe("statistics", () => {
|
|
5
|
+
it("computes mean correctly", () => {
|
|
6
|
+
expect(mean([1, 2, 3, 4, 5])).toBe(3);
|
|
7
|
+
expect(mean([10])).toBe(10);
|
|
8
|
+
expect(mean([])).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("computes variance correctly", () => {
|
|
12
|
+
// Sample variance of [2, 4, 4, 4, 5, 5, 7, 9]
|
|
13
|
+
const v = variance([2, 4, 4, 4, 5, 5, 7, 9]);
|
|
14
|
+
expect(v).toBeCloseTo(4.571, 2);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns zero variance for constant array", () => {
|
|
18
|
+
expect(variance([5, 5, 5, 5])).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("computes skewness correctly", () => {
|
|
22
|
+
// Symmetric distribution has ~0 skewness
|
|
23
|
+
const s = skewness([1, 2, 3, 4, 5]);
|
|
24
|
+
expect(Math.abs(s)).toBeLessThan(0.01);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("computes positive skewness for right-skewed data", () => {
|
|
28
|
+
const s = skewness([1, 1, 1, 1, 1, 1, 1, 10]);
|
|
29
|
+
expect(s).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("computes kurtosis for normal-like data", () => {
|
|
33
|
+
// Excess kurtosis near 0 for normal-like data
|
|
34
|
+
const k = kurtosis([2, 3, 4, 4, 5, 5, 5, 6, 6, 7, 7, 8]);
|
|
35
|
+
expect(Math.abs(k)).toBeLessThan(2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("condense returns all four stats", () => {
|
|
39
|
+
const result = condense([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
40
|
+
expect(result.mean).toBeCloseTo(5.5);
|
|
41
|
+
expect(result.variance).toBeGreaterThan(0);
|
|
42
|
+
expect(typeof result.skewness).toBe("number");
|
|
43
|
+
expect(typeof result.kurtosis).toBe("number");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles edge cases", () => {
|
|
47
|
+
expect(variance([1])).toBe(0);
|
|
48
|
+
expect(skewness([1, 2])).toBe(0);
|
|
49
|
+
expect(kurtosis([1, 2, 3])).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2020", "DOM"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"noImplicitReturns": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"outDir": "dist",
|
|
16
|
+
"rootDir": "src",
|
|
17
|
+
"skipLibCheck": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
21
|
+
}
|
package/tsup.config.ts
ADDED