@openzeppelin/compact-contracts 0.4.0-alpha.1 → 0.4.0-alpha.3
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 +9 -5
- package/access/AccessControl.compact +13 -13
- package/access/Ownable.compact +15 -9
- package/access/ShieldedAccessControl.compact +25 -1
- package/access/ZOwnablePK.compact +8 -8
- package/crypto/EcdhMask.compact +1 -1
- package/crypto/Ecdsa.compact +4 -4
- package/crypto/Eip712.compact +168 -0
- package/crypto/ElGamal.compact +44 -3
- package/multisig/EcdsaSignerManager.compact +32 -15
- package/multisig/ForwarderPrivate.compact +11 -5
- package/multisig/ForwarderShielded.compact +2 -2
- package/multisig/ForwarderUnshielded.compact +2 -2
- package/multisig/ProposalManager.compact +194 -22
- package/multisig/ShieldedTreasury.compact +28 -1
- package/multisig/ShieldedTreasuryStateless.compact +1 -1
- package/multisig/Signer.compact +27 -79
- package/multisig/UnshieldedTreasury.compact +9 -1
- package/multisig/{presets/forwarder/ForwarderPrivate.compact → examples/ForwarderPrivateExample.compact} +3 -3
- package/multisig/{presets/forwarder/ForwarderShielded.compact → examples/ForwarderShieldedExample.compact} +3 -3
- package/multisig/{presets/forwarder/ForwarderUnshielded.compact → examples/ForwarderUnshieldedExample.compact} +3 -3
- package/multisig/examples/NativeShieldedTokenIssuerExample.compact +143 -0
- package/multisig/examples/ShieldedMultiSigV2Example.compact +77 -0
- package/multisig/presets/NativeShieldedTokenIssuer.compact +557 -0
- package/multisig/presets/ShieldedMultiSigV2.compact +251 -188
- package/package.json +15 -11
- package/security/Allowlist.compact +5 -5
- package/security/Blocklist.compact +5 -5
- package/security/Initializable.compact +8 -4
- package/security/Pausable.compact +10 -6
- package/token/ConfidentialFungibleToken.compact +337 -79
- package/token/FungibleToken.compact +35 -21
- package/token/MultiToken.compact +15 -15
- package/token/NativeShieldedToken.compact +65 -16
- package/token/NativeShieldedTokenCore.compact +57 -15
- package/token/NativeShieldedTokenFamily.compact +66 -16
- package/token/NonFungibleToken.compact +83 -34
- package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +11 -6
- package/token/extensions/NativeShieldedTokenDerivedNonce.compact +2 -2
- package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +7 -7
- package/token/extensions/NativeShieldedTokenPublicSupply.compact +7 -7
- package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +5 -5
- package/utils/EvmAbi.compact +134 -0
- package/utils/Utils.compact +11 -1
- package/multisig/presets/ShieldedMultiSig.compact +0 -227
- package/multisig/presets/ShieldedMultiSigV3.compact +0 -311
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.4.0-alpha.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.4.0-alpha.3 (token/ConfidentialFungibleToken.compact)
|
|
3
3
|
|
|
4
4
|
pragma language_version >= 0.26.0;
|
|
5
5
|
|
|
@@ -11,7 +11,17 @@ pragma language_version >= 0.26.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
|
|
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`.
|
|
15
25
|
*
|
|
16
26
|
* @notice Supply. The mint/burn building blocks (`_mint`/`_burn`/`_burnFrom`)
|
|
17
27
|
* live here; total-supply tracking is an optional add-on
|
|
@@ -42,6 +52,8 @@ pragma language_version >= 0.26.0;
|
|
|
42
52
|
*
|
|
43
53
|
* - `wit_ConfidentialTokenEK`: verified by `ElGamal_assertDecryptsTo`,
|
|
44
54
|
* 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`.
|
|
45
57
|
*
|
|
46
58
|
* - `wit_PlaintextBalance(ct)`: verified by `ElGamal_assertDecryptsTo`,
|
|
47
59
|
* which asserts `Dec(ct, EK) == claimedValue`. The ciphertext is passed
|
|
@@ -62,13 +74,36 @@ pragma language_version >= 0.26.0;
|
|
|
62
74
|
* high-entropy, and secret for every invocation.
|
|
63
75
|
*
|
|
64
76
|
* Seed REUSE leaks plaintext differences, and the circuit does NOT prevent
|
|
65
|
-
* it. The debit and
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
77
|
+
* it. The debit and re-approve-refund ciphertexts (`_debit`, `approve`) derive
|
|
78
|
+
* randomness from a static domain tag with no per-op nonce, so two
|
|
79
|
+
* operations under one seed share randomness and their public ciphertext deltas
|
|
80
|
+
* reveal the amount difference. `_spendEscrow` is the exception: every value it
|
|
81
|
+
* derives folds in the spend epoch AND both counterparties, so its randomness
|
|
82
|
+
* is unique per spend regardless of the seed. The credit path folds the
|
|
83
|
+
* recipient's `_creditEpochs` counter in as a nonce (defense in depth). A sender
|
|
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.
|
|
72
107
|
*
|
|
73
108
|
* Seed generation is a WALLET responsibility and OUT OF SCOPE for this
|
|
74
109
|
* contract, which cannot enforce it. The witness shipped in this repo
|
|
@@ -86,13 +121,31 @@ pragma language_version >= 0.26.0;
|
|
|
86
121
|
* - approve does not disclose the cap (private-cap escrow design).
|
|
87
122
|
* - Counterparty graph (sender_id, recipient_id) is public on every
|
|
88
123
|
* 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`.
|
|
89
130
|
*
|
|
90
131
|
* @dev Per-transfer value bound. Value-bearing circuits check
|
|
91
132
|
* `value <= MAX_TRANSFER_VALUE()`, which is now the `Uint<128>` maximum: the ECDH
|
|
92
133
|
* memo delivers the amount directly (no discrete-log recovery), so the former
|
|
93
134
|
* `2^48` cap that kept wallet-side BSGS tractable is gone. Amounts stay well under
|
|
94
135
|
* the Jubjub scalar-field order, so the homomorphic balance sums (and the
|
|
95
|
-
* `g^value` encoding) do not wrap.
|
|
136
|
+
* `g^value` encoding) do not wrap at any plausible scale. Accumulation makes
|
|
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.
|
|
96
149
|
*
|
|
97
150
|
* @dev Value delivery via memo channel. Every credit made through `_credit`
|
|
98
151
|
* (mint / transfer / transferFrom) pushes an ECDH one-time-pad memo (see
|
|
@@ -174,15 +227,15 @@ pragma language_version >= 0.26.0;
|
|
|
174
227
|
* into the derivations is a possible defense-in-depth hardening; under review, deferred.
|
|
175
228
|
*
|
|
176
229
|
* Block weight. A deploy bundles every exported entry point's on-chain IR into
|
|
177
|
-
* one transaction, and the value circuits are SHA256-heavy (k=16
|
|
178
|
-
* full-surface deployment is byte-heavy. Whether it fits is a property of the
|
|
230
|
+
* one transaction, and the value circuits are SHA256-heavy (k=16, k=17 for
|
|
231
|
+
* `transferFrom`), so a full-surface deployment is byte-heavy. Whether it fits is a property of the
|
|
179
232
|
* target network, not the contract: the per-transaction block byte budget is a
|
|
180
233
|
* governance-configurable, node-side ledger parameter. On the local ledger-8 dev
|
|
181
234
|
* stack the full surface exceeded it and was rejected (`1010: Transaction would
|
|
182
235
|
* exhaust the block limits`), while a reduced surface (e.g. receive + transfer,
|
|
183
236
|
* dropping allowances and supply accounting) fit. So confirm deployability
|
|
184
237
|
* against the target network; if a deployment is over budget there, the lever is
|
|
185
|
-
* exposing only the
|
|
238
|
+
* exposing only the value operations it needs, not just trimming getters.
|
|
186
239
|
* A deployed contract can also be upgraded by its Contract Maintenance Authority
|
|
187
240
|
* (CMA), so an over-budget deployment can start with a lean surface and add
|
|
188
241
|
* operations by upgrade instead of fitting the whole surface into one deploy.
|
|
@@ -223,6 +276,7 @@ module ConfidentialFungibleToken {
|
|
|
223
276
|
import CompactStandardLibrary;
|
|
224
277
|
import "../crypto/ElGamal" prefix ElGamal_;
|
|
225
278
|
import "../crypto/EcdhMask" prefix EcdhMask_;
|
|
279
|
+
import "../utils/Utils" prefix Utils_;
|
|
226
280
|
|
|
227
281
|
// ---------------------------------------------------------------------------
|
|
228
282
|
// Types
|
|
@@ -254,26 +308,137 @@ module ConfidentialFungibleToken {
|
|
|
254
308
|
ownerMemo: EcdhMask_Ciphertext;
|
|
255
309
|
}
|
|
256
310
|
|
|
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
|
+
|
|
257
325
|
// ---------------------------------------------------------------------------
|
|
258
326
|
// Ledger state
|
|
259
327
|
// ---------------------------------------------------------------------------
|
|
260
328
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
329
|
+
/**
|
|
330
|
+
* @description Spendable (confirmed) balances, encrypted under each account's
|
|
331
|
+
* ElGamal key. Debits draw from here. Paired with `_pending`: only the owner
|
|
332
|
+
* moves pending into spendable, via `sweep()`, so a third party spamming dust
|
|
333
|
+
* credits cannot churn a victim's spendable ciphertext and invalidate their
|
|
334
|
+
* in-flight spend proofs. A wallet's total is `balanceOf` + `pendingOf`.
|
|
335
|
+
* @type {Bytes<32>} accountId - The account identifier.
|
|
336
|
+
* @type {ElGamal_Ciphertext} balance - The encrypted spendable balance.
|
|
337
|
+
* @type {Map<accountId, balance>}
|
|
338
|
+
* @type {Map<Bytes<32>, ElGamal_Ciphertext>} _balances
|
|
339
|
+
*/
|
|
267
340
|
export ledger _balances: Map<Bytes<32>, ElGamal_Ciphertext>;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @description Incoming (unconfirmed) balances, encrypted under each account's
|
|
344
|
+
* ElGamal key. Credits land here rather than in `_balances`; the owner moves
|
|
345
|
+
* them across with `sweep()`.
|
|
346
|
+
* @type {Bytes<32>} accountId - The account identifier.
|
|
347
|
+
* @type {ElGamal_Ciphertext} pending - The encrypted pending balance.
|
|
348
|
+
* @type {Map<accountId, pending>}
|
|
349
|
+
* @type {Map<Bytes<32>, ElGamal_Ciphertext>} _pending
|
|
350
|
+
*/
|
|
268
351
|
export ledger _pending: Map<Bytes<32>, ElGamal_Ciphertext>;
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @description Registered ElGamal encryption keys, one per account.
|
|
355
|
+
* Registration is one-shot: a second registration for the same account is
|
|
356
|
+
* rejected.
|
|
357
|
+
* @type {Bytes<32>} accountId - The account identifier.
|
|
358
|
+
* @type {JubjubPoint} encryptionKey - The account's ElGamal public key.
|
|
359
|
+
* @type {Map<accountId, encryptionKey>}
|
|
360
|
+
* @type {Map<Bytes<32>, JubjubPoint>} _encryptionKeys
|
|
361
|
+
*/
|
|
269
362
|
export ledger _encryptionKeys: Map<Bytes<32>, JubjubPoint>;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @description Per-account memo inbox. A credit pushes a masked memo the
|
|
366
|
+
* recipient decrypts off-chain; `pushFront` means newest first.
|
|
367
|
+
* @type {Bytes<32>} accountId - The recipient's identifier.
|
|
368
|
+
* @type {List<EcdhMask_Ciphertext>} memos - The recipient's memos.
|
|
369
|
+
* @type {Map<accountId, memos>}
|
|
370
|
+
* @type {Map<Bytes<32>, List<EcdhMask_Ciphertext>>} _memos
|
|
371
|
+
*/
|
|
270
372
|
export ledger _memos: Map<Bytes<32>, List<EcdhMask_Ciphertext>>;
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @description Escrowed allowances, keyed by owner then spender. Membership is
|
|
376
|
+
* checked independently of the entry, so an allowance that was never escrowed
|
|
377
|
+
* is unspendable.
|
|
378
|
+
* @type {Bytes<32>} owner - The account that escrowed the allowance.
|
|
379
|
+
* @type {Bytes<32>} spender - The account allowed to spend it.
|
|
380
|
+
* @type {EscrowEntry} entry - The escrowed allowance.
|
|
381
|
+
* @type {Map<owner, Map<spender, entry>>}
|
|
382
|
+
* @type {Map<Bytes<32>, Map<Bytes<32>, EscrowEntry>>} _escrow
|
|
383
|
+
*/
|
|
271
384
|
export ledger _escrow: Map<Bytes<32>, Map<Bytes<32>, EscrowEntry>>;
|
|
272
385
|
|
|
386
|
+
/**
|
|
387
|
+
* @description Per-(owner, spender) escrow-spend counter, folded into the
|
|
388
|
+
* randomness `_spendEscrow` derives so repeat spends by one spender against
|
|
389
|
+
* one owner never share a one-time pad. It restarts for each pair, which is
|
|
390
|
+
* why both counterparties are bound in too; see `SpendRandomnessPreimage`.
|
|
391
|
+
*
|
|
392
|
+
* Kept out of `EscrowEntry`: `approve` replaces that entry wholesale, so a
|
|
393
|
+
* counter living there would restart on every re-approve.
|
|
394
|
+
*
|
|
395
|
+
* Note that this exposes a per-pair spend count which is already derivable
|
|
396
|
+
* from tx history.
|
|
397
|
+
* @type {Bytes<32>} owner - The account that escrowed the allowance.
|
|
398
|
+
* @type {Bytes<32>} spender - The account spending it.
|
|
399
|
+
* @type {Counter} epoch - The pair's lifetime escrow-spend count.
|
|
400
|
+
* @type {Map<owner, Map<spender, epoch>>}
|
|
401
|
+
* @type {Map<Bytes<32>, Map<Bytes<32>, Counter>>} _escrowSpendEpochs
|
|
402
|
+
*/
|
|
403
|
+
export ledger _escrowSpendEpochs: Map<Bytes<32>, Map<Bytes<32>, Counter>>;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* @description Per-account credit counter. `_credit` uses it as the nonce that
|
|
407
|
+
* keeps each credit's randomness distinct, so it MUST only ever increase. No
|
|
408
|
+
* circuit resets it. It is kept out of `_memos` for that reason: the memo list
|
|
409
|
+
* is prunable by its owner, and a nonce that could be walked back would let a
|
|
410
|
+
* reused seed repeat an earlier credit's randomness. The cost is that an
|
|
411
|
+
* account's lifetime credit count is permanently readable from state.
|
|
412
|
+
*
|
|
413
|
+
* `Counter` is chosen for no-reset monotonicity, not commutativity. Its
|
|
414
|
+
* `increment` emits no pinned read, but `_credit` reads the value back to
|
|
415
|
+
* derive the nonce, and that read pins the recipient's key. Credits to one
|
|
416
|
+
* recipient still serialize; see the Concurrency note above.
|
|
417
|
+
* @type {Bytes<32>} accountId - The account identifier.
|
|
418
|
+
* @type {Counter} epoch - The account's lifetime credit count.
|
|
419
|
+
* @type {Map<accountId, epoch>}
|
|
420
|
+
* @type {Map<Bytes<32>, Counter>} _creditEpochs
|
|
421
|
+
*/
|
|
422
|
+
export ledger _creditEpochs: Map<Bytes<32>, Counter>;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* @description Whether `initialize` has run. Guards against re-initialization
|
|
426
|
+
* and against use before initialization.
|
|
427
|
+
*/
|
|
273
428
|
export ledger _isInitialized: Boolean;
|
|
274
429
|
|
|
430
|
+
/**
|
|
431
|
+
* @description The token's human-readable name, set once at construction.
|
|
432
|
+
*/
|
|
275
433
|
export sealed ledger _name: Opaque<"string">;
|
|
434
|
+
/**
|
|
435
|
+
* @description The token's ticker symbol, set once at construction.
|
|
436
|
+
*/
|
|
276
437
|
export sealed ledger _symbol: Opaque<"string">;
|
|
438
|
+
/**
|
|
439
|
+
* @description The number of decimals used to display balances, set once at
|
|
440
|
+
* construction. Purely presentational; no circuit scales by it.
|
|
441
|
+
*/
|
|
277
442
|
export sealed ledger _decimals: Uint<8>;
|
|
278
443
|
|
|
279
444
|
|
|
@@ -290,8 +455,9 @@ module ConfidentialFungibleToken {
|
|
|
290
455
|
* well under the Jubjub scalar-field order, so homomorphic sums do not wrap.
|
|
291
456
|
*
|
|
292
457
|
* @notice This equals the argument type's own maximum, so the per-op check is
|
|
293
|
-
*
|
|
294
|
-
*
|
|
458
|
+
* redundant today. KEEP IT: it compares against a hard constant rather than
|
|
459
|
+
* the argument type, so it still catches a later widening of the transfer
|
|
460
|
+
* type — which is what the balance-claim width is sized against.
|
|
295
461
|
*/
|
|
296
462
|
export pure circuit MAX_TRANSFER_VALUE(): Uint<128> {
|
|
297
463
|
return 340282366920938463463374607431768211455; // 2^128 - 1
|
|
@@ -326,7 +492,7 @@ module ConfidentialFungibleToken {
|
|
|
326
492
|
* The ciphertext is passed explicitly so the wallet can route to the right
|
|
327
493
|
* cached plaintext by ciphertext identity. This avoids call-order
|
|
328
494
|
* conventions (which would be brittle to circuit refactors) and lets the
|
|
329
|
-
* wallet maintain a `Map<ElGamal_Ciphertext,
|
|
495
|
+
* wallet maintain a `Map<ElGamal_Ciphertext, Uint248>` lookup.
|
|
330
496
|
*
|
|
331
497
|
* Verified in-circuit by `ElGamal_assertDecryptsTo`: a wrong plaintext
|
|
332
498
|
* fails the assertion `Dec(ct, EK) == claimedValue`.
|
|
@@ -334,7 +500,7 @@ module ConfidentialFungibleToken {
|
|
|
334
500
|
* @param ct - The ciphertext whose plaintext is being requested.
|
|
335
501
|
* @returns The wallet's cached plaintext for `ct`.
|
|
336
502
|
*/
|
|
337
|
-
witness wit_PlaintextBalance(ct: ElGamal_Ciphertext): Uint<
|
|
503
|
+
witness wit_PlaintextBalance(ct: ElGamal_Ciphertext): Uint<248>;
|
|
338
504
|
|
|
339
505
|
/**
|
|
340
506
|
* @witness wit_RandomnessSeed
|
|
@@ -345,10 +511,11 @@ module ConfidentialFungibleToken {
|
|
|
345
511
|
* module header). The derivation is deterministic and public, so an observer
|
|
346
512
|
* who knows the seed can strip a memo's mask and read the delivered amount, and
|
|
347
513
|
* recover balance/escrow amounts from the public ciphertexts. Seed REUSE leaks
|
|
348
|
-
* plaintext differences and the circuit does NOT prevent it
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
514
|
+
* plaintext differences and the circuit does NOT prevent it on most paths.
|
|
515
|
+
* `_spendEscrow` is an exception (its randomness folds the spend epoch and both
|
|
516
|
+
* counterparties); the credit path folds the recipient's `_creditEpochs`
|
|
517
|
+
* counter as defense in depth, and that covers credits alone. The seed MUST be
|
|
518
|
+
* fresh, high-entropy, and secret for every invocation.
|
|
352
519
|
*/
|
|
353
520
|
witness wit_RandomnessSeed(): Bytes<32>;
|
|
354
521
|
|
|
@@ -390,7 +557,7 @@ module ConfidentialFungibleToken {
|
|
|
390
557
|
* ciphertext if the account is unregistered. The amount is encrypted; only the
|
|
391
558
|
* account (or an authorized viewer) can decrypt it.
|
|
392
559
|
*
|
|
393
|
-
* @
|
|
560
|
+
* @constraints k=10, rows=984
|
|
394
561
|
*
|
|
395
562
|
* Requirements:
|
|
396
563
|
*
|
|
@@ -416,7 +583,7 @@ module ConfidentialFungibleToken {
|
|
|
416
583
|
* balance is `balanceOf`; a wallet's total is the sum of the two. See the
|
|
417
584
|
* dual-balance note on the ledger declarations.
|
|
418
585
|
*
|
|
419
|
-
* @
|
|
586
|
+
* @constraints k=10, rows=981
|
|
420
587
|
*
|
|
421
588
|
* Requirements:
|
|
422
589
|
*
|
|
@@ -447,7 +614,7 @@ module ConfidentialFungibleToken {
|
|
|
447
614
|
* getter grants nothing, the spend path (`transferFrom`/`burnFrom`) gates on
|
|
448
615
|
* `_escrow` membership independently, so a non-existent allowance is unspendable.
|
|
449
616
|
*
|
|
450
|
-
* @
|
|
617
|
+
* @constraints k=10, rows=936
|
|
451
618
|
*
|
|
452
619
|
* @param {Bytes<32>} owner - The account that granted the allowance.
|
|
453
620
|
* @param {Bytes<32>} spender - The approved spender.
|
|
@@ -473,7 +640,7 @@ module ConfidentialFungibleToken {
|
|
|
473
640
|
/**
|
|
474
641
|
* @description Returns whether `account` has registered an encryption key.
|
|
475
642
|
*
|
|
476
|
-
* @
|
|
643
|
+
* @constraints k=9, rows=303
|
|
477
644
|
*
|
|
478
645
|
* @param {Bytes<32>} account - The account to query.
|
|
479
646
|
* @return {Boolean} - True if `account` is registered.
|
|
@@ -491,7 +658,7 @@ module ConfidentialFungibleToken {
|
|
|
491
658
|
* `wit_ConfidentialTokenEK` and initializes their balance to an encryption of
|
|
492
659
|
* zero. Registration is a prerequisite for sending or receiving.
|
|
493
660
|
*
|
|
494
|
-
* @
|
|
661
|
+
* @constraints k=13, rows=8046
|
|
495
662
|
*
|
|
496
663
|
* @notice Returns the caller's `accountId`. A composing contract can gate on
|
|
497
664
|
* the returned value (e.g. assert it is KYC-approved) to restrict who may
|
|
@@ -555,14 +722,14 @@ module ConfidentialFungibleToken {
|
|
|
555
722
|
// (its `mint` increments the total before crediting; its `burn` decrements it
|
|
556
723
|
// after debiting).
|
|
557
724
|
//
|
|
558
|
-
// Randomness: `_credit` folds a per-recipient nonce (
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
725
|
+
// Randomness: `_credit` folds a per-recipient nonce (`_creditEpochs`) into
|
|
726
|
+
// its seed-derived randomness as defense in depth. The nonce only ever
|
|
727
|
+
// increases, so it never repeats a value, but it is NOT a substitute for seed
|
|
728
|
+
// freshness (see the header note); seed reuse still leaks credited-amount
|
|
729
|
+
// differences on every other path. It also keeps a single circuit's
|
|
730
|
+
// multi-party credits distinct, each recipient's counter being independent.
|
|
731
|
+
// The debit side has no such nonce, so seed freshness is the only defense
|
|
732
|
+
// there.
|
|
566
733
|
// ---------------------------------------------------------------------------
|
|
567
734
|
|
|
568
735
|
/**
|
|
@@ -618,11 +785,11 @@ module ConfidentialFungibleToken {
|
|
|
618
785
|
* account's balance ciphertext and pushes an ECDH one-time-pad memo that
|
|
619
786
|
* delivers `value` directly to the recipient (no discrete-log recovery, so no
|
|
620
787
|
* `2^48` bound). All per-credit randomness is derived internally from the
|
|
621
|
-
* witness seed and a per-recipient monotonic nonce (
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
788
|
+
* witness seed and a per-recipient monotonic nonce (`_creditEpochs`), which
|
|
789
|
+
* keeps randomness distinct across credits (defense in depth for the memo's
|
|
790
|
+
* one-time pad; see `crypto/EcdhMask`). That nonce only ever increases and
|
|
791
|
+
* nothing resets it, but it is never a substitute for seed freshness (see the
|
|
792
|
+
* module header and the section note above `_debit`).
|
|
626
793
|
*
|
|
627
794
|
* @warning Does not adjust any supply total. Called outside a supply-accounted
|
|
628
795
|
* flow, `_credit` mints unbacked value (inflation) and breaks the
|
|
@@ -642,20 +809,25 @@ module ConfidentialFungibleToken {
|
|
|
642
809
|
|
|
643
810
|
const recipientPk = _encryptionKeys.lookup(disclose(account));
|
|
644
811
|
|
|
645
|
-
// Ensure the recipient's memo list exists
|
|
646
|
-
// monotonic nonce that makes this credit's randomness unique even if the
|
|
647
|
-
// wallet reuses its seed. Freshness is essential for the memo OTP: a repeated
|
|
648
|
-
// ephemeral to the same recipient would reuse the pad.
|
|
812
|
+
// Ensure the recipient's memo list exists.
|
|
649
813
|
if (!_memos.member(disclose(account))) {
|
|
650
814
|
_memos.insert(disclose(account), default<List<EcdhMask_Ciphertext>>);
|
|
651
815
|
}
|
|
652
|
-
|
|
816
|
+
|
|
817
|
+
// Advance the recipient's credit epoch and use it as this credit's nonce.
|
|
818
|
+
// The nonce must come from a source that only increases; anything the
|
|
819
|
+
// recipient can reset would let a reused seed repeat the randomness below.
|
|
820
|
+
if (!_creditEpochs.member(disclose(account))) {
|
|
821
|
+
_creditEpochs.insert(disclose(account), default<Counter>);
|
|
822
|
+
}
|
|
823
|
+
_creditEpochs.lookup(disclose(account)).increment(1);
|
|
824
|
+
const count = _creditEpochs.lookup(disclose(account)) as Field;
|
|
653
825
|
const seed = wit_RandomnessSeed();
|
|
654
826
|
|
|
655
827
|
// Randomness must be a VALID Jubjub scalar. `transientHash`'s raw Field output
|
|
656
|
-
// can exceed the Jubjub scalar-field order
|
|
657
|
-
// `degradeToTransient(persistentHash(...))`, which
|
|
658
|
-
// field. The per-recipient
|
|
828
|
+
// can exceed the Jubjub scalar-field order and fault `ecMulGenerator`, so each
|
|
829
|
+
// scalar is derived via `degradeToTransient(persistentHash(...))`, which
|
|
830
|
+
// truncates into the scalar field. The per-recipient epoch, folded into a
|
|
659
831
|
// Bytes<32> nonce, keeps each credit's randomness unique even if the seed
|
|
660
832
|
// repeats (essential for the memo OTP). This inlines the tag + expansion into
|
|
661
833
|
// a single hash per scalar. The residual persistentHash cost is poseidon-gated
|
|
@@ -693,7 +865,7 @@ module ConfidentialFungibleToken {
|
|
|
693
865
|
* `extensions/ConfidentialFungibleTokenPublicSupply`) if the deployment tracks
|
|
694
866
|
* `totalSupply`.
|
|
695
867
|
*
|
|
696
|
-
* @
|
|
868
|
+
* @constraints k=15, rows=26962
|
|
697
869
|
*
|
|
698
870
|
* Requirements:
|
|
699
871
|
*
|
|
@@ -716,7 +888,7 @@ module ConfidentialFungibleToken {
|
|
|
716
888
|
* `extensions/ConfidentialFungibleTokenPublicSupply`) if the deployment tracks
|
|
717
889
|
* `totalSupply`.
|
|
718
890
|
*
|
|
719
|
-
* @
|
|
891
|
+
* @constraints k=14, rows=14889
|
|
720
892
|
*
|
|
721
893
|
* Requirements:
|
|
722
894
|
*
|
|
@@ -740,7 +912,7 @@ module ConfidentialFungibleToken {
|
|
|
740
912
|
* credits `to`, and pushes an encrypted memo to `to`. The amount is hidden;
|
|
741
913
|
* the (sender, recipient) pair is public.
|
|
742
914
|
*
|
|
743
|
-
* @
|
|
915
|
+
* @constraints k=16, rows=41623
|
|
744
916
|
*
|
|
745
917
|
* @notice Returns the caller's (sender's) `accountId` for caller-side gating.
|
|
746
918
|
* The sender is derived from the same authentication `_debit` performs, so the
|
|
@@ -778,7 +950,7 @@ module ConfidentialFungibleToken {
|
|
|
778
950
|
* operations (`mint`/`burn`) live in a separate layer outside this conserving
|
|
779
951
|
* surface, and only they may break conservation.
|
|
780
952
|
*
|
|
781
|
-
* @
|
|
953
|
+
* @constraints k=16, rows=41612
|
|
782
954
|
*
|
|
783
955
|
* @notice Like `transfer`, the credit lands in the recipient's pending pool
|
|
784
956
|
* (see the dual-balance note); the caller sweeps their own incoming value.
|
|
@@ -814,12 +986,13 @@ module ConfidentialFungibleToken {
|
|
|
814
986
|
* @description Sweeps the caller's incoming `pending` pool into their
|
|
815
987
|
* `spendable` balance. Credits land in `pending` (see the dual-balance note);
|
|
816
988
|
* this `sweep` is the ONLY path from pending into spendable, and only the owner
|
|
817
|
-
* can invoke it (the
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
989
|
+
* can invoke it (the caller proves BOTH witness secrets: the account secret
|
|
990
|
+
* identifies the account, the encryption secret proves the caller can read it).
|
|
991
|
+
* So a third party spamming credits cannot force a victim's spendable ciphertext
|
|
992
|
+
* to change, which is what would otherwise invalidate the victim's in-flight
|
|
993
|
+
* spend proofs (a liveness grief).
|
|
821
994
|
*
|
|
822
|
-
* @
|
|
995
|
+
* @constraints k=13, rows=7541
|
|
823
996
|
*
|
|
824
997
|
* @notice Purely homomorphic: adds the two ciphertexts (same key) and resets
|
|
825
998
|
* pending to Enc(0). No plaintext is needed; the wallet already learned the
|
|
@@ -829,14 +1002,14 @@ module ConfidentialFungibleToken {
|
|
|
829
1002
|
*
|
|
830
1003
|
* - Contract is initialized.
|
|
831
1004
|
* - The caller is registered.
|
|
1005
|
+
* - The caller holds the encryption secret registered for their account.
|
|
832
1006
|
*
|
|
833
1007
|
* @return {Bytes<32>} - The caller's accountId.
|
|
834
1008
|
*/
|
|
835
1009
|
export circuit sweep(): Bytes<32> {
|
|
836
1010
|
assertInitialized();
|
|
837
1011
|
const accountId = _computeAccountId();
|
|
838
|
-
|
|
839
|
-
"ConfidentialFungibleToken: not registered");
|
|
1012
|
+
_assertOwnsEncryptionKey(accountId);
|
|
840
1013
|
const newSpendable = ElGamal_add(
|
|
841
1014
|
_balances.lookup(disclose(accountId)),
|
|
842
1015
|
_pending.lookup(disclose(accountId)));
|
|
@@ -852,7 +1025,7 @@ module ConfidentialFungibleToken {
|
|
|
852
1025
|
* refunds any prior escrow, so allowances replace rather than stack. The cap
|
|
853
1026
|
* is not public.
|
|
854
1027
|
*
|
|
855
|
-
* @
|
|
1028
|
+
* @constraints k=16, rows=42938
|
|
856
1029
|
*
|
|
857
1030
|
* @notice Returns the caller's (approver's) `accountId` for caller-side gating.
|
|
858
1031
|
*
|
|
@@ -936,7 +1109,7 @@ module ConfidentialFungibleToken {
|
|
|
936
1109
|
* caller via `approve`) and credits it to `to`, reducing both escrow copies by
|
|
937
1110
|
* `value`. The amount is hidden.
|
|
938
1111
|
*
|
|
939
|
-
* @
|
|
1112
|
+
* ???? @constraints k=17, rows=68920
|
|
940
1113
|
*
|
|
941
1114
|
* @notice Returns the caller's (spender's) `accountId` for caller-side gating.
|
|
942
1115
|
*
|
|
@@ -976,7 +1149,14 @@ module ConfidentialFungibleToken {
|
|
|
976
1149
|
* @description Clears the caller's memo list, letting a wallet that has folded
|
|
977
1150
|
* its memos into its local balance cache prune on-chain memo storage.
|
|
978
1151
|
*
|
|
979
|
-
*
|
|
1152
|
+
* `expectedEpoch` is the caller's view of its `_creditEpochs` value at the
|
|
1153
|
+
* moment it read the list. The call is rejected unless that still matches, so
|
|
1154
|
+
* a credit landing between the read and this transaction is never silently
|
|
1155
|
+
* discarded. The wallet retries against the memos it has now seen. A wallet
|
|
1156
|
+
* therefore reads its memo list and its epoch together, folds the memos into
|
|
1157
|
+
* its cache, and passes that epoch back here.
|
|
1158
|
+
*
|
|
1159
|
+
* @constraints k=13, rows=7019
|
|
980
1160
|
*
|
|
981
1161
|
* @warning Clearing memos is destructive and can permanently lock funds. The
|
|
982
1162
|
* on-chain balance ciphertext is not directly decryptable (discrete-log
|
|
@@ -984,20 +1164,35 @@ module ConfidentialFungibleToken {
|
|
|
984
1164
|
* them without a durable off-chain record of the balance loses the ability to
|
|
985
1165
|
* recover it; and because spending requires proving the balance plaintext, the
|
|
986
1166
|
* funds become unspendable, not merely undisplayable. Clear only memos already
|
|
987
|
-
* folded into a durable cache/backup
|
|
1167
|
+
* folded into a durable cache/backup. The epoch check only rules out a credit
|
|
1168
|
+
* the caller has not seen; it does not make clearing safe for memos the caller
|
|
1169
|
+
* never folded in.
|
|
1170
|
+
*
|
|
1171
|
+
* @param {Uint<64>} expectedEpoch - The caller's view of its credit epoch.
|
|
988
1172
|
*
|
|
989
1173
|
* Requirements:
|
|
990
1174
|
*
|
|
991
1175
|
* - Contract is initialized.
|
|
1176
|
+
* - The caller is registered.
|
|
1177
|
+
* - The caller holds the encryption secret registered for their account.
|
|
1178
|
+
* - The caller has a memo list.
|
|
1179
|
+
* - `expectedEpoch` equals the caller's current credit epoch.
|
|
992
1180
|
*
|
|
993
1181
|
* @return {[]} - Empty tuple.
|
|
994
1182
|
*/
|
|
995
|
-
export circuit clearMemos(): [] {
|
|
1183
|
+
export circuit clearMemos(expectedEpoch: Uint<64>): [] {
|
|
996
1184
|
assertInitialized();
|
|
997
1185
|
const accountId = _computeAccountId();
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1186
|
+
_assertOwnsEncryptionKey(accountId);
|
|
1187
|
+
|
|
1188
|
+
// Both reads are unconditional, so both are pinned in the transcript and a
|
|
1189
|
+
// concurrent credit invalidates this tx
|
|
1190
|
+
assert(_memos.member(disclose(accountId)),
|
|
1191
|
+
"ConfidentialFungibleToken: no memo list");
|
|
1192
|
+
assert(_creditEpochs.lookup(disclose(accountId)) as Field == expectedEpoch as Field,
|
|
1193
|
+
"ConfidentialFungibleToken: memo list changed");
|
|
1194
|
+
|
|
1195
|
+
_memos.insert(disclose(accountId), default<List<EcdhMask_Ciphertext>>);
|
|
1001
1196
|
}
|
|
1002
1197
|
|
|
1003
1198
|
// ---------------------------------------------------------------------------
|
|
@@ -1026,16 +1221,40 @@ module ConfidentialFungibleToken {
|
|
|
1026
1221
|
return computeAccountId(wit_ConfidentialTokenSK());
|
|
1027
1222
|
}
|
|
1028
1223
|
|
|
1224
|
+
/**
|
|
1225
|
+
* @description Asserts `accountId` is registered and that the caller holds the
|
|
1226
|
+
* encryption secret registered for it, by re-deriving the public key and
|
|
1227
|
+
* comparing it to ledger state.
|
|
1228
|
+
*
|
|
1229
|
+
* @notice Circuits that move or destroy value prove this implicitly, via
|
|
1230
|
+
* `ElGamal_assertDecryptsTo` on a balance. `sweep` and `clearMemos` have no
|
|
1231
|
+
* plaintext to decrypt, so they assert it directly: holding the account secret
|
|
1232
|
+
* alone must not be enough to clear a memo list or merge pending value, since
|
|
1233
|
+
* doing both leaves the holder unable to state a balance they can no longer
|
|
1234
|
+
* learn.
|
|
1235
|
+
*/
|
|
1236
|
+
circuit _assertOwnsEncryptionKey(accountId: Bytes<32>): [] {
|
|
1237
|
+
assert(_encryptionKeys.member(disclose(accountId)),
|
|
1238
|
+
"ConfidentialFungibleToken: not registered");
|
|
1239
|
+
assert(ElGamal_derivePk(wit_ConfidentialTokenEK()) ==
|
|
1240
|
+
_encryptionKeys.lookup(disclose(accountId)),
|
|
1241
|
+
"ConfidentialFungibleToken: wrong encryption key");
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1029
1244
|
/**
|
|
1030
1245
|
* @description Derives the public `accountId` from a secret key, as
|
|
1031
1246
|
* `persistentHash(sk)`. Pure, so a wallet can compute its own account
|
|
1032
1247
|
* identifier off-chain (no proof) before transacting.
|
|
1033
1248
|
*
|
|
1249
|
+
* @dev Delegates to `Utils.computeAccountId` rather than reimplementing the
|
|
1250
|
+
* hash. The identifier is deliberately GLOBAL. The same key material yields
|
|
1251
|
+
* the same id in every module that derives it.
|
|
1252
|
+
*
|
|
1034
1253
|
* @param {Bytes<32>} sk - The account secret key.
|
|
1035
1254
|
* @return {Bytes<32>} - The derived accountId.
|
|
1036
1255
|
*/
|
|
1037
1256
|
export pure circuit computeAccountId(sk: Bytes<32>): Bytes<32> {
|
|
1038
|
-
return
|
|
1257
|
+
return Utils_computeAccountId(sk);
|
|
1039
1258
|
}
|
|
1040
1259
|
|
|
1041
1260
|
/**
|
|
@@ -1080,15 +1299,35 @@ module ConfidentialFungibleToken {
|
|
|
1080
1299
|
|
|
1081
1300
|
const ek = wit_ConfidentialTokenEK();
|
|
1082
1301
|
const spenderPk = _encryptionKeys.lookup(disclose(spenderId));
|
|
1083
|
-
|
|
1084
|
-
|
|
1302
|
+
// Balance claims are `Uint<248>`, but an escrow cannot legitimately exceed
|
|
1303
|
+
// the transfer bound: `approve` caps it at `Uint<128>` and replaces rather
|
|
1304
|
+
// than stacks, and `_spendEscrow` only reduces it. Narrowing states that
|
|
1305
|
+
// invariant here instead of leaving it to inspection of those two circuits.
|
|
1306
|
+
// The cast fails closed, so an out-of-range claim aborts rather than wraps.
|
|
1307
|
+
const escrowPlaintext =
|
|
1308
|
+
wit_PlaintextBalance(entry.spenderCt) as Uint<128>;
|
|
1309
|
+
ElGamal_assertDecryptsTo(entry.spenderCt, spenderPk, ek, escrowPlaintext as Uint<248>);
|
|
1085
1310
|
assert(escrowPlaintext >= value,
|
|
1086
1311
|
"ConfidentialFungibleToken: insufficient allowance");
|
|
1087
1312
|
|
|
1088
1313
|
const seed = wit_RandomnessSeed();
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1314
|
+
|
|
1315
|
+
// The seed is the SPENDER's, but all three values protect the OWNER. The
|
|
1316
|
+
// epoch below plus both counterparties in the preimage are what keep them
|
|
1317
|
+
// distinct when a seed is reused. See the module header's @notice.
|
|
1318
|
+
if (!_escrowSpendEpochs.member(disclose(fromAddress))) {
|
|
1319
|
+
_escrowSpendEpochs.insert(disclose(fromAddress), default<Map<Bytes<32>, Counter>>);
|
|
1320
|
+
}
|
|
1321
|
+
if (!_escrowSpendEpochs.lookup(disclose(fromAddress)).member(disclose(spenderId))) {
|
|
1322
|
+
_escrowSpendEpochs.lookup(disclose(fromAddress)).insert(disclose(spenderId), default<Counter>);
|
|
1323
|
+
}
|
|
1324
|
+
_escrowSpendEpochs.lookup(disclose(fromAddress)).lookup(disclose(spenderId)).increment(1);
|
|
1325
|
+
const spendEpoch =
|
|
1326
|
+
_escrowSpendEpochs.lookup(disclose(fromAddress)).lookup(disclose(spenderId)) as Field;
|
|
1327
|
+
|
|
1328
|
+
const rSpender = _expandSpendRandomness(seed, pad(32, "spend_escrow_spender"), fromAddress, spenderId, spendEpoch);
|
|
1329
|
+
const rOwner = _expandSpendRandomness(seed, pad(32, "spend_escrow_owner"), fromAddress, spenderId, spendEpoch);
|
|
1330
|
+
const eOwnerMemo = _expandSpendRandomness(seed, pad(32, "spend_owner_memo"), fromAddress, spenderId, spendEpoch);
|
|
1092
1331
|
|
|
1093
1332
|
const ownerPk = _encryptionKeys.lookup(disclose(fromAddress));
|
|
1094
1333
|
const newSpenderCt = ElGamal_subEncrypted(entry.spenderCt, spenderPk, value, rSpender);
|
|
@@ -1119,7 +1358,7 @@ module ConfidentialFungibleToken {
|
|
|
1119
1358
|
* recipient credit. The building block for a composing contract's
|
|
1120
1359
|
* `burnFrom`; it performs no supply accounting (see `_burn`).
|
|
1121
1360
|
*
|
|
1122
|
-
* @
|
|
1361
|
+
* ???? @constraints k=16, rows=42201
|
|
1123
1362
|
*
|
|
1124
1363
|
* Requirements:
|
|
1125
1364
|
*
|
|
@@ -1135,6 +1374,25 @@ module ConfidentialFungibleToken {
|
|
|
1135
1374
|
return _spendEscrow(fromAddress, value);
|
|
1136
1375
|
}
|
|
1137
1376
|
|
|
1377
|
+
/**
|
|
1378
|
+
* @description Derives one per-spend random scalar for `_spendEscrow`. Does
|
|
1379
|
+
* what `ElGamal_expandRandomness` does, but folds both counterparties and the
|
|
1380
|
+
* escrow-spend epoch into the preimage as well as the seed and domain tag, so
|
|
1381
|
+
* each spend gets distinct randomness even when a seed is reused.
|
|
1382
|
+
*/
|
|
1383
|
+
pure circuit _expandSpendRandomness(
|
|
1384
|
+
seed: Bytes<32>, domain: Bytes<32>, owner: Bytes<32>, spender: Bytes<32>,
|
|
1385
|
+
epoch: Field): JubjubScalar {
|
|
1386
|
+
return degradeToTransient(
|
|
1387
|
+
persistentHash<SpendRandomnessPreimage>(
|
|
1388
|
+
SpendRandomnessPreimage {
|
|
1389
|
+
seed: seed, domain: domain, owner: owner, spender: spender,
|
|
1390
|
+
epoch: epoch
|
|
1391
|
+
}
|
|
1392
|
+
)
|
|
1393
|
+
) as JubjubScalar;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1138
1396
|
/**
|
|
1139
1397
|
* @description Refunds any prior escrow for (ownerId, spender) to the owner's
|
|
1140
1398
|
* main balance and clears the slot, so re-approving replaces rather than
|