@soteria1/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 +54 -0
- package/dist/confidential/confidential.d.ts +142 -0
- package/dist/confidential/confidential.js +266 -0
- package/dist/confidential/index.d.ts +1 -0
- package/dist/confidential/index.js +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +24 -0
- package/dist/pool/crypto.d.ts +17 -0
- package/dist/pool/crypto.js +79 -0
- package/dist/pool/index.d.ts +4 -0
- package/dist/pool/index.js +4 -0
- package/dist/pool/note.d.ts +22 -0
- package/dist/pool/note.js +66 -0
- package/dist/pool/pdas.d.ts +54 -0
- package/dist/pool/pdas.js +109 -0
- package/dist/pool/prover.d.ts +44 -0
- package/dist/pool/prover.js +73 -0
- package/dist/shielded/index.d.ts +5 -0
- package/dist/shielded/index.js +5 -0
- package/dist/shielded/instruction.d.ts +20 -0
- package/dist/shielded/instruction.js +50 -0
- package/dist/shielded/keypair.d.ts +23 -0
- package/dist/shielded/keypair.js +72 -0
- package/dist/shielded/note.d.ts +22 -0
- package/dist/shielded/note.js +32 -0
- package/dist/shielded/prover.d.ts +46 -0
- package/dist/shielded/prover.js +90 -0
- package/dist/shielded/scan.d.ts +21 -0
- package/dist/shielded/scan.js +28 -0
- package/dist/stealth/index.d.ts +2 -0
- package/dist/stealth/index.js +2 -0
- package/dist/stealth/scanner.d.ts +20 -0
- package/dist/stealth/scanner.js +31 -0
- package/dist/stealth/stealth.d.ts +46 -0
- package/dist/stealth/stealth.js +119 -0
- package/dist/zk/index.d.ts +2 -0
- package/dist/zk/index.js +2 -0
- package/dist/zk/merkle.d.ts +36 -0
- package/dist/zk/merkle.js +92 -0
- package/dist/zk/prover.d.ts +50 -0
- package/dist/zk/prover.js +87 -0
- package/package.json +65 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { buildPoseidon } from "circomlibjs";
|
|
2
|
+
// BN254 scalar field order. nullifier/secret must be reduced mod this.
|
|
3
|
+
const FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
4
|
+
let _poseidon;
|
|
5
|
+
async function poseidon() {
|
|
6
|
+
if (!_poseidon)
|
|
7
|
+
_poseidon = await buildPoseidon();
|
|
8
|
+
return _poseidon;
|
|
9
|
+
}
|
|
10
|
+
function randomFieldElement() {
|
|
11
|
+
// 31 bytes = 248 bits < field order, so a single reduction is unbiased enough.
|
|
12
|
+
const bytes = new Uint8Array(31);
|
|
13
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
14
|
+
let v = 0n;
|
|
15
|
+
for (const b of bytes)
|
|
16
|
+
v = (v << 8n) | BigInt(b);
|
|
17
|
+
return v % FIELD;
|
|
18
|
+
}
|
|
19
|
+
/** Generate a fresh note for a pool. */
|
|
20
|
+
export function randomNote(poolId) {
|
|
21
|
+
return {
|
|
22
|
+
poolId: BigInt(poolId),
|
|
23
|
+
nullifier: randomFieldElement(),
|
|
24
|
+
secret: randomFieldElement(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Note commitment = Poseidon(nullifier, secret) — the tree leaf. */
|
|
28
|
+
export async function commitment(note) {
|
|
29
|
+
const p = await poseidon();
|
|
30
|
+
return BigInt(p.F.toString(p([note.nullifier, note.secret])));
|
|
31
|
+
}
|
|
32
|
+
/** nullifierHash = Poseidon(nullifier) — revealed at withdraw, public. */
|
|
33
|
+
export async function nullifierHash(note) {
|
|
34
|
+
const p = await poseidon();
|
|
35
|
+
return BigInt(p.F.toString(p([note.nullifier])));
|
|
36
|
+
}
|
|
37
|
+
const PREFIX = "soteria-note-v1";
|
|
38
|
+
function toHex(v) {
|
|
39
|
+
return v.toString(16);
|
|
40
|
+
}
|
|
41
|
+
/** Serialize a note to the backup string the user must save to claim funds. */
|
|
42
|
+
export function encodeNote(note) {
|
|
43
|
+
return [PREFIX, note.poolId.toString(), toHex(note.nullifier), toHex(note.secret)].join(":");
|
|
44
|
+
}
|
|
45
|
+
/** Parse a backup string back into a note. */
|
|
46
|
+
export function decodeNote(s) {
|
|
47
|
+
const parts = s.trim().split(":");
|
|
48
|
+
if (parts.length !== 4 || parts[0] !== PREFIX) {
|
|
49
|
+
throw new Error("invalid note backup string");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
poolId: BigInt(parts[1]),
|
|
53
|
+
nullifier: BigInt("0x" + parts[2]),
|
|
54
|
+
secret: BigInt("0x" + parts[3]),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** 32-byte big-endian encoding of a field element (tree leaf / PDA seed). */
|
|
58
|
+
export function toBytes32(v) {
|
|
59
|
+
const out = new Uint8Array(32);
|
|
60
|
+
let x = v;
|
|
61
|
+
for (let i = 31; i >= 0; i--) {
|
|
62
|
+
out[i] = Number(x & 0xffn);
|
|
63
|
+
x >>= 8n;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
import type { FormattedProof } from "./prover.js";
|
|
3
|
+
export declare const POOL_PROGRAM_ID: PublicKey;
|
|
4
|
+
export declare function poolPda(poolId: bigint | number): [PublicKey, number];
|
|
5
|
+
export declare function vaultPda(poolId: bigint | number): [PublicKey, number];
|
|
6
|
+
export declare function commitmentPda(poolId: bigint | number, commitment: bigint): [PublicKey, number];
|
|
7
|
+
export declare function poolNullifierPda(poolId: bigint | number, nullifierHash: bigint): [PublicKey, number];
|
|
8
|
+
/** Accounts for `init_pool(pool_id, denomination)`. */
|
|
9
|
+
export declare function buildInitPoolAccounts(authority: PublicKey, poolId: bigint | number): {
|
|
10
|
+
programId: PublicKey;
|
|
11
|
+
keys: {
|
|
12
|
+
pubkey: PublicKey;
|
|
13
|
+
isSigner: boolean;
|
|
14
|
+
isWritable: boolean;
|
|
15
|
+
}[];
|
|
16
|
+
};
|
|
17
|
+
/** Accounts for `deposit(commitment)`. */
|
|
18
|
+
export declare function buildDepositAccounts(depositor: PublicKey, poolId: bigint | number, commitment: bigint): {
|
|
19
|
+
programId: PublicKey;
|
|
20
|
+
keys: {
|
|
21
|
+
pubkey: PublicKey;
|
|
22
|
+
isSigner: boolean;
|
|
23
|
+
isWritable: boolean;
|
|
24
|
+
}[];
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Full `deposit(commitment)` instruction, signed client-side by the depositor.
|
|
28
|
+
* Build a transaction with this and send it through the wallet — the SDK has no
|
|
29
|
+
* Anchor dependency, so the discriminator is encoded directly.
|
|
30
|
+
*/
|
|
31
|
+
export declare function depositInstruction(depositor: PublicKey, poolId: bigint | number, commitment: bigint): TransactionInstruction;
|
|
32
|
+
/** Accounts for `publish_pool_root` / `set_association_root` (authority only). */
|
|
33
|
+
export declare function buildUpdatePoolRootAccounts(authority: PublicKey, poolId: bigint | number): {
|
|
34
|
+
programId: PublicKey;
|
|
35
|
+
keys: {
|
|
36
|
+
pubkey: PublicKey;
|
|
37
|
+
isSigner: boolean;
|
|
38
|
+
isWritable: boolean;
|
|
39
|
+
}[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Accounts for `withdraw(...)`. `nullifierHash` is publicInputs[0] decoded back
|
|
43
|
+
* to a bigint; pass the same value used to derive the proof.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildWithdrawAccounts(relayer: PublicKey, poolId: bigint | number, recipient: PublicKey, nullifierHash: bigint): {
|
|
46
|
+
programId: PublicKey;
|
|
47
|
+
keys: {
|
|
48
|
+
pubkey: PublicKey;
|
|
49
|
+
isSigner: boolean;
|
|
50
|
+
isWritable: boolean;
|
|
51
|
+
}[];
|
|
52
|
+
};
|
|
53
|
+
/** Convenience: the bigint nullifierHash carried in a formatted proof. */
|
|
54
|
+
export declare function nullifierHashFromProof(proof: FormattedProof): bigint;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
import { toBytes32 } from "./note.js";
|
|
3
|
+
export const POOL_PROGRAM_ID = new PublicKey("9HNLpUVFX61pX759oy1vuMMwQaQaGnK9KgMyhTrDrRGs");
|
|
4
|
+
// sha256("global:deposit")[0..8] — the Anchor instruction discriminator.
|
|
5
|
+
const DEPOSIT_DISCRIMINATOR = new Uint8Array([242, 35, 198, 137, 82, 225, 242, 182]);
|
|
6
|
+
function u64le(n) {
|
|
7
|
+
const b = Buffer.alloc(8);
|
|
8
|
+
b.writeBigUInt64LE(BigInt(n));
|
|
9
|
+
return b;
|
|
10
|
+
}
|
|
11
|
+
export function poolPda(poolId) {
|
|
12
|
+
return PublicKey.findProgramAddressSync([Buffer.from("pool"), u64le(poolId)], POOL_PROGRAM_ID);
|
|
13
|
+
}
|
|
14
|
+
export function vaultPda(poolId) {
|
|
15
|
+
return PublicKey.findProgramAddressSync([Buffer.from("vault"), u64le(poolId)], POOL_PROGRAM_ID);
|
|
16
|
+
}
|
|
17
|
+
export function commitmentPda(poolId, commitment) {
|
|
18
|
+
const [pool] = poolPda(poolId);
|
|
19
|
+
return PublicKey.findProgramAddressSync([Buffer.from("commit"), pool.toBuffer(), Buffer.from(toBytes32(commitment))], POOL_PROGRAM_ID);
|
|
20
|
+
}
|
|
21
|
+
export function poolNullifierPda(poolId, nullifierHash) {
|
|
22
|
+
const [pool] = poolPda(poolId);
|
|
23
|
+
return PublicKey.findProgramAddressSync([Buffer.from("pool_null"), pool.toBuffer(), Buffer.from(toBytes32(nullifierHash))], POOL_PROGRAM_ID);
|
|
24
|
+
}
|
|
25
|
+
const SYS = { pubkey: SystemProgram.programId, isSigner: false, isWritable: false };
|
|
26
|
+
/** Accounts for `init_pool(pool_id, denomination)`. */
|
|
27
|
+
export function buildInitPoolAccounts(authority, poolId) {
|
|
28
|
+
const [pool] = poolPda(poolId);
|
|
29
|
+
const [vault] = vaultPda(poolId);
|
|
30
|
+
return {
|
|
31
|
+
programId: POOL_PROGRAM_ID,
|
|
32
|
+
keys: [
|
|
33
|
+
{ pubkey: authority, isSigner: true, isWritable: true },
|
|
34
|
+
{ pubkey: pool, isSigner: false, isWritable: true },
|
|
35
|
+
{ pubkey: vault, isSigner: false, isWritable: false },
|
|
36
|
+
SYS,
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Accounts for `deposit(commitment)`. */
|
|
41
|
+
export function buildDepositAccounts(depositor, poolId, commitment) {
|
|
42
|
+
const [pool] = poolPda(poolId);
|
|
43
|
+
const [vault] = vaultPda(poolId);
|
|
44
|
+
const [commitmentRecord] = commitmentPda(poolId, commitment);
|
|
45
|
+
return {
|
|
46
|
+
programId: POOL_PROGRAM_ID,
|
|
47
|
+
keys: [
|
|
48
|
+
{ pubkey: depositor, isSigner: true, isWritable: true },
|
|
49
|
+
{ pubkey: pool, isSigner: false, isWritable: true },
|
|
50
|
+
{ pubkey: vault, isSigner: false, isWritable: true },
|
|
51
|
+
{ pubkey: commitmentRecord, isSigner: false, isWritable: true },
|
|
52
|
+
SYS,
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Full `deposit(commitment)` instruction, signed client-side by the depositor.
|
|
58
|
+
* Build a transaction with this and send it through the wallet — the SDK has no
|
|
59
|
+
* Anchor dependency, so the discriminator is encoded directly.
|
|
60
|
+
*/
|
|
61
|
+
export function depositInstruction(depositor, poolId, commitment) {
|
|
62
|
+
const { keys } = buildDepositAccounts(depositor, poolId, commitment);
|
|
63
|
+
const data = new Uint8Array(8 + 32);
|
|
64
|
+
data.set(DEPOSIT_DISCRIMINATOR, 0);
|
|
65
|
+
data.set(toBytes32(commitment), 8);
|
|
66
|
+
return new TransactionInstruction({
|
|
67
|
+
programId: POOL_PROGRAM_ID,
|
|
68
|
+
keys,
|
|
69
|
+
data: Buffer.from(data),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Accounts for `publish_pool_root` / `set_association_root` (authority only). */
|
|
73
|
+
export function buildUpdatePoolRootAccounts(authority, poolId) {
|
|
74
|
+
const [pool] = poolPda(poolId);
|
|
75
|
+
return {
|
|
76
|
+
programId: POOL_PROGRAM_ID,
|
|
77
|
+
keys: [
|
|
78
|
+
{ pubkey: authority, isSigner: true, isWritable: false },
|
|
79
|
+
{ pubkey: pool, isSigner: false, isWritable: true },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Accounts for `withdraw(...)`. `nullifierHash` is publicInputs[0] decoded back
|
|
85
|
+
* to a bigint; pass the same value used to derive the proof.
|
|
86
|
+
*/
|
|
87
|
+
export function buildWithdrawAccounts(relayer, poolId, recipient, nullifierHash) {
|
|
88
|
+
const [pool] = poolPda(poolId);
|
|
89
|
+
const [vault] = vaultPda(poolId);
|
|
90
|
+
const [nullifier] = poolNullifierPda(poolId, nullifierHash);
|
|
91
|
+
return {
|
|
92
|
+
programId: POOL_PROGRAM_ID,
|
|
93
|
+
keys: [
|
|
94
|
+
{ pubkey: relayer, isSigner: true, isWritable: true },
|
|
95
|
+
{ pubkey: pool, isSigner: false, isWritable: false },
|
|
96
|
+
{ pubkey: vault, isSigner: false, isWritable: true },
|
|
97
|
+
{ pubkey: recipient, isSigner: false, isWritable: true },
|
|
98
|
+
{ pubkey: nullifier, isSigner: false, isWritable: true },
|
|
99
|
+
SYS,
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Convenience: the bigint nullifierHash carried in a formatted proof. */
|
|
104
|
+
export function nullifierHashFromProof(proof) {
|
|
105
|
+
let v = 0n;
|
|
106
|
+
for (const byte of proof.publicInputs[0])
|
|
107
|
+
v = (v << 8n) | BigInt(byte);
|
|
108
|
+
return v;
|
|
109
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { PoseidonMerkleTree } from "../zk/merkle.js";
|
|
3
|
+
import { Note } from "./note.js";
|
|
4
|
+
/** Split a 32-byte pubkey into two 128-bit field limbs (hi = first 16 bytes). */
|
|
5
|
+
export declare function recipientLimbs(recipient: PublicKey): {
|
|
6
|
+
hi: bigint;
|
|
7
|
+
lo: bigint;
|
|
8
|
+
};
|
|
9
|
+
export interface WithdrawInputs {
|
|
10
|
+
note: Note;
|
|
11
|
+
/** Pool deposit tree and the note's leaf index in it. */
|
|
12
|
+
depositTree: PoseidonMerkleTree;
|
|
13
|
+
depositLeafIndex: number;
|
|
14
|
+
/** Association-set tree and the note's leaf index in it. For a non-gated pool
|
|
15
|
+
* this is the same tree as `depositTree`. */
|
|
16
|
+
assocTree: PoseidonMerkleTree;
|
|
17
|
+
assocLeafIndex: number;
|
|
18
|
+
recipient: PublicKey;
|
|
19
|
+
fee: bigint;
|
|
20
|
+
}
|
|
21
|
+
export interface FormattedProof {
|
|
22
|
+
proofA: number[];
|
|
23
|
+
proofB: number[];
|
|
24
|
+
proofC: number[];
|
|
25
|
+
publicInputs: number[][];
|
|
26
|
+
}
|
|
27
|
+
export interface RawProof {
|
|
28
|
+
proof: {
|
|
29
|
+
pi_a: string[];
|
|
30
|
+
pi_b: string[][];
|
|
31
|
+
pi_c: string[];
|
|
32
|
+
};
|
|
33
|
+
publicSignals: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Raw snarkjs proof + public signals. Use this for the relay path (the relayer
|
|
37
|
+
* formats bytes server-side). Requires app/public/withdraw.{wasm,zkey}.
|
|
38
|
+
*/
|
|
39
|
+
export declare function proveWithdrawRaw(inputs: WithdrawInputs, wasmPath: string, zkeyPath: string): Promise<RawProof>;
|
|
40
|
+
/**
|
|
41
|
+
* Withdraw proof formatted for the on-chain `withdraw` instruction (direct
|
|
42
|
+
* submission). For the relay path, use proveWithdrawRaw.
|
|
43
|
+
*/
|
|
44
|
+
export declare function proveWithdraw(inputs: WithdrawInputs, wasmPath: string, zkeyPath: string): Promise<FormattedProof>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { groth16 } from "snarkjs";
|
|
2
|
+
// BN254 base field prime (for negating proof.A, per groth16-solana convention).
|
|
3
|
+
const Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583n;
|
|
4
|
+
function toBE32(dec) {
|
|
5
|
+
let v = BigInt(dec);
|
|
6
|
+
const out = new Array(32).fill(0);
|
|
7
|
+
for (let i = 31; i >= 0; i--) {
|
|
8
|
+
out[i] = Number(v & 0xffn);
|
|
9
|
+
v >>= 8n;
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
/** Split a 32-byte pubkey into two 128-bit field limbs (hi = first 16 bytes). */
|
|
14
|
+
export function recipientLimbs(recipient) {
|
|
15
|
+
const b = recipient.toBytes();
|
|
16
|
+
let hi = 0n;
|
|
17
|
+
for (let i = 0; i < 16; i++)
|
|
18
|
+
hi = (hi << 8n) | BigInt(b[i]);
|
|
19
|
+
let lo = 0n;
|
|
20
|
+
for (let i = 16; i < 32; i++)
|
|
21
|
+
lo = (lo << 8n) | BigInt(b[i]);
|
|
22
|
+
return { hi, lo };
|
|
23
|
+
}
|
|
24
|
+
function buildWitness(inputs) {
|
|
25
|
+
const { note, depositTree, depositLeafIndex, assocTree, assocLeafIndex, recipient, fee } = inputs;
|
|
26
|
+
const dep = depositTree.proof(depositLeafIndex);
|
|
27
|
+
const assoc = assocTree.proof(assocLeafIndex);
|
|
28
|
+
const { hi, lo } = recipientLimbs(recipient);
|
|
29
|
+
return {
|
|
30
|
+
nullifier: note.nullifier.toString(),
|
|
31
|
+
secret: note.secret.toString(),
|
|
32
|
+
depositPathElements: dep.pathElements.map((x) => x.toString()),
|
|
33
|
+
depositPathIndices: dep.pathIndices.map((x) => x.toString()),
|
|
34
|
+
assocPathElements: assoc.pathElements.map((x) => x.toString()),
|
|
35
|
+
assocPathIndices: assoc.pathIndices.map((x) => x.toString()),
|
|
36
|
+
depositRoot: depositTree.root().toString(),
|
|
37
|
+
associationRoot: assocTree.root().toString(),
|
|
38
|
+
recipientHi: hi.toString(),
|
|
39
|
+
recipientLo: lo.toString(),
|
|
40
|
+
fee: fee.toString(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Raw snarkjs proof + public signals. Use this for the relay path (the relayer
|
|
45
|
+
* formats bytes server-side). Requires app/public/withdraw.{wasm,zkey}.
|
|
46
|
+
*/
|
|
47
|
+
export async function proveWithdrawRaw(inputs, wasmPath, zkeyPath) {
|
|
48
|
+
const witness = buildWitness(inputs);
|
|
49
|
+
const { proof, publicSignals } = await groth16.fullProve(witness, wasmPath, zkeyPath);
|
|
50
|
+
return { proof, publicSignals };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Withdraw proof formatted for the on-chain `withdraw` instruction (direct
|
|
54
|
+
* submission). For the relay path, use proveWithdrawRaw.
|
|
55
|
+
*/
|
|
56
|
+
export async function proveWithdraw(inputs, wasmPath, zkeyPath) {
|
|
57
|
+
const { proof, publicSignals } = await proveWithdrawRaw(inputs, wasmPath, zkeyPath);
|
|
58
|
+
// proof.A : negate y, then x||y big-endian (groth16-solana wants -A)
|
|
59
|
+
const ax = BigInt(proof.pi_a[0]);
|
|
60
|
+
const ay = (Q - (BigInt(proof.pi_a[1]) % Q)) % Q;
|
|
61
|
+
const proofA = [...toBE32(ax), ...toBE32(ay)];
|
|
62
|
+
// proof.B : G2, swap the c0/c1 ordering snarkjs emits
|
|
63
|
+
const proofB = [
|
|
64
|
+
...toBE32(proof.pi_b[0][1]),
|
|
65
|
+
...toBE32(proof.pi_b[0][0]),
|
|
66
|
+
...toBE32(proof.pi_b[1][1]),
|
|
67
|
+
...toBE32(proof.pi_b[1][0]),
|
|
68
|
+
];
|
|
69
|
+
// proof.C : x||y big-endian
|
|
70
|
+
const proofC = [...toBE32(proof.pi_c[0]), ...toBE32(proof.pi_c[1])];
|
|
71
|
+
const publicInputs = publicSignals.map((s) => toBE32(s));
|
|
72
|
+
return { proofA, proofB, proofC, publicInputs };
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
import type { FormattedTx } from "./prover.js";
|
|
3
|
+
export declare const SHIELDED_PROGRAM_ID: PublicKey;
|
|
4
|
+
export declare function shieldedPda(id: bigint | number): PublicKey;
|
|
5
|
+
export declare function shieldedVaultPda(id: bigint | number): PublicKey;
|
|
6
|
+
export declare function shieldedNullifierPda(id: bigint | number, nullifier: number[]): PublicKey;
|
|
7
|
+
/**
|
|
8
|
+
* Build the `transact` instruction (client-signed — used for deposits, where the
|
|
9
|
+
* signer funds the vault). The SDK has no Anchor dep, so the args are encoded by
|
|
10
|
+
* hand: disc ++ proofA ++ proofB ++ proofC ++ publicInputs(7*32) ++ ext(i64) ++ fee(u64).
|
|
11
|
+
*/
|
|
12
|
+
export declare function transactInstruction(opts: {
|
|
13
|
+
shieldedId: bigint | number;
|
|
14
|
+
signer: PublicKey;
|
|
15
|
+
recipient: PublicKey;
|
|
16
|
+
relayer: PublicKey;
|
|
17
|
+
tx: FormattedTx;
|
|
18
|
+
extAmount: bigint;
|
|
19
|
+
fee: bigint;
|
|
20
|
+
}): TransactionInstruction;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
export const SHIELDED_PROGRAM_ID = new PublicKey("9HNLpUVFX61pX759oy1vuMMwQaQaGnK9KgMyhTrDrRGs");
|
|
3
|
+
// sha256("global:transact")[0..8]
|
|
4
|
+
const TRANSACT_DISCRIMINATOR = new Uint8Array([217, 149, 130, 143, 221, 52, 252, 119]);
|
|
5
|
+
function u64le(n) {
|
|
6
|
+
const b = Buffer.alloc(8);
|
|
7
|
+
b.writeBigUInt64LE(BigInt(n));
|
|
8
|
+
return b;
|
|
9
|
+
}
|
|
10
|
+
export function shieldedPda(id) {
|
|
11
|
+
return PublicKey.findProgramAddressSync([Buffer.from("shielded"), u64le(id)], SHIELDED_PROGRAM_ID)[0];
|
|
12
|
+
}
|
|
13
|
+
export function shieldedVaultPda(id) {
|
|
14
|
+
return PublicKey.findProgramAddressSync([Buffer.from("shvault"), u64le(id)], SHIELDED_PROGRAM_ID)[0];
|
|
15
|
+
}
|
|
16
|
+
export function shieldedNullifierPda(id, nullifier) {
|
|
17
|
+
return PublicKey.findProgramAddressSync([Buffer.from("shnull"), shieldedPda(id).toBuffer(), Buffer.from(nullifier)], SHIELDED_PROGRAM_ID)[0];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the `transact` instruction (client-signed — used for deposits, where the
|
|
21
|
+
* signer funds the vault). The SDK has no Anchor dep, so the args are encoded by
|
|
22
|
+
* hand: disc ++ proofA ++ proofB ++ proofC ++ publicInputs(7*32) ++ ext(i64) ++ fee(u64).
|
|
23
|
+
*/
|
|
24
|
+
export function transactInstruction(opts) {
|
|
25
|
+
const { shieldedId, signer, recipient, relayer, tx, extAmount, fee } = opts;
|
|
26
|
+
const flatPublicInputs = tx.publicInputs.flat(); // 7 * 32 = 224 bytes
|
|
27
|
+
const extLe = Buffer.alloc(8);
|
|
28
|
+
extLe.writeBigInt64LE(extAmount);
|
|
29
|
+
const data = Buffer.concat([
|
|
30
|
+
Buffer.from(TRANSACT_DISCRIMINATOR),
|
|
31
|
+
Buffer.from(tx.proofA),
|
|
32
|
+
Buffer.from(tx.proofB),
|
|
33
|
+
Buffer.from(tx.proofC),
|
|
34
|
+
Buffer.from(flatPublicInputs),
|
|
35
|
+
extLe,
|
|
36
|
+
u64le(fee),
|
|
37
|
+
]);
|
|
38
|
+
const keys = [
|
|
39
|
+
{ pubkey: signer, isSigner: true, isWritable: true },
|
|
40
|
+
{ pubkey: shieldedPda(shieldedId), isSigner: false, isWritable: true },
|
|
41
|
+
{ pubkey: shieldedVaultPda(shieldedId), isSigner: false, isWritable: true },
|
|
42
|
+
{ pubkey: recipient, isSigner: false, isWritable: true },
|
|
43
|
+
{ pubkey: relayer, isSigner: false, isWritable: true },
|
|
44
|
+
// publicInputs[3], [4] = the two nullifiers (byte form), per the circuit order
|
|
45
|
+
{ pubkey: shieldedNullifierPda(shieldedId, tx.publicInputs[3]), isSigner: false, isWritable: true },
|
|
46
|
+
{ pubkey: shieldedNullifierPda(shieldedId, tx.publicInputs[4]), isSigner: false, isWritable: true },
|
|
47
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
48
|
+
];
|
|
49
|
+
return new TransactionInstruction({ programId: SHIELDED_PROGRAM_ID, keys, data });
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
2
|
+
export declare function poseidon(): Promise<any>;
|
|
3
|
+
export declare function H(xs: bigint[]): Promise<bigint>;
|
|
4
|
+
/**
|
|
5
|
+
* A shielded identity. `privateKey`/`publicKey` are the in-circuit spending key
|
|
6
|
+
* (used to derive note nullifiers); `encPriv`/`encPub` are an X25519 pair used
|
|
7
|
+
* to encrypt note secrets to the owner. Both are derived from one wallet
|
|
8
|
+
* signature, so the whole identity is recoverable and nothing is stored.
|
|
9
|
+
*/
|
|
10
|
+
export interface ShieldedKeypair {
|
|
11
|
+
privateKey: bigint;
|
|
12
|
+
publicKey: bigint;
|
|
13
|
+
encPriv: Uint8Array;
|
|
14
|
+
encPub: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
/** Derive a shielded identity from a 32+ byte seed (e.g. a wallet signature). */
|
|
17
|
+
export declare function deriveShieldedKeypair(seed: Uint8Array): Promise<ShieldedKeypair>;
|
|
18
|
+
/** Shareable shielded address = base64url(publicKey || encPub). */
|
|
19
|
+
export declare function encodeShieldedAddress(kp: ShieldedKeypair): string;
|
|
20
|
+
export declare function decodeShieldedAddress(s: string): {
|
|
21
|
+
publicKey: bigint;
|
|
22
|
+
encPub: Uint8Array;
|
|
23
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { buildPoseidon } from "circomlibjs";
|
|
2
|
+
import { x25519 } from "@noble/curves/ed25519";
|
|
3
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
4
|
+
// BN254 scalar field.
|
|
5
|
+
export const FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
6
|
+
let _poseidon;
|
|
7
|
+
export async function poseidon() {
|
|
8
|
+
if (!_poseidon)
|
|
9
|
+
_poseidon = await buildPoseidon();
|
|
10
|
+
return _poseidon;
|
|
11
|
+
}
|
|
12
|
+
export async function H(xs) {
|
|
13
|
+
const p = await poseidon();
|
|
14
|
+
return BigInt(p.F.toString(p(xs)));
|
|
15
|
+
}
|
|
16
|
+
const enc = new TextEncoder();
|
|
17
|
+
function concat(a, b) {
|
|
18
|
+
const out = new Uint8Array(a.length + b.length);
|
|
19
|
+
out.set(a, 0);
|
|
20
|
+
out.set(b, a.length);
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function bytesToField(b) {
|
|
24
|
+
let v = 0n;
|
|
25
|
+
for (const x of b)
|
|
26
|
+
v = (v << 8n) | BigInt(x);
|
|
27
|
+
return v % FIELD;
|
|
28
|
+
}
|
|
29
|
+
function toBytes32(v) {
|
|
30
|
+
const out = new Uint8Array(32);
|
|
31
|
+
let x = v;
|
|
32
|
+
for (let i = 31; i >= 0; i--) {
|
|
33
|
+
out[i] = Number(x & 0xffn);
|
|
34
|
+
x >>= 8n;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function b64url(bytes) {
|
|
39
|
+
let s = "";
|
|
40
|
+
for (const x of bytes)
|
|
41
|
+
s += String.fromCharCode(x);
|
|
42
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
43
|
+
}
|
|
44
|
+
function unb64url(s) {
|
|
45
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
46
|
+
const bin = atob(b64 + "=".repeat((4 - (b64.length % 4)) % 4));
|
|
47
|
+
const out = new Uint8Array(bin.length);
|
|
48
|
+
for (let i = 0; i < bin.length; i++)
|
|
49
|
+
out[i] = bin.charCodeAt(i);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/** Derive a shielded identity from a 32+ byte seed (e.g. a wallet signature). */
|
|
53
|
+
export async function deriveShieldedKeypair(seed) {
|
|
54
|
+
const privateKey = bytesToField(sha256(concat(seed, enc.encode("soteria-shielded-spend"))));
|
|
55
|
+
const encPriv = sha256(concat(seed, enc.encode("soteria-shielded-enc")));
|
|
56
|
+
return {
|
|
57
|
+
privateKey,
|
|
58
|
+
publicKey: await H([privateKey]),
|
|
59
|
+
encPriv,
|
|
60
|
+
encPub: x25519.getPublicKey(encPriv),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Shareable shielded address = base64url(publicKey || encPub). */
|
|
64
|
+
export function encodeShieldedAddress(kp) {
|
|
65
|
+
return b64url(concat(toBytes32(kp.publicKey), kp.encPub));
|
|
66
|
+
}
|
|
67
|
+
export function decodeShieldedAddress(s) {
|
|
68
|
+
const bytes = unb64url(s.trim());
|
|
69
|
+
if (bytes.length !== 64)
|
|
70
|
+
throw new Error("invalid shielded address");
|
|
71
|
+
return { publicKey: bytesToField(bytes.slice(0, 32)), encPub: bytes.slice(32, 64) };
|
|
72
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A shielded UTXO. `amount` and `blinding` are secret; `pubkey` is the owner's
|
|
3
|
+
* shielded publicKey. commitment = Poseidon(amount, pubkey, blinding).
|
|
4
|
+
*/
|
|
5
|
+
export interface Note {
|
|
6
|
+
amount: bigint;
|
|
7
|
+
pubkey: bigint;
|
|
8
|
+
blinding: bigint;
|
|
9
|
+
}
|
|
10
|
+
export declare function randomBlinding(): bigint;
|
|
11
|
+
export declare function newNote(amount: bigint, pubkey: bigint): Note;
|
|
12
|
+
export declare const commitment: (n: Note) => Promise<bigint>;
|
|
13
|
+
/** nullifier = Poseidon(commitment, Poseidon(privateKey, commitment)). Needs the
|
|
14
|
+
* owner's spending key, so only the owner can spend the note. */
|
|
15
|
+
export declare function nullifier(n: Note, privateKey: bigint): Promise<bigint>;
|
|
16
|
+
/** Encrypt a note's secret (amount, blinding) to a recipient's X25519 enc key. */
|
|
17
|
+
export declare function encryptNoteSecret(n: Note, recipientEncPub: Uint8Array): Promise<string>;
|
|
18
|
+
/** Decrypt a note secret with the owner's X25519 enc key. Throws if not ours. */
|
|
19
|
+
export declare function decryptNoteSecret(blob: string, encPriv: Uint8Array): Promise<{
|
|
20
|
+
amount: bigint;
|
|
21
|
+
blinding: bigint;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { H, FIELD } from "./keypair.js";
|
|
2
|
+
import { encryptNote as eciesEncrypt, decryptNote as eciesDecrypt } from "../pool/crypto.js";
|
|
3
|
+
export function randomBlinding() {
|
|
4
|
+
const b = new Uint8Array(31);
|
|
5
|
+
globalThis.crypto.getRandomValues(b);
|
|
6
|
+
let v = 0n;
|
|
7
|
+
for (const x of b)
|
|
8
|
+
v = (v << 8n) | BigInt(x);
|
|
9
|
+
return v % FIELD;
|
|
10
|
+
}
|
|
11
|
+
export function newNote(amount, pubkey) {
|
|
12
|
+
return { amount, pubkey, blinding: randomBlinding() };
|
|
13
|
+
}
|
|
14
|
+
export const commitment = (n) => H([n.amount, n.pubkey, n.blinding]);
|
|
15
|
+
/** nullifier = Poseidon(commitment, Poseidon(privateKey, commitment)). Needs the
|
|
16
|
+
* owner's spending key, so only the owner can spend the note. */
|
|
17
|
+
export async function nullifier(n, privateKey) {
|
|
18
|
+
const cm = await commitment(n);
|
|
19
|
+
const sig = await H([privateKey, cm]);
|
|
20
|
+
return H([cm, sig]);
|
|
21
|
+
}
|
|
22
|
+
// ── note-secret encryption (so a recipient can find + spend the note) ──
|
|
23
|
+
/** Encrypt a note's secret (amount, blinding) to a recipient's X25519 enc key. */
|
|
24
|
+
export function encryptNoteSecret(n, recipientEncPub) {
|
|
25
|
+
return eciesEncrypt(`${n.amount.toString(16)}:${n.blinding.toString(16)}`, recipientEncPub);
|
|
26
|
+
}
|
|
27
|
+
/** Decrypt a note secret with the owner's X25519 enc key. Throws if not ours. */
|
|
28
|
+
export async function decryptNoteSecret(blob, encPriv) {
|
|
29
|
+
const s = await eciesDecrypt(blob, encPriv);
|
|
30
|
+
const [a, b] = s.split(":");
|
|
31
|
+
return { amount: BigInt("0x" + a), blinding: BigInt("0x" + b) };
|
|
32
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { ShieldedKeypair } from "./keypair.js";
|
|
3
|
+
import { Note } from "./note.js";
|
|
4
|
+
export declare function extDataHash(recipient: PublicKey, relayer: PublicKey, extAmount: bigint, fee: bigint): bigint;
|
|
5
|
+
/** publicAmount = (extAmount - fee) mod p. */
|
|
6
|
+
export declare function publicAmount(extAmount: bigint, fee: bigint): bigint;
|
|
7
|
+
export interface TxInput {
|
|
8
|
+
note: Note;
|
|
9
|
+
pathElements: bigint[];
|
|
10
|
+
pathIndices: number[];
|
|
11
|
+
}
|
|
12
|
+
export interface TxOutput {
|
|
13
|
+
note: Note;
|
|
14
|
+
/** Recipient's X25519 enc key; the note secret is encrypted to it. */
|
|
15
|
+
encPub: Uint8Array;
|
|
16
|
+
}
|
|
17
|
+
export interface BuildTxParams {
|
|
18
|
+
/** Real notes being spent (0–2). Padded to 2 with dummies. */
|
|
19
|
+
inputs: TxInput[];
|
|
20
|
+
/** Outputs (1–2). Padded to 2 with a dummy owned by the spender. */
|
|
21
|
+
outputs: TxOutput[];
|
|
22
|
+
/** Spender — owns every input and any padding dummies. */
|
|
23
|
+
spendKeypair: ShieldedKeypair;
|
|
24
|
+
extAmount: bigint;
|
|
25
|
+
fee: bigint;
|
|
26
|
+
recipient: PublicKey;
|
|
27
|
+
relayer: PublicKey;
|
|
28
|
+
root: bigint;
|
|
29
|
+
wasmPath: string;
|
|
30
|
+
zkeyPath: string;
|
|
31
|
+
}
|
|
32
|
+
export interface FormattedTx {
|
|
33
|
+
proofA: number[];
|
|
34
|
+
proofB: number[];
|
|
35
|
+
proofC: number[];
|
|
36
|
+
publicInputs: number[][];
|
|
37
|
+
nullifiers: bigint[];
|
|
38
|
+
outputCommitments: bigint[];
|
|
39
|
+
/** Encrypted note secrets, aligned to outputCommitments — hand to the operator. */
|
|
40
|
+
encryptedSecrets: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build a hidden-amount transaction proof for the on-chain `transact`
|
|
44
|
+
* instruction. Caller must ensure Σinputs + extAmount == Σoutputs + fee.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildTransaction(p: BuildTxParams): Promise<FormattedTx>;
|