@openzeppelin/compact-contracts 0.3.0-rc.1 → 0.4.0-alpha.2
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 +14 -4
- package/access/AccessControl.compact +14 -14
- package/access/Ownable.compact +16 -10
- package/access/ShieldedAccessControl.compact +26 -2
- package/access/ZOwnablePK.compact +9 -9
- package/crypto/EcdhMask.compact +5 -5
- package/crypto/Ecdsa.compact +111 -0
- package/crypto/Eip712.compact +168 -0
- package/crypto/ElGamal.compact +33 -107
- package/multisig/EcdsaSignerManager.compact +246 -0
- package/multisig/ForwarderPrivate.compact +12 -6
- package/multisig/ForwarderShielded.compact +3 -3
- package/multisig/ForwarderUnshielded.compact +3 -3
- package/multisig/ProposalManager.compact +195 -23
- package/multisig/ShieldedTreasury.compact +29 -2
- package/multisig/ShieldedTreasuryStateless.compact +2 -2
- package/multisig/Signer.compact +28 -80
- package/multisig/UnshieldedTreasury.compact +10 -2
- package/multisig/{presets/forwarder/ForwarderPrivate.compact → examples/ForwarderPrivateExample.compact} +4 -4
- package/multisig/{presets/forwarder/ForwarderShielded.compact → examples/ForwarderShieldedExample.compact} +4 -4
- package/multisig/{presets/forwarder/ForwarderUnshielded.compact → examples/ForwarderUnshieldedExample.compact} +4 -4
- package/multisig/examples/NativeShieldedTokenIssuerExample.compact +143 -0
- package/multisig/examples/ShieldedMultiSigV2Example.compact +77 -0
- package/multisig/presets/NativeShieldedTokenIssuer.compact +542 -0
- package/multisig/presets/ShieldedMultiSigV2.compact +267 -259
- package/package.json +31 -25
- package/security/Allowlist.compact +6 -6
- package/security/Blocklist.compact +6 -6
- package/security/Initializable.compact +9 -5
- package/security/Pausable.compact +11 -7
- package/token/ConfidentialFungibleToken.compact +143 -270
- package/token/FungibleToken.compact +36 -22
- package/token/MultiToken.compact +16 -16
- package/token/NativeShieldedToken.compact +17 -66
- package/token/NativeShieldedTokenCore.compact +26 -48
- package/token/NativeShieldedTokenFamily.compact +17 -67
- package/token/NonFungibleToken.compact +40 -79
- package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +12 -7
- package/token/extensions/NativeShieldedTokenDerivedNonce.compact +3 -3
- package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +8 -8
- package/token/extensions/NativeShieldedTokenPublicSupply.compact +8 -8
- package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +6 -6
- package/utils/EvmAbi.compact +134 -0
- package/utils/Utils.compact +2 -12
- package/multisig/presets/ShieldedMultiSig.compact +0 -227
- package/multisig/presets/ShieldedMultiSigV3.compact +0 -368
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.4.0-alpha.2 (token/ConfidentialFungibleToken.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 0.
|
|
4
|
+
pragma language_version >= 0.26.0;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @module ConfidentialFungibleToken
|
|
@@ -11,17 +11,7 @@ pragma language_version >= 0.23.0;
|
|
|
11
11
|
* follows the same pattern as `FungibleToken`: a witness-derived
|
|
12
12
|
* `accountId = persistentHash(secretKey)`. Encryption uses a separate witness
|
|
13
13
|
* `wit_ConfidentialTokenEK` to derive an ElGamal keypair `(ek, pk)` via
|
|
14
|
-
* `ElGamal_derivePk`, where
|
|
15
|
-
* `pk = g^degradeToTransient(persistentHash([ElGamal domain tag, EK]))`.
|
|
16
|
-
*
|
|
17
|
-
* @notice Sharing key material between the two witnesses is PERMITTED. The
|
|
18
|
-
* account identifier is an untagged hash of the SK and is published as a ledger
|
|
19
|
-
* map key; the encryption scalar is a DOMAIN-SEPARATED hash of the EK. Because
|
|
20
|
-
* the two derivations are tagged differently, a wallet may return the same
|
|
21
|
-
* value from `wit_ConfidentialTokenSK` and `wit_ConfidentialTokenEK`, or derive
|
|
22
|
-
* both from one master secret, without the published identifier revealing the
|
|
23
|
-
* encryption key. The domain separation is what makes that safe. Do not remove
|
|
24
|
-
* it. See `ElGamal.secretToScalar`.
|
|
14
|
+
* `ElGamal_derivePk`, where `pk = g^degradeToTransient(persistentHash(EK))`.
|
|
25
15
|
*
|
|
26
16
|
* @notice Supply. The mint/burn building blocks (`_mint`/`_burn`/`_burnFrom`)
|
|
27
17
|
* live here; total-supply tracking is an optional add-on
|
|
@@ -52,8 +42,6 @@ pragma language_version >= 0.23.0;
|
|
|
52
42
|
*
|
|
53
43
|
* - `wit_ConfidentialTokenEK`: verified by `ElGamal_assertDecryptsTo`,
|
|
54
44
|
* which re-derives `pk` and asserts it equals the on-chain stored pk.
|
|
55
|
-
* `sweep` and `clearMemos` have no plaintext to decrypt, so they assert the
|
|
56
|
-
* same binding directly via `_assertOwnsEncryptionKey`.
|
|
57
45
|
*
|
|
58
46
|
* - `wit_PlaintextBalance(ct)`: verified by `ElGamal_assertDecryptsTo`,
|
|
59
47
|
* which asserts `Dec(ct, EK) == claimedValue`. The ciphertext is passed
|
|
@@ -74,36 +62,13 @@ pragma language_version >= 0.23.0;
|
|
|
74
62
|
* high-entropy, and secret for every invocation.
|
|
75
63
|
*
|
|
76
64
|
* Seed REUSE leaks plaintext differences, and the circuit does NOT prevent
|
|
77
|
-
* it. The debit and
|
|
78
|
-
* randomness from a static domain tag with
|
|
79
|
-
* operations under one seed share randomness
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* advances it with every credit and nothing rolls it back (neither the recipient
|
|
85
|
-
* nor any circuit) so the party whose randomness it protects is the one moving
|
|
86
|
-
* it, and a repeated seed still yields distinct randomness there. It is a
|
|
87
|
-
* backstop, not a substitute for freshness. It covers only credits, and only
|
|
88
|
-
* against reuse of one seed to one recipient.
|
|
89
|
-
*
|
|
90
|
-
* @notice WHOSE confidentiality a seed protects is not always the caller's. On
|
|
91
|
-
* most paths a wallet's seed hygiene guards its own amounts, so the incentive
|
|
92
|
-
* and the exposure sit with the same party. The escrow spend path is the
|
|
93
|
-
* exception: `_spendEscrow` expands every random value it uses from the
|
|
94
|
-
* SPENDER's seed, but all three protect the OWNER. Two re-encrypt their escrow
|
|
95
|
-
* and the third masks their remaining allowance. A spender reusing a seed leaks
|
|
96
|
-
* a counterparty's balance, not their own, and the owner cannot prevent it by
|
|
97
|
-
* rotating their own seed as the leaking call is not theirs.
|
|
98
|
-
*
|
|
99
|
-
* The accidental case is removed by binding both counterparties into the
|
|
100
|
-
* derivation alongside the epoch, which counts per (owner, spender) and so
|
|
101
|
-
* restarts for each pair. That covers the two ways one seed repeats randomness
|
|
102
|
-
* across calls: one spender drawing on two owners, and two wallets sharing a
|
|
103
|
-
* seed drawing on one owner. Nothing stops a spender who deliberately publishes
|
|
104
|
-
* their seed, since any construction must let them compute the pad. Wallet
|
|
105
|
-
* authors supplying a seed for `transferFrom` / `burnFrom` should understand
|
|
106
|
-
* they are protecting the owner they are spending on behalf of.
|
|
65
|
+
* it. The debit and escrow ciphertexts (`_debit`, `approve`, `_spendEscrow`,
|
|
66
|
+
* and the re-approve refund) derive randomness from a static domain tag with
|
|
67
|
+
* no per-operation nonce, so two operations under one seed share randomness
|
|
68
|
+
* and their public ciphertext deltas reveal the amount difference. The credit
|
|
69
|
+
* path folds the recipient's memo-list length in as a nonce (defense in
|
|
70
|
+
* depth), but that counter RESETS on `clearMemos`, so it is an in-epoch
|
|
71
|
+
* backstop only, never a substitute for freshness.
|
|
107
72
|
*
|
|
108
73
|
* Seed generation is a WALLET responsibility and OUT OF SCOPE for this
|
|
109
74
|
* contract, which cannot enforce it. The witness shipped in this repo
|
|
@@ -121,31 +86,13 @@ pragma language_version >= 0.23.0;
|
|
|
121
86
|
* - approve does not disclose the cap (private-cap escrow design).
|
|
122
87
|
* - Counterparty graph (sender_id, recipient_id) is public on every
|
|
123
88
|
* transfer.
|
|
124
|
-
* - Per-(owner, spender) escrow spend count is public and permanent
|
|
125
|
-
* (`_escrowSpendEpochs`); each spend is already visible in tx
|
|
126
|
-
* history, so this adds no fact, only a cheaper way to read it.
|
|
127
|
-
* - Per-account lifetime credit count is public and permanent
|
|
128
|
-
* (`_creditEpochs`); unlike the memo list, it does not reset on
|
|
129
|
-
* `clearMemos`.
|
|
130
89
|
*
|
|
131
90
|
* @dev Per-transfer value bound. Value-bearing circuits check
|
|
132
91
|
* `value <= MAX_TRANSFER_VALUE()`, which is now the `Uint<128>` maximum: the ECDH
|
|
133
92
|
* memo delivers the amount directly (no discrete-log recovery), so the former
|
|
134
93
|
* `2^48` cap that kept wallet-side BSGS tractable is gone. Amounts stay well under
|
|
135
94
|
* the Jubjub scalar-field order, so the homomorphic balance sums (and the
|
|
136
|
-
* `g^value` encoding) do not wrap
|
|
137
|
-
* that a resource bound rather than an invariant: ~2^124 maximal credits into
|
|
138
|
-
* one account reach `l`, past which the stored point encodes the sum MOD `l` and
|
|
139
|
-
* the balance verifies against a smaller number than was deposited.
|
|
140
|
-
*
|
|
141
|
-
* @dev Balance claims are `Uint<248>`, wider than the per-transfer bound. A
|
|
142
|
-
* balance grows by homomorphic addition with no running total to bound, so a
|
|
143
|
-
* long enough run of in-range credits could otherwise push the stored quantity
|
|
144
|
-
* past what any `Uint<128>` claim can state, leaving the balance unspendable.
|
|
145
|
-
* The 120-bit gap between the claim width and the transfer bound puts that at
|
|
146
|
-
* ~2^120 credits. The two widths are therefore coupled: raising
|
|
147
|
-
* `MAX_TRANSFER_VALUE` narrows the gap, and 248 is the widest `Uint` Compact
|
|
148
|
-
* offers, so the claim cannot absorb it.
|
|
95
|
+
* `g^value` encoding) do not wrap. Balances remain Uint<128>.
|
|
149
96
|
*
|
|
150
97
|
* @dev Value delivery via memo channel. Every credit made through `_credit`
|
|
151
98
|
* (mint / transfer / transferFrom) pushes an ECDH one-time-pad memo (see
|
|
@@ -184,7 +131,7 @@ pragma language_version >= 0.23.0;
|
|
|
184
131
|
* Midnight runtime enforcing two curve invariants this module does NOT re-check:
|
|
185
132
|
* every JubjubPoint reaching a curve op is cofactor-cleared into the prime-order
|
|
186
133
|
* subgroup (so off-curve, low-order, or mixed-order points make the circuit
|
|
187
|
-
* unsatisfiable), and
|
|
134
|
+
* unsatisfiable), and a scalar >= the subgroup order is rejected. These are
|
|
188
135
|
* regression-pinned with adversarial inputs in
|
|
189
136
|
* `crypto/test/CurveRuntimeInvariants.test.ts`, which fails loudly if a future
|
|
190
137
|
* runtime stops enforcing them. That is behavioral verification of the shipped
|
|
@@ -227,15 +174,15 @@ pragma language_version >= 0.23.0;
|
|
|
227
174
|
* into the derivations is a possible defense-in-depth hardening; under review, deferred.
|
|
228
175
|
*
|
|
229
176
|
* Block weight. A deploy bundles every exported entry point's on-chain IR into
|
|
230
|
-
* one transaction, and the value circuits are SHA256-heavy (k=16,
|
|
231
|
-
*
|
|
177
|
+
* one transaction, and the value circuits are SHA256-heavy (k=16), so a
|
|
178
|
+
* full-surface deployment is byte-heavy. Whether it fits is a property of the
|
|
232
179
|
* target network, not the contract: the per-transaction block byte budget is a
|
|
233
180
|
* governance-configurable, node-side ledger parameter. On the local ledger-8 dev
|
|
234
181
|
* stack the full surface exceeded it and was rejected (`1010: Transaction would
|
|
235
182
|
* exhaust the block limits`), while a reduced surface (e.g. receive + transfer,
|
|
236
183
|
* dropping allowances and supply accounting) fit. So confirm deployability
|
|
237
184
|
* against the target network; if a deployment is over budget there, the lever is
|
|
238
|
-
* exposing only the value operations it needs, not just trimming getters.
|
|
185
|
+
* exposing only the k=16 value operations it needs, not just trimming getters.
|
|
239
186
|
* A deployed contract can also be upgraded by its Contract Maintenance Authority
|
|
240
187
|
* (CMA), so an over-budget deployment can start with a lean surface and add
|
|
241
188
|
* operations by upgrade instead of fitting the whole surface into one deploy.
|
|
@@ -276,7 +223,6 @@ module ConfidentialFungibleToken {
|
|
|
276
223
|
import CompactStandardLibrary;
|
|
277
224
|
import "../crypto/ElGamal" prefix ElGamal_;
|
|
278
225
|
import "../crypto/EcdhMask" prefix EcdhMask_;
|
|
279
|
-
import "../utils/Utils" prefix Utils_;
|
|
280
226
|
|
|
281
227
|
// ---------------------------------------------------------------------------
|
|
282
228
|
// Types
|
|
@@ -308,65 +254,85 @@ module ConfidentialFungibleToken {
|
|
|
308
254
|
ownerMemo: EcdhMask_Ciphertext;
|
|
309
255
|
}
|
|
310
256
|
|
|
311
|
-
// Hash preimage for the per-spend randomness `_spendEscrow` derives. The epoch
|
|
312
|
-
// counts per (owner, spender) and so restarts for each pair, which is why both
|
|
313
|
-
// parties are bound in alongside it. Drop the owner and one spender's first
|
|
314
|
-
// spend against two owners repeats `rSpender`; drop the spender and two wallets
|
|
315
|
-
// sharing a seed repeat `rOwner` against one owner. Either repeat cancels on
|
|
316
|
-
// subtraction, because the colliding pair is encrypted under one key.
|
|
317
|
-
struct SpendRandomnessPreimage {
|
|
318
|
-
seed: Bytes<32>;
|
|
319
|
-
domain: Bytes<32>;
|
|
320
|
-
owner: Bytes<32>;
|
|
321
|
-
spender: Bytes<32>;
|
|
322
|
-
epoch: Field;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
257
|
// ---------------------------------------------------------------------------
|
|
326
258
|
// Ledger state
|
|
327
259
|
// ---------------------------------------------------------------------------
|
|
328
260
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
261
|
+
/**
|
|
262
|
+
* @description Spendable (confirmed) balances, encrypted under each account's
|
|
263
|
+
* ElGamal key. Debits draw from here. Paired with `_pending`: only the owner
|
|
264
|
+
* moves pending into spendable, via `sweep()`, so a third party spamming dust
|
|
265
|
+
* credits cannot churn a victim's spendable ciphertext and invalidate their
|
|
266
|
+
* in-flight spend proofs. A wallet's total is `balanceOf` + `pendingOf`.
|
|
267
|
+
* @type {Bytes<32>} accountId - The account identifier.
|
|
268
|
+
* @type {ElGamal_Ciphertext} balance - The encrypted spendable balance.
|
|
269
|
+
* @type {Map<accountId, balance>}
|
|
270
|
+
* @type {Map<Bytes<32>, ElGamal_Ciphertext>} _balances
|
|
271
|
+
*/
|
|
335
272
|
export ledger _balances: Map<Bytes<32>, ElGamal_Ciphertext>;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @description Incoming (unconfirmed) balances, encrypted under each account's
|
|
276
|
+
* ElGamal key. Credits land here rather than in `_balances`; the owner moves
|
|
277
|
+
* them across with `sweep()`.
|
|
278
|
+
* @type {Bytes<32>} accountId - The account identifier.
|
|
279
|
+
* @type {ElGamal_Ciphertext} pending - The encrypted pending balance.
|
|
280
|
+
* @type {Map<accountId, pending>}
|
|
281
|
+
* @type {Map<Bytes<32>, ElGamal_Ciphertext>} _pending
|
|
282
|
+
*/
|
|
336
283
|
export ledger _pending: Map<Bytes<32>, ElGamal_Ciphertext>;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @description Registered ElGamal encryption keys, one per account.
|
|
287
|
+
* Registration is one-shot: a second registration for the same account is
|
|
288
|
+
* rejected.
|
|
289
|
+
* @type {Bytes<32>} accountId - The account identifier.
|
|
290
|
+
* @type {JubjubPoint} encryptionKey - The account's ElGamal public key.
|
|
291
|
+
* @type {Map<accountId, encryptionKey>}
|
|
292
|
+
* @type {Map<Bytes<32>, JubjubPoint>} _encryptionKeys
|
|
293
|
+
*/
|
|
337
294
|
export ledger _encryptionKeys: Map<Bytes<32>, JubjubPoint>;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @description Per-account memo inbox. A credit pushes a masked memo the
|
|
298
|
+
* recipient decrypts off-chain; `pushFront` means newest first.
|
|
299
|
+
* @type {Bytes<32>} accountId - The recipient's identifier.
|
|
300
|
+
* @type {List<EcdhMask_Ciphertext>} memos - The recipient's memos.
|
|
301
|
+
* @type {Map<accountId, memos>}
|
|
302
|
+
* @type {Map<Bytes<32>, List<EcdhMask_Ciphertext>>} _memos
|
|
303
|
+
*/
|
|
338
304
|
export ledger _memos: Map<Bytes<32>, List<EcdhMask_Ciphertext>>;
|
|
339
|
-
export ledger _escrow: Map<Bytes<32>, Map<Bytes<32>, EscrowEntry>>;
|
|
340
305
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
export ledger
|
|
352
|
-
|
|
353
|
-
// Per-account credit counter. `_credit` uses it as the nonce that keeps each
|
|
354
|
-
// credit's randomness distinct, so it MUST only ever increase. No circuit
|
|
355
|
-
// resets it. It is kept out of `_memos` for that reason: the memo list is
|
|
356
|
-
// prunable by its owner, and a nonce that could be walked back would let a
|
|
357
|
-
// reused seed repeat an earlier credit's randomness. The cost is that an
|
|
358
|
-
// account's lifetime credit count is now permanently readable from state.
|
|
359
|
-
//
|
|
360
|
-
// `Counter` is chosen for no-reset monotonicity, not commutativity. Its
|
|
361
|
-
// `increment` emits no pinned read, but `_credit` reads the value back to
|
|
362
|
-
// derive the nonce, and that read pins the recipient's key. Credits to one
|
|
363
|
-
// recipient still serialize; see the Concurrency note above.
|
|
364
|
-
export ledger _creditEpochs: Map<Bytes<32>, Counter>;
|
|
306
|
+
/**
|
|
307
|
+
* @description Escrowed allowances, keyed by owner then spender. Membership is
|
|
308
|
+
* checked independently of the entry, so an allowance that was never escrowed
|
|
309
|
+
* is unspendable.
|
|
310
|
+
* @type {Bytes<32>} owner - The account that escrowed the allowance.
|
|
311
|
+
* @type {Bytes<32>} spender - The account allowed to spend it.
|
|
312
|
+
* @type {EscrowEntry} entry - The escrowed allowance.
|
|
313
|
+
* @type {Map<owner, Map<spender, entry>>}
|
|
314
|
+
* @type {Map<Bytes<32>, Map<Bytes<32>, EscrowEntry>>} _escrow
|
|
315
|
+
*/
|
|
316
|
+
export ledger _escrow: Map<Bytes<32>, Map<Bytes<32>, EscrowEntry>>;
|
|
365
317
|
|
|
318
|
+
/**
|
|
319
|
+
* @description Whether `initialize` has run. Guards against re-initialization
|
|
320
|
+
* and against use before initialization.
|
|
321
|
+
*/
|
|
366
322
|
export ledger _isInitialized: Boolean;
|
|
367
323
|
|
|
324
|
+
/**
|
|
325
|
+
* @description The token's human-readable name, set once at construction.
|
|
326
|
+
*/
|
|
368
327
|
export sealed ledger _name: Opaque<"string">;
|
|
328
|
+
/**
|
|
329
|
+
* @description The token's ticker symbol, set once at construction.
|
|
330
|
+
*/
|
|
369
331
|
export sealed ledger _symbol: Opaque<"string">;
|
|
332
|
+
/**
|
|
333
|
+
* @description The number of decimals used to display balances, set once at
|
|
334
|
+
* construction. Purely presentational; no circuit scales by it.
|
|
335
|
+
*/
|
|
370
336
|
export sealed ledger _decimals: Uint<8>;
|
|
371
337
|
|
|
372
338
|
|
|
@@ -383,9 +349,8 @@ module ConfidentialFungibleToken {
|
|
|
383
349
|
* well under the Jubjub scalar-field order, so homomorphic sums do not wrap.
|
|
384
350
|
*
|
|
385
351
|
* @notice This equals the argument type's own maximum, so the per-op check is
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* type — which is what the balance-claim width is sized against.
|
|
352
|
+
* effectively a no-op, retained as a single explicit ceiling / documentation
|
|
353
|
+
* point. Removing the now-vestigial check entirely is a follow-up cleanup.
|
|
389
354
|
*/
|
|
390
355
|
export pure circuit MAX_TRANSFER_VALUE(): Uint<128> {
|
|
391
356
|
return 340282366920938463463374607431768211455; // 2^128 - 1
|
|
@@ -420,7 +385,7 @@ module ConfidentialFungibleToken {
|
|
|
420
385
|
* The ciphertext is passed explicitly so the wallet can route to the right
|
|
421
386
|
* cached plaintext by ciphertext identity. This avoids call-order
|
|
422
387
|
* conventions (which would be brittle to circuit refactors) and lets the
|
|
423
|
-
* wallet maintain a `Map<ElGamal_Ciphertext,
|
|
388
|
+
* wallet maintain a `Map<ElGamal_Ciphertext, Uint128>` lookup.
|
|
424
389
|
*
|
|
425
390
|
* Verified in-circuit by `ElGamal_assertDecryptsTo`: a wrong plaintext
|
|
426
391
|
* fails the assertion `Dec(ct, EK) == claimedValue`.
|
|
@@ -428,7 +393,7 @@ module ConfidentialFungibleToken {
|
|
|
428
393
|
* @param ct - The ciphertext whose plaintext is being requested.
|
|
429
394
|
* @returns The wallet's cached plaintext for `ct`.
|
|
430
395
|
*/
|
|
431
|
-
witness wit_PlaintextBalance(ct: ElGamal_Ciphertext): Uint<
|
|
396
|
+
witness wit_PlaintextBalance(ct: ElGamal_Ciphertext): Uint<128>;
|
|
432
397
|
|
|
433
398
|
/**
|
|
434
399
|
* @witness wit_RandomnessSeed
|
|
@@ -439,11 +404,10 @@ module ConfidentialFungibleToken {
|
|
|
439
404
|
* module header). The derivation is deterministic and public, so an observer
|
|
440
405
|
* who knows the seed can strip a memo's mask and read the delivered amount, and
|
|
441
406
|
* recover balance/escrow amounts from the public ciphertexts. Seed REUSE leaks
|
|
442
|
-
* plaintext differences and the circuit does NOT prevent it
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* fresh, high-entropy, and secret for every invocation.
|
|
407
|
+
* plaintext differences and the circuit does NOT prevent it: only the credit
|
|
408
|
+
* path folds a per-recipient nonce (the memo-list length) in as defense in
|
|
409
|
+
* depth, and even that resets on `clearMemos`. The seed MUST be fresh,
|
|
410
|
+
* high-entropy, and secret for every invocation.
|
|
447
411
|
*/
|
|
448
412
|
witness wit_RandomnessSeed(): Bytes<32>;
|
|
449
413
|
|
|
@@ -485,7 +449,7 @@ module ConfidentialFungibleToken {
|
|
|
485
449
|
* ciphertext if the account is unregistered. The amount is encrypted; only the
|
|
486
450
|
* account (or an authorized viewer) can decrypt it.
|
|
487
451
|
*
|
|
488
|
-
* @
|
|
452
|
+
* @constraints k=10, rows=984
|
|
489
453
|
*
|
|
490
454
|
* Requirements:
|
|
491
455
|
*
|
|
@@ -511,7 +475,7 @@ module ConfidentialFungibleToken {
|
|
|
511
475
|
* balance is `balanceOf`; a wallet's total is the sum of the two. See the
|
|
512
476
|
* dual-balance note on the ledger declarations.
|
|
513
477
|
*
|
|
514
|
-
* @
|
|
478
|
+
* @constraints k=10, rows=981
|
|
515
479
|
*
|
|
516
480
|
* Requirements:
|
|
517
481
|
*
|
|
@@ -542,7 +506,7 @@ module ConfidentialFungibleToken {
|
|
|
542
506
|
* getter grants nothing, the spend path (`transferFrom`/`burnFrom`) gates on
|
|
543
507
|
* `_escrow` membership independently, so a non-existent allowance is unspendable.
|
|
544
508
|
*
|
|
545
|
-
* @
|
|
509
|
+
* @constraints k=10, rows=936
|
|
546
510
|
*
|
|
547
511
|
* @param {Bytes<32>} owner - The account that granted the allowance.
|
|
548
512
|
* @param {Bytes<32>} spender - The approved spender.
|
|
@@ -568,7 +532,7 @@ module ConfidentialFungibleToken {
|
|
|
568
532
|
/**
|
|
569
533
|
* @description Returns whether `account` has registered an encryption key.
|
|
570
534
|
*
|
|
571
|
-
* @
|
|
535
|
+
* @constraints k=9, rows=303
|
|
572
536
|
*
|
|
573
537
|
* @param {Bytes<32>} account - The account to query.
|
|
574
538
|
* @return {Boolean} - True if `account` is registered.
|
|
@@ -586,7 +550,7 @@ module ConfidentialFungibleToken {
|
|
|
586
550
|
* `wit_ConfidentialTokenEK` and initializes their balance to an encryption of
|
|
587
551
|
* zero. Registration is a prerequisite for sending or receiving.
|
|
588
552
|
*
|
|
589
|
-
* @
|
|
553
|
+
* @constraints k=13, rows=6157
|
|
590
554
|
*
|
|
591
555
|
* @notice Returns the caller's `accountId`. A composing contract can gate on
|
|
592
556
|
* the returned value (e.g. assert it is KYC-approved) to restrict who may
|
|
@@ -650,14 +614,14 @@ module ConfidentialFungibleToken {
|
|
|
650
614
|
// (its `mint` increments the total before crediting; its `burn` decrements it
|
|
651
615
|
// after debiting).
|
|
652
616
|
//
|
|
653
|
-
// Randomness: `_credit` folds a per-recipient nonce (
|
|
654
|
-
// its seed-derived randomness as defense in depth.
|
|
655
|
-
//
|
|
656
|
-
// freshness (see the header note); seed reuse still leaks
|
|
657
|
-
// differences
|
|
658
|
-
// multi-party credits distinct
|
|
659
|
-
// The debit side has no such nonce, so seed freshness is the
|
|
660
|
-
// there.
|
|
617
|
+
// Randomness: `_credit` folds a per-recipient nonce (the memo-list length)
|
|
618
|
+
// into its seed-derived randomness as defense in depth. This helps only
|
|
619
|
+
// within a memo epoch: `clearMemos` resets the counter, and it is NOT a
|
|
620
|
+
// substitute for seed freshness (see the header note); seed reuse still leaks
|
|
621
|
+
// credited-amount differences. Within an epoch the nonce keeps a single
|
|
622
|
+
// circuit's multi-party credits distinct (each recipient's count is
|
|
623
|
+
// independent). The debit side has no such nonce, so seed freshness is the
|
|
624
|
+
// only defense there.
|
|
661
625
|
// ---------------------------------------------------------------------------
|
|
662
626
|
|
|
663
627
|
/**
|
|
@@ -713,11 +677,11 @@ module ConfidentialFungibleToken {
|
|
|
713
677
|
* account's balance ciphertext and pushes an ECDH one-time-pad memo that
|
|
714
678
|
* delivers `value` directly to the recipient (no discrete-log recovery, so no
|
|
715
679
|
* `2^48` bound). All per-credit randomness is derived internally from the
|
|
716
|
-
* witness seed and a per-recipient monotonic nonce (
|
|
717
|
-
* keeps randomness distinct
|
|
718
|
-
* one-time pad; see `crypto/EcdhMask`). That nonce
|
|
719
|
-
*
|
|
720
|
-
* module header and the section note above `_debit`).
|
|
680
|
+
* witness seed and a per-recipient monotonic nonce (the memo-list length),
|
|
681
|
+
* which keeps randomness distinct WITHIN a memo epoch (defense in depth for
|
|
682
|
+
* the memo's one-time pad; see `crypto/EcdhMask`). That nonce RESETS on
|
|
683
|
+
* `clearMemos`, so it is an in-epoch backstop only, never a substitute for
|
|
684
|
+
* seed freshness (see the module header and the section note above `_debit`).
|
|
721
685
|
*
|
|
722
686
|
* @warning Does not adjust any supply total. Called outside a supply-accounted
|
|
723
687
|
* flow, `_credit` mints unbacked value (inflation) and breaks the
|
|
@@ -737,25 +701,20 @@ module ConfidentialFungibleToken {
|
|
|
737
701
|
|
|
738
702
|
const recipientPk = _encryptionKeys.lookup(disclose(account));
|
|
739
703
|
|
|
740
|
-
// Ensure the recipient's memo list exists
|
|
704
|
+
// Ensure the recipient's memo list exists; its length is a per-recipient
|
|
705
|
+
// monotonic nonce that makes this credit's randomness unique even if the
|
|
706
|
+
// wallet reuses its seed. Freshness is essential for the memo OTP: a repeated
|
|
707
|
+
// ephemeral to the same recipient would reuse the pad.
|
|
741
708
|
if (!_memos.member(disclose(account))) {
|
|
742
709
|
_memos.insert(disclose(account), default<List<EcdhMask_Ciphertext>>);
|
|
743
710
|
}
|
|
744
|
-
|
|
745
|
-
// Advance the recipient's credit epoch and use it as this credit's nonce.
|
|
746
|
-
// The nonce must come from a source that only increases; anything the
|
|
747
|
-
// recipient can reset would let a reused seed repeat the randomness below.
|
|
748
|
-
if (!_creditEpochs.member(disclose(account))) {
|
|
749
|
-
_creditEpochs.insert(disclose(account), default<Counter>);
|
|
750
|
-
}
|
|
751
|
-
_creditEpochs.lookup(disclose(account)).increment(1);
|
|
752
|
-
const count = _creditEpochs.lookup(disclose(account)) as Field;
|
|
711
|
+
const count = _memos.lookup(disclose(account)).length() as Field;
|
|
753
712
|
const seed = wit_RandomnessSeed();
|
|
754
713
|
|
|
755
714
|
// Randomness must be a VALID Jubjub scalar. `transientHash`'s raw Field output
|
|
756
|
-
// can exceed the Jubjub scalar-field order
|
|
757
|
-
//
|
|
758
|
-
//
|
|
715
|
+
// can exceed the Jubjub scalar-field order, so each scalar is derived via
|
|
716
|
+
// `degradeToTransient(persistentHash(...))`, which truncates into the scalar
|
|
717
|
+
// field. The per-recipient count, folded into a
|
|
759
718
|
// Bytes<32> nonce, keeps each credit's randomness unique even if the seed
|
|
760
719
|
// repeats (essential for the memo OTP). This inlines the tag + expansion into
|
|
761
720
|
// a single hash per scalar. The residual persistentHash cost is poseidon-gated
|
|
@@ -769,10 +728,10 @@ module ConfidentialFungibleToken {
|
|
|
769
728
|
const countHash = persistentHash<Field>(count);
|
|
770
729
|
const rBalance = degradeToTransient(
|
|
771
730
|
persistentHash<Vector<4, Bytes<32>>>([seed, pad(32, "credit_balance"), account, countHash])
|
|
772
|
-
);
|
|
731
|
+
) as JubjubScalar;
|
|
773
732
|
const e = degradeToTransient(
|
|
774
733
|
persistentHash<Vector<4, Bytes<32>>>([seed, pad(32, "credit_memo"), account, countHash])
|
|
775
|
-
);
|
|
734
|
+
) as JubjubScalar;
|
|
776
735
|
|
|
777
736
|
// Credit lands in the recipient's PENDING pool (not spendable), so incoming
|
|
778
737
|
// value never churns their spendable ciphertext; they sweep it when ready.
|
|
@@ -793,7 +752,7 @@ module ConfidentialFungibleToken {
|
|
|
793
752
|
* `extensions/ConfidentialFungibleTokenPublicSupply`) if the deployment tracks
|
|
794
753
|
* `totalSupply`.
|
|
795
754
|
*
|
|
796
|
-
* @
|
|
755
|
+
* @constraints k=15, rows=26925
|
|
797
756
|
*
|
|
798
757
|
* Requirements:
|
|
799
758
|
*
|
|
@@ -816,7 +775,7 @@ module ConfidentialFungibleToken {
|
|
|
816
775
|
* `extensions/ConfidentialFungibleTokenPublicSupply`) if the deployment tracks
|
|
817
776
|
* `totalSupply`.
|
|
818
777
|
*
|
|
819
|
-
* @
|
|
778
|
+
* @constraints k=14, rows=12860
|
|
820
779
|
*
|
|
821
780
|
* Requirements:
|
|
822
781
|
*
|
|
@@ -840,7 +799,7 @@ module ConfidentialFungibleToken {
|
|
|
840
799
|
* credits `to`, and pushes an encrypted memo to `to`. The amount is hidden;
|
|
841
800
|
* the (sender, recipient) pair is public.
|
|
842
801
|
*
|
|
843
|
-
* @
|
|
802
|
+
* @constraints k=16, rows=39561
|
|
844
803
|
*
|
|
845
804
|
* @notice Returns the caller's (sender's) `accountId` for caller-side gating.
|
|
846
805
|
* The sender is derived from the same authentication `_debit` performs, so the
|
|
@@ -878,7 +837,7 @@ module ConfidentialFungibleToken {
|
|
|
878
837
|
* operations (`mint`/`burn`) live in a separate layer outside this conserving
|
|
879
838
|
* surface, and only they may break conservation.
|
|
880
839
|
*
|
|
881
|
-
* @
|
|
840
|
+
* @constraints k=16, rows=39550
|
|
882
841
|
*
|
|
883
842
|
* @notice Like `transfer`, the credit lands in the recipient's pending pool
|
|
884
843
|
* (see the dual-balance note); the caller sweeps their own incoming value.
|
|
@@ -914,13 +873,12 @@ module ConfidentialFungibleToken {
|
|
|
914
873
|
* @description Sweeps the caller's incoming `pending` pool into their
|
|
915
874
|
* `spendable` balance. Credits land in `pending` (see the dual-balance note);
|
|
916
875
|
* this `sweep` is the ONLY path from pending into spendable, and only the owner
|
|
917
|
-
* can invoke it (the
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
* spend proofs (a liveness grief).
|
|
876
|
+
* can invoke it (the account is derived from the caller's witness secret). So a
|
|
877
|
+
* third party spamming credits cannot force a victim's spendable ciphertext to
|
|
878
|
+
* change, which is what would otherwise invalidate the victim's in-flight spend
|
|
879
|
+
* proofs (a liveness grief).
|
|
922
880
|
*
|
|
923
|
-
* @
|
|
881
|
+
* @constraints k=13, rows=2860
|
|
924
882
|
*
|
|
925
883
|
* @notice Purely homomorphic: adds the two ciphertexts (same key) and resets
|
|
926
884
|
* pending to Enc(0). No plaintext is needed; the wallet already learned the
|
|
@@ -930,14 +888,14 @@ module ConfidentialFungibleToken {
|
|
|
930
888
|
*
|
|
931
889
|
* - Contract is initialized.
|
|
932
890
|
* - The caller is registered.
|
|
933
|
-
* - The caller holds the encryption secret registered for their account.
|
|
934
891
|
*
|
|
935
892
|
* @return {Bytes<32>} - The caller's accountId.
|
|
936
893
|
*/
|
|
937
894
|
export circuit sweep(): Bytes<32> {
|
|
938
895
|
assertInitialized();
|
|
939
896
|
const accountId = _computeAccountId();
|
|
940
|
-
|
|
897
|
+
assert(_encryptionKeys.member(disclose(accountId)),
|
|
898
|
+
"ConfidentialFungibleToken: not registered");
|
|
941
899
|
const newSpendable = ElGamal_add(
|
|
942
900
|
_balances.lookup(disclose(accountId)),
|
|
943
901
|
_pending.lookup(disclose(accountId)));
|
|
@@ -953,7 +911,7 @@ module ConfidentialFungibleToken {
|
|
|
953
911
|
* refunds any prior escrow, so allowances replace rather than stack. The cap
|
|
954
912
|
* is not public.
|
|
955
913
|
*
|
|
956
|
-
* @
|
|
914
|
+
* @constraints k=16, rows=40913
|
|
957
915
|
*
|
|
958
916
|
* @notice Returns the caller's (approver's) `accountId` for caller-side gating.
|
|
959
917
|
*
|
|
@@ -1037,7 +995,7 @@ module ConfidentialFungibleToken {
|
|
|
1037
995
|
* caller via `approve`) and credits it to `to`, reducing both escrow copies by
|
|
1038
996
|
* `value`. The amount is hidden.
|
|
1039
997
|
*
|
|
1040
|
-
*
|
|
998
|
+
* @constraints k=16, rows=59844
|
|
1041
999
|
*
|
|
1042
1000
|
* @notice Returns the caller's (spender's) `accountId` for caller-side gating.
|
|
1043
1001
|
*
|
|
@@ -1077,14 +1035,7 @@ module ConfidentialFungibleToken {
|
|
|
1077
1035
|
* @description Clears the caller's memo list, letting a wallet that has folded
|
|
1078
1036
|
* its memos into its local balance cache prune on-chain memo storage.
|
|
1079
1037
|
*
|
|
1080
|
-
*
|
|
1081
|
-
* moment it read the list. The call is rejected unless that still matches, so
|
|
1082
|
-
* a credit landing between the read and this transaction is never silently
|
|
1083
|
-
* discarded. The wallet retries against the memos it has now seen. A wallet
|
|
1084
|
-
* therefore reads its memo list and its epoch together, folds the memos into
|
|
1085
|
-
* its cache, and passes that epoch back here.
|
|
1086
|
-
*
|
|
1087
|
-
* @circuitInfo k=13, rows=7053
|
|
1038
|
+
* @constraints k=13, rows=2308
|
|
1088
1039
|
*
|
|
1089
1040
|
* @warning Clearing memos is destructive and can permanently lock funds. The
|
|
1090
1041
|
* on-chain balance ciphertext is not directly decryptable (discrete-log
|
|
@@ -1092,35 +1043,20 @@ module ConfidentialFungibleToken {
|
|
|
1092
1043
|
* them without a durable off-chain record of the balance loses the ability to
|
|
1093
1044
|
* recover it; and because spending requires proving the balance plaintext, the
|
|
1094
1045
|
* funds become unspendable, not merely undisplayable. Clear only memos already
|
|
1095
|
-
* folded into a durable cache/backup
|
|
1096
|
-
* the caller has not seen; it does not make clearing safe for memos the caller
|
|
1097
|
-
* never folded in.
|
|
1098
|
-
*
|
|
1099
|
-
* @param {Uint<64>} expectedEpoch - The caller's view of its credit epoch.
|
|
1046
|
+
* folded into a durable cache/backup (see §7 of the design doc).
|
|
1100
1047
|
*
|
|
1101
1048
|
* Requirements:
|
|
1102
1049
|
*
|
|
1103
1050
|
* - Contract is initialized.
|
|
1104
|
-
* - The caller is registered.
|
|
1105
|
-
* - The caller holds the encryption secret registered for their account.
|
|
1106
|
-
* - The caller has a memo list.
|
|
1107
|
-
* - `expectedEpoch` equals the caller's current credit epoch.
|
|
1108
1051
|
*
|
|
1109
1052
|
* @return {[]} - Empty tuple.
|
|
1110
1053
|
*/
|
|
1111
|
-
export circuit clearMemos(
|
|
1054
|
+
export circuit clearMemos(): [] {
|
|
1112
1055
|
assertInitialized();
|
|
1113
1056
|
const accountId = _computeAccountId();
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
// concurrent credit invalidates this tx
|
|
1118
|
-
assert(_memos.member(disclose(accountId)),
|
|
1119
|
-
"ConfidentialFungibleToken: no memo list");
|
|
1120
|
-
assert(_creditEpochs.lookup(disclose(accountId)) as Field == expectedEpoch as Field,
|
|
1121
|
-
"ConfidentialFungibleToken: memo list changed");
|
|
1122
|
-
|
|
1123
|
-
_memos.insert(disclose(accountId), default<List<EcdhMask_Ciphertext>>);
|
|
1057
|
+
if (_memos.member(disclose(accountId))) {
|
|
1058
|
+
_memos.insert(disclose(accountId), default<List<EcdhMask_Ciphertext>>);
|
|
1059
|
+
}
|
|
1124
1060
|
}
|
|
1125
1061
|
|
|
1126
1062
|
// ---------------------------------------------------------------------------
|
|
@@ -1149,40 +1085,16 @@ module ConfidentialFungibleToken {
|
|
|
1149
1085
|
return computeAccountId(wit_ConfidentialTokenSK());
|
|
1150
1086
|
}
|
|
1151
1087
|
|
|
1152
|
-
/**
|
|
1153
|
-
* @description Asserts `accountId` is registered and that the caller holds the
|
|
1154
|
-
* encryption secret registered for it, by re-deriving the public key and
|
|
1155
|
-
* comparing it to ledger state.
|
|
1156
|
-
*
|
|
1157
|
-
* @notice Circuits that move or destroy value prove this implicitly, via
|
|
1158
|
-
* `ElGamal_assertDecryptsTo` on a balance. `sweep` and `clearMemos` have no
|
|
1159
|
-
* plaintext to decrypt, so they assert it directly: holding the account secret
|
|
1160
|
-
* alone must not be enough to clear a memo list or merge pending value, since
|
|
1161
|
-
* doing both leaves the holder unable to state a balance they can no longer
|
|
1162
|
-
* learn.
|
|
1163
|
-
*/
|
|
1164
|
-
circuit _assertOwnsEncryptionKey(accountId: Bytes<32>): [] {
|
|
1165
|
-
assert(_encryptionKeys.member(disclose(accountId)),
|
|
1166
|
-
"ConfidentialFungibleToken: not registered");
|
|
1167
|
-
assert(ElGamal_derivePk(wit_ConfidentialTokenEK()) ==
|
|
1168
|
-
_encryptionKeys.lookup(disclose(accountId)),
|
|
1169
|
-
"ConfidentialFungibleToken: wrong encryption key");
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
1088
|
/**
|
|
1173
1089
|
* @description Derives the public `accountId` from a secret key, as
|
|
1174
1090
|
* `persistentHash(sk)`. Pure, so a wallet can compute its own account
|
|
1175
1091
|
* identifier off-chain (no proof) before transacting.
|
|
1176
1092
|
*
|
|
1177
|
-
* @dev Delegates to `Utils.computeAccountId` rather than reimplementing the
|
|
1178
|
-
* hash. The identifier is deliberately GLOBAL. The same key material yields
|
|
1179
|
-
* the same id in every module that derives it.
|
|
1180
|
-
*
|
|
1181
1093
|
* @param {Bytes<32>} sk - The account secret key.
|
|
1182
1094
|
* @return {Bytes<32>} - The derived accountId.
|
|
1183
1095
|
*/
|
|
1184
1096
|
export pure circuit computeAccountId(sk: Bytes<32>): Bytes<32> {
|
|
1185
|
-
return
|
|
1097
|
+
return persistentHash<Vector<1, Bytes<32>>>([sk]);
|
|
1186
1098
|
}
|
|
1187
1099
|
|
|
1188
1100
|
/**
|
|
@@ -1227,35 +1139,15 @@ module ConfidentialFungibleToken {
|
|
|
1227
1139
|
|
|
1228
1140
|
const ek = wit_ConfidentialTokenEK();
|
|
1229
1141
|
const spenderPk = _encryptionKeys.lookup(disclose(spenderId));
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
// than stacks, and `_spendEscrow` only reduces it. Narrowing states that
|
|
1233
|
-
// invariant here instead of leaving it to inspection of those two circuits.
|
|
1234
|
-
// The cast fails closed, so an out-of-range claim aborts rather than wraps.
|
|
1235
|
-
const escrowPlaintext =
|
|
1236
|
-
wit_PlaintextBalance(entry.spenderCt) as Uint<128>;
|
|
1237
|
-
ElGamal_assertDecryptsTo(entry.spenderCt, spenderPk, ek, escrowPlaintext as Uint<248>);
|
|
1142
|
+
const escrowPlaintext = wit_PlaintextBalance(entry.spenderCt);
|
|
1143
|
+
ElGamal_assertDecryptsTo(entry.spenderCt, spenderPk, ek, escrowPlaintext);
|
|
1238
1144
|
assert(escrowPlaintext >= value,
|
|
1239
1145
|
"ConfidentialFungibleToken: insufficient allowance");
|
|
1240
1146
|
|
|
1241
1147
|
const seed = wit_RandomnessSeed();
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
// distinct when a seed is reused. See the module header's @notice.
|
|
1246
|
-
if (!_escrowSpendEpochs.member(disclose(fromAddress))) {
|
|
1247
|
-
_escrowSpendEpochs.insert(disclose(fromAddress), default<Map<Bytes<32>, Counter>>);
|
|
1248
|
-
}
|
|
1249
|
-
if (!_escrowSpendEpochs.lookup(disclose(fromAddress)).member(disclose(spenderId))) {
|
|
1250
|
-
_escrowSpendEpochs.lookup(disclose(fromAddress)).insert(disclose(spenderId), default<Counter>);
|
|
1251
|
-
}
|
|
1252
|
-
_escrowSpendEpochs.lookup(disclose(fromAddress)).lookup(disclose(spenderId)).increment(1);
|
|
1253
|
-
const spendEpoch =
|
|
1254
|
-
_escrowSpendEpochs.lookup(disclose(fromAddress)).lookup(disclose(spenderId)) as Field;
|
|
1255
|
-
|
|
1256
|
-
const rSpender = _expandSpendRandomness(seed, pad(32, "spend_escrow_spender"), fromAddress, spenderId, spendEpoch);
|
|
1257
|
-
const rOwner = _expandSpendRandomness(seed, pad(32, "spend_escrow_owner"), fromAddress, spenderId, spendEpoch);
|
|
1258
|
-
const eOwnerMemo = _expandSpendRandomness(seed, pad(32, "spend_owner_memo"), fromAddress, spenderId, spendEpoch);
|
|
1148
|
+
const rSpender = ElGamal_expandRandomness(seed, pad(32, "spend_escrow_spender"));
|
|
1149
|
+
const rOwner = ElGamal_expandRandomness(seed, pad(32, "spend_escrow_owner"));
|
|
1150
|
+
const eOwnerMemo = ElGamal_expandRandomness(seed, pad(32, "spend_owner_memo"));
|
|
1259
1151
|
|
|
1260
1152
|
const ownerPk = _encryptionKeys.lookup(disclose(fromAddress));
|
|
1261
1153
|
const newSpenderCt = ElGamal_subEncrypted(entry.spenderCt, spenderPk, value, rSpender);
|
|
@@ -1286,7 +1178,7 @@ module ConfidentialFungibleToken {
|
|
|
1286
1178
|
* recipient credit. The building block for a composing contract's
|
|
1287
1179
|
* `burnFrom`; it performs no supply accounting (see `_burn`).
|
|
1288
1180
|
*
|
|
1289
|
-
*
|
|
1181
|
+
* @constraints k=16, rows=33128
|
|
1290
1182
|
*
|
|
1291
1183
|
* Requirements:
|
|
1292
1184
|
*
|
|
@@ -1302,25 +1194,6 @@ module ConfidentialFungibleToken {
|
|
|
1302
1194
|
return _spendEscrow(fromAddress, value);
|
|
1303
1195
|
}
|
|
1304
1196
|
|
|
1305
|
-
/**
|
|
1306
|
-
* @description Derives one per-spend random scalar for `_spendEscrow`. Does
|
|
1307
|
-
* what `ElGamal_expandRandomness` does, but folds both counterparties and the
|
|
1308
|
-
* escrow-spend epoch into the preimage as well as the seed and domain tag, so
|
|
1309
|
-
* each spend gets distinct randomness even when a seed is reused.
|
|
1310
|
-
*/
|
|
1311
|
-
pure circuit _expandSpendRandomness(
|
|
1312
|
-
seed: Bytes<32>, domain: Bytes<32>, owner: Bytes<32>, spender: Bytes<32>,
|
|
1313
|
-
epoch: Field): Field {
|
|
1314
|
-
return degradeToTransient(
|
|
1315
|
-
persistentHash<SpendRandomnessPreimage>(
|
|
1316
|
-
SpendRandomnessPreimage {
|
|
1317
|
-
seed: seed, domain: domain, owner: owner, spender: spender,
|
|
1318
|
-
epoch: epoch
|
|
1319
|
-
}
|
|
1320
|
-
)
|
|
1321
|
-
);
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
1197
|
/**
|
|
1325
1198
|
* @description Refunds any prior escrow for (ownerId, spender) to the owner's
|
|
1326
1199
|
* main balance and clears the slot, so re-approving replaces rather than
|
|
@@ -1353,7 +1226,7 @@ module ConfidentialFungibleToken {
|
|
|
1353
1226
|
// never decrypted. Use an on-curve identity point (never a `default`,
|
|
1354
1227
|
// which is off-curve).
|
|
1355
1228
|
ownerMemo: EcdhMask_Ciphertext {
|
|
1356
|
-
ephemeralPk: ecMulGenerator(0 as
|
|
1229
|
+
ephemeralPk: ecMulGenerator(0 as JubjubScalar),
|
|
1357
1230
|
ct: 0 as Field
|
|
1358
1231
|
}
|
|
1359
1232
|
})
|