@metoncash/sdk-v1 0.2.0 → 0.2.1
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/README.md +31 -8
- package/artifacts/deposit/deposit_final.zkey +0 -0
- package/artifacts/deposit/deposit_js/deposit.wasm +0 -0
- package/artifacts/transfer/transfer_final.zkey +0 -0
- package/artifacts/transfer/transfer_js/transfer.wasm +0 -0
- package/artifacts/withdraw/withdraw_final.zkey +0 -0
- package/artifacts/withdraw/withdraw_js/withdraw.wasm +0 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +21 -2
- package/dist/jubjub.d.ts +110 -6
- package/dist/jubjub.js +131 -9
- package/dist/mimc.d.ts +16 -1
- package/dist/mimc.js +16 -1
- package/dist/note.d.ts +74 -7
- package/dist/note.js +71 -10
- package/dist/prover.d.ts +110 -6
- package/dist/prover.js +138 -10
- package/dist/utils/account.d.ts +21 -0
- package/dist/utils/account.js +27 -0
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +6 -1
- package/dist/utils/keys.d.ts +49 -7
- package/dist/utils/keys.js +57 -7
- package/dist/utils/note.d.ts +32 -9
- package/dist/utils/note.js +23 -6
- package/dist/wrappers/Minter.gen.d.ts +28 -0
- package/dist/wrappers/Minter.gen.js +27 -1
- package/dist/wrappers/Wallet.gen.d.ts +36 -0
- package/dist/wrappers/Wallet.gen.js +44 -2
- package/package.json +1 -1
package/dist/note.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* note.ts
|
|
3
|
+
* note.ts - ECDH encrypted note + view/spend key derivation.
|
|
4
4
|
*
|
|
5
5
|
* Notes are encrypted to the receiver's VIEWING key (`pk_view`). Holding
|
|
6
6
|
* `sk_view` lets the receiver (or a granted auditor) read amounts but not spend.
|
|
7
|
+
*
|
|
8
|
+
* Scheme (mirrors circuits/lib/encrypted_note.circom, which proves the sender
|
|
9
|
+
* formed the note this way):
|
|
10
|
+
* R = k*G (ephemeral key, published)
|
|
11
|
+
* shared = k*pk_view == sk_view*R (ECDH)
|
|
12
|
+
* mask_i = MiMC(DOMAIN_i, key = shared.x)
|
|
13
|
+
* enc_amount = amount + mask_1, enc_blind = amount_blind + mask_2 (mod r)
|
|
14
|
+
* The masks are a one-time pad keyed by shared.x: k MUST be fresh per note.
|
|
15
|
+
* Reusing k toward the same receiver reuses the pad and leaks the difference
|
|
16
|
+
* of the two plaintexts. The circuit cannot enforce freshness across proofs.
|
|
7
17
|
*/
|
|
8
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
19
|
exports.deriveViewKey = deriveViewKey;
|
|
@@ -15,17 +25,39 @@ const mimc_1 = require("./mimc");
|
|
|
15
25
|
const DOMAIN_AMOUNT = 1n; // keystream block for amount
|
|
16
26
|
const DOMAIN_BLIND = 2n; // keystream block for amount_blind
|
|
17
27
|
const DOMAIN_VIEW = 7n; // viewing-key derivation tag
|
|
18
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Derive a viewing keypair one-way from the spend secret:
|
|
30
|
+
* sk_view = MiMC(DOMAIN_VIEW = 7, key = spendSk) mod L, pk_view = sk_view*G.
|
|
31
|
+
*
|
|
32
|
+
* One-way, so `skView` can be handed to an auditor without exposing `spendSk`.
|
|
33
|
+
* Deterministic: the same spend secret always yields the same view key, which is
|
|
34
|
+
* what makes the (immutable) on-chain `Register` recoverable from the seed.
|
|
35
|
+
* @param spendSk root secret (any integer; reduced mod r before hashing)
|
|
36
|
+
* @returns skView in [1, L) (0 is mapped to 1 so pkView is never the identity)
|
|
37
|
+
* and pkView, always a prime-order-subgroup point
|
|
38
|
+
*/
|
|
19
39
|
function deriveViewKey(spendSk) {
|
|
20
40
|
let skView = (0, jubjub_1.mod)((0, mimc_1.mimc5perm)(DOMAIN_VIEW, (0, jubjub_1.mod)(spendSk)), jubjub_1.L);
|
|
21
41
|
if (skView === 0n)
|
|
22
42
|
skView = 1n;
|
|
23
43
|
return { skView, pkView: (0, jubjub_1.mul)(jubjub_1.Gx, jubjub_1.Gy, skView) };
|
|
24
44
|
}
|
|
25
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Encrypt (amount, amount_blind) to a receiver viewing key with ephemeral k.
|
|
47
|
+
*
|
|
48
|
+
* Performs NO validation: the receiver key is not checked for subgroup
|
|
49
|
+
* membership and k is not checked against 0 mod L. Use `MetonSDK.transfer()`
|
|
50
|
+
* or `sealNote()` for the validated path; the transfer circuit rejects a
|
|
51
|
+
* low-order key or an identity R anyway, but only after witness generation.
|
|
52
|
+
* @param amount token amount (64-bit in the circuit)
|
|
53
|
+
* @param amountBlind blinding of the amount commitment (252-bit scalar)
|
|
54
|
+
* @param receiverViewPkX,receiverViewPkY the recipient's pk_view (subgroup point)
|
|
55
|
+
* @param ephScalar k - MUST be fresh, uniformly random per note (see file header)
|
|
56
|
+
* @returns the public note; k itself is a secret the caller may keep or discard
|
|
57
|
+
*/
|
|
26
58
|
function encryptNote(amount, amountBlind, receiverViewPkX, receiverViewPkY, ephScalar) {
|
|
27
|
-
const [Rx, Ry] = (0, jubjub_1.mul)(jubjub_1.Gx, jubjub_1.Gy, ephScalar); // R = k
|
|
28
|
-
const [sx] = (0, jubjub_1.mul)(receiverViewPkX, receiverViewPkY, ephScalar); // shared = k
|
|
59
|
+
const [Rx, Ry] = (0, jubjub_1.mul)(jubjub_1.Gx, jubjub_1.Gy, ephScalar); // R = k*G
|
|
60
|
+
const [sx] = (0, jubjub_1.mul)(receiverViewPkX, receiverViewPkY, ephScalar); // shared = k*pk_view
|
|
29
61
|
const maskAmount = (0, mimc_1.mimc5perm)(DOMAIN_AMOUNT, sx);
|
|
30
62
|
const maskBlind = (0, mimc_1.mimc5perm)(DOMAIN_BLIND, sx);
|
|
31
63
|
return {
|
|
@@ -33,13 +65,32 @@ function encryptNote(amount, amountBlind, receiverViewPkX, receiverViewPkY, ephS
|
|
|
33
65
|
Ry,
|
|
34
66
|
enc_amount: (0, jubjub_1.mod)(amount + maskAmount),
|
|
35
67
|
enc_blind: (0, jubjub_1.mod)(amountBlind + maskBlind),
|
|
36
|
-
eph_scalar: ephScalar,
|
|
37
|
-
shared_x: sx,
|
|
38
68
|
};
|
|
39
69
|
}
|
|
40
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Decrypt a note with a viewing secret (sk_view).
|
|
72
|
+
*
|
|
73
|
+
* R is validated first: it must be a non-identity point of the prime-order
|
|
74
|
+
* subgroup. An honest note always is (the circuit proves R = k*G, k != 0 mod L);
|
|
75
|
+
* anything else is malformed, and multiplying sk_view into a low-order point
|
|
76
|
+
* would leak bits of the viewing key.
|
|
77
|
+
*
|
|
78
|
+
* Decryption cannot fail otherwise: a wrong key or a note addressed to someone
|
|
79
|
+
* else yields garbage, not an error. Use `viewNote` to check the result against
|
|
80
|
+
* the on-chain amount commitment.
|
|
81
|
+
* @param skView the recipient's viewing secret (or an auditor's copy of it)
|
|
82
|
+
* @param Rx,Ry the note's ephemeral key, canonical coordinates in [0, r)
|
|
83
|
+
* @param encAmount,encBlind the masked values from the note
|
|
84
|
+
* @returns amount and amountBlind as field elements; an honest note gives
|
|
85
|
+
* amount < 2^64 and amountBlind < 2^252
|
|
86
|
+
* @throws Error if R is out of range, the identity, off-curve or not in the
|
|
87
|
+
* prime-order subgroup (malformed note; a proven note never is)
|
|
88
|
+
*/
|
|
41
89
|
function decryptNote(skView, Rx, Ry, encAmount, encBlind) {
|
|
42
|
-
|
|
90
|
+
if (Rx < 0n || Rx >= jubjub_1.r || Ry < 0n || Ry >= jubjub_1.r || (0, jubjub_1.isIdentity)(Rx, Ry) || !(0, jubjub_1.isInPrimeSubgroup)(Rx, Ry)) {
|
|
91
|
+
throw new Error("decryptNote: R is not a valid non-identity Jubjub subgroup point");
|
|
92
|
+
}
|
|
93
|
+
const [sx] = (0, jubjub_1.mul)(Rx, Ry, skView); // shared = sk_view*R
|
|
43
94
|
const maskAmount = (0, mimc_1.mimc5perm)(DOMAIN_AMOUNT, sx);
|
|
44
95
|
const maskBlind = (0, mimc_1.mimc5perm)(DOMAIN_BLIND, sx);
|
|
45
96
|
return {
|
|
@@ -47,7 +98,17 @@ function decryptNote(skView, Rx, Ry, encAmount, encBlind) {
|
|
|
47
98
|
amountBlind: (0, jubjub_1.mod)(encBlind + jubjub_1.r - maskBlind),
|
|
48
99
|
};
|
|
49
100
|
}
|
|
50
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Decrypt AND verify the note opens the on-chain amount_commit point.
|
|
103
|
+
*
|
|
104
|
+
* `valid` is true iff commit(amount, amountBlind) equals (amountCommitX,
|
|
105
|
+
* amountCommitY) exactly (canonical coordinates). Since the transfer proof binds
|
|
106
|
+
* the encrypted values to that commitment, `valid == false` means the note was
|
|
107
|
+
* decrypted with the wrong viewing key or paired with the wrong commitment,
|
|
108
|
+
* not that the sender cheated. Callers that credit a balance from a note MUST
|
|
109
|
+
* check `valid`.
|
|
110
|
+
* @throws Error from decryptNote on a malformed R
|
|
111
|
+
*/
|
|
51
112
|
function viewNote(skView, note, amountCommitX, amountCommitY) {
|
|
52
113
|
const { amount, amountBlind } = decryptNote(skView, note.Rx, note.Ry, note.enc_amount, note.enc_blind);
|
|
53
114
|
const [cx, cy] = (0, jubjub_1.commit)(amount, amountBlind);
|
package/dist/prover.d.ts
CHANGED
|
@@ -1,65 +1,137 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* prover.ts
|
|
2
|
+
* prover.ts - Build Groth16 proofs for the deposit / withdraw / transfer circuits.
|
|
3
|
+
*
|
|
4
|
+
* Every method validates its arguments, derives the public inputs (commitments,
|
|
5
|
+
* note) from the caller's secrets, runs snarkjs on the matching circuit and
|
|
6
|
+
* returns the proof together with every secret the caller must persist to be
|
|
7
|
+
* able to spend the resulting commitment later (blinding factors, and for a
|
|
8
|
+
* transfer the note's ephemeral scalar). Losing a blinding factor means losing
|
|
9
|
+
* the ability to open - and therefore spend - the balance behind it.
|
|
10
|
+
*
|
|
11
|
+
* Replay protection: each proof is bound to (accountId, seqNo). `accountId` is
|
|
12
|
+
* the WALLET contract's address hash (see utils/account.ts, NOT the owner's
|
|
13
|
+
* address) and `seqNo` must equal the wallet's current sequence number; the
|
|
14
|
+
* contract rejects anything else, so a proof cannot be replayed or redirected.
|
|
3
15
|
*/
|
|
4
16
|
import { Point } from "./jubjub";
|
|
5
17
|
import { Note } from "./note";
|
|
6
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* A compiled circuit's artifacts: a wasm witness-calculator and a proving key.
|
|
20
|
+
* Either a file path or the file contents in memory (browser bundles).
|
|
21
|
+
*/
|
|
7
22
|
export interface CircuitArtifacts {
|
|
8
23
|
wasm: string | Uint8Array;
|
|
9
24
|
zkey: string | Uint8Array;
|
|
10
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Artifacts for the three circuits. They must come from the same trusted setup
|
|
28
|
+
* as the verification keys embedded in the on-chain verifiers.
|
|
29
|
+
*/
|
|
11
30
|
export interface MetonConfig {
|
|
12
31
|
deposit: CircuitArtifacts;
|
|
13
32
|
withdraw: CircuitArtifacts;
|
|
14
33
|
transfer: CircuitArtifacts;
|
|
15
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* A Groth16 proof as returned by snarkjs. `publicSignals` are decimal strings in
|
|
37
|
+
* the circuit's `public [...]` order; the contract wrappers serialize both.
|
|
38
|
+
*/
|
|
16
39
|
export interface Proof {
|
|
17
40
|
proof: unknown;
|
|
18
41
|
publicSignals: string[];
|
|
19
42
|
}
|
|
43
|
+
/** Inputs for `MetonSDK.deposit`. */
|
|
20
44
|
export interface DepositArgs {
|
|
45
|
+
/** public amount to deposit, in [1, 2^64) */
|
|
21
46
|
amount: bigint;
|
|
47
|
+
/** wallet address hash, see accountIdOf(); values >= r are reduced mod r */
|
|
22
48
|
accountId: bigint;
|
|
49
|
+
/** the wallet's current seqNo, in [0, 2^32) */
|
|
23
50
|
seqNo: bigint;
|
|
51
|
+
/** blinding of the new commitment, in [0, 2^252). Default: random. PERSIST IT. */
|
|
24
52
|
blinding?: bigint;
|
|
25
53
|
}
|
|
54
|
+
/** Inputs for `MetonSDK.withdraw`. */
|
|
26
55
|
export interface WithdrawArgs {
|
|
56
|
+
/** public amount to withdraw, in [1, 2^64) and <= balance */
|
|
27
57
|
amount: bigint;
|
|
58
|
+
/** the hidden balance behind the current commitment, in [0, 2^64) */
|
|
28
59
|
balance: bigint;
|
|
60
|
+
/** blinding of the current commitment (the secret persisted from the last op) */
|
|
29
61
|
oldBlind: bigint;
|
|
62
|
+
/** wallet address hash, see accountIdOf() */
|
|
30
63
|
accountId: bigint;
|
|
64
|
+
/** the wallet's current seqNo, in [0, 2^32) */
|
|
31
65
|
seqNo: bigint;
|
|
66
|
+
/** blinding of the remainder commitment, in [0, 2^252). Default: random. PERSIST IT. */
|
|
32
67
|
newBlind?: bigint;
|
|
33
68
|
}
|
|
69
|
+
/** Inputs for `MetonSDK.transfer`. */
|
|
34
70
|
export interface TransferArgs {
|
|
71
|
+
/** hidden amount to send, in [1, 2^64) and <= senderBalance (0 is rejected) */
|
|
35
72
|
amount: bigint;
|
|
73
|
+
/** the sender's hidden balance behind its current commitment, in [0, 2^64) */
|
|
36
74
|
senderBalance: bigint;
|
|
75
|
+
/** blinding of the sender's current commitment */
|
|
37
76
|
senderOldBlind: bigint;
|
|
77
|
+
/** recipient's viewing public key (pk_view as registered on-chain); must be a
|
|
78
|
+
* non-identity prime-order-subgroup point with canonical coordinates */
|
|
38
79
|
receiverViewPk: Point;
|
|
80
|
+
/** SENDER wallet address hash, see accountIdOf() */
|
|
39
81
|
accountId: bigint;
|
|
82
|
+
/** the sender wallet's current seqNo, in [0, 2^32) */
|
|
40
83
|
seqNo: bigint;
|
|
84
|
+
/** blinding of the sender's remainder commitment. Default: random. PERSIST IT. */
|
|
41
85
|
senderNewBlind?: bigint;
|
|
86
|
+
/** blinding of the amount commitment the receiver gets. Default:
|
|
87
|
+
* senderOldBlind - senderNewBlind (mod L), so that old == new (+) amount holds
|
|
88
|
+
* as points (homomorphic relation, checkable by any client) */
|
|
42
89
|
amountBlind?: bigint;
|
|
90
|
+
/** ephemeral scalar k for the note. Default: random. MUST be fresh per note -
|
|
91
|
+
* reuse toward the same receiver reuses the one-time pad (see note.ts). */
|
|
43
92
|
ephScalar?: bigint;
|
|
44
93
|
}
|
|
94
|
+
/** Output of `MetonSDK.deposit`. */
|
|
45
95
|
export interface DepositResult extends Proof {
|
|
96
|
+
/** commit(amount, blinding) - the wallet's commitment after the deposit */
|
|
46
97
|
commitment: Point;
|
|
98
|
+
/** SECRET: needed to open `commitment` in the next withdraw / transfer */
|
|
47
99
|
blinding: bigint;
|
|
48
100
|
}
|
|
101
|
+
/** Output of `MetonSDK.withdraw`. */
|
|
49
102
|
export interface WithdrawResult extends Proof {
|
|
103
|
+
/** commit(remainder, newBlind) - the wallet's commitment after the withdraw */
|
|
50
104
|
newCommitment: Point;
|
|
105
|
+
/** balance - amount, the hidden balance behind `newCommitment` */
|
|
51
106
|
remainder: bigint;
|
|
107
|
+
/** SECRET: needed to open `newCommitment` next time */
|
|
52
108
|
newBlind: bigint;
|
|
53
109
|
}
|
|
110
|
+
/** Output of `MetonSDK.transfer`. */
|
|
54
111
|
export interface TransferResult extends Proof {
|
|
112
|
+
/** commit(remainder, senderNewBlind) - the sender's commitment after the transfer */
|
|
55
113
|
newSenderCommitment: Point;
|
|
114
|
+
/** commit(amount, amountBlind) - the receiver's wallet adds this point to its own
|
|
115
|
+
* commitment, so the receiver's opening becomes (balance + amount, blind + amountBlind mod L) */
|
|
56
116
|
amountCommitment: Point;
|
|
117
|
+
/** the PUBLIC encrypted note that travels with the transfer */
|
|
57
118
|
note: Note;
|
|
119
|
+
/** senderBalance - amount, the hidden balance behind `newSenderCommitment` */
|
|
58
120
|
remainder: bigint;
|
|
121
|
+
/** SECRET: needed to open `newSenderCommitment` next time */
|
|
59
122
|
senderNewBlind: bigint;
|
|
123
|
+
/** the receiver learns this by decrypting the note; the sender may keep it to
|
|
124
|
+
* audit its own outgoing payment */
|
|
60
125
|
amountBlind: bigint;
|
|
126
|
+
/** SECRET: the ephemeral scalar k. Never publish it (it re-derives the masks);
|
|
127
|
+
* keep it only if you want to re-open the note without the receiver's key. */
|
|
61
128
|
ephScalar: bigint;
|
|
62
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Proof builder for the three Meton circuits. Stateless apart from the artifact
|
|
132
|
+
* config; one instance can be shared across wallets. Construct with explicit
|
|
133
|
+
* artifacts, `fromBuildDir()` or `bundled()`.
|
|
134
|
+
*/
|
|
63
135
|
export declare class MetonSDK {
|
|
64
136
|
private readonly cfg;
|
|
65
137
|
constructor(cfg: MetonConfig);
|
|
@@ -72,14 +144,46 @@ export declare class MetonSDK {
|
|
|
72
144
|
/**
|
|
73
145
|
* Zero-config: use the proving artifacts (wasm + zkey) shipped with this
|
|
74
146
|
* package under `artifacts/` (populated at build time from the repo's `build/`).
|
|
75
|
-
* These are tied to a specific trusted setup
|
|
147
|
+
* These are tied to a specific trusted setup - verify against the on-chain vkey.
|
|
76
148
|
*/
|
|
77
149
|
static bundled(): MetonSDK;
|
|
78
150
|
private prove;
|
|
79
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Deposit a public `amount`, producing a commitment to it.
|
|
153
|
+
*
|
|
154
|
+
* Proves commitment == commit(amount, blinding) for the deposit circuit. The
|
|
155
|
+
* Wallet contract ADDS this point to its stored commitment (edwardsAdd), so
|
|
156
|
+
* the caller's local opening becomes (balance + amount, oldBlind + blinding
|
|
157
|
+
* mod L); a fresh wallet starts from the identity, i.e. (0, 0).
|
|
158
|
+
* @returns the proof, the commitment and the blinding to persist
|
|
159
|
+
* @throws TypeError / RangeError on invalid args (see DepositArgs ranges)
|
|
160
|
+
*/
|
|
80
161
|
deposit(args: DepositArgs): Promise<DepositResult>;
|
|
81
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* Withdraw a public `amount`, proving `balance >= amount`.
|
|
164
|
+
*
|
|
165
|
+
* Proves old == commit(balance, oldBlind), new == commit(balance - amount,
|
|
166
|
+
* newBlind) and that the remainder is a 64-bit value (no underflow). The
|
|
167
|
+
* old commitment is derived here and must equal the wallet's stored one, or
|
|
168
|
+
* the contract rejects the proof.
|
|
169
|
+
* @returns the proof, the new commitment, the remainder and newBlind to persist
|
|
170
|
+
* @throws RangeError if amount > balance or any arg is out of range
|
|
171
|
+
*/
|
|
82
172
|
withdraw(args: WithdrawArgs): Promise<WithdrawResult>;
|
|
83
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* Private transfer of a hidden `amount` to a receiver's viewing key.
|
|
175
|
+
*
|
|
176
|
+
* Proves the three commitments open correctly with 64-bit values (sender old,
|
|
177
|
+
* sender new = remainder, amount), that the note (R, enc_amount, enc_blind)
|
|
178
|
+
* encrypts exactly (amount, amountBlind) to `receiverViewPk` with k, that
|
|
179
|
+
* `receiverViewPk` is a subgroup point (via the cofactor witness computed
|
|
180
|
+
* here) and that R is not the identity. The amount and the blindings stay
|
|
181
|
+
* private; the receiver learns them by decrypting the note.
|
|
182
|
+
* @returns the proof, both new commitments, the public note and the secrets
|
|
183
|
+
* to persist (senderNewBlind; amountBlind and ephScalar optionally)
|
|
184
|
+
* @throws RangeError if amount > senderBalance, ephScalar == 0 mod L,
|
|
185
|
+
* receiverViewPk is the identity / off-curve / low-order / non-canonical,
|
|
186
|
+
* or any arg is out of range
|
|
187
|
+
*/
|
|
84
188
|
transfer(args: TransferArgs): Promise<TransferResult>;
|
|
85
189
|
}
|
package/dist/prover.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* prover.ts
|
|
3
|
+
* prover.ts - Build Groth16 proofs for the deposit / withdraw / transfer circuits.
|
|
4
|
+
*
|
|
5
|
+
* Every method validates its arguments, derives the public inputs (commitments,
|
|
6
|
+
* note) from the caller's secrets, runs snarkjs on the matching circuit and
|
|
7
|
+
* returns the proof together with every secret the caller must persist to be
|
|
8
|
+
* able to spend the resulting commitment later (blinding factors, and for a
|
|
9
|
+
* transfer the note's ephemeral scalar). Losing a blinding factor means losing
|
|
10
|
+
* the ability to open - and therefore spend - the balance behind it.
|
|
11
|
+
*
|
|
12
|
+
* Replay protection: each proof is bound to (accountId, seqNo). `accountId` is
|
|
13
|
+
* the WALLET contract's address hash (see utils/account.ts, NOT the owner's
|
|
14
|
+
* address) and `seqNo` must equal the wallet's current sequence number; the
|
|
15
|
+
* contract rejects anything else, so a proof cannot be replayed or redirected.
|
|
4
16
|
*/
|
|
5
17
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
18
|
if (k2 === undefined) k2 = k;
|
|
@@ -41,6 +53,61 @@ const path = __importStar(require("path"));
|
|
|
41
53
|
const snarkjs = __importStar(require("snarkjs"));
|
|
42
54
|
const jubjub_1 = require("./jubjub");
|
|
43
55
|
const note_1 = require("./note");
|
|
56
|
+
// -- input validation --
|
|
57
|
+
// The circuits reject out-of-range inputs, but only after a full witness
|
|
58
|
+
// computation and with an opaque "Assert Failed"; the contracts reject some
|
|
59
|
+
// later still. Check the caller's inputs up front with a precise message.
|
|
60
|
+
// Every check here mirrors a constraint that exists elsewhere (circuit or
|
|
61
|
+
// contract); none of them is the security boundary.
|
|
62
|
+
const TWO_32 = 1n << 32n;
|
|
63
|
+
const TWO_64 = 1n << 64n;
|
|
64
|
+
const TWO_252 = 1n << 252n;
|
|
65
|
+
// throws TypeError for a non-bigint, RangeError unless lo <= v < hi
|
|
66
|
+
function checkRange(name, v, lo, hi) {
|
|
67
|
+
if (typeof v !== "bigint")
|
|
68
|
+
throw new TypeError(`${name} must be a bigint`);
|
|
69
|
+
if (v < lo || v >= hi)
|
|
70
|
+
throw new RangeError(`${name} must be in [${lo}, ${hi}), got ${v}`);
|
|
71
|
+
}
|
|
72
|
+
/** value fields (amounts, balances) are 64-bit; amounts must be > 0 (contracts and
|
|
73
|
+
* transfer circuit) */
|
|
74
|
+
const checkValue = (name, v, allowZero = false) => checkRange(name, v, allowZero ? 0n : 1n, TWO_64);
|
|
75
|
+
/** blinding factors are 252-bit scalars (Num2Bits_strict(252) in ScalarMulH) */
|
|
76
|
+
const checkBlind = (name, v) => checkRange(name, v, 0n, TWO_252);
|
|
77
|
+
/** public inputs shared by every circuit. accountId is a field element: the raw
|
|
78
|
+
* 256-bit address hash is accepted and reduced mod r (as the contract does).
|
|
79
|
+
* seqNo is a uint32 on-chain. */
|
|
80
|
+
function checkCommon(args) {
|
|
81
|
+
checkRange("accountId", args.accountId, 0n, 1n << 256n);
|
|
82
|
+
checkRange("seqNo", args.seqNo, 0n, TWO_32);
|
|
83
|
+
}
|
|
84
|
+
/** the ephemeral scalar must be a 252-bit scalar that is non-zero mod L (R != identity).
|
|
85
|
+
* k = L itself is 252 bits and non-zero but gives R = O, hence the mod-L test;
|
|
86
|
+
* the circuit enforces the same via NonZero on R.x. */
|
|
87
|
+
function checkEphScalar(k) {
|
|
88
|
+
checkRange("ephScalar", k, 1n, TWO_252);
|
|
89
|
+
if (k % jubjub_1.L === 0n)
|
|
90
|
+
throw new RangeError("ephScalar must not be 0 mod L (it would make R the identity)");
|
|
91
|
+
}
|
|
92
|
+
/** the receiver key must be a non-identity point of the prime-order subgroup, as the
|
|
93
|
+
* circuit requires (AssertInSubgroup + NonZero on pk.x). A low-order key would push
|
|
94
|
+
* the ECDH secret into a tiny subgroup; a non-canonical coordinate would never
|
|
95
|
+
* match the key the receiver registered. */
|
|
96
|
+
function checkReceiverViewPk(pk) {
|
|
97
|
+
const [x, y] = pk;
|
|
98
|
+
checkRange("receiverViewPk.x", x, 0n, jubjub_1.r);
|
|
99
|
+
checkRange("receiverViewPk.y", y, 0n, jubjub_1.r);
|
|
100
|
+
if ((0, jubjub_1.isIdentity)(x, y))
|
|
101
|
+
throw new RangeError("receiverViewPk must not be the identity");
|
|
102
|
+
if (!(0, jubjub_1.isInPrimeSubgroup)(x, y)) {
|
|
103
|
+
throw new RangeError("receiverViewPk is not a point of the Jubjub prime-order subgroup (off-curve or low/mixed order)");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Proof builder for the three Meton circuits. Stateless apart from the artifact
|
|
108
|
+
* config; one instance can be shared across wallets. Construct with explicit
|
|
109
|
+
* artifacts, `fromBuildDir()` or `bundled()`.
|
|
110
|
+
*/
|
|
44
111
|
class MetonSDK {
|
|
45
112
|
constructor(cfg) {
|
|
46
113
|
this.cfg = cfg;
|
|
@@ -60,31 +127,61 @@ class MetonSDK {
|
|
|
60
127
|
/**
|
|
61
128
|
* Zero-config: use the proving artifacts (wasm + zkey) shipped with this
|
|
62
129
|
* package under `artifacts/` (populated at build time from the repo's `build/`).
|
|
63
|
-
* These are tied to a specific trusted setup
|
|
130
|
+
* These are tied to a specific trusted setup - verify against the on-chain vkey.
|
|
64
131
|
*/
|
|
65
132
|
static bundled() {
|
|
66
133
|
return MetonSDK.fromBuildDir(path.join(__dirname, "..", "artifacts"));
|
|
67
134
|
}
|
|
135
|
+
// witness generation + Groth16 proving in one step; input keys are the
|
|
136
|
+
// circuit's signal names, values decimal strings (snarkjs convention)
|
|
68
137
|
prove(art, input) {
|
|
69
138
|
return snarkjs.groth16.fullProve(input, art.wasm, art.zkey);
|
|
70
139
|
}
|
|
71
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Deposit a public `amount`, producing a commitment to it.
|
|
142
|
+
*
|
|
143
|
+
* Proves commitment == commit(amount, blinding) for the deposit circuit. The
|
|
144
|
+
* Wallet contract ADDS this point to its stored commitment (edwardsAdd), so
|
|
145
|
+
* the caller's local opening becomes (balance + amount, oldBlind + blinding
|
|
146
|
+
* mod L); a fresh wallet starts from the identity, i.e. (0, 0).
|
|
147
|
+
* @returns the proof, the commitment and the blinding to persist
|
|
148
|
+
* @throws TypeError / RangeError on invalid args (see DepositArgs ranges)
|
|
149
|
+
*/
|
|
72
150
|
async deposit(args) {
|
|
73
151
|
const blinding = args.blinding ?? (0, jubjub_1.randomScalar)(252);
|
|
152
|
+
checkValue("amount", args.amount);
|
|
153
|
+
checkBlind("blinding", blinding);
|
|
154
|
+
checkCommon(args);
|
|
74
155
|
const [cx, cy] = (0, jubjub_1.commit)(args.amount, blinding);
|
|
75
156
|
const { proof, publicSignals } = await this.prove(this.cfg.deposit, {
|
|
76
157
|
amount: args.amount.toString(),
|
|
77
158
|
commitment_x: cx.toString(),
|
|
78
159
|
commitment_y: cy.toString(),
|
|
79
160
|
seqNo: args.seqNo.toString(),
|
|
80
|
-
account_id: args.accountId.toString(),
|
|
161
|
+
account_id: (0, jubjub_1.mod)(args.accountId).toString(),
|
|
81
162
|
blinding: blinding.toString(),
|
|
82
163
|
});
|
|
83
164
|
return { proof, publicSignals, commitment: [cx, cy], blinding };
|
|
84
165
|
}
|
|
85
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Withdraw a public `amount`, proving `balance >= amount`.
|
|
168
|
+
*
|
|
169
|
+
* Proves old == commit(balance, oldBlind), new == commit(balance - amount,
|
|
170
|
+
* newBlind) and that the remainder is a 64-bit value (no underflow). The
|
|
171
|
+
* old commitment is derived here and must equal the wallet's stored one, or
|
|
172
|
+
* the contract rejects the proof.
|
|
173
|
+
* @returns the proof, the new commitment, the remainder and newBlind to persist
|
|
174
|
+
* @throws RangeError if amount > balance or any arg is out of range
|
|
175
|
+
*/
|
|
86
176
|
async withdraw(args) {
|
|
87
177
|
const newBlind = args.newBlind ?? (0, jubjub_1.randomScalar)(252);
|
|
178
|
+
checkValue("amount", args.amount);
|
|
179
|
+
checkValue("balance", args.balance, true);
|
|
180
|
+
if (args.amount > args.balance)
|
|
181
|
+
throw new RangeError(`amount (${args.amount}) exceeds balance (${args.balance})`);
|
|
182
|
+
checkBlind("oldBlind", args.oldBlind);
|
|
183
|
+
checkBlind("newBlind", newBlind);
|
|
184
|
+
checkCommon(args);
|
|
88
185
|
const remainder = args.balance - args.amount;
|
|
89
186
|
const [ox, oy] = (0, jubjub_1.commit)(args.balance, args.oldBlind);
|
|
90
187
|
const [nx, ny] = (0, jubjub_1.commit)(remainder, newBlind);
|
|
@@ -95,25 +192,54 @@ class MetonSDK {
|
|
|
95
192
|
new_commitment_x: nx.toString(),
|
|
96
193
|
new_commitment_y: ny.toString(),
|
|
97
194
|
seqNo: args.seqNo.toString(),
|
|
98
|
-
account_id: args.accountId.toString(),
|
|
195
|
+
account_id: (0, jubjub_1.mod)(args.accountId).toString(),
|
|
99
196
|
balance: args.balance.toString(),
|
|
100
197
|
old_blind: args.oldBlind.toString(),
|
|
101
198
|
new_blind: newBlind.toString(),
|
|
102
199
|
});
|
|
103
200
|
return { proof, publicSignals, newCommitment: [nx, ny], remainder, newBlind };
|
|
104
201
|
}
|
|
105
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Private transfer of a hidden `amount` to a receiver's viewing key.
|
|
204
|
+
*
|
|
205
|
+
* Proves the three commitments open correctly with 64-bit values (sender old,
|
|
206
|
+
* sender new = remainder, amount), that the note (R, enc_amount, enc_blind)
|
|
207
|
+
* encrypts exactly (amount, amountBlind) to `receiverViewPk` with k, that
|
|
208
|
+
* `receiverViewPk` is a subgroup point (via the cofactor witness computed
|
|
209
|
+
* here) and that R is not the identity. The amount and the blindings stay
|
|
210
|
+
* private; the receiver learns them by decrypting the note.
|
|
211
|
+
* @returns the proof, both new commitments, the public note and the secrets
|
|
212
|
+
* to persist (senderNewBlind; amountBlind and ephScalar optionally)
|
|
213
|
+
* @throws RangeError if amount > senderBalance, ephScalar == 0 mod L,
|
|
214
|
+
* receiverViewPk is the identity / off-curve / low-order / non-canonical,
|
|
215
|
+
* or any arg is out of range
|
|
216
|
+
*/
|
|
106
217
|
async transfer(args) {
|
|
107
218
|
const senderNewBlind = args.senderNewBlind ?? (0, jubjub_1.randomScalar)(252);
|
|
108
|
-
|
|
109
|
-
//
|
|
219
|
+
// Keeps old == new (+) amount as points (not required by the contract, but
|
|
220
|
+
// lets a client re-check the homomorphic relation locally).
|
|
110
221
|
const amountBlind = args.amountBlind ?? (0, jubjub_1.mod)(args.senderOldBlind - senderNewBlind + jubjub_1.L, jubjub_1.L);
|
|
111
222
|
const ephScalar = args.ephScalar ?? (0, jubjub_1.randomScalar)(252);
|
|
223
|
+
checkValue("amount", args.amount);
|
|
224
|
+
checkValue("senderBalance", args.senderBalance, true);
|
|
225
|
+
if (args.amount > args.senderBalance) {
|
|
226
|
+
throw new RangeError(`amount (${args.amount}) exceeds senderBalance (${args.senderBalance})`);
|
|
227
|
+
}
|
|
228
|
+
checkBlind("senderOldBlind", args.senderOldBlind);
|
|
229
|
+
checkBlind("senderNewBlind", senderNewBlind);
|
|
230
|
+
checkBlind("amountBlind", amountBlind);
|
|
231
|
+
checkEphScalar(ephScalar);
|
|
232
|
+
checkReceiverViewPk(args.receiverViewPk);
|
|
233
|
+
checkCommon(args);
|
|
234
|
+
const remainder = args.senderBalance - args.amount;
|
|
112
235
|
const [ox, oy] = (0, jubjub_1.commit)(args.senderBalance, args.senderOldBlind);
|
|
113
236
|
const [snx, sny] = (0, jubjub_1.commit)(remainder, senderNewBlind);
|
|
114
237
|
const [amx, amy] = (0, jubjub_1.commit)(args.amount, amountBlind);
|
|
115
238
|
const [pkx, pky] = args.receiverViewPk;
|
|
116
239
|
const note = (0, note_1.encryptNote)(args.amount, amountBlind, pkx, pky, ephScalar);
|
|
240
|
+
// cofactor witness for the circuit's subgroup check: Q = [8^-1 mod L]*pk, so [8]*Q = pk
|
|
241
|
+
// (private input; the circuit re-derives [2]Q, [4]Q, [8]Q itself)
|
|
242
|
+
const [qx, qy] = (0, jubjub_1.cofactorWitness)(pkx, pky).q;
|
|
117
243
|
const { proof, publicSignals } = await this.prove(this.cfg.transfer, {
|
|
118
244
|
old_sender_cx: ox.toString(),
|
|
119
245
|
old_sender_cy: oy.toString(),
|
|
@@ -128,13 +254,15 @@ class MetonSDK {
|
|
|
128
254
|
enc_amount: note.enc_amount.toString(),
|
|
129
255
|
enc_blind: note.enc_blind.toString(),
|
|
130
256
|
seqNo: args.seqNo.toString(),
|
|
131
|
-
account_id: args.accountId.toString(),
|
|
257
|
+
account_id: (0, jubjub_1.mod)(args.accountId).toString(),
|
|
132
258
|
sender_balance: args.senderBalance.toString(),
|
|
133
259
|
sender_old_blind: args.senderOldBlind.toString(),
|
|
134
260
|
sender_new_blind: senderNewBlind.toString(),
|
|
135
261
|
amount: args.amount.toString(),
|
|
136
262
|
amount_blind: amountBlind.toString(),
|
|
137
263
|
eph_scalar: ephScalar.toString(),
|
|
264
|
+
pk_witness_x: qx.toString(),
|
|
265
|
+
pk_witness_y: qy.toString(),
|
|
138
266
|
});
|
|
139
267
|
return {
|
|
140
268
|
proof,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/account.ts - the `account_id` public input.
|
|
3
|
+
*
|
|
4
|
+
* Every proof is bound to the Wallet contract it will be verified by:
|
|
5
|
+
* account_id = hash(cell(wallet address)) mod r, exactly what the contract
|
|
6
|
+
* computes as `(contract.getAddress() as slice).hash() % JUBJUB_R`. Derive it
|
|
7
|
+
* from the wallet's address (Minter.getWalletAddress(owner)), NOT the owner's.
|
|
8
|
+
*/
|
|
9
|
+
import { Address } from "@ton/core";
|
|
10
|
+
/**
|
|
11
|
+
* The `account_id` public input for proofs verified by `walletAddress`.
|
|
12
|
+
*
|
|
13
|
+
* Computed as the 256-bit representation hash of a cell holding the serialized
|
|
14
|
+
* MsgAddressInt, reduced mod r - identical to the contract's slice hash of its
|
|
15
|
+
* own address. Binding the proof to the wallet (together with seqNo) is what
|
|
16
|
+
* stops a valid proof from being replayed against another wallet.
|
|
17
|
+
* @param walletAddress the Wallet CONTRACT address (Minter.getWalletAddress(owner)),
|
|
18
|
+
* not the owner's address - a proof built with the owner's address is rejected
|
|
19
|
+
* @returns a field element in [0, r); pass it as `accountId` to MetonSDK
|
|
20
|
+
*/
|
|
21
|
+
export declare function accountIdOf(walletAddress: Address): bigint;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* utils/account.ts - the `account_id` public input.
|
|
4
|
+
*
|
|
5
|
+
* Every proof is bound to the Wallet contract it will be verified by:
|
|
6
|
+
* account_id = hash(cell(wallet address)) mod r, exactly what the contract
|
|
7
|
+
* computes as `(contract.getAddress() as slice).hash() % JUBJUB_R`. Derive it
|
|
8
|
+
* from the wallet's address (Minter.getWalletAddress(owner)), NOT the owner's.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.accountIdOf = accountIdOf;
|
|
12
|
+
const core_1 = require("@ton/core");
|
|
13
|
+
const jubjub_1 = require("../jubjub");
|
|
14
|
+
/**
|
|
15
|
+
* The `account_id` public input for proofs verified by `walletAddress`.
|
|
16
|
+
*
|
|
17
|
+
* Computed as the 256-bit representation hash of a cell holding the serialized
|
|
18
|
+
* MsgAddressInt, reduced mod r - identical to the contract's slice hash of its
|
|
19
|
+
* own address. Binding the proof to the wallet (together with seqNo) is what
|
|
20
|
+
* stops a valid proof from being replayed against another wallet.
|
|
21
|
+
* @param walletAddress the Wallet CONTRACT address (Minter.getWalletAddress(owner)),
|
|
22
|
+
* not the owner's address - a proof built with the owner's address is rejected
|
|
23
|
+
* @returns a field element in [0, r); pass it as `accountId` to MetonSDK
|
|
24
|
+
*/
|
|
25
|
+
function accountIdOf(walletAddress) {
|
|
26
|
+
return BigInt("0x" + (0, core_1.beginCell)().storeAddress(walletAddress).endCell().hash().toString("hex")) % jubjub_1.r;
|
|
27
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { WalletKeys, generateWalletKeys, walletKeysFromSpend, viewKeyFromSpend } from "./keys";
|
|
1
|
+
export { WalletKeys, generateWalletKeys, walletKeysFromSpend, viewKeyFromSpend, registerWitness } from "./keys";
|
|
2
2
|
export { SealNoteArgs, SealedNote, sealNote, openNote } from "./note";
|
|
3
|
+
export { accountIdOf } from "./account";
|
package/dist/utils/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.openNote = exports.sealNote = exports.viewKeyFromSpend = exports.walletKeysFromSpend = exports.generateWalletKeys = void 0;
|
|
3
|
+
exports.accountIdOf = exports.openNote = exports.sealNote = exports.registerWitness = exports.viewKeyFromSpend = exports.walletKeysFromSpend = exports.generateWalletKeys = void 0;
|
|
4
|
+
// utils/index.ts - high-level helpers on top of jubjub.ts / note.ts:
|
|
5
|
+
// key generation + Register witness, note sealing/opening, account_id derivation.
|
|
4
6
|
var keys_1 = require("./keys");
|
|
5
7
|
Object.defineProperty(exports, "generateWalletKeys", { enumerable: true, get: function () { return keys_1.generateWalletKeys; } });
|
|
6
8
|
Object.defineProperty(exports, "walletKeysFromSpend", { enumerable: true, get: function () { return keys_1.walletKeysFromSpend; } });
|
|
7
9
|
Object.defineProperty(exports, "viewKeyFromSpend", { enumerable: true, get: function () { return keys_1.viewKeyFromSpend; } });
|
|
10
|
+
Object.defineProperty(exports, "registerWitness", { enumerable: true, get: function () { return keys_1.registerWitness; } });
|
|
8
11
|
var note_1 = require("./note");
|
|
9
12
|
Object.defineProperty(exports, "sealNote", { enumerable: true, get: function () { return note_1.sealNote; } });
|
|
10
13
|
Object.defineProperty(exports, "openNote", { enumerable: true, get: function () { return note_1.openNote; } });
|
|
14
|
+
var account_1 = require("./account");
|
|
15
|
+
Object.defineProperty(exports, "accountIdOf", { enumerable: true, get: function () { return account_1.accountIdOf; } });
|