@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/dist/note.js CHANGED
@@ -1,9 +1,19 @@
1
1
  "use strict";
2
2
  /**
3
- * note.ts ECDH encrypted note + view/spend key derivation.
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
- /** Derive a viewing keypair one-way from the spend secret. */
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
- /** Encrypt (amount, amount_blind) to a receiver viewing key with ephemeral k. */
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·G
28
- const [sx] = (0, jubjub_1.mul)(receiverViewPkX, receiverViewPkY, ephScalar); // shared = k·pk_view
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
- /** Decrypt a note with a viewing secret (sk_view). */
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
- const [sx] = (0, jubjub_1.mul)(Rx, Ry, skView); // shared = sk_view·R
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
- /** Decrypt AND verify the note opens the on-chain amount_commit point. */
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 Build Groth16 proofs for the deposit / withdraw / transfer circuits.
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
- /** A compiled circuit's artifacts: a wasm witness-calculator and a proving key. */
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 verify against the on-chain vkey.
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
- /** Deposit a public `amount`, producing a commitment to it. */
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
- /** Withdraw a public `amount`, proving `balance ≥ amount`. */
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
- /** Private transfer of a hidden `amount` to a receiver's viewing key. */
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 Build Groth16 proofs for the deposit / withdraw / transfer circuits.
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 verify against the on-chain vkey.
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
- /** Deposit a public `amount`, producing a commitment to it. */
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
- /** Withdraw a public `amount`, proving `balance ≥ amount`. */
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
- /** Private transfer of a hidden `amount` to a receiver's viewing key. */
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
- const remainder = args.senderBalance - args.amount;
109
- // Homomorphic relation old == new amount holds for amount_blind below.
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
+ }
@@ -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";
@@ -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; } });