diff --git a/content/contracts-compact/access-control.mdx b/content/contracts-compact/access-control.mdx index 846282ee..b4ec0879 100644 --- a/content/contracts-compact/access-control.mdx +++ b/content/contracts-compact/access-control.mdx @@ -8,17 +8,18 @@ title: AccessControl [FungibleToken]: ./fungible-token.mdx [Ownable]: ./ownable.mdx -[Initializable]: ./security#initializable +[Utils]: ./utils.mdx [AccessControl]: api/access-control [ShieldedAccessControl]: ./shielded-access-control.mdx [assertOnlyRole]: api/access-control#AccessControl-assertOnlyRole +[hasRole]: api/access-control#AccessControl-hasRole [grantRole]: api/access-control#AccessControl-grantRole [_grantRole]: api/access-control#AccessControl-_grantRole [_unsafeGrantRole]: api/access-control#AccessControl-_unsafeGrantRole [revokeRole]: api/access-control#AccessControl-revokeRole [_setRoleAdmin]: api/access-control#AccessControl-_setRoleAdmin -[computeAccountId]: api/access-control#AccessControl-computeAccountId +[computeAccountId]: api/utils#Utils-computeAccountId This module provides a role-based access control mechanism, where roles can be used to represent a set of permissions providing the flexibility to create different levels of account authorization. @@ -50,13 +51,37 @@ some users are regular users, some may be supervisors or managers, and a few wil AccessControl uses a witness-derived identity scheme. Each caller proves knowledge of a secret key by injecting it via the `wit_AccessControlSK` witness. The module computes an account identifier -as `persistentHash(secretKey)`, which is a commitment that hides the key while providing a stable, +as `H(secretKey)` (a `persistentHash` of the key), which is a commitment that hides the key while providing a stable, pseudonymous on-chain identity. -The same secret key produces the same identity across all contracts (analogous to Solidity's `msg.sender`). +What is public and what is private: + +* **On the ledger**: `_operatorRoles`, a public map from role identifier to account identifier to a Boolean, + and `_adminRoles`. Every grant, revoke, and role check discloses the role identifier and the account identifier it touches, + so an observer can see which identifiers hold which roles. +* **Held privately by each account**: the 32-byte `secretKey`, supplied through `wit_AccessControlSK` at proving time. + The key never reaches the ledger; only `H(secretKey)` does. + +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + +The same secret key produces the same identity across all contracts. Users who desire cross-contract unlinkability can use different secret keys per contract at the wallet layer. -To derive an account identifier off-chain (e.g. before passing it to a grant operation), use [computeAccountId]. +To derive an account identifier off-chain (e.g. before passing it to a grant operation), +use [computeAccountId] from the [Utils] module: + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} +``` + +A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side. +The [Utils guide](./utils#deriving-an-identifier-off-chain) shows the equivalent computation with the Compact runtime. ### Using `AccessControl` @@ -164,7 +189,7 @@ an internal circuit that is useful when programmatically assigning roles (such a But what if we later want to grant the 'minter' role to additional accounts? By default, **accounts with a role cannot grant it or revoke it from other accounts**: -all having a role does is making the `hasRole` check pass. +all having a role does is making the [hasRole] check pass. To grant and revoke roles dynamically, you will need help from the _role's admin_. Every role has an associated admin role, @@ -212,7 +237,7 @@ constructor( BURNER_ROLE = persistentHash>(pad(32, "BURNER_ROLE")); // Grant the default admin role to the provided account identifier. // This account will be able to grant and revoke any roles. - // The account identifier should be derived off-chain using computeAccountId(secretKey). + // The account identifier should be derived off-chain using Utils_computeAccountId(secretKey). AccessControl__grantRole(AccessControl_DEFAULT_ADMIN_ROLE(), defaultAdmin); } @@ -232,8 +257,10 @@ Note that, unlike the previous examples, no accounts are granted the 'minter' or However, because those roles' admin role is the default admin role, and _that_ role was granted to `defaultAdmin`, that same account can call [grantRole] to give minting or burning permission, and [revokeRole] to remove it. -The `defaultAdmin` account identifier should be derived off-chain using `AccessControl_computeAccountId(secretKey)`, +The `defaultAdmin` account identifier should be derived off-chain using [computeAccountId] from the [Utils] module +(`Utils_computeAccountId(secretKey)`), where `secretKey` is a 32-byte cryptographically secure random value held by the deployer. +Pass it as `left, ContractAddress>(accountId)`. Dynamic role allocation is often a desirable property, for example in systems where trust in a participant may vary over time. @@ -241,11 +268,11 @@ It can also be used to support use cases such as KYC, where the list of role-bearers may not be known up-front, or may be prohibitively expensive to include in a single transaction. -### Experimental features +### Unsafe circuits -This module offers an experimental circuit that allows access control permissions to be granted to contract addresses [_unsafeGrantRole]. +This module offers a circuit that allows access control permissions to be granted to contract addresses, [_unsafeGrantRole]. -Note that the circuit name is very explicit ("unsafe") with this experimental circuit. +Note that the circuit name is very explicit ("unsafe"). Until contract-to-contract calls are supported, there is no direct way for a contract to call permissioned circuits of other contracts or grant/revoke role permissions. diff --git a/content/contracts-compact/api/access-control.mdx b/content/contracts-compact/api/access-control.mdx index 1686289b..4e8d4fb9 100644 --- a/content/contracts-compact/api/access-control.mdx +++ b/content/contracts-compact/api/access-control.mdx @@ -49,7 +49,7 @@ More complex role relationships can be created by using [_setRoleAdmin](#AccessC ## AccessControl [toc] [#AccessControl] ```ts @@ -84,6 +84,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; ### Witnesses [toc] [#AccessControl-Witnesses] ### Witnesses [!toc] [#AccessControl-Witnesses] +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + #### wit_AccessControlSK [toc] [#AccessControl-wit_AccessControlSK] #### [!toc] [#AccessControl-wit_AccessControlSK] - The default admin role for all roles. Returns zero bytes (`default>`). - Only accounts with this role will be able to grant or revoke other roles - unless custom admin roles are created via [_setRoleAdmin](#AccessControl-_setRoleAdmin). + The default admin role for all roles. Only accounts with this role will be able + to grant or revoke other roles unless custom admin roles are created via [_setRoleAdmin](#AccessControl-_setRoleAdmin). #### hasRole [toc] [#AccessControl-hasRole] @@ -123,19 +125,6 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; Returns `true` if `account` has been granted `roleId`. -#### _hasRole [toc] [#AccessControl-_hasRole] -#### [!toc] [#AccessControl-_hasRole] - - Returns `true` if `account` has been granted `roleId`. - Internal variant that assumes `account` has already been canonicalized by the caller. - External consumers should use `hasRole` which canonicalizes the input automatically. - - #### assertOnlyRole [toc] [#AccessControl-assertOnlyRole] #### [!toc] [#AccessControl-assertOnlyRole] Reverts if the caller cannot prove ownership of `roleId`. - The caller's identity is derived from the `wit_AccessControlSK` witness as `persistentHash(secretKey)`. + In the case of an external (non-contract) caller, the caller’s identity is + derived from the [wit_AccessControlSK](#AccessControl-wit_AccessControlSK) witness as `persistentHash(secretKey)`. Requirements: @@ -157,7 +147,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; Reverts if `account` is missing `roleId`. @@ -175,8 +165,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; complexity="k=9, rows=373" modulePrefix="AccessControl" > - Returns the admin role that controls `roleId` or a byte array with all zero bytes if `roleId` doesn't exist. - See [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole). + Returns the admin role that controls `roleId` or + a byte array with all zero bytes if `roleId` doesn't exist. See [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole). To change a role's admin use [_setRoleAdmin](#AccessControl-_setRoleAdmin). @@ -191,11 +181,6 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; > Grants `roleId` to `account`. - - Granting roles to contract addresses is currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents permanently disabling access to a circuit. - - Requirements: * `account` must not be a ContractAddress. @@ -228,17 +213,14 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; > Revokes `roleId` from the calling account. - Roles are often managed via [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole): - this circuit's purpose is to provide a mechanism for accounts to lose their privileges - if they are compromised (such as when a trusted device is misplaced). - - The caller's identity is derived from the `wit_AccessControlSK` witness as - `persistentHash(secretKey)`. The `callerConfirmation` parameter must match - the caller's computed account identifier to prevent accidental renunciation. - - We do not provide functionality for smart contracts to renounce roles because self-executing transactions are not supported on Midnight at this time. - We may revisit this in future if this feature is made available in Compact. + Roles are often managed via [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole): this circuit's + purpose is to provide a mechanism for accounts to lose their privileges + if they are compromised (such as when a trusted device is misplaced). + + The caller's identity is derived from the [wit_AccessControlSK](#AccessControl-wit_AccessControlSK) witness as + `persistentHash(secretKey)`. The `callerConfirmation` parameter must match + the caller's computed account identifier to prevent accidental renunciation. Requirements: @@ -267,14 +249,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; modulePrefix="AccessControl" > Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. - Internal circuit without access restriction. - - Granting roles to contract addresses is currently disallowed in this circuit until contract-to-contract interactions are supported in Compact. - This restriction prevents permanently disabling access to a circuit. - - Requirements: * `account` must not be a ContractAddress. @@ -288,12 +264,12 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; complexity="k=11, rows=1142" modulePrefix="AccessControl" > - Unsafe variant of [_grantRole](#AccessControl-_grantRole). + Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. + Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress. - - Granting roles to contract addresses is considered unsafe because contract-to-contract calls are not currently supported. - Granting a role to a smart contract may render a circuit permanently inaccessible. - Once contract-to-contract calls are supported, this circuit may be deprecated. + + External smart contracts cannot call the access control contract at this time, so granting a role to a ContractAddress may + render a circuit permanently inaccessible. @@ -306,42 +282,5 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; modulePrefix="AccessControl" > Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. - Internal circuit without access restriction. - -#### _computeAccountId [toc] [#AccessControl-_computeAccountId] -#### [!toc] [#AccessControl-_computeAccountId] - - Computes the caller's account identifier from the `wit_AccessControlSK` witness. - - ID Derivation: `accountId = persistentHash(secretKey)` - - The result is a 32-byte commitment that uniquely identifies the caller. - - -#### computeAccountId [toc] [#AccessControl-computeAccountId] -#### [!toc] [#AccessControl-computeAccountId] - - Computes an account identifier without on-chain state, allowing a user to derive - their identity commitment before submitting it in a grant or revoke operation. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. - - ID Derivation: `accountId = persistentHash(secretKey)` - - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the privacy guarantees of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss. - A lost key cannot be recovered. - - diff --git a/content/contracts-compact/api/confidential-fungible-token.mdx b/content/contracts-compact/api/confidential-fungible-token.mdx new file mode 100644 index 00000000..ff4a2660 --- /dev/null +++ b/content/contracts-compact/api/confidential-fungible-token.mdx @@ -0,0 +1,747 @@ +--- +title: ConfidentialFungibleToken API +--- + +This module provides the full ConfidentialFungibleToken module API. + + + For an overview of the module, read the [ConfidentialFungibleToken guide](../confidential-fungible-token). + The `ElGamal_Ciphertext` and `EcdhMask_Ciphertext` types used below are documented in the [Crypto API](./crypto). + + +The page covers the following modules: + +- [ConfidentialFungibleToken](#ConfidentialFungibleToken): the token with ElGamal-encrypted balances. +- [ConfidentialFungibleTokenPublicSupply](#ConfidentialFungibleTokenPublicSupply): an extension that tracks a public total supply. + +## ConfidentialFungibleToken [toc] [#ConfidentialFungibleToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibleToken"; +``` + +--- + +### Types [toc] [#ConfidentialFungibleToken-Types] +### Types [!toc] [#ConfidentialFungibleToken-Types] + +#### EscrowEntry [toc] [#ConfidentialFungibleToken-EscrowEntry] +#### [!toc] [#ConfidentialFungibleToken-EscrowEntry] + + Per-(owner, spender) encrypted escrow. Stored as two + ciphertexts of the same value: one to the spender, one to the owner. + No public cap. + + +### Ledger [toc] [#ConfidentialFungibleToken-Ledger] +### Ledger [!toc] [#ConfidentialFungibleToken-Ledger] + +#### _balances [toc] [#ConfidentialFungibleToken-_balances] +#### [!toc] [#ConfidentialFungibleToken-_balances] + + Dual-balance grief fix. `_balances` is the SPENDABLE (confirmed) balance that + debits draw from; `_pending` is the incoming pool that credits land in. Only + the owner moves pending -> spendable, via [sweep](#ConfidentialFungibleToken-sweep). So a third party spamming + dust credits cannot churn a victim's spendable ciphertext and invalidate the + victim's in-flight spend proofs (a liveness grief). A wallet's total balance + is [balanceOf](#ConfidentialFungibleToken-balanceOf) (spendable) + [pendingOf](#ConfidentialFungibleToken-pendingOf) (pending). + + +#### _pending [toc] [#ConfidentialFungibleToken-_pending] +#### [!toc] [#ConfidentialFungibleToken-_pending] + + Mapping from account identifiers to their pending balance ciphertext, where incoming credits land. + Only the owner moves pending value into `_balances`, with [sweep](#ConfidentialFungibleToken-sweep). + + +#### _encryptionKeys [toc] [#ConfidentialFungibleToken-_encryptionKeys] +#### [!toc] [#ConfidentialFungibleToken-_encryptionKeys] + + Mapping from account identifiers to their ElGamal public key, set by [register](#ConfidentialFungibleToken-register). + Membership in this map is what "registered" means throughout the module. + + +#### _memos [toc] [#ConfidentialFungibleToken-_memos] +#### [!toc] [#ConfidentialFungibleToken-_memos] + + Mapping from account identifiers to their memo list. Every credit pushes one memo carrying the credited amount, + encrypted to the recipient's key, to the front of the list. Memos record credits only; debits leave none. + + +#### _escrow [toc] [#ConfidentialFungibleToken-_escrow] +#### [!toc] [#ConfidentialFungibleToken-_escrow] + + Mapping from owner accounts to spender accounts to their [EscrowEntry](#ConfidentialFungibleToken-EscrowEntry). + + +#### _escrowSpendEpochs [toc] [#ConfidentialFungibleToken-_escrowSpendEpochs] +#### [!toc] [#ConfidentialFungibleToken-_escrowSpendEpochs] + + Per-(owner, spender) escrow-spend counter, folded into the randomness + `_spendEscrow` derives so repeat spends by one spender against one owner + never share a one-time pad. It restarts for each pair, which is why both + counterparties are bound in too; see `SpendRandomnessPreimage`. + + Kept out of `EscrowEntry`: [approve](#ConfidentialFungibleToken-approve) replaces that entry wholesale, so a + counter living there would restart on every re-approve. + + Note that this exposes a per-pair spend count which is already derivable + from tx history. + + +#### _creditEpochs [toc] [#ConfidentialFungibleToken-_creditEpochs] +#### [!toc] [#ConfidentialFungibleToken-_creditEpochs] + + Per-account credit counter. `_credit` uses it as the nonce that keeps each + credit's randomness distinct, so it MUST only ever increase. No circuit + resets it. It is kept out of `_memos` for that reason: the memo list is + prunable by its owner, and a nonce that could be walked back would let a + reused seed repeat an earlier credit's randomness. The cost is that an + account's lifetime credit count is now permanently readable from state. + + `Counter` is chosen for no-reset monotonicity, not commutativity. Its + `increment` emits no pinned read, but `_credit` reads the value back to + derive the nonce, and that read pins the recipient's key. Credits to one + recipient still serialize; see the Concurrency note above. + + +#### _isInitialized [toc] [#ConfidentialFungibleToken-_isInitialized] +#### [!toc] [#ConfidentialFungibleToken-_isInitialized] + + Boolean indicating if initialized. + + +#### _name [toc] [#ConfidentialFungibleToken-_name] +#### [!toc] [#ConfidentialFungibleToken-_name] + + The immutable token name. + + +#### _symbol [toc] [#ConfidentialFungibleToken-_symbol] +#### [!toc] [#ConfidentialFungibleToken-_symbol] + + The immutable token symbol. + + +#### _decimals [toc] [#ConfidentialFungibleToken-_decimals] +#### [!toc] [#ConfidentialFungibleToken-_decimals] + + The immutable token decimals. + + +### Witnesses [toc] [#ConfidentialFungibleToken-Witnesses] +### Witnesses [!toc] [#ConfidentialFungibleToken-Witnesses] + +The TypeScript witnesses in the repository are test-only and are not published; a consumer authors its own. + +#### wit_ConfidentialTokenSK [toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenSK] +#### [!toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenSK] + + Returns the caller's account secret. Verified in-circuit by + matching `persistentHash(SK)` against the public accountId. + + +#### wit_ConfidentialTokenEK [toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenEK] +#### [!toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenEK] + + Returns the caller's ElGamal secret. Verified in-circuit by + `ElGamal_assertDecryptsTo`, which re-derives the pk and compares + against the stored pk. + + +#### wit_PlaintextBalance [toc] [#ConfidentialFungibleToken-wit_PlaintextBalance] +#### [!toc] [#ConfidentialFungibleToken-wit_PlaintextBalance] + + Returns the wallet's cached plaintext for the given + ciphertext. Called whenever the circuit needs to verify a ciphertext's + plaintext value (sender's main balance, escrow ciphertext during spend, + prior escrow during refund, etc.). + + The ciphertext is passed explicitly so the wallet can route to the right + cached plaintext by ciphertext identity. This avoids call-order + conventions (which would be brittle to circuit refactors) and lets the + wallet maintain a `Map` lookup. + + Verified in-circuit by `ElGamal_assertDecryptsTo`: a wrong plaintext + fails the assertion `Dec(ct, EK) == claimedValue`. + + +#### wit_RandomnessSeed [toc] [#ConfidentialFungibleToken-wit_RandomnessSeed] +#### [!toc] [#ConfidentialFungibleToken-wit_RandomnessSeed] + + Returns a single 32-byte seed from which all per-operation + randomness (and each memo's ephemeral) is derived in-circuit. + + Seed freshness is load-bearing for confidentiality on EVERY path (see the + module header). The derivation is deterministic and public, so an observer + who knows the seed can strip a memo's mask and read the delivered amount, and + recover balance/escrow amounts from the public ciphertexts. Seed REUSE leaks + plaintext differences and the circuit does NOT prevent it on most paths. + `_spendEscrow` is an exception (its randomness folds the spend epoch and both + counterparties); the credit path folds the recipient's `_creditEpochs` + counter as defense in depth, and that covers credits alone. The seed MUST be + fresh, high-entropy, and secret for every invocation. + + +### Circuits [toc] [#ConfidentialFungibleToken-Circuits] +### Circuits [!toc] [#ConfidentialFungibleToken-Circuits] + +#### MAX_TRANSFER_VALUE [toc] [#ConfidentialFungibleToken-MAX_TRANSFER_VALUE] +#### [!toc] [#ConfidentialFungibleToken-MAX_TRANSFER_VALUE] + + Maximum allowed value per single transfer / mint / burn / + approve: the `Uint<128>` maximum. The prior `2^48` cap existed only to keep + wallet-side discrete-log recovery tractable; with the ECDH memo delivering + values directly (see `crypto/EcdhMask`), that constraint is gone. Values stay + well under the Jubjub scalar-field order, so homomorphic sums do not wrap. + + + This equals the argument type's own maximum, so the per-op check is + redundant today. KEEP IT: it compares against a hard constant rather than + the argument type, so it still catches a later widening of the transfer + type — which is what the balance-claim width is sized against. + + + +#### initialize [toc] [#ConfidentialFungibleToken-initialize] +#### [!toc] [#ConfidentialFungibleToken-initialize] + + Initializes the token's name, symbol, and decimals. Intended to + be called once, from the deploying contract's constructor. + + Requirements: + + * Contract is not already initialized. + + +#### balanceOf [toc] [#ConfidentialFungibleToken-balanceOf] +#### [!toc] [#ConfidentialFungibleToken-balanceOf] + + Returns `account`'s balance ciphertext, or the default (zero) + ciphertext if the account is unregistered. The amount is encrypted; only the + account (or an authorized viewer) can decrypt it. + + Requirements: + + * Contract is initialized. + + +#### pendingOf [toc] [#ConfidentialFungibleToken-pendingOf] +#### [!toc] [#ConfidentialFungibleToken-pendingOf] + + Returns `account`'s PENDING balance ciphertext (incoming credits + not yet swept into spendable), or Enc(0) if unregistered. The spendable + balance is [balanceOf](#ConfidentialFungibleToken-balanceOf); a wallet's total is the sum of the two. See the + dual-balance note on the ledger declarations. + + Requirements: + + * Contract is initialized. + + +#### allowance [toc] [#ConfidentialFungibleToken-allowance] +#### [!toc] [#ConfidentialFungibleToken-allowance] + + Returns the escrow entry (the encrypted allowance) that `owner` + granted `spender`. When no allowance exists, returns a well-formed + encrypted-zero entry (its encrypted amounts decrypt to 0, analogous to an + ERC-20 zero allowance) rather than `default`: the returned + Jubjub points are on-curve, so the view is safe to prove (e.g. through a + contract-to-contract call) and safe for a wallet to decrypt, whereas a zeroed + default holds off-curve points. The cap is encrypted, not public. + + + "No allowance" and "an allowance of 0" both read as encrypted-zero; + a caller that must distinguish them reads `_escrow` membership off-chain. This + getter grants nothing, the spend path ([transferFrom](#ConfidentialFungibleToken-transferFrom)/`burnFrom`) gates on + `_escrow` membership independently, so a non-existent allowance is unspendable. + + + +#### isRegistered [toc] [#ConfidentialFungibleToken-isRegistered] +#### [!toc] [#ConfidentialFungibleToken-isRegistered] + + Returns whether `account` has registered an encryption key. + + +#### register [toc] [#ConfidentialFungibleToken-register] +#### [!toc] [#ConfidentialFungibleToken-register] + + Registers the caller: derives their encryption public key from + [wit_ConfidentialTokenEK](#ConfidentialFungibleToken-wit_ConfidentialTokenEK) and initializes their balance to an encryption of + zero. Registration is a prerequisite for sending or receiving. + + + Returns the caller's `accountId`. A composing contract can gate on + the returned value (e.g. assert it is KYC-approved) to restrict who may + register. See the module header note on caller-identity gating. + + + Requirements: + + * Contract is initialized. + * The caller is not already registered. + + +#### _mint [toc] [#ConfidentialFungibleToken-_mint] +#### [!toc] [#ConfidentialFungibleToken-_mint] + + Mints `value` to `account`: `_credit` under its intent name. + The building block for a composing contract's gated `mint`; it performs no + supply accounting. Pair it with a supply tracker (see + `extensions/ConfidentialFungibleTokenPublicSupply`) if the deployment tracks + `totalSupply`. + + Requirements: + + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * `account` is registered. + + +#### _burn [toc] [#ConfidentialFungibleToken-_burn] +#### [!toc] [#ConfidentialFungibleToken-_burn] + + Burns `value` from the caller's own balance: `_debit` under + its intent name. The building block for a composing contract's `burn`; it + performs no supply accounting. Pair it with a supply tracker (see + `extensions/ConfidentialFungibleTokenPublicSupply`) if the deployment tracks + `totalSupply`. + + Requirements: + + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * The caller is registered and has a balance of at least `value`. + + +#### transfer [toc] [#ConfidentialFungibleToken-transfer] +#### [!toc] [#ConfidentialFungibleToken-transfer] + + Transfers `value` from the caller to `to`: debits the caller, + credits `to`, and pushes an encrypted memo to `to`. The amount is hidden; + the (sender, recipient) pair is public. + + + Returns the caller's (sender's) `accountId` for caller-side gating. + The sender is derived from the same authentication `_debit` performs, so the + returned id is exactly the account whose funds moved. + + + Requirements: + + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * The caller is registered with a balance of at least `value`. + * `to` is registered and is not the caller (no self-transfer). + + +#### _move [toc] [#ConfidentialFungibleToken-_move] +#### [!toc] [#ConfidentialFungibleToken-_move] + + The conserving value-movement primitive: debits the caller and + credits `to` by the same `value`, net zero. It NEVER changes supply, so it is + the safe surface for composition and the primitive this supply-free base + exposes. [transfer](#ConfidentialFungibleToken-transfer) is [_move](#ConfidentialFungibleToken-_move) plus a self-transfer guard; supply-changing + operations (`mint`/`burn`) live in a separate layer outside this conserving + surface, and only they may break conservation. + + + Like [transfer](#ConfidentialFungibleToken-transfer), the credit lands in the recipient's pending pool + (see the dual-balance note); the caller sweeps their own incoming value. + + + Self-move (`to` == caller) is PERMITTED here, unlike [transfer](#ConfidentialFungibleToken-transfer), which + rejects it. A self-move is conserving and safe: it debits the caller's + spendable and credits the caller's own pending (net zero, distinct ledger + cells; the value is recovered with [sweep](#ConfidentialFungibleToken-sweep)). [transfer](#ConfidentialFungibleToken-transfer)'s self-transfer guard + is therefore a UX choice, not a safety requirement: it spares users a + self-transfer with no foreseeable benefit (a shuffle of spendable into + pending plus a self-memo). This primitive stays permissive so a composer can + set its own policy; add a `to != caller` check in the wrapper to reject a + self-move. + + Requirements: + + * Contract is initialized. + * The caller is registered with a spendable balance of at least `value`. + * `to` is registered. + + +#### sweep [toc] [#ConfidentialFungibleToken-sweep] +#### [!toc] [#ConfidentialFungibleToken-sweep] + + Sweeps the caller's incoming `pending` pool into their + `spendable` balance. Credits land in `pending` (see the dual-balance note); + this [sweep](#ConfidentialFungibleToken-sweep) is the ONLY path from pending into spendable, and only the owner + can invoke it (the caller proves BOTH witness secrets: the account secret + identifies the account, the encryption secret proves the caller can read it). + So a third party spamming credits cannot force a victim's spendable ciphertext + to change, which is what would otherwise invalidate the victim's in-flight + spend proofs (a liveness grief). + + + Purely homomorphic: adds the two ciphertexts (same key) and resets + pending to Enc(0). No plaintext is needed; the wallet already learned the + pending amounts from its memos. + + + Requirements: + + * Contract is initialized. + * The caller is registered. + * The caller holds the encryption secret registered for their account. + + +#### approve [toc] [#ConfidentialFungibleToken-approve] +#### [!toc] [#ConfidentialFungibleToken-approve] + + Sets `spender`'s allowance over the caller's balance to `value` + via an escrow: the caller's main balance is debited by `value`, and matching + encryptions are stored for the spender and the owner. Re-approving first + refunds any prior escrow, so allowances replace rather than stack. The cap + is not public. + + + Returns the caller's (approver's) `accountId` for caller-side gating. + + + Also stores an owner-readable `ownerMemo` (ECDH one-time pad; see + `crypto/EcdhMask`) in the escrow entry, carrying the current remaining + allowance (= `value` here, refreshed to the reduced remaining on every + spend). This is what lets the owner prove the post-refund balance on a later + re-approve or revoke after a partial spend; without it a partial spend would + strand the escrow. See the `EscrowEntry` field doc. + + Requirements: + + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * `spender` is registered, is not the zero account, and is not the caller. + * The caller is registered with a balance of at least `value`. + + +#### transferFrom [toc] [#ConfidentialFungibleToken-transferFrom] +#### [!toc] [#ConfidentialFungibleToken-transferFrom] + + Spends `value` from `fromAddress`'s escrow (granted to the + caller via [approve](#ConfidentialFungibleToken-approve)) and credits it to `to`, reducing both escrow copies by + `value`. The amount is hidden. + + + Returns the caller's (spender's) `accountId` for caller-side gating. + + + Requirements: + + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * `to` is registered and is not the zero account. + * An escrow from `fromAddress` to the caller exists with at least `value`. + + +#### clearMemos [toc] [#ConfidentialFungibleToken-clearMemos] +#### [!toc] [#ConfidentialFungibleToken-clearMemos] + + Clears the caller's memo list, letting a wallet that has folded + its memos into its local balance cache prune on-chain memo storage. + + `expectedEpoch` is the caller's view of its `_creditEpochs` value at the + moment it read the list. The call is rejected unless that still matches, so + a credit landing between the read and this transaction is never silently + discarded. The wallet retries against the memos it has now seen. A wallet + therefore reads its memo list and its epoch together, folds the memos into + its cache, and passes that epoch back here. + + + Clearing memos is destructive and can permanently lock funds. The + on-chain balance ciphertext is not directly decryptable (discrete-log + bound), so a wallet reconstructs its balance by replaying memos. Clearing + them without a durable off-chain record of the balance loses the ability to + recover it; and because spending requires proving the balance plaintext, the + funds become unspendable, not merely undisplayable. Clear only memos already + folded into a durable cache/backup. The epoch check only rules out a credit + the caller has not seen; it does not make clearing safe for memos the caller + never folded in. + + + Requirements: + + * Contract is initialized. + * The caller is registered. + * The caller holds the encryption secret registered for their account. + * The caller has a memo list. + * `expectedEpoch` equals the caller's current credit epoch. + + +#### computeAccountId [toc] [#ConfidentialFungibleToken-computeAccountId] +#### [!toc] [#ConfidentialFungibleToken-computeAccountId] + + Derives the public `accountId` from a secret key, as + `persistentHash(sk)`. Pure, so a wallet can compute its own account + identifier off-chain (no proof) before transacting. + + Delegates to `Utils.computeAccountId` rather than reimplementing the + hash. The identifier is deliberately GLOBAL. The same key material yields + the same id in every module that derives it. + + +#### _burnFrom [toc] [#ConfidentialFungibleToken-_burnFrom] +#### [!toc] [#ConfidentialFungibleToken-_burnFrom] + + Burns `value` from `fromAddress` by consuming an escrow that + owner granted to the caller: `_spendEscrow` under its intent name, with no + recipient credit. The building block for a composing contract's + `burnFrom`; it performs no supply accounting (see [_burn](#ConfidentialFungibleToken-_burn)). + + Requirements: + + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * An escrow from `fromAddress` to the caller exists with at least `value`. + + +--- + +## ConfidentialFungibleTokenPublicSupply [toc] [#ConfidentialFungibleTokenPublicSupply] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/ConfidentialFungibleTokenPublicSupply"; +``` + +Optional standalone extension that adds public supply accounting to a ConfidentialFungibleToken. It imports no token +module and needs no initialization. "Public" supply means the total, and therefore each mint and burn amount, is +disclosed on chain through the delta; balances stay confidential. The composing contract pairs each accounting block +with the matching token operation. + +--- + +### Ledger [toc] [#ConfidentialFungibleTokenPublicSupply-Ledger] +### Ledger [!toc] [#ConfidentialFungibleTokenPublicSupply-Ledger] + +#### _totalSupply [toc] [#ConfidentialFungibleTokenPublicSupply-_totalSupply] +#### [!toc] [#ConfidentialFungibleTokenPublicSupply-_totalSupply] + + The public circulating supply. Mint and burn disclose their amounts + through the delta on this value. + + +### Witnesses [toc] [#ConfidentialFungibleTokenPublicSupply-Witnesses] +### Witnesses [!toc] [#ConfidentialFungibleTokenPublicSupply-Witnesses] + +None. + +### Circuits [toc] [#ConfidentialFungibleTokenPublicSupply-Circuits] +### Circuits [!toc] [#ConfidentialFungibleTokenPublicSupply-Circuits] + +#### _addSupply [toc] [#ConfidentialFungibleTokenPublicSupply-_addSupply] +#### [!toc] [#ConfidentialFungibleTokenPublicSupply-_addSupply] + + Increases [totalSupply](#ConfidentialFungibleTokenPublicSupply-totalSupply) by `value`. Call alongside the token + module's `_mint`. The added amount is disclosed via the supply delta. + + Requirements: + + * `totalSupply + value` does not overflow `Uint<128>`. + + +#### _subSupply [toc] [#ConfidentialFungibleTokenPublicSupply-_subSupply] +#### [!toc] [#ConfidentialFungibleTokenPublicSupply-_subSupply] + + Decreases [totalSupply](#ConfidentialFungibleTokenPublicSupply-totalSupply) by `value`. Call alongside the token + module's `_burn` or `_burnFrom`. The removed amount is disclosed via the + supply delta. + + Requirements: + + * [totalSupply](#ConfidentialFungibleTokenPublicSupply-totalSupply) is at least `value`. + + +#### totalSupply [toc] [#ConfidentialFungibleTokenPublicSupply-totalSupply] +#### [!toc] [#ConfidentialFungibleTokenPublicSupply-totalSupply] + + Returns the public total token supply. + diff --git a/content/contracts-compact/api/crypto.mdx b/content/contracts-compact/api/crypto.mdx new file mode 100644 index 00000000..dc5b7561 --- /dev/null +++ b/content/contracts-compact/api/crypto.mdx @@ -0,0 +1,523 @@ +--- +title: Crypto API +--- + +This page provides the full API for the Crypto modules. + + + For an overview of the modules, read the [Crypto guide](../crypto). + + +The page covers the following modules: + +- [ElGamal](#ElGamal): exponential ElGamal encryption over Jubjub with homomorphic addition and re-randomization. +- [EcdhMask](#EcdhMask): an ECDH-derived mask for encrypting a value to a recipient's public key. + +## ElGamal [toc] [#ElGamal] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/crypto/ElGamal"; +``` + +--- + +### Types [toc] [#ElGamal-Types] +### Types [!toc] [#ElGamal-Types] + +#### Ciphertext [toc] [#ElGamal-Ciphertext] +#### [!toc] [#ElGamal-Ciphertext] + + Single-receiver ElGamal ciphertext over Jubjub. + c1 = g^r + c2 = pk^r * m (lifted: m = g^v) + + +### Ledger [toc] [#ElGamal-Ledger] +### Ledger [!toc] [#ElGamal-Ledger] + +None. + +### Witnesses [toc] [#ElGamal-Witnesses] +### Witnesses [!toc] [#ElGamal-Witnesses] + +None. + +### Circuits [toc] [#ElGamal-Circuits] +### Circuits [!toc] [#ElGamal-Circuits] + +#### secretToScalar [toc] [#ElGamal-secretToScalar] +#### [!toc] [#ElGamal-secretToScalar] + + Maps a 32-byte secret to a valid Jubjub scalar. See the + module-level hash-to-scalar note for why `degradeToTransient` is required. + + Domain separation here is load-bearing, not cosmetic. This output is + the private key protecting every ciphertext held under an account. Public + account identifiers derived from `Utils.computeAccountId`, for example, + are computed as `persistentHash([secretKey])`, and that identifier + is stored in the clear as a ledger map key. Untagged, this circuit + would compute that same hash, so a secret used in both roles would + have its encryption key recoverable from a published identifier by applying + `degradeToTransient`. The tag makes the two derivations unrelated, so a + wallet MAY safely derive its account secret and its encryption secret from + common key material. + + The tag is FIRST, deliberately. [expandRandomness](#ElGamal-expandRandomness) hashes + `[seed, tag]` with both slots caller-supplied, so tagging this circuit as + `[secret, tag]` would make `secretToScalar(x)` identical to + `expandRandomness(x, )` reachable by passing this domain string + to the parameter that exists to receive domain strings. Leading with the + tag means reproducing this output through [expandRandomness](#ElGamal-expandRandomness) would require + passing the domain constant as the *seed*. Do not normalise the orderings. + + +#### derivePk [toc] [#ElGamal-derivePk] +#### [!toc] [#ElGamal-derivePk] + + Derives the ElGamal public key `pk = g^secretToScalar(ek)` + from a secret `ek`. + + +#### expandRandomness [toc] [#ElGamal-expandRandomness] +#### [!toc] [#ElGamal-expandRandomness] + + Deterministically expands a 32-byte seed into a Jubjub scalar + tagged by a distinguishing string, so a single witness seed can yield many + distinct randomness values within one circuit invocation. Distinct tags + always produce distinct outputs (collision resistance of the hash); the + caller is responsible for using distinct tags. + + +#### encryptZero [toc] [#ElGamal-encryptZero] +#### [!toc] [#ElGamal-encryptZero] + + The identity ciphertext `Enc(0)` (both components are the + curve identity). Useful for initializing a fresh balance. + + + This is a fixed, deterministic value `(O, O)` — NOT a hiding + encryption. It is publicly recognizable, so a slot left at [encryptZero](#ElGamal-encryptZero) is + distinguishable as never-written. Rerandomize (or credit with fresh `r`) + before treating it as private. + + + +#### encryptPoint [toc] [#ElGamal-encryptPoint] +#### [!toc] [#ElGamal-encryptPoint] + + Encrypts an arbitrary message point `m` under `pk` with + randomness `r`, producing the ciphertext `(g^r, pk^r * m)`. This is the + general (non-lifted) ElGamal encryption; [encrypt](#ElGamal-encrypt) is the lifted special + case `encryptPoint(pk, g^value, r)`. + + + `m` (like `pk`) is constrained into the prime-order subgroup by the + runtime (see the module-level subgroup note), so no in-circuit membership + check is performed. `pk` is asserted non-identity and `r` non-zero — the + two inputs that would zero the mask and expose `m` (see the weak-keys and + randomness notes). Decrypting `m` is only feasible when it is a known or + small-exponent point; the caller owns that concern (the lifted [encrypt](#ElGamal-encrypt) + bounds it via the discrete-log range). + + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### encrypt [toc] [#ElGamal-encrypt] +#### [!toc] [#ElGamal-encrypt] + + Produces a fresh ciphertext of `value` under `pk` with + randomness `r`, lifting `value` to the point `g^value`. Randomness is taken + as a `Field` directly to avoid a redundant `Bytes<32> -> Field` conversion + at every call site; [expandRandomness](#ElGamal-expandRandomness) produces `Field` for the same reason. + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### negate [toc] [#ElGamal-negate] +#### [!toc] [#ElGamal-negate] + + Negates a ciphertext componentwise: if `ct` encrypts `v` then + the result encrypts `-v` under the same key (with negated randomness). + This is the building block [sub](#ElGamal-sub) rests on. + + +#### add [toc] [#ElGamal-add] +#### [!toc] [#ElGamal-add] + + Homomorphically adds two ciphertexts: `Enc(a) + Enc(b)` + decrypts to `a + b`. This is the defining additive-homomorphism primitive + and combines two independently produced ciphertexts componentwise. + + + Both inputs MUST be encrypted under the same public key for the + result to decrypt under that key's secret; combining ciphertexts under + different keys yields a ciphertext no single key can open. The randomness + of the result is the sum of the inputs' randomness, so the result is a + valid fresh ciphertext under that key. + + + +#### sub [toc] [#ElGamal-sub] +#### [!toc] [#ElGamal-sub] + + Homomorphically subtracts two ciphertexts: `Enc(a) - Enc(b)` + decrypts to `a - b`. Implemented as `add(a, negate(b))`. + + + Same-key requirement as [add](#ElGamal-add). The caller MUST ensure the + underlying plaintext subtraction does not underflow; this circuit cannot + check it (see [subEncrypted](#ElGamal-subEncrypted)). + + + +#### scalarMul [toc] [#ElGamal-scalarMul] +#### [!toc] [#ElGamal-scalarMul] + + Homomorphically multiplies the plaintext of `ct` by a public + scalar `k`: if `ct` encrypts `v` then the result encrypts `k * v` under the + same key. Both components are scaled by `k`, so the randomness scales to + `k * r` and the result is a valid fresh ciphertext under that key. + + This is the scalar-multiplication leg of the homomorphism, and the only way + to express linear combinations: a weighted sum `Enc(3a + 5b)` is + `add(scalarMul(ca, 3), scalarMul(cb, 5))`, which repeated [add](#ElGamal-add) cannot do + for large weights. + + + `k` MUST be a valid Jubjub scalar (less than the subgroup order); + larger values fault `ecMul` (see the module-level hash-to-scalar note). The + caller also remains responsible for the discrete-log bound — `k * v` must + stay within the range a recovery search can clear. + + + +#### addEncrypted [toc] [#ElGamal-addEncrypted] +#### [!toc] [#ElGamal-addEncrypted] + + Homomorphically adds `value` to the plaintext encrypted by + `old`, producing a fresh ciphertext under `pk` with new randomness `r`. + Convenience over [add](#ElGamal-add) for folding a known plaintext into a ciphertext: + `add(old, encrypt(pk, value, r))`. + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### subEncrypted [toc] [#ElGamal-subEncrypted] +#### [!toc] [#ElGamal-subEncrypted] + + Homomorphically subtracts `value` from the plaintext encrypted + by `old`. Convenience over [sub](#ElGamal-sub): `sub(old, encrypt(pk, value, r))`. + + + The caller MUST ensure the underlying plaintext subtraction does + not underflow; this circuit cannot check it. Callers verify sufficiency by + reading the plaintext via their own witness and asserting it with + [assertDecryptsTo](#ElGamal-assertDecryptsTo) before calling. + + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### rerandomize [toc] [#ElGamal-rerandomize] +#### [!toc] [#ElGamal-rerandomize] + + Rerandomizes `ct`: returns a fresh ciphertext of the same + plaintext under the same key `pk`, using new randomness `r`. The output is + unlinkable to the input (it is a uniformly fresh encryption of the same + value) yet decrypts identically. Equivalent to homomorphically adding an + encryption of zero. + + + `r = 0` is rejected (it would be a no-op via the [encryptPoint](#ElGamal-encryptPoint) + zero-randomness assert). One silent failure mode remains that the circuit + cannot catch: a `pk` that is not the key `ct` was encrypted under corrupts + the plaintext (the result no longer decrypts to the original value). Pass + fresh randomness and the matching `pk`. + + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### assertKeyPair [toc] [#ElGamal-assertKeyPair] +#### [!toc] [#ElGamal-assertKeyPair] + + Asserts that `ek` is the secret for `pk`, i.e. + `derivePk(ek) == pk`. Lets a caller prove ownership of a public key + without touching any ciphertext or plaintext. + + Requirements: + + * `ek` is the secret for `pk` (`derivePk(ek) == pk`). + + +#### assertDecryptsToPoint [toc] [#ElGamal-assertDecryptsToPoint] +#### [!toc] [#ElGamal-assertDecryptsToPoint] + + Asserts that `ct` decrypts under `(pk, ek)` to the message + point `m`, and that `ek` is the secret for `pk`. This is the general + (non-lifted) verification; [assertDecryptsTo](#ElGamal-assertDecryptsTo) is the lifted special case + with `m = g^claimedValue`. + + Requirements: + + * `ek` is the secret for `pk`. + * `ct` decrypts under `(pk, ek)` to `m`. + + +#### assertDecryptsTo [toc] [#ElGamal-assertDecryptsTo] +#### [!toc] [#ElGamal-assertDecryptsTo] + + Asserts that `ct` decrypts under `(pk, ek)` to `claimedValue`, + and that `ek` is the secret for `pk`. This is the in-circuit verification + path an importing module uses to bind its witness-supplied `ek` and cached + plaintext to publicly stored state: a wrong `ek` fails the key check, a + wrong plaintext fails the decryption check. Lifted special case of + [assertDecryptsToPoint](#ElGamal-assertDecryptsToPoint) with the message point `g^claimedValue`. + + + `claimedValue` is the widest `Uint` Compact offers, so this parameter + never bounds a caller. A narrower value costs proportionally fewer rows: the + range constraint belongs to where the value is declared, not to this + signature. + + + + The claim domain must stay below the Jubjub subgroup order `l`: + verification compares `g^claimedValue`, so two claims congruent mod `l` are + indistinguishable. `Uint<248>` clears `l` (~2^251.86) by under 4 bits, and + that width is set by Compact's cap rather than by this bound. 251 is the + widest sound `Uint` if the cap ever rises. + + + Requirements: + + * `ek` is the secret for `pk`. + * `ct` decrypts under `(pk, ek)` to `claimedValue`. + + +--- + +## EcdhMask [toc] [#EcdhMask] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/crypto/EcdhMask"; +``` + +--- + +### Types [toc] [#EcdhMask-Types] +### Types [!toc] [#EcdhMask-Types] + +#### Ciphertext [toc] [#EcdhMask-Ciphertext] +#### [!toc] [#EcdhMask-Ciphertext] + + An ECDH one-time-pad ciphertext: the ephemeral public key and + the field-masked value. + + +### Ledger [toc] [#EcdhMask-Ledger] +### Ledger [!toc] [#EcdhMask-Ledger] + +None. + +### Witnesses [toc] [#EcdhMask-Witnesses] +### Witnesses [!toc] [#EcdhMask-Witnesses] + +None. + +### Circuits [toc] [#EcdhMask-Circuits] +### Circuits [!toc] [#EcdhMask-Circuits] + +#### kdf [toc] [#EcdhMask-kdf] +#### [!toc] [#EcdhMask-kdf] + + Derives the field mask from the ECDH shared secret point, + domain-separated by a caller-supplied `domain` tag. Hashes the point to + `Bytes<32>`, then re-hashes it with the domain tag and truncates into the + field via `degradeToTransient`. The consumer chooses `domain` (so this + module is not tied to any one protocol) and MUST use the same value on + encrypt and decrypt. + + Uses `persistentHash` deliberately: the recipient reproduces this mask + to decrypt, possibly across a platform upgrade, so it must be upgrade-stable. + + +#### encrypt [toc] [#EcdhMask-encrypt] +#### [!toc] [#EcdhMask-encrypt] + + Encrypts `value` to `recipientPk` under ephemeral scalar `e`. + + + `e` MUST be fresh per call (see the module freshness precondition). + + + Requirements: + + * `recipientPk` is not the identity point. + * `e` is non-zero, a valid Jubjub scalar (`< ℓ`), secret, and fresh per call. + + +#### decrypt [toc] [#EcdhMask-decrypt] +#### [!toc] [#EcdhMask-decrypt] + + Recovers the value from a ciphertext using the recipient's + secret scalar. Pure and off-chain (no proof needed to read a received value). + diff --git a/content/contracts-compact/api/fungible-token.mdx b/content/contracts-compact/api/fungible-token.mdx index b458cd81..d0120079 100644 --- a/content/contracts-compact/api/fungible-token.mdx +++ b/content/contracts-compact/api/fungible-token.mdx @@ -11,7 +11,7 @@ This module provides the full FungibleToken module API. ## FungibleToken [toc] [#FungibleToken] ```ts @@ -23,6 +23,18 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; ### Ledger [toc] [#FungibleToken-Ledger] ### Ledger [!toc] [#FungibleToken-Ledger] +#### _isInitialized [toc] [#FungibleToken-_isInitialized] +#### [!toc] [#FungibleToken-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + #### _balances [toc] [#FungibleToken-_balances] #### [!toc] [#FungibleToken-_balances] - Mapping from owner accounts to spender accounts to their allowances. + Mapping from owner accounts to spender accounts and their allowances. #### _totalSupply [toc] [#FungibleToken-_totalSupply] @@ -102,17 +114,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; ### Circuits [toc] [#FungibleToken-Circuits] ### Circuits [!toc] [#FungibleToken-Circuits] -#### ZERO [toc] [#FungibleToken-ZERO] -#### [!toc] [#FungibleToken-ZERO] - - Returns a canonical zero `Either` value for `Bytes<32>` and `ContractAddress`. - Returns the left variant (`Bytes<32>`) to avoid misleading contract-to-contract error messages. - - #### initialize [toc] [#FungibleToken-initialize] #### [!toc] [#FungibleToken-initialize] Initializes the contract by setting the name, symbol, and decimals. - - This MUST be called in the implementing contract's constructor. - Failure to do so can lead to an irreparable contract. - - Requirements: - - * Contract is not initialized. + This MUST be called in the implementing contract's constructor. Failure to do so + can lead to an irreparable contract. #### name [toc] [#FungibleToken-name] @@ -206,6 +202,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; > Returns the value of tokens owned by `account`. + Manually checks if `account` is a key in the map and returns 0 if it is not. + Requirements: * Contract is initialized. @@ -222,8 +220,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Moves a `value` amount of tokens from the caller's account to `to`. - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: @@ -245,8 +244,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Unsafe variant of [transfer](#FungibleToken-transfer) which allows transfers to contract addresses. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. @@ -266,8 +265,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" stackParams="true" > - Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` through [transferFrom](#FungibleToken-transferFrom). - This value changes when [approve](#FungibleToken-approve) or [transferFrom](#FungibleToken-transferFrom) are called. + Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` + through [transferFrom](#FungibleToken-transferFrom). This value changes when [approve](#FungibleToken-approve) or [transferFrom](#FungibleToken-transferFrom) are called. + + Manually checks if `owner` and `spender` are keys in the map and returns 0 if they are not. Requirements: @@ -304,11 +305,12 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" > Moves `value` tokens from `fromAddress` to `to` using the allowance mechanism. - `value` is then deducted from the caller's allowance. + `value` is the deducted from the caller's allowance. - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: @@ -337,8 +339,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Unsafe variant of [transferFrom](#FungibleToken-transferFrom) which allows transfers to contract addresses. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. @@ -365,12 +367,13 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" > Moves a `value` amount of tokens from `fromAddress` to `to`. - This circuit is equivalent to [transfer](#FungibleToken-transfer), and can be used to e.g. - implement automatic token fees, slashing mechanisms, etc. + This circuit is equivalent to [transfer](#FungibleToken-transfer), and can be used to + e.g. implement automatic token fees, slashing mechanisms, etc. - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: @@ -395,11 +398,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" stackParams="true" > - Unsafe variant of [_transfer](#FungibleToken-_transfer) which allows transfers to contract addresses. + Unsafe variant of [transferFrom](#FungibleToken-transferFrom) which allows transfers to contract addresses. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. @@ -410,27 +413,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; * `to` is not the zero address. -#### _update [toc] [#FungibleToken-_update] -#### [!toc] [#FungibleToken-_update] - - Transfers a `value` amount of tokens from `fromAddress` to `to`, - or alternatively mints (or burns) if `fromAddress` (or `to`) is the zero address. - - Requirements: - - * Contract is initialized. - - #### _mint [toc] [#FungibleToken-_mint] #### [!toc] [#FungibleToken-_mint] - Creates a `value` amount of tokens and assigns them to `account`, by transferring it from the zero address. - Relies on the `_update` mechanism. + Creates a `value` amount of tokens and assigns them to `account`, + by transferring it from the zero address. Relies on the `update` mechanism. - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: @@ -465,8 +448,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Unsafe variant of [_mint](#FungibleToken-_mint) which allows transfers to contract addresses. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. @@ -508,7 +491,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; stackParams="true" > Sets `value` as the allowance of `spender` over the `owner`'s tokens. - This circuit is equivalent to `approve`, and can be used to e.g. set automatic allowances for certain subsystems, etc. + This circuit is equivalent to [approve](#FungibleToken-approve), and can be used to + e.g. set automatic allowances for certain subsystems, etc. Requirements: @@ -539,48 +523,3 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; * `spender` must have at least an allowance of `value` from `owner`. -#### _computeAccountId [toc] [#FungibleToken-_computeAccountId] -#### [!toc] [#FungibleToken-_computeAccountId] - - Computes the caller's account identifier from the `wit_FungibleTokenSK` witness. - - ID Derivation: `accountId = persistentHash(secretKey)` - - The result is a 32-byte commitment that uniquely identifies the caller. - - -#### computeAccountId [toc] [#FungibleToken-computeAccountId] -#### [!toc] [#FungibleToken-computeAccountId] - - Computes an account identifier without on-chain state, allowing a user to derive - their identity commitment before submitting a token operation. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. - - ID Derivation: `accountId = persistentHash(secretKey)` - - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the security of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss. - A lost key cannot be recovered. - - - -#### _isTargetZero [toc] [#FungibleToken-_isTargetZero] -#### [!toc] [#FungibleToken-_isTargetZero] - - Returns `true` if `target`'s active branch (as indicated by `is_left`) holds the zero value. - diff --git a/content/contracts-compact/api/multi-token.mdx b/content/contracts-compact/api/multi-token.mdx index 5444b95b..e7942a17 100644 --- a/content/contracts-compact/api/multi-token.mdx +++ b/content/contracts-compact/api/multi-token.mdx @@ -11,11 +11,11 @@ This module provides the full MultiToken module API. ## MultiToken [toc] [#MultiToken] ```ts -import "./node_modules/@openzeppelin/token/src/MultiToken"; +import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; ``` --- @@ -23,6 +23,18 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; ### Ledger [toc] [#MultiToken-Ledger] ### Ledger [!toc] [#MultiToken-Ledger] +#### _isInitialized [toc] [#MultiToken-_isInitialized] +#### [!toc] [#MultiToken-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + #### _balances [toc] [#MultiToken-_balances] #### [!toc] [#MultiToken-_balances] Base URI for computing token URIs. + Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/`id`.json ### Witnesses [toc] [#MultiToken-Witnesses] @@ -72,18 +85,6 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; ### Circuits [toc] [#MultiToken-Circuits] ### Circuits [!toc] [#MultiToken-Circuits] -#### ZERO [toc] [#MultiToken-ZERO] -#### [!toc] [#MultiToken-ZERO] - - Returns a canonical zero `Either` value (left variant with zero `Bytes<32>`). - Used as the zero value for mint/burn operations in `_update`, where a zero - `fromAddress` signals a mint and a zero `to` signals a burn. - - #### initialize [toc] [#MultiToken-initialize] #### [!toc] [#MultiToken-initialize] Initializes the contract by setting the base URI for all tokens. - This MUST be called in the implementing contract's constructor. - Failure to do so can lead to an irreparable contract. - Requirements: - * Contract is not initialized. + * Must be called in the contract's constructor. #### uri [toc] [#MultiToken-uri] @@ -110,9 +108,11 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; complexity="k=8, rows=176" modulePrefix="MultiToken" > - This implementation returns the same URI for **all** token types. - It relies on the token type ID substitution mechanism defined in the EIP: [ERC1155-Metadata](https://eips.ethereum.org/EIPS/eip-1155#metadata). - Clients calling this function must replace the `\{id\}` substring with the actual token type ID. + This implementation returns the same URI for *all* token types. It relies + on the token type ID substitution mechanism defined in the EIP: + https://eips.ethereum.org/EIPS/eip-1155#metadata. + Clients calling this function must replace the `\{id\}` substring with the + actual token type ID. Requirements: @@ -151,7 +151,9 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; modulePrefix="MultiToken" > Enables or disables approval for `operator` to manage all of the caller's assets. - The caller's identity is derived from the `wit_MultiTokenSK` witness as `persistentHash(secretKey)`. + + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_MultiTokenSK](#MultiToken-wit_MultiTokenSK) + witness as `persistentHash(secretKey)`. Requirements: @@ -193,11 +195,14 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; > Transfers ownership of `value` amount of `id` tokens from `fromAddress` to `to`. The caller must be `fromAddress` or approved to transfer on their behalf. - The caller's identity is derived from the `wit_MultiTokenSK` witness as `persistentHash(secretKey)`. + + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_MultiTokenSK](#MultiToken-wit_MultiTokenSK) + witness as `persistentHash(secretKey)`. - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: @@ -225,11 +230,13 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; modulePrefix="MultiToken" > Transfers ownership of `value` amount of `id` tokens from `fromAddress` to `to`. - Does not impose restrictions on the caller, making it suitable for composition in higher-level contract logic. + Does not impose restrictions on the caller, making it suitable for composition + in higher-level contract logic. - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: @@ -241,28 +248,6 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; * `fromAddress` must have an `id` balance of at least `value`. -#### _update [toc] [#MultiToken-_update] -#### [!toc] [#MultiToken-_update] - - Transfers a value amount of tokens of type `id` from `fromAddress` to `to`. - This circuit will mint (or burn) if `fromAddress` (or `to`) is zero. - - Requirements: - - * Contract is initialized. - * If `fromAddress` is not zero, the balance of `id` of `fromAddress` must be >= `value`. - - #### _unsafeTransferFrom [toc] [#MultiToken-_unsafeTransferFrom] #### [!toc] [#MultiToken-_unsafeTransferFrom] Unsafe variant of [transferFrom](#MultiToken-transferFrom) which allows transfers to contract addresses. The caller must be `fromAddress` or approved to transfer on their behalf. - The caller's identity is derived from the `wit_MultiTokenSK` witness as `persistentHash(secretKey)`. + + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_MultiTokenSK](#MultiToken-wit_MultiTokenSK) + witness as `persistentHash(secretKey)`. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. @@ -311,11 +298,12 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; modulePrefix="MultiToken" > Unsafe variant of [_transfer](#MultiToken-_transfer) which allows transfers to contract addresses. - Does not impose restrictions on the caller, making it suitable as a low-level building block for advanced contract logic. + Does not impose restrictions on the caller, making it suitable as a low-level + building block for advanced contract logic. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. @@ -335,14 +323,20 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; complexity="k=6, rows=28" modulePrefix="MultiToken" > - Sets a new URI for all token types, by relying on the token type ID substitution mechanism defined in the MultiToken standard. + Sets a new URI for all token types, by relying on the token type ID + substitution mechanism defined in the MultiToken standard. See https://eips.ethereum.org/EIPS/eip-1155#metadata. - By this mechanism, any occurrence of the `\{id\}` substring - in either the URI or any of the values in the JSON file at said URI will be replaced by clients with the token type ID. - - For example, the `https://token-cdn-domain/\{id\}.json` URI would be interpreted by clients as - `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` for token type ID 0x4cce0. + + By this mechanism, any occurrence of the `\{id\}` substring in either the + URI or any of the values in the JSON file at said URI will be replaced by + clients with the token type ID. + + For example, the `https://token-cdn-domain/\{id\}.json` URI would be + interpreted by clients as + `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` + for token type ID 0x4cce0. + Requirements: @@ -361,8 +355,9 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; Creates a `value` amount of tokens of type `id`, and assigns them to `to`. - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: @@ -388,8 +383,8 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; Unsafe variant of [_mint](#MultiToken-_mint) which allows transfers to contract addresses. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. @@ -435,7 +430,11 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; modulePrefix="MultiToken" > Enables or disables approval for `operator` to manage all of `owner`'s assets. - This circuit does not check for access permissions but can be useful as a building block for more complex contract logic. + + + This circuit does not check for access permissions but can be useful as a building block + for more complex contract logic. + Requirements: @@ -444,47 +443,3 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; * `operator` is not zero. -#### _computeAccountId [toc] [#MultiToken-_computeAccountId] -#### [!toc] [#MultiToken-_computeAccountId] - - Computes the caller's account identifier from the `wit_MultiTokenSK` witness. - - ID Derivation: `accountId = persistentHash(secretKey)` - - The result is a 32-byte commitment that uniquely identifies the caller. - - -#### computeAccountId [toc] [#MultiToken-computeAccountId] -#### [!toc] [#MultiToken-computeAccountId] - - Computes an account identifier without on-chain state, allowing a user to derive - their identity commitment before submitting it in a token operation. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. - - ID Derivation: `accountId = persistentHash(secretKey)` - - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the security of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss — a lost key cannot be recovered. - - - -#### _isTargetZero [toc] [#MultiToken-_isTargetZero] -#### [!toc] [#MultiToken-_isTargetZero] - - Returns `true` if `target`'s active branch (as indicated by `is_left`) holds the zero value. - diff --git a/content/contracts-compact/api/multisig.mdx b/content/contracts-compact/api/multisig.mdx new file mode 100644 index 00000000..487d3469 --- /dev/null +++ b/content/contracts-compact/api/multisig.mdx @@ -0,0 +1,1318 @@ +--- +title: Multisig API +--- + +This page provides the full API for the Multisig modules. + + + For an overview of the modules, read the [Multisig guide](../multisig). + + +The page covers the following modules: + +- [Signer](#Signer): the registry of who may approve and how many approvals are required, generic over the signer identity type. +- [ProposalManager](#ProposalManager): the lifecycle of transfer proposals, independent of the token kind. +- [ShieldedTreasury](#ShieldedTreasury): holds one shielded coin per token color on the ledger and sends from it. +- [ShieldedTreasuryStateless](#ShieldedTreasuryStateless): receives and sends shielded coins without keeping any ledger state. +- [UnshieldedTreasury](#UnshieldedTreasury): holds unshielded tokens with a tracked per-color balance. +- [ForwarderPrivate](#ForwarderPrivate): forwards shielded deposits to a parent stored as a commitment. +- [ForwarderShielded](#ForwarderShielded): forwards shielded deposits to a public parent in the same transaction. +- [ForwarderUnshielded](#ForwarderUnshielded): forwards unshielded deposits to a public parent in the same transaction. + +## Signer [toc] [#Signer] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/Signer">; +``` + +`Signer` is generic over the signer identity type `T`, chosen at import time (for example `Bytes<32>`, `JubjubPoint`, or `Either`). +The module never resolves the caller; the consuming contract passes an identity in. + +--- + +### Ledger [toc] [#Signer-Ledger] +### Ledger [!toc] [#Signer-Ledger] + +#### _signers [toc] [#Signer-_signers] +#### [!toc] [#Signer-_signers] + + The set of registered signer identities. + + +#### _signerCount [toc] [#Signer-_signerCount] +#### [!toc] [#Signer-_signerCount] + + The number of registered signers (at most 255). + + +#### _threshold [toc] [#Signer-_threshold] +#### [!toc] [#Signer-_threshold] + + The minimum number of approvals required. Zero until it is set. + + +#### _isInitialized [toc] [#Signer-_isInitialized] +#### [!toc] [#Signer-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + +### Witnesses [toc] [#Signer-Witnesses] +### Witnesses [!toc] [#Signer-Witnesses] + +None. + +### Circuits [toc] [#Signer-Circuits] +### Circuits [!toc] [#Signer-Circuits] + +#### initialize [toc] [#Signer-initialize] +#### [!toc] [#Signer-initialize] + + Initializes the signer module with the given threshold + and an initial set of signers. + If used, it should only be called in the contract's constructor. + + Requirements: + + * If used, can only be called once (in the constructor). + * `thresh` must not be zero. + * `thresh` must not exceed the number of `signers`. + * `signers` must not contain duplicates. + + +#### assertSigner [toc] [#Signer-assertSigner] +#### [!toc] [#Signer-assertSigner] + + Asserts that the given caller is an active signer. + + Requirements: + + * `caller` must be a member of the signers registry. + + +#### assertThresholdMet [toc] [#Signer-assertThresholdMet] +#### [!toc] [#Signer-assertThresholdMet] + + Asserts that the given approval count meets the threshold. + + + `approvalCount` is supplied by the consuming contract and is + compared against the threshold only. This circuit never consults the signer + registry. Ensuring the count reflects distinct, currently-registered signers + is the consuming contract's responsibility, and a contract whose approval + accounting is not itself gated by [assertSigner](#Signer-assertSigner) gets no protection here. + + The count is deliberately left unbounded by `_signerCount`: approvals + collected before a signer was removed must still count toward a threshold + the remaining signers satisfy, and bounding the count would reject those. + See the stale-signer-set guidance in the module header. + + + Requirements: + + * Ledger threshold must be set (not be zero). An unconfigured module has a + zero threshold, so this rejects before any approval count is compared. + * `approvalCount` must be >= threshold. + + +#### getSignerCount [toc] [#Signer-getSignerCount] +#### [!toc] [#Signer-getSignerCount] + + Returns the current signer count. Returns zero when no + signers have been registered. + + +#### getThreshold [toc] [#Signer-getThreshold] +#### [!toc] [#Signer-getThreshold] + + Returns the approval threshold. Returns zero when the + threshold has not been set. + + +#### isSigner [toc] [#Signer-isSigner] +#### [!toc] [#Signer-isSigner] + + Returns whether the given account is an active signer. + + +#### _addSigner [toc] [#Signer-_addSigner] +#### [!toc] [#Signer-_addSigner] + + Adds a new signer to the registry. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * `signer` must not already be an active signer. + + +#### _removeSigner [toc] [#Signer-_removeSigner] +#### [!toc] [#Signer-_removeSigner] + + Removes a signer from the registry. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * `signer` must be an active signer. + * Removal must not drop signer count below threshold. + + +#### _changeThreshold [toc] [#Signer-_changeThreshold] +#### [!toc] [#Signer-_changeThreshold] + + Updates the approval threshold. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * `newThreshold` must not be zero. + * `newThreshold` must not exceed the current signer count. + + +#### _setThreshold [toc] [#Signer-_setThreshold] +#### [!toc] [#Signer-_setThreshold] + + Sets the approval threshold without checking + against the current signer count. + + + This is intended for use during contract construction + or custom setup flows where signers may not yet be registered. + + Leaving `newThreshold` above the signer count is permitted and expected + mid-setup, and is not self-enforcing: [assertThresholdMet](#Signer-assertThresholdMet) compares + `approvalCount` against the threshold alone, so a caller supplying a matching + count satisfies it even against an empty registry. Contracts must not gate + operations on [assertThresholdMet](#Signer-assertThresholdMet) while setup is incomplete. Lower the + threshold with [_setThreshold](#Signer-_setThreshold); [_changeThreshold](#Signer-_changeThreshold) cannot lower one while the + signer count is zero, because that would require a zero threshold. + + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Use [_changeThreshold](#Signer-_changeThreshold) for + operational threshold changes with signer count validation. + + + Requirements: + + * `newThreshold` must not be zero. + + +--- + +## ProposalManager [toc] [#ProposalManager] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ProposalManager"; +``` + +--- + +### Types [toc] [#ProposalManager-Types] +### Types [!toc] [#ProposalManager-Types] + +#### ProposalStatus [toc] [#ProposalManager-ProposalStatus] +#### [!toc] [#ProposalManager-ProposalStatus] + + Proposal lifecycle states. `Inactive` is the zero value and is + never stored: [_createProposal](#ProposalManager-_createProposal) writes `Active`, and every view asserts the + proposal exists before reading, so a stored proposal can never read + `Inactive`. + + +#### RecipientKind [toc] [#ProposalManager-RecipientKind] +#### [!toc] [#ProposalManager-RecipientKind] + + Says how a recipient's 32 address bytes are to be read: as a Zswap coin public key, a user address, or a contract address. + `Contract` is accepted by both [toShieldedRecipient](#ProposalManager-toShieldedRecipient) and [toUnshieldedRecipient](#ProposalManager-toUnshieldedRecipient), + so the tag alone does not determine the rail. + + +#### Recipient [toc] [#ProposalManager-Recipient] +#### [!toc] [#ProposalManager-Recipient] + + A typed recipient. Construct it with the recipient helper circuits rather than directly. + + +#### Proposal [toc] [#ProposalManager-Proposal] +#### [!toc] [#ProposalManager-Proposal] + + A transfer proposal: recipient, token color, amount, and lifecycle status. + + +### Ledger [toc] [#ProposalManager-Ledger] +### Ledger [!toc] [#ProposalManager-Ledger] + +#### _nextProposalId [toc] [#ProposalManager-_nextProposalId] +#### [!toc] [#ProposalManager-_nextProposalId] + + Counter of proposals created. Its value is the id of the most recent proposal. + + +#### _proposals [toc] [#ProposalManager-_proposals] +#### [!toc] [#ProposalManager-_proposals] + + Mapping from proposal id to proposal. Proposals are public: every recipient address, color, amount, and status is readable off-chain. + + +### Witnesses [toc] [#ProposalManager-Witnesses] +### Witnesses [!toc] [#ProposalManager-Witnesses] + +None. + +### Circuits [toc] [#ProposalManager-Circuits] +### Circuits [!toc] [#ProposalManager-Circuits] + +#### shieldedUserRecipient [toc] [#ProposalManager-shieldedUserRecipient] +#### [!toc] [#ProposalManager-shieldedUserRecipient] + + Constructs a shielded user recipient. + + +#### unshieldedUserRecipient [toc] [#ProposalManager-unshieldedUserRecipient] +#### [!toc] [#ProposalManager-unshieldedUserRecipient] + + Constructs an unshielded user recipient. + + +#### contractRecipient [toc] [#ProposalManager-contractRecipient] +#### [!toc] [#ProposalManager-contractRecipient] + + Constructs a contract recipient. + + +#### toShieldedRecipient [toc] [#ProposalManager-toShieldedRecipient] +#### [!toc] [#ProposalManager-toShieldedRecipient] + + Converts a Recipient to a shielded send recipient. + Handles both ShieldedUser and Contract kinds. + + Requirements: + + * Recipient kind must be ShieldedUser or Contract. + + +#### toUnshieldedRecipient [toc] [#ProposalManager-toUnshieldedRecipient] +#### [!toc] [#ProposalManager-toUnshieldedRecipient] + + Converts a Recipient to an unshielded send recipient. + Handles both UnshieldedUser and Contract kinds. + + Requirements: + + * Recipient kind must be UnshieldedUser or Contract. + + +#### assertProposalExists [toc] [#ProposalManager-assertProposalExists] +#### [!toc] [#ProposalManager-assertProposalExists] + + Asserts that a proposal exists. + + Requirements: + + * Proposal with `id` must have been created. + + +#### assertProposalActive [toc] [#ProposalManager-assertProposalActive] +#### [!toc] [#ProposalManager-assertProposalActive] + + Asserts that a proposal exists and is active. + + Requirements: + + * Proposal must exist. + * Proposal status must be Active. + + +#### _createProposal [toc] [#ProposalManager-_createProposal] +#### [!toc] [#ProposalManager-_createProposal] + + Creates a new proposal. + + + The recipient is NOT validated beyond being well-typed. Two + consequences the consumer must handle: + + * A `kind` the execution rail cannot accept (an `UnshieldedUser` recipient + on a shielded treasury, say) creates a proposal that passes here and then + reverts in [toShieldedRecipient](#ProposalManager-toShieldedRecipient) at execution. It stays `Active` and + unexecutable on that rail until [_cancelProposal](#ProposalManager-_cancelProposal) retires it. + * A zero or otherwise unspendable `address` is accepted, and funds sent to + it on execution are unrecoverable. + + + + `_nextProposalId` is incremented and then read back for the map key, + making this a pinned read-modify-write. + + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * `amount` must be greater than 0. + + +#### _cancelProposal [toc] [#ProposalManager-_cancelProposal] +#### [!toc] [#ProposalManager-_cancelProposal] + + Cancels a proposal. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * Proposal must be active. + + +#### _markExecuted [toc] [#ProposalManager-_markExecuted] +#### [!toc] [#ProposalManager-_markExecuted] + + Marks a proposal as executed. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * Proposal must be active. + + +#### getProposal [toc] [#ProposalManager-getProposal] +#### [!toc] [#ProposalManager-getProposal] + + Returns the full proposal data. + + +#### getProposalRecipient [toc] [#ProposalManager-getProposalRecipient] +#### [!toc] [#ProposalManager-getProposalRecipient] + + Returns the recipient of a proposal. + + +#### getProposalAmount [toc] [#ProposalManager-getProposalAmount] +#### [!toc] [#ProposalManager-getProposalAmount] + + Returns the amount of a proposal. + + +#### getProposalColor [toc] [#ProposalManager-getProposalColor] +#### [!toc] [#ProposalManager-getProposalColor] + + Returns the token color of a proposal. + + +#### getProposalStatus [toc] [#ProposalManager-getProposalStatus] +#### [!toc] [#ProposalManager-getProposalStatus] + + Returns the status of a proposal. + + +--- + +## ShieldedTreasury [toc] [#ShieldedTreasury] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasury"; +``` + +--- + +### Ledger [toc] [#ShieldedTreasury-Ledger] +### Ledger [!toc] [#ShieldedTreasury-Ledger] + +#### _coins [toc] [#ShieldedTreasury-_coins] +#### [!toc] [#ShieldedTreasury-_coins] + + One stored coin per token color. Every coin held (nonce, color, value, and Merkle index) is readable off-chain in plaintext. + + +#### _received [toc] [#ShieldedTreasury-_received] +#### [!toc] [#ShieldedTreasury-_received] + + Cumulative value received per token color. + + +#### _sent [toc] [#ShieldedTreasury-_sent] +#### [!toc] [#ShieldedTreasury-_sent] + + Cumulative value sent per token color. + + +### Witnesses [toc] [#ShieldedTreasury-Witnesses] +### Witnesses [!toc] [#ShieldedTreasury-Witnesses] + +None. + +### Circuits [toc] [#ShieldedTreasury-Circuits] +### Circuits [!toc] [#ShieldedTreasury-Circuits] + +#### _deposit [toc] [#ShieldedTreasury-_deposit] +#### [!toc] [#ShieldedTreasury-_deposit] + + Receives a shielded coin into the treasury. + + The coin is first claimed at the protocol level via `receiveShielded`, + which allocates the Merkle tree index required by `insertCoin`. + The coin is then merged with any existing coin of the same color, + or inserted as a new entry if no coin of that color exists. + + Zero-value deposits are permitted and are not always a no-op. On a color the + treasury does not yet hold, a zero-value coin is inserted and occupies that + color's single slot until a later deposit merges into it; if a coin of that + color already exists, the zero merges into it via `mergeCoinImmediate` and + the stored value is unchanged. They may serve as signaling mechanisms when + events are supported. They are not useful as privacy decoys, this + treasury's holdings are public (see the module header). + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * Deposit must not cause received total overflow. + + +#### _send [toc] [#ShieldedTreasury-_send] +#### [!toc] [#ShieldedTreasury-_send] + + Sends shielded tokens from the treasury. + + Looks up the stored coin by color, verifies sufficient value, + and executes the shielded send. `sendShielded` already routes any + change back to this contract as a self-owned output. Unlike the + stateless variant, this treasury RETAINS the change: it is recorded in + the UTXO map for the next spend. The returned `result.change` therefore + belongs to the treasury and must not be re-spent by the caller (doing + so would double-spend the recorded coin). + + + The returned `ShieldedSendResult` carries the sent and change coins + in plaintext, so sinking it (returning it from an entry point, writing it + to ledger, passing it cross-contract) discloses them. `_coins` publishes only + the retained change coin, and nothing at all when the send leaves no change, + so the sent coin's plaintext is disclosed beyond anything in public state. + + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * A coin of the given `color` must exist in the treasury. + * The coin's value must be >= `amount`. + * Send must not cause sent total overflow. + + +#### getTokenBalance [toc] [#ShieldedTreasury-getTokenBalance] +#### [!toc] [#ShieldedTreasury-getTokenBalance] + + Returns the current token balance for a color. + Reads the actual coin value from the UTXO map. + + +#### getReceivedTotal [toc] [#ShieldedTreasury-getReceivedTotal] +#### [!toc] [#ShieldedTreasury-getReceivedTotal] + + Returns the cumulative received total for a color. + + +#### getSentTotal [toc] [#ShieldedTreasury-getSentTotal] +#### [!toc] [#ShieldedTreasury-getSentTotal] + + Returns the cumulative sent total for a color. + + +#### getReceivedMinusSent [toc] [#ShieldedTreasury-getReceivedMinusSent] +#### [!toc] [#ShieldedTreasury-getReceivedMinusSent] + + Returns the difference between cumulative received + and cumulative sent totals for a color. Should equal + [getTokenBalance](#ShieldedTreasury-getTokenBalance) if accounting is consistent. + + + The subtraction is unguarded. [_send](#ShieldedTreasury-_send) cannot drive the sent total + past the received total. Change re-insertion never increments `_received` + so the invariant holds for any consumer that goes through [_deposit](#ShieldedTreasury-_deposit) and + [_send](#ShieldedTreasury-_send). A consumer that writes the exported `_coins` directly can break + it, and this circuit then fails as an unprovable range violation rather + than a named assert. + + + +--- + +## ShieldedTreasuryStateless [toc] [#ShieldedTreasuryStateless] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasuryStateless"; +``` + +--- + +### Ledger [toc] [#ShieldedTreasuryStateless-Ledger] +### Ledger [!toc] [#ShieldedTreasuryStateless-Ledger] + +None. The module keeps no coin map, balances, or totals; the operator tracks coins off-chain. + +### Witnesses [toc] [#ShieldedTreasuryStateless-Witnesses] +### Witnesses [!toc] [#ShieldedTreasuryStateless-Witnesses] + +None. + +### Circuits [toc] [#ShieldedTreasuryStateless-Circuits] +### Circuits [!toc] [#ShieldedTreasuryStateless-Circuits] + +#### _deposit [toc] [#ShieldedTreasuryStateless-_deposit] +#### [!toc] [#ShieldedTreasuryStateless-_deposit] + + Receives a shielded coin into the treasury. + + +#### _send [toc] [#ShieldedTreasuryStateless-_send] +#### [!toc] [#ShieldedTreasuryStateless-_send] + + Sends shielded tokens using a caller-provided coin. + + The coin is supplied by the caller rather than looked up from + on-chain state; the token color is derived from `coin.color`. + Executes the shielded send and returns the result untouched. + `sendShielded` already routes any change back to this contract as a + self-owned output, so `result.change` is a live, spendable coin. The + implementing contract chooses what to do with it: retain it off-chain and + pass it back as the `coin` for a later spend, or spend it onward to a + different recipient in the same transaction via `sendImmediateShielded`. + (Re-spending change is only meaningful when it goes somewhere other than + self — `sendShielded` already routes it to self.) No balance or + received/sent accounting is tracked on the ledger. + + + The returned `ShieldedSendResult` holds the sent and change coins + in plaintext (`nonce`, `color`, `value`). Consuming it in-circuit is safe + (passing `result.change` to `sendImmediateShielded`, for instance). Returning + it from your entry point, writing it to ledger, or passing it to another + contract publishes those coin details, including the amount left over from + the coin that was spent. Note the compiler will NOT stop you: the + `disclose` calls below clear the witness taint, so such a return compiles + with no `disclose` of your own and no marker at the disclosure site. If you + do not need in-circuit change handling, ignore the result. The change is + recoverable off-chain from the transaction's private proving state. + + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * `coin.value` must be >= `amount`. + + +--- + +## UnshieldedTreasury [toc] [#UnshieldedTreasury] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/UnshieldedTreasury"; +``` + +--- + +### Ledger [toc] [#UnshieldedTreasury-Ledger] +### Ledger [!toc] [#UnshieldedTreasury-Ledger] + +#### _balances [toc] [#UnshieldedTreasury-_balances] +#### [!toc] [#UnshieldedTreasury-_balances] + + Tracked balance per token color. It is the authority for the module's sufficiency and overflow guards and stays in step with the protocol balance only while every credit and debit goes through `_deposit` and `_send`. + + +### Witnesses [toc] [#UnshieldedTreasury-Witnesses] +### Witnesses [!toc] [#UnshieldedTreasury-Witnesses] + +None. + +### Circuits [toc] [#UnshieldedTreasury-Circuits] +### Circuits [!toc] [#UnshieldedTreasury-Circuits] + +#### _deposit [toc] [#UnshieldedTreasury-_deposit] +#### [!toc] [#UnshieldedTreasury-_deposit] + + Receives unshielded tokens into the treasury. + + The token receive is executed at the protocol level first via + `receiveUnshielded`. The balance map is then updated. + + Zero-value deposits are permitted. While currently a no-op + economically, they may serve as signaling mechanisms when events + are supported. On a color the treasury has not held, a zero-value + operation creates a zero-valued `_balances` entry, which + [getTokenBalance](#UnshieldedTreasury-getTokenBalance) reports the same as absence. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * Deposit must not cause balance overflow. + + +#### _send [toc] [#UnshieldedTreasury-_send] +#### [!toc] [#UnshieldedTreasury-_send] + + Sends unshielded tokens from the treasury. Zero-amount sends + are permitted and are value-preserving no-ops. + + + Recipients are not validated. An unspendable recipient (such as a + zero address) is accepted and permanently removes the funds from the + contract. This is not a sanctioned burn — no unshielded equivalent of + `shieldedBurnAddress` is exposed to contracts — so treat recipient + validation as the consuming contract's responsibility. + + + + Sending to this contract's own address zeroes the tracked balance + while the funds stay held and spendable at the protocol level, after which + [_send](#UnshieldedTreasury-_send) will refuse to touch them. Recovery is only possible outside this + module: a consumer may reconcile the exported `_balances`, or spend the + funds through its own `sendUnshielded` call. + + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * The treasury must hold sufficient balance for the given token color. + + +#### getTokenBalance [toc] [#UnshieldedTreasury-getTokenBalance] +#### [!toc] [#UnshieldedTreasury-getTokenBalance] + + Returns the balance for a given token color. + + +--- + +## ForwarderPrivate [toc] [#ForwarderPrivate] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderPrivate"; +``` + +--- + +### Ledger [toc] [#ForwarderPrivate-Ledger] +### Ledger [!toc] [#ForwarderPrivate-Ledger] + +#### _parentCommitment [toc] [#ForwarderPrivate-_parentCommitment] +#### [!toc] [#ForwarderPrivate-_parentCommitment] + + Commitment to the `(parent, opSecret)` pair, `H(pad(32, "ForwarderPrivate:commitment") || parent || opSecret)`. Immutable after initialization. + + +#### _isInitialized [toc] [#ForwarderPrivate-_isInitialized] +#### [!toc] [#ForwarderPrivate-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + +### Witnesses [toc] [#ForwarderPrivate-Witnesses] +### Witnesses [!toc] [#ForwarderPrivate-Witnesses] + +None. + +### Circuits [toc] [#ForwarderPrivate-Circuits] +### Circuits [!toc] [#ForwarderPrivate-Circuits] + +#### initialize [toc] [#ForwarderPrivate-initialize] +#### [!toc] [#ForwarderPrivate-initialize] + + Initializes the forwarder with a parent commitment. + Called once from the preset constructor. The commitment is + immutable after init. + + Requirements: + + * Contract must not be initialized. + * `parentCommitment` must not be the zero bytes. A zero commitment + is the sole drain gate and would leave every deposited coin + permanently unrecoverable (no preimage exists for `default>` + under the domain-tagged hash). + + +#### _deposit [toc] [#ForwarderPrivate-_deposit] +#### [!toc] [#ForwarderPrivate-_deposit] + + Receives a shielded coin into the forwarder's custody. + No ledger write — coins dwell at the contract address until drained. + + Requirements: + + * Contract must be initialized. + + +#### _drain [toc] [#ForwarderPrivate-_drain] +#### [!toc] [#ForwarderPrivate-_drain] + + Spends a previously-deposited shielded coin to the parent, + a coin public key. The caller proves knowledge of `(parent, opSecret)` + matching the stored commitment; the coin is sent as a plain shielded note, + so the parent stays hidden on-chain. If `coin.value` exceeds `value`, + `sendShielded` routes the change back to this contract as a self-owned + output; [_drain](#ForwarderPrivate-_drain) returns it untouched via `result.change`. The + implementing contract chooses what to do with it: leave it dwelling at + the contract for a future drain, or spend it onward to a different + recipient in the same transaction via `sendImmediateShielded`. + + The parent is a `ZswapCoinPublicKey`, never a `ContractAddress`: a shielded + send to a contract publishes that contract's address in cleartext on the + transaction (the protocol must route the coin to a named contract), which + would defeat the private-parent guarantee. A coin public key is encrypted + inside the Zswap output and never appears on the public transcript. + + Requirements: + + * Contract must be initialized. + * `parent` must not be the zero key. + * `_calculateParentCommitment(parent.bytes, opSecret) == _parentCommitment`. + * `coin.value` must be >= `value` (enforced by `sendShielded`). + + + **Losing the operational secret is permanent fund loss.** It + is the sole drain authorization, with no rotation, revocation, or + recovery path. If the operator misplaces it, every shielded coin + accumulated at this contract is forever inaccessible, equivalent to + losing a hot-wallet private key. Back it up offline before the first + deposit and treat it with the same hygiene as a signing key. + + + +#### _calculateParentCommitment [toc] [#ForwarderPrivate-_calculateParentCommitment] +#### [!toc] [#ForwarderPrivate-_calculateParentCommitment] + + Computes the parent commitment from `(parentAddr, opSecret)`. + Pure circuit — used off-chain by the deployer to compute the + constructor argument, and inside [_drain](#ForwarderPrivate-_drain) for the preimage check. + Callable without initialization. + + The first hash input is a fixed domain tag + (`pad(32, "ForwarderPrivate:commitment")`). The tag prevents + preimage collisions with other `persistentHash` users in the + system that hash two `Bytes<32>` values — a colliding preimage + crafted under a different domain cannot satisfy this commitment. + + +--- + +## ForwarderShielded [toc] [#ForwarderShielded] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderShielded"; +``` + +--- + +### Ledger [toc] [#ForwarderShielded-Ledger] +### Ledger [!toc] [#ForwarderShielded-Ledger] + +#### _parent [toc] [#ForwarderShielded-_parent] +#### [!toc] [#ForwarderShielded-_parent] + + The parent recipient, stored generically as + `Either` for forward compatibility. + [initialize](#ForwarderShielded-initialize) only ever writes the `left` (coin public key) arm today. See + the module header. + + +#### _isInitialized [toc] [#ForwarderShielded-_isInitialized] +#### [!toc] [#ForwarderShielded-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + +### Witnesses [toc] [#ForwarderShielded-Witnesses] +### Witnesses [!toc] [#ForwarderShielded-Witnesses] + +None. + +### Circuits [toc] [#ForwarderShielded-Circuits] +### Circuits [!toc] [#ForwarderShielded-Circuits] + +#### initialize [toc] [#ForwarderShielded-initialize] +#### [!toc] [#ForwarderShielded-initialize] + + Initializes the forwarder with a parent recipient. + Called once from the preset constructor. The coin public key is + stored in the `left` arm of the generic `_parent` ledger field. + + Requirements: + + * Contract must not be initialized. + * `parent` must not be the zero key. A zero parent would forward + every deposit to an unspendable key with no recovery path. + + + Beyond the zero-key check, the caller must ensure `parent` can + spend forwarded coins. An unspendable parent strands every deposit with + no recovery path. + + + +#### _deposit [toc] [#ForwarderShielded-_deposit] +#### [!toc] [#ForwarderShielded-_deposit] + + Receives a shielded coin and atomically forwards it to + `_parent`. The coin is claimed at the protocol level via + `receiveShielded`, then immediately re-sent via + `sendImmediateShielded` to the stored parent recipient (always the + `left` / coin-public-key arm today). + + Requirements: + + * Contract must be initialized. + + +--- + +## ForwarderUnshielded [toc] [#ForwarderUnshielded] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderUnshielded"; +``` + +--- + +### Ledger [toc] [#ForwarderUnshielded-Ledger] +### Ledger [!toc] [#ForwarderUnshielded-Ledger] + +#### _parent [toc] [#ForwarderUnshielded-_parent] +#### [!toc] [#ForwarderUnshielded-_parent] + + The parent recipient, stored generically as + `Either` for forward compatibility. + [initialize](#ForwarderUnshielded-initialize) only ever writes the `right` (user address) arm today. See + the module header. + + +#### _isInitialized [toc] [#ForwarderUnshielded-_isInitialized] +#### [!toc] [#ForwarderUnshielded-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + +### Witnesses [toc] [#ForwarderUnshielded-Witnesses] +### Witnesses [!toc] [#ForwarderUnshielded-Witnesses] + +None. + +### Circuits [toc] [#ForwarderUnshielded-Circuits] +### Circuits [!toc] [#ForwarderUnshielded-Circuits] + +#### initialize [toc] [#ForwarderUnshielded-initialize] +#### [!toc] [#ForwarderUnshielded-initialize] + + Initializes the forwarder with a parent recipient. + Called once from the preset constructor. The user address is stored + in the `right` arm of the generic `_parent` ledger field. + + Requirements: + + * Contract must not be initialized. + * `parent` must not be the zero address. A zero parent would forward + every deposit to an unspendable address with no recovery path. + + + Beyond the zero-address check, the caller must ensure `parent` + can spend forwarded amounts. An unspendable parent strands every + deposit with no recovery path. + + + +#### _deposit [toc] [#ForwarderUnshielded-_deposit] +#### [!toc] [#ForwarderUnshielded-_deposit] + + Receives an unshielded amount of `color` and atomically + forwards it to `_parent` via `sendUnshielded` (always the `right` / + user-address arm today). + + Requirements: + + * Contract must be initialized. + + diff --git a/content/contracts-compact/api/native-shielded-token.mdx b/content/contracts-compact/api/native-shielded-token.mdx new file mode 100644 index 00000000..496db788 --- /dev/null +++ b/content/contracts-compact/api/native-shielded-token.mdx @@ -0,0 +1,1695 @@ +--- +title: NativeShieldedToken API +--- + +This module provides the full NativeShieldedToken module API: the two token flavors, their shared core, +the supply and derived-nonce extensions, and the owner-gated preset. + + + For an overview of the modules, read the [NativeShieldedToken guide](../native-shielded-token). + + +The page covers the following modules: + +- **Token modules** + - [NativeShieldedToken](#NativeShieldedToken): a single token type issued and burned by the contract. + - [NativeShieldedTokenFamily](#NativeShieldedTokenFamily): many token types, keyed by domain, under one contract. + - [NativeShieldedTokenCore](#NativeShieldedTokenCore): the shared core behind the two flavors; not composed directly. +- **Extensions** + - [NativeShieldedTokenPublicSupply](#NativeShieldedTokenPublicSupply): on-chain minted and burned totals for a `NativeShieldedToken`. + - [NativeShieldedTokenFamilyPublicSupply](#NativeShieldedTokenFamilyPublicSupply): per-domain on-chain totals for a `NativeShieldedTokenFamily`. + - [NativeShieldedTokenPublicSupplyCore](#NativeShieldedTokenPublicSupplyCore): the shared core behind the two supply extensions; not composed directly. + - [NativeShieldedTokenDerivedNonce](#NativeShieldedTokenDerivedNonce): mint coin nonces derived from a counter. +- **Preset** + - [ZOwnableNativeShieldedToken](#ZOwnableNativeShieldedToken): a single token whose `ZOwnablePK` owner mints and burns, with a public minted total. + +These modules export no structs or enums of their own. Coins are the standard library's `ShieldedCoinInfo` +(a `nonce`, `color`, and `value`) and `QualifiedShieldedCoinInfo` (the same plus the Merkle-tree index `mt_index` +of a coin the contract holds). + +## NativeShieldedToken [toc] [#NativeShieldedToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedToken"; +``` + +--- + +### Ledger [toc] [#NativeShieldedToken-Ledger] +### Ledger [!toc] [#NativeShieldedToken-Ledger] + +#### _domain [toc] [#NativeShieldedToken-_domain] +#### [!toc] [#NativeShieldedToken-_domain] + + Domain separator fixed at construction; with the contract + address it determines this token's color. + + +#### _name [toc] [#NativeShieldedToken-_name] +#### [!toc] [#NativeShieldedToken-_name] + + The immutable token name. Re-exported from `NativeShieldedTokenCore`. + + +#### _symbol [toc] [#NativeShieldedToken-_symbol] +#### [!toc] [#NativeShieldedToken-_symbol] + + The immutable token symbol. Re-exported from `NativeShieldedTokenCore`. + + +#### _decimals [toc] [#NativeShieldedToken-_decimals] +#### [!toc] [#NativeShieldedToken-_decimals] + + The immutable token decimals. Re-exported from `NativeShieldedTokenCore`. + + +#### _isInitialized [toc] [#NativeShieldedToken-_isInitialized] +#### [!toc] [#NativeShieldedToken-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + +### Witnesses [toc] [#NativeShieldedToken-Witnesses] +### Witnesses [!toc] [#NativeShieldedToken-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedToken-Circuits] +### Circuits [!toc] [#NativeShieldedToken-Circuits] + +#### initialize [toc] [#NativeShieldedToken-initialize] +#### [!toc] [#NativeShieldedToken-initialize] + + Initializes the module's domain and metadata. + This MUST be called in the implementing contract's constructor, and it + MUST be the only token operation performed there. Failure to call it can + lead to an irreparable contract; perform mint, burn, and [tokenColor](#NativeShieldedToken-tokenColor) only + in post-deployment circuits, never in the constructor. + + +#### isInitialized [toc] [#NativeShieldedToken-isInitialized] +#### [!toc] [#NativeShieldedToken-isInitialized] + + Returns whether the contract has been initialized. + + +#### name [toc] [#NativeShieldedToken-name] +#### [!toc] [#NativeShieldedToken-name] + + Returns the token name. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#NativeShieldedToken-symbol] +#### [!toc] [#NativeShieldedToken-symbol] + + Returns the symbol of the token. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#NativeShieldedToken-decimals] +#### [!toc] [#NativeShieldedToken-decimals] + + Returns the number of decimals used to get its user representation. + + Requirements: + + * Contract is initialized. + + +#### tokenColor [toc] [#NativeShieldedToken-tokenColor] +#### [!toc] [#NativeShieldedToken-tokenColor] + + Returns this token's coin color: + `tokenType(_domain, kernel.self())`, computed at call time. + + Requirements: + + * Contract is initialized. + + +#### _mint [toc] [#NativeShieldedToken-_mint] +#### [!toc] [#NativeShieldedToken-_mint] + + Mints `amount` of the token to `recipient`, using a + caller-supplied nonce. + + The caller is fully responsible for nonce uniqueness. Reusing a + nonce for the same (value, recipient) produces a duplicate commitment, + which the protocol rejects. With a secret random nonce this is the + recipient-private mint. For mints that require no caller-managed nonce, + compose the `NativeShieldedTokenDerivedNonce` extension and pass its + `_deriveNonce()` output. + + + The returned coin info is the only copy available to the + recipient; callers SHOULD deliver it out of band. Wallets cannot detect + contract-minted coins by scanning the chain. + + + Requirements: + + * Contract is initialized. + * `recipient` is not zero. + + +#### _mintToSelf [toc] [#NativeShieldedToken-_mintToSelf] +#### [!toc] [#NativeShieldedToken-_mintToSelf] + + Mints `amount` to this contract itself, claiming the coin in + the same call. Forwards to `NativeShieldedTokenCore._mint` with + `right(kernel.self())`. + + Nonce uniqueness is the caller's responsibility, as in [_mint](#NativeShieldedToken-_mint). Spend + the coin later with [_burnFromSelf](#NativeShieldedToken-_burnFromSelf), once it carries an `mt_index`. + + + The consumer SHOULD persist the returned coin info in its own + ledger state: it is the contract's holding and is not otherwise + recoverable. + + + Requirements: + + * Contract is initialized. + + +#### _burn [toc] [#NativeShieldedToken-_burn] +#### [!toc] [#NativeShieldedToken-_burn] + + Burns `amount` from `coin`, a coin provided within the + current transaction (e.g. paid in by the caller's wallet), and routes the + remaining change to `refundTo`. + + To destroy `coin` in full, pass `coin.value` as `amount`; the change + branch will not fire and `none` is returned. The `refundTo` value is + inert in that case but must still be supplied and non-zero. + + The coin is received by the contract and spent in the same + transaction, so the spend goes through `sendImmediateShielded` (the + transient path). For coins the contract already holds, use + [_burnFromSelf](#NativeShieldedToken-_burnFromSelf) instead. + + + The returned refund coin info is the only copy available to + `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect + contract-sent coins by scanning the chain. + + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token color. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not zero (the zero key is the burn address; a zero + `refundTo` would silently burn the change as well). + + +#### _burnFromSelf [toc] [#NativeShieldedToken-_burnFromSelf] +#### [!toc] [#NativeShieldedToken-_burnFromSelf] + + Burns `amount` from `coin`, a coin this contract already + holds (a Merkle-tree entry with a valid `mt_index`). Any change is + auto-received by the contract and returned. + + The consumer SHOULD persist the returned change coin info in its own + ledger state: the change replaces `coin` as the contract's holding, and + its info is not otherwise recoverable. + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token color. + * `amount` is less than or equal to `coin.value`. + + +--- + +## NativeShieldedTokenFamily [toc] [#NativeShieldedTokenFamily] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedTokenFamily"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenFamily-Ledger] +### Ledger [!toc] [#NativeShieldedTokenFamily-Ledger] + +#### _name [toc] [#NativeShieldedTokenFamily-_name] +#### [!toc] [#NativeShieldedTokenFamily-_name] + + The immutable family name shared by all token types. Re-exported from `NativeShieldedTokenCore`. + + +#### _symbol [toc] [#NativeShieldedTokenFamily-_symbol] +#### [!toc] [#NativeShieldedTokenFamily-_symbol] + + The immutable family symbol shared by all token types. Re-exported from `NativeShieldedTokenCore`. + + +#### _decimals [toc] [#NativeShieldedTokenFamily-_decimals] +#### [!toc] [#NativeShieldedTokenFamily-_decimals] + + The immutable family-wide decimals. Re-exported from `NativeShieldedTokenCore`. + + +#### _isInitialized [toc] [#NativeShieldedTokenFamily-_isInitialized] +#### [!toc] [#NativeShieldedTokenFamily-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + +### Witnesses [toc] [#NativeShieldedTokenFamily-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenFamily-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenFamily-Circuits] +### Circuits [!toc] [#NativeShieldedTokenFamily-Circuits] + +#### initialize [toc] [#NativeShieldedTokenFamily-initialize] +#### [!toc] [#NativeShieldedTokenFamily-initialize] + + Initializes the module's family metadata. + This MUST be called in the implementing contract's constructor, and it + MUST be the only token operation performed there. Failure to call it can + lead to an irreparable contract; perform mint, burn, and [tokenColor](#NativeShieldedTokenFamily-tokenColor) only + in post-deployment circuits, never in the constructor. + + +#### isInitialized [toc] [#NativeShieldedTokenFamily-isInitialized] +#### [!toc] [#NativeShieldedTokenFamily-isInitialized] + + Returns whether the contract has been initialized. + + +#### name [toc] [#NativeShieldedTokenFamily-name] +#### [!toc] [#NativeShieldedTokenFamily-name] + + Returns the family name shared by all token types of this + contract. Per-type identity belongs in the consumer's own state; see the + module notes. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#NativeShieldedTokenFamily-symbol] +#### [!toc] [#NativeShieldedTokenFamily-symbol] + + Returns the family symbol shared by all token types of this + contract. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#NativeShieldedTokenFamily-decimals] +#### [!toc] [#NativeShieldedTokenFamily-decimals] + + Returns the family-wide number of decimals used to get the + user representation. + + Requirements: + + * Contract is initialized. + + +#### tokenColor [toc] [#NativeShieldedTokenFamily-tokenColor] +#### [!toc] [#NativeShieldedTokenFamily-tokenColor] + + Returns the coin color (token type) for `domain` under this + contract's address, so integrators do not have to re-derive it by hand. + + Requirements: + + * Contract is initialized. + + +#### _mint [toc] [#NativeShieldedTokenFamily-_mint] +#### [!toc] [#NativeShieldedTokenFamily-_mint] + + Mints `amount` of the token identified by `domain` to + `recipient`, using a caller-supplied nonce. + + The caller is fully responsible for nonce uniqueness. Reusing a + nonce for the same (domain, value, recipient) produces a duplicate + commitment, which the protocol rejects. With a secret random nonce this + is the recipient-private mint. For mints that require no caller-managed + nonce, compose the `NativeShieldedTokenDerivedNonce` extension and pass + its `_deriveNonce()` output. + + + The returned coin info is the only copy available to the + recipient; callers SHOULD deliver it out of band. Wallets cannot detect + contract-minted coins by scanning the chain. + + + Requirements: + + * Contract is initialized. + * `recipient` is not zero. + + +#### _mintToSelf [toc] [#NativeShieldedTokenFamily-_mintToSelf] +#### [!toc] [#NativeShieldedTokenFamily-_mintToSelf] + + Mints `amount` of the token identified by `domain` to this + contract itself, claiming the coin in the same call. Forwards to + `NativeShieldedTokenCore._mint` with `right(kernel.self())`. + + Nonce uniqueness is the caller's responsibility, as in [_mint](#NativeShieldedTokenFamily-_mint). Spend + the coin later with [_burnFromSelf](#NativeShieldedTokenFamily-_burnFromSelf), once it carries an `mt_index`. + + + The consumer SHOULD persist the returned coin info in its own + ledger state: it is the contract's holding and is not otherwise + recoverable. + + + Requirements: + + * Contract is initialized. + + +#### _burn [toc] [#NativeShieldedTokenFamily-_burn] +#### [!toc] [#NativeShieldedTokenFamily-_burn] + + Burns `amount` from `coin`, a coin provided within the + current transaction (e.g. paid in by the caller's wallet), and routes the + remaining change to `refundTo`. + + To destroy `coin` in full, pass `coin.value` as `amount`; the change + branch will not fire and `none` is returned. The `refundTo` value is + inert in that case but must still be supplied and non-zero. + + The coin is received by the contract and spent in the same + transaction, so the spend goes through `sendImmediateShielded` (the + transient path). For coins the contract already holds, use + [_burnFromSelf](#NativeShieldedTokenFamily-_burnFromSelf) instead. + + + The returned refund coin info is the only copy available to + `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect + contract-sent coins by scanning the chain. + + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not zero (the zero key is the burn address; a zero + `refundTo` would silently burn the change as well). + + +#### _burnFromSelf [toc] [#NativeShieldedTokenFamily-_burnFromSelf] +#### [!toc] [#NativeShieldedTokenFamily-_burnFromSelf] + + Burns `amount` from `coin`, a coin this contract already + holds (a Merkle-tree entry with a valid `mt_index`). Any change is + auto-received by the contract and returned. + + The consumer SHOULD persist the returned change coin info in its own + ledger state: the change replaces `coin` as the contract's holding, and + its info is not otherwise recoverable. + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. + + +--- + +## NativeShieldedTokenCore [toc] [#NativeShieldedTokenCore] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedTokenCore"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenCore-Ledger] +### Ledger [!toc] [#NativeShieldedTokenCore-Ledger] + +#### _isInitialized [toc] [#NativeShieldedTokenCore-_isInitialized] +#### [!toc] [#NativeShieldedTokenCore-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + +#### _name [toc] [#NativeShieldedTokenCore-_name] +#### [!toc] [#NativeShieldedTokenCore-_name] + + The immutable token name. + + +#### _symbol [toc] [#NativeShieldedTokenCore-_symbol] +#### [!toc] [#NativeShieldedTokenCore-_symbol] + + The immutable token symbol. + + +#### _decimals [toc] [#NativeShieldedTokenCore-_decimals] +#### [!toc] [#NativeShieldedTokenCore-_decimals] + + The immutable token decimals. + + +### Witnesses [toc] [#NativeShieldedTokenCore-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenCore-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenCore-Circuits] +### Circuits [!toc] [#NativeShieldedTokenCore-Circuits] + +#### initialize [toc] [#NativeShieldedTokenCore-initialize] +#### [!toc] [#NativeShieldedTokenCore-initialize] + + Initializes the contract-wide metadata. + This MUST be called in the implementing contract's constructor (via + the wrapping flavor's [initialize](#NativeShieldedTokenCore-initialize)). Failure to do so can lead to an + irreparable contract. + + +#### assertInitialized [toc] [#NativeShieldedTokenCore-assertInitialized] +#### [!toc] [#NativeShieldedTokenCore-assertInitialized] + + Asserts that the contract has been initialized, throwing an + error if not. + + Requirements: + + * Contract must be initialized. + + +#### assertNotInitialized [toc] [#NativeShieldedTokenCore-assertNotInitialized] +#### [!toc] [#NativeShieldedTokenCore-assertNotInitialized] + + Asserts that the contract has not been initialized, throwing + an error if it has. + + Requirements: + + * Contract must not be initialized. + + +#### isInitialized [toc] [#NativeShieldedTokenCore-isInitialized] +#### [!toc] [#NativeShieldedTokenCore-isInitialized] + + Returns whether the contract has been initialized. + Does not gate on init (it reports it), so it is safe to call before + [initialize](#NativeShieldedTokenCore-initialize). + + +#### name [toc] [#NativeShieldedTokenCore-name] +#### [!toc] [#NativeShieldedTokenCore-name] + + Returns the token name. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#NativeShieldedTokenCore-symbol] +#### [!toc] [#NativeShieldedTokenCore-symbol] + + Returns the symbol of the token. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#NativeShieldedTokenCore-decimals] +#### [!toc] [#NativeShieldedTokenCore-decimals] + + Returns the number of decimals used to get its user representation. + This is a display convention only. The protocol stores and operates + on integer values without reference to decimals. + + Requirements: + + * Contract is initialized. + + +#### tokenColor [toc] [#NativeShieldedTokenCore-tokenColor] +#### [!toc] [#NativeShieldedTokenCore-tokenColor] + + Returns the coin color for `domain` under this contract's + address: `tokenType(domain, kernel.self())`, computed at call time. + + Requirements: + + * Contract is initialized. + + +#### _mint [toc] [#NativeShieldedTokenCore-_mint] +#### [!toc] [#NativeShieldedTokenCore-_mint] + + Mints `amount` of the token identified by `domain` to + `recipient`, using a caller-supplied nonce. + + The caller is fully responsible for nonce uniqueness. Reusing a + nonce for the same (domain, value, recipient) produces a duplicate + commitment, which the protocol rejects. A `right(kernel.self())` recipient + is claimed via `receiveShielded` in this same call, so the consumer SHOULD + persist the returned coin info: it is the contract's holding and is not + otherwise recoverable. + + + The returned coin info is the only copy available to the + recipient; callers SHOULD deliver it out of band. Wallets cannot detect + contract-minted coins by scanning the chain. + + + Requirements: + + * Contract is initialized. + * `recipient` is not zero. + * `recipient`, if a contract address, is this contract's own address. + + +#### _burn [toc] [#NativeShieldedTokenCore-_burn] +#### [!toc] [#NativeShieldedTokenCore-_burn] + + Burns `amount` from `coin`, a coin provided within the + current transaction (e.g. paid in by the caller's wallet), and routes the + remaining change to `refundTo`. + + To destroy `coin` in full, pass `coin.value` as `amount`; the change + branch will not fire and `none` is returned. The `refundTo` value is + inert in that case but must still be supplied and non-zero. + + The coin is received by the contract and spent in the same + transaction, so the spend goes through `sendImmediateShielded` (the + transient path). For coins the contract already holds, use + [_burnFromSelf](#NativeShieldedTokenCore-_burnFromSelf) instead. A `right(kernel.self())` refund is claimed via + `receiveShielded` in this same call. + + + The returned refund coin info is the only copy available to + `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect + contract-sent coins by scanning the chain. + + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not zero (the zero key is the burn address; a zero + `refundTo` would silently burn the change as well). + * `refundTo`, if a contract address, is this contract's own address. + + +#### _burnFromSelf [toc] [#NativeShieldedTokenCore-_burnFromSelf] +#### [!toc] [#NativeShieldedTokenCore-_burnFromSelf] + + Burns `amount` from `coin`, a coin this contract already + holds (a Merkle-tree entry with a valid `mt_index`). Any change is + auto-received by the contract and returned. + + The consumer SHOULD persist the returned change coin info in its own + ledger state: the change replaces `coin` as the contract's holding, and + its info is not otherwise recoverable. + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. + + +--- + +## NativeShieldedTokenPublicSupply [toc] [#NativeShieldedTokenPublicSupply] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenPublicSupply"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenPublicSupply-Ledger] +### Ledger [!toc] [#NativeShieldedTokenPublicSupply-Ledger] + +#### _totalMinted [toc] [#NativeShieldedTokenPublicSupply-_totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupply-_totalMinted] + + Exact amount minted per domain. See "Accounting guarantees". + + +#### _totalBurned [toc] [#NativeShieldedTokenPublicSupply-_totalBurned] +#### [!toc] [#NativeShieldedTokenPublicSupply-_totalBurned] + + Contract-mediated amount burned per domain (lower bound). + + +### Witnesses [toc] [#NativeShieldedTokenPublicSupply-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenPublicSupply-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenPublicSupply-Circuits] +### Circuits [!toc] [#NativeShieldedTokenPublicSupply-Circuits] + +#### _addMinted [toc] [#NativeShieldedTokenPublicSupply-_addMinted] +#### [!toc] [#NativeShieldedTokenPublicSupply-_addMinted] + + Adds `amount` to the exact minted total. Call once per + successful `NativeShieldedToken._mint`. + + +#### _addBurned [toc] [#NativeShieldedTokenPublicSupply-_addBurned] +#### [!toc] [#NativeShieldedTokenPublicSupply-_addBurned] + + Adds `amount` to the contract-mediated burned total. Call once + per successful `NativeShieldedToken._burn` / `_burnFromSelf`. + Enforces `burned <= minted`; an unpaired or duplicate burn reverts. + + +#### totalMinted [toc] [#NativeShieldedTokenPublicSupply-totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupply-totalMinted] + + Returns the exact amount ever minted. + + +#### totalBurned [toc] [#NativeShieldedTokenPublicSupply-totalBurned] +#### [!toc] [#NativeShieldedTokenPublicSupply-totalBurned] + + Returns the contract-mediated amount burned. + + This is a lower bound: coins sent directly to the burn address + without going through the contract are not counted. + + + +#### totalSupply [toc] [#NativeShieldedTokenPublicSupply-totalSupply] +#### [!toc] [#NativeShieldedTokenPublicSupply-totalSupply] + + Returns `totalMinted() - totalBurned()`. + + This is an UPPER BOUND on circulating supply, not an exact value: + burns that bypass the contract are invisible. See the module notes. + + + +--- + +## NativeShieldedTokenFamilyPublicSupply [toc] [#NativeShieldedTokenFamilyPublicSupply] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenFamilyPublicSupply"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenFamilyPublicSupply-Ledger] +### Ledger [!toc] [#NativeShieldedTokenFamilyPublicSupply-Ledger] + +#### _totalMinted [toc] [#NativeShieldedTokenFamilyPublicSupply-_totalMinted] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_totalMinted] + + Exact amount minted per domain. See "Accounting guarantees". + + +#### _totalBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-_totalBurned] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_totalBurned] + + Contract-mediated amount burned per domain (lower bound). + + +### Witnesses [toc] [#NativeShieldedTokenFamilyPublicSupply-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenFamilyPublicSupply-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenFamilyPublicSupply-Circuits] +### Circuits [!toc] [#NativeShieldedTokenFamilyPublicSupply-Circuits] + +#### _addMinted [toc] [#NativeShieldedTokenFamilyPublicSupply-_addMinted] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_addMinted] + + Adds `amount` to the exact minted total for `domain`. Call + once per successful `NativeShieldedTokenFamily._mint`. + + +#### _addBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-_addBurned] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_addBurned] + + Adds `amount` to the contract-mediated burned total for + `domain`. Call once per successful `NativeShieldedTokenFamily._burn` / + `_burnFromSelf`. + Enforces per-domain `burned <= minted`; an unpaired, duplicate, or + wrong-domain burn reverts. + + +#### totalMinted [toc] [#NativeShieldedTokenFamilyPublicSupply-totalMinted] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-totalMinted] + + Returns the exact amount ever minted for `domain`. + + +#### totalBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-totalBurned] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-totalBurned] + + Returns the contract-mediated amount burned for `domain`. + + This is a lower bound: coins sent directly to the burn address + without going through the contract are not counted. + + + +#### totalSupply [toc] [#NativeShieldedTokenFamilyPublicSupply-totalSupply] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-totalSupply] + + Returns `totalMinted(domain) - totalBurned(domain)`. + + This is an UPPER BOUND on circulating supply, not an exact value: + burns that bypass the contract are invisible. See the module notes. + + + +--- + +## NativeShieldedTokenPublicSupplyCore [toc] [#NativeShieldedTokenPublicSupplyCore] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenPublicSupplyCore"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenPublicSupplyCore-Ledger] +### Ledger [!toc] [#NativeShieldedTokenPublicSupplyCore-Ledger] + +#### _totalMinted [toc] [#NativeShieldedTokenPublicSupplyCore-_totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_totalMinted] + + Exact amount minted per domain. See "Accounting guarantees". + + +#### _totalBurned [toc] [#NativeShieldedTokenPublicSupplyCore-_totalBurned] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_totalBurned] + + Contract-mediated amount burned per domain (lower bound). + + +### Witnesses [toc] [#NativeShieldedTokenPublicSupplyCore-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenPublicSupplyCore-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenPublicSupplyCore-Circuits] +### Circuits [!toc] [#NativeShieldedTokenPublicSupplyCore-Circuits] + +#### _addMinted [toc] [#NativeShieldedTokenPublicSupplyCore-_addMinted] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_addMinted] + + Adds `amount` to the exact minted total for `domain`. Call + once per successful token `_mint`. + Checks for overflow in order to output a readable error message. + + +#### _addBurned [toc] [#NativeShieldedTokenPublicSupplyCore-_addBurned] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_addBurned] + + Adds `amount` to the contract-mediated burned total for + `domain`. Call once per successful token `_burn` / `_burnFromSelf`. + Enforces the per-domain `burned <= minted` invariant: a wrong-domain, + duplicate, or unpaired burn reverts rather than corrupting [totalSupply](#NativeShieldedTokenPublicSupplyCore-totalSupply). + The guard also keeps the add and the [totalSupply](#NativeShieldedTokenPublicSupplyCore-totalSupply) subtraction within + bounds, so no separate overflow/underflow check is needed. + + +#### totalMinted [toc] [#NativeShieldedTokenPublicSupplyCore-totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-totalMinted] + + Returns the exact amount ever minted for `domain`. + + +#### totalBurned [toc] [#NativeShieldedTokenPublicSupplyCore-totalBurned] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-totalBurned] + + Returns the contract-mediated amount burned for `domain`. + + This is a lower bound: coins sent directly to the burn address + without going through the contract are not counted. + + + +#### totalSupply [toc] [#NativeShieldedTokenPublicSupplyCore-totalSupply] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-totalSupply] + + Returns `totalMinted(domain) - totalBurned(domain)`, or 0 if + [totalBurned](#NativeShieldedTokenPublicSupplyCore-totalBurned) somehow exceeds [totalMinted](#NativeShieldedTokenPublicSupplyCore-totalMinted). + + This is an UPPER BOUND on circulating supply, not an exact value: + burns that bypass the contract are invisible. See the module notes. + + The `burned <= minted` invariant enforced by [_addBurned](#NativeShieldedTokenPublicSupplyCore-_addBurned) already makes + `burned > minted` unreachable through the accounting API, so the clamp never + fires in normal operation. It is kept so the getter is a total function that + reads cleanly (returning 0) rather than reverting on an out-of-bound state. + + +--- + +## NativeShieldedTokenDerivedNonce [toc] [#NativeShieldedTokenDerivedNonce] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenDerivedNonce"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenDerivedNonce-Ledger] +### Ledger [!toc] [#NativeShieldedTokenDerivedNonce-Ledger] + +#### _counter [toc] [#NativeShieldedTokenDerivedNonce-_counter] +#### [!toc] [#NativeShieldedTokenDerivedNonce-_counter] + + Monotonic index feeding the derived-nonce derivation. + + +### Witnesses [toc] [#NativeShieldedTokenDerivedNonce-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenDerivedNonce-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenDerivedNonce-Circuits] +### Circuits [!toc] [#NativeShieldedTokenDerivedNonce-Circuits] + +#### _deriveNonce [toc] [#NativeShieldedTokenDerivedNonce-_deriveNonce] +#### [!toc] [#NativeShieldedTokenDerivedNonce-_deriveNonce] + + Advances the counter and returns the next derived coin nonce, + for use as the `nonce` argument of a native token mint. + + + The derivation input is public ledger state, so mints using this + nonce are recipient-public; see the module notes. + + + +--- + +## ZOwnableNativeShieldedToken [toc] [#ZOwnableNativeShieldedToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/presets/ZOwnableNativeShieldedToken"; +``` + +--- + +### Ledger [toc] [#ZOwnableNativeShieldedToken-Ledger] +### Ledger [!toc] [#ZOwnableNativeShieldedToken-Ledger] + +#### _ownerCommitment [toc] [#ZOwnableNativeShieldedToken-_ownerCommitment] +#### [!toc] [#ZOwnableNativeShieldedToken-_ownerCommitment] + + Stores the current hashed commitment representing the owner. + This commitment is derived from the public identifier (e.g., `SHA256(pk, nonce)`), + the `instanceSalt`, the transfer `counter`, and a domain separator. + + A commitment of `default>` (i.e., zero) indicates the contract is unowned. + + +#### _counter [toc] [#ZOwnableNativeShieldedToken-_counter] +#### [!toc] [#ZOwnableNativeShieldedToken-_counter] + + Internal transfer counter used to prevent commitment reuse. + + Increments by 1 on every successful ownership transfer. Combined with `id` and + `instanceSalt` to compute unique owner commitments over time. + + +#### _instanceSalt [toc] [#ZOwnableNativeShieldedToken-_instanceSalt] +#### [!toc] [#ZOwnableNativeShieldedToken-_instanceSalt] + + A per-instance value provided at initialization used to namespace + commitments for this contract instance. + + This salt prevents commitment collisions across contracts that might otherwise use + the same owner identifiers or domain parameters. It is immutable after initialization. + + +#### _ownableIsInitialized [toc] [#ZOwnableNativeShieldedToken-_ownableIsInitialized] +#### [!toc] [#ZOwnableNativeShieldedToken-_ownableIsInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + +#### _domain [toc] [#ZOwnableNativeShieldedToken-_domain] +#### [!toc] [#ZOwnableNativeShieldedToken-_domain] + + Domain separator fixed at construction; with the contract + address it determines this token's color. + + +#### _name [toc] [#ZOwnableNativeShieldedToken-_name] +#### [!toc] [#ZOwnableNativeShieldedToken-_name] + + The immutable token name. Re-exported from `NativeShieldedToken`. + + +#### _symbol [toc] [#ZOwnableNativeShieldedToken-_symbol] +#### [!toc] [#ZOwnableNativeShieldedToken-_symbol] + + The immutable token symbol. Re-exported from `NativeShieldedToken`. + + +#### _decimals [toc] [#ZOwnableNativeShieldedToken-_decimals] +#### [!toc] [#ZOwnableNativeShieldedToken-_decimals] + + The immutable token decimals. Re-exported from `NativeShieldedToken`. + + +#### _isInitialized [toc] [#ZOwnableNativeShieldedToken-_isInitialized] +#### [!toc] [#ZOwnableNativeShieldedToken-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + +#### _totalMinted [toc] [#ZOwnableNativeShieldedToken-_totalMinted] +#### [!toc] [#ZOwnableNativeShieldedToken-_totalMinted] + + Exact amount minted per domain. See "Accounting guarantees". + + +#### _totalBurned [toc] [#ZOwnableNativeShieldedToken-_totalBurned] +#### [!toc] [#ZOwnableNativeShieldedToken-_totalBurned] + + Contract-mediated amount burned per domain (lower bound). + + +### Witnesses [toc] [#ZOwnableNativeShieldedToken-Witnesses] +### Witnesses [!toc] [#ZOwnableNativeShieldedToken-Witnesses] + +None declared by this module. The composed `ZOwnablePK` requires +[wit_secretNonce](./ownable#ZOwnablePK-wit_secretNonce), which every owner-gated circuit calls through +`assertOnlyOwner`. The implementing DApp supplies it; the TypeScript witnesses in the library repository are +test-only and are not published. + +### Circuits [toc] [#ZOwnableNativeShieldedToken-Circuits] +### Circuits [!toc] [#ZOwnableNativeShieldedToken-Circuits] + +#### initialize [toc] [#ZOwnableNativeShieldedToken-initialize] +#### [!toc] [#ZOwnableNativeShieldedToken-initialize] + + Sets the owner and the token's domain and metadata. + + + Call from the implementing contract's constructor only. It writes + sealed fields, so implementing contracts MUST NOT expose it as a + post-deploy circuit. + + + + `ownerId` must be `_computeOwnerId(pk, secretNonce)` for the coin + public key the owner's wallet will supply as `ownPublicKey()`. The circuit + cannot check this at deploy; any other key leaves the contract with no + usable owner and no recovery. + + + Requirements: + + * Contract is not initialized. + * `ownerId` is not zero. + + +#### mint [toc] [#ZOwnableNativeShieldedToken-mint] +#### [!toc] [#ZOwnableNativeShieldedToken-mint] + + Mints `amount` to `recipient` and adds it to the minted total. + Owner only. + + Requirements: + + * Caller is the owner. + * `recipient` is not zero. + + +#### burn [toc] [#ZOwnableNativeShieldedToken-burn] +#### [!toc] [#ZOwnableNativeShieldedToken-burn] + + Burns `amount` of `coin`, paid into the tx by the caller's + wallet, and refunds the rest to `refundTo`. Owner only. + + Requirements: + + * Caller is the owner. + * `coin.color` is this token's color. + * `amount` \<= `coin.value`. + * `refundTo` is not zero. + + +#### transferOwnership [toc] [#ZOwnableNativeShieldedToken-transferOwnership] +#### [!toc] [#ZOwnableNativeShieldedToken-transferOwnership] + + Transfers ownership to `newOwnerId`. Owner only. + + + `newOwnerId` must be `_computeOwnerId(pk, secretNonce)` for the + coin public key the new owner's wallet will supply as `ownPublicKey()`. + The circuit cannot check this; any other key leaves the contract with no + usable owner and no recovery. + + + Requirements: + + * Caller is the owner. + * `newOwnerId` is not zero. + + +#### renounceOwnership [toc] [#ZOwnableNativeShieldedToken-renounceOwnership] +#### [!toc] [#ZOwnableNativeShieldedToken-renounceOwnership] + + Clears the owner. Owner only. + + + Irreversible: mint and burn stop forever. + + + Requirements: + + * Caller is the owner. + + +#### owner [toc] [#ZOwnableNativeShieldedToken-owner] +#### [!toc] [#ZOwnableNativeShieldedToken-owner] + + Returns the current owner commitment. + + Requirements: + + * Contract is initialized. + + +#### _computeOwnerId [toc] [#ZOwnableNativeShieldedToken-_computeOwnerId] +#### [!toc] [#ZOwnableNativeShieldedToken-_computeOwnerId] + + Returns the owner id `H(pk, nonce)`. + + Requirements: + + * `pk` is a coin public key. + + +#### totalMinted [toc] [#ZOwnableNativeShieldedToken-totalMinted] +#### [!toc] [#ZOwnableNativeShieldedToken-totalMinted] + + Returns the total amount ever minted. + + +#### name [toc] [#ZOwnableNativeShieldedToken-name] +#### [!toc] [#ZOwnableNativeShieldedToken-name] + + Returns the token name. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#ZOwnableNativeShieldedToken-symbol] +#### [!toc] [#ZOwnableNativeShieldedToken-symbol] + + Returns the token symbol. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#ZOwnableNativeShieldedToken-decimals] +#### [!toc] [#ZOwnableNativeShieldedToken-decimals] + + Returns the token decimals. + + Requirements: + + * Contract is initialized. + + +#### tokenColor [toc] [#ZOwnableNativeShieldedToken-tokenColor] +#### [!toc] [#ZOwnableNativeShieldedToken-tokenColor] + + Returns this contract's token color, + `tokenType(tokenDomain, kernel.self())`. + + Requirements: + + * Contract is initialized. + diff --git a/content/contracts-compact/api/non-fungible-token.mdx b/content/contracts-compact/api/non-fungible-token.mdx index 4bfa6409..a1ef742c 100644 --- a/content/contracts-compact/api/non-fungible-token.mdx +++ b/content/contracts-compact/api/non-fungible-token.mdx @@ -11,7 +11,7 @@ This module provides the full NonFungibleToken module API. ## NonFungibleToken [toc] [#NonFungibleToken] ```ts @@ -30,7 +30,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; kind="sealed ledger" id="NonFungibleToken-_name" > - The immutable token name. + / Public state #### _symbol [toc] [#NonFungibleToken-_symbol] @@ -43,6 +43,18 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; The immutable token symbol. +#### _isInitialized [toc] [#NonFungibleToken-_isInitialized] +#### [!toc] [#NonFungibleToken-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + #### _owners [toc] [#NonFungibleToken-_owners] #### [!toc] [#NonFungibleToken-_owners] - Returns a canonical zero `Either` value (left variant with zero `Bytes<32>`). - Used as the zero value for mint/burn operations and cleared approvals. - - #### initialize [toc] [#NonFungibleToken-initialize] #### [!toc] [#NonFungibleToken-initialize] #### ownerOf [toc] [#NonFungibleToken-ownerOf] @@ -184,7 +185,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * Contract is initialized. + * The contract is initialized. #### symbol [toc] [#NonFungibleToken-symbol] @@ -199,7 +200,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * Contract is initialized. + * The contract is initialized. #### tokenURI [toc] [#NonFungibleToken-tokenURI] @@ -210,8 +211,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; complexity="k=9, rows=326" modulePrefix="NonFungibleToken" > - Returns the token URI for the given `tokenId`. - Returns an empty string if a tokenURI does not exist. + Returns the token URI for the given `tokenId`. Returns the empty + string if a tokenURI does not exist. Requirements: @@ -234,12 +235,16 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; complexity="k=9, rows=289" modulePrefix="NonFungibleToken" > - Sets the URI as `tokenURI` for the given `tokenId`. + Sets the URI as [tokenURI](#NonFungibleToken-tokenURI) for the given `tokenId`. Requirements: * The contract is initialized. * The `tokenId` must exist. + + + The URI for a given NFT is usually set when the NFT is minted. + #### approve [toc] [#NonFungibleToken-approve] @@ -247,15 +252,17 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. - The caller's identity is derived from the `wit_NonFungibleTokenSK` witness as `persistentHash(secretKey)`. Only a single account can be approved at a time, so approving the zero value clears previous approvals. + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_NonFungibleTokenSK](#NonFungibleToken-wit_NonFungibleTokenSK) + witness as `persistentHash(secretKey)`. + Requirements: * The contract is initialized. @@ -287,13 +294,15 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; approved: Boolean ) → []' kind="circuit" - complexity="k=13, rows=2936" + complexity="k=13, rows=2947" modulePrefix="NonFungibleToken" stackParams="true" > Approve or remove `operator` as an operator for the caller. Operators can call [transferFrom](#NonFungibleToken-transferFrom) for any token owned by the caller. - The caller's identity is derived from the `wit_NonFungibleTokenSK` witness as `persistentHash(secretKey)`. + + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_NonFungibleTokenSK](#NonFungibleToken-wit_NonFungibleTokenSK) + witness as `persistentHash(secretKey)`. Requirements: @@ -322,16 +331,19 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Transfers `tokenId` token from `fromAddress` to `to`. - The caller's identity is derived from the `wit_NonFungibleTokenSK` witness as `persistentHash(secretKey)`. - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. + Transfers to contract addresses are currently disallowed until contract-to-contract interactions + are supported in Compact. This restriction prevents assets from being inadvertently locked in contracts that cannot + currently handle token receipt. + + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_NonFungibleTokenSK](#NonFungibleToken-wit_NonFungibleTokenSK) + witness as `persistentHash(secretKey)`. Requirements: @@ -349,16 +361,18 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; - Unsafe variant of [transferFrom](#NonFungibleToken-transferFrom) which allows transfers to contract addresses. - The caller's identity is derived from the `wit_NonFungibleTokenSK` witness as `persistentHash(secretKey)`. + Transfers `tokenId` token from `fromAddress` to `to`. It does NOT check if the recipient is a ContractAddress. + + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_NonFungibleTokenSK](#NonFungibleToken-wit_NonFungibleTokenSK) + witness as `persistentHash(secretKey)`. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract calls + are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. @@ -394,8 +408,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; complexity="k=9, rows=308" modulePrefix="NonFungibleToken" > - Returns the approved address for `tokenId`. - Returns the zero value if `tokenId` is not minted. + Returns the approved address for `tokenId`. Returns the zero value if `tokenId` is not minted. Requirements: @@ -411,15 +424,16 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; stackParams="true" modulePrefix="NonFungibleToken" > - Returns whether `spender` is allowed to manage `owner`'s tokens, - or `tokenId` in particular (ignoring whether it is owned by `owner`). + Returns whether `spender` is allowed to manage `owner`'s tokens, or `tokenId` in + particular (ignoring whether it is owned by `owner`). Requirements: * The contract is initialized. - This function assumes that `owner` is the actual owner of `tokenId` and does not verify this assumption. + This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + assumption. @@ -440,44 +454,21 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; * `spender` has approval from `owner` for `tokenId` OR `spender` has approval to manage all of `owner`'s assets. - This function assumes that `owner` is the actual owner of `tokenId` and does not verify this assumption. + This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + assumption. -#### _update [toc] [#NonFungibleToken-_update] -#### [!toc] [#NonFungibleToken-_update] - - Transfers `tokenId` from its current owner to `to`, - or alternatively mints (or burns) if the current owner (or `to`) is the zero address. - Returns the owner of the `tokenId` before the update. - - Requirements: - - * The contract is initialized. - * If `auth` is non 0, then this function will check that `auth` is either the owner of the token, - or approved to operate on the token (by the owner). - - #### _mint [toc] [#NonFungibleToken-_mint] #### [!toc] [#NonFungibleToken-_mint] Mints `tokenId` and transfers it to `to`. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - - Requirements: * The contract is initialized. @@ -491,22 +482,22 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; - Unsafe variant of [_mint](#NonFungibleToken-_mint) which allows transfers to contract addresses. - - - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. - Once contract-to-contract calls are supported, this circuit may be deprecated. - + Mints `tokenId` and transfers it to `to`. It does NOT check if the recipient is a ContractAddress. Requirements: - * Contract is initialized. + * The contract is initialized. * `tokenId` must not exist. * `to` is not zero. + + + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. + Once contract-to-contract calls are supported, this circuit may be deprecated. + #### _burn [toc] [#NonFungibleToken-_burn] @@ -514,7 +505,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Destroys `tokenId`. @@ -532,16 +523,17 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; - Transfers `tokenId` from `fromAddress` to `to`. As opposed to [transferFrom](#NonFungibleToken-transferFrom), - this imposes no restrictions on the caller's identity. + Transfers `tokenId` from `fromAddress` to `to`. + As opposed to [transferFrom](#NonFungibleToken-transferFrom), this imposes no restrictions on the caller's identity. - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from being inadvertently + locked in contracts that cannot currently handle token receipt. Requirements: @@ -557,25 +549,25 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; - Unsafe variant of [_transfer](#NonFungibleToken-_transfer) which allows transfers to contract addresses. - - Transfers `tokenId` from `fromAddress` to `to`. As opposed to [_unsafeTransferFrom](#NonFungibleToken-_unsafeTransferFrom), this imposes no restrictions on the caller's identity. It does NOT check if the recipient is a `ContractAddress`. - - - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. - Once contract-to-contract calls are supported, this circuit may be deprecated. - + Transfers `tokenId` from `fromAddress` to `to`. + As opposed to [_unsafeTransferFrom](#NonFungibleToken-_unsafeTransferFrom), this imposes no restrictions on the caller's identity. + It does NOT check if the recipient is a ContractAddress. Requirements: - * Contract is initialized. + * The contract is initialized. * `to` is not zero. * `tokenId` token must be owned by `fromAddress`. + + + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. + Once contract-to-contract calls are supported, this circuit may be deprecated. + #### _approve [toc] [#NonFungibleToken-_approve] @@ -583,7 +575,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -592,6 +584,43 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: * The contract is initialized. + * `tokenId` must exist. + * If `auth` is non 0, then this function will check that `auth` is either the owner of the token, + or approved to operate on the token (by the owner). + + +#### _unsafeApprove [toc] [#NonFungibleToken-_unsafeApprove] +#### [!toc] [#NonFungibleToken-_unsafeApprove] + + Approve `to` to operate on `tokenId`, requiring the token to exist + only when `isExistenceRequired` is true. + + + Passing false skips the check that `tokenId` was minted. Both + [_unsafeTransferFrom](#NonFungibleToken-_unsafeTransferFrom) and `_update` read that invariant back, so a planted approval + can mint a token nobody authorized or outlive a later mint. Pass false only where + the owner has already been read, as `_update` does. + + + Counterpart to Solidity's `_approve(to, tokenId, auth, bool emitEvent)`, where + the flag also suppresses the `Approval` event. Compact has no events, so it carries + the existence check alone. + + Requirements: + + * The contract is initialized. + * `tokenId` must exist, unless `isExistenceRequired` is false and `auth` is zero. * If `auth` is non 0, then this function will check that `auth` is either the owner of the token, or approved to operate on the token (by the owner). @@ -601,7 +630,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -631,47 +660,3 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; * `tokenId` must exist. -#### _computeAccountId [toc] [#NonFungibleToken-_computeAccountId] -#### [!toc] [#NonFungibleToken-_computeAccountId] - - Computes the caller's account identifier from the `wit_NonFungibleTokenSK` witness. - - ID Derivation: `accountId = persistentHash(secretKey)` - - The result is a 32-byte commitment that uniquely identifies the caller. - - -#### computeAccountId [toc] [#NonFungibleToken-computeAccountId] -#### [!toc] [#NonFungibleToken-computeAccountId] - - Computes an account identifier without on-chain state, allowing a user to derive - their identity commitment before submitting it in a token operation. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. - - ID Derivation: `accountId = persistentHash(secretKey)` - - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the security of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss — a lost key cannot be recovered. - - - -#### _isTargetZero [toc] [#NonFungibleToken-_isTargetZero] -#### [!toc] [#NonFungibleToken-_isTargetZero] - - Returns `true` if `target`'s active branch (as indicated by `is_left`) holds the zero value. - diff --git a/content/contracts-compact/api/ownable.mdx b/content/contracts-compact/api/ownable.mdx index 8d4cd78d..31a29a89 100644 --- a/content/contracts-compact/api/ownable.mdx +++ b/content/contracts-compact/api/ownable.mdx @@ -8,10 +8,15 @@ This module provides the full Ownable module API. For an overview of the module, read the [Ownable guide](../ownable). +The page covers the following modules: + +- [Ownable](#Ownable): a single owner identified by a public account identifier. +- [ZOwnablePK](#ZOwnablePK): a single owner identified by a shielded commitment. + ## Ownable [toc] [#Ownable] ```ts @@ -23,6 +28,18 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; ### Ledger [toc] [#Ownable-Ledger] ### Ledger [!toc] [#Ownable-Ledger] +#### _isInitialized [toc] [#Ownable-_isInitialized] +#### [!toc] [#Ownable-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + #### _owner [toc] [#Ownable-_owner] #### [!toc] [#Ownable-_owner] - Either a `Bytes<32>` account identifier or `ContractAddress` representing the owner. + Either a `Bytes<32>` account identifier or a `ContractAddress` representing the owner. + The value is stored canonicalized (the inactive branch of the `Either` is zeroed). + A `Bytes<32>` value of zero means the contract is unowned. ### Witnesses [toc] [#Ownable-Witnesses] ### Witnesses [!toc] [#Ownable-Witnesses] +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + #### wit_OwnableSK [toc] [#Ownable-wit_OwnableSK] #### [!toc] [#Ownable-wit_OwnableSK] - Ownership transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. + Ownership transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents permanently disabling access to a circuit. @@ -116,10 +139,11 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; complexity="k=13, rows=2956" modulePrefix="Ownable" > - Unsafe variant of [`transferOwnership`](#Ownable-transferOwnership). + Unsafe variant of [transferOwnership](#Ownable-transferOwnership). - Ownership transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. + Ownership transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Ownership privileges sent to a contract address may become uncallable. Once contract-to-contract calls are supported, this circuit may be deprecated. @@ -140,7 +164,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; modulePrefix="Ownable" > Leaves the contract without an owner. - It will not be possible to call [`assertOnlyOwner`](#Ownable-assertOnlyOwner) circuits anymore. + It will not be possible to call [assertOnlyOwner](#Ownable-assertOnlyOwner) circuits anymore. Can only be called by the current owner. Requirements: @@ -159,7 +183,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; > Throws if called by any account other than the owner. Use this to restrict access of specific circuits to the owner. - The caller's identity is derived from the `wit_OwnableSK` witness as `persistentHash(secretKey)`. + + In the case of an external (non-contract) caller, the caller’s identity is + derived from the [wit_OwnableSK](#Ownable-wit_OwnableSK) witness as `persistentHash(secretKey)`. Requirements: @@ -175,10 +201,12 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; complexity="k=10, rows=600" modulePrefix="Ownable" > - Transfers ownership of the contract to a `newOwner` without enforcing permission checks on the caller. + Transfers ownership of the contract to `newOwner` without + enforcing permission checks on the caller. - Ownership transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. + Ownership transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents circuits from being inadvertently locked in contracts. @@ -196,10 +224,11 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; complexity="k=10, rows=597" modulePrefix="Ownable" > - Unsafe variant of [`_transferOwnership`](#Ownable-_transferOwnership). + Unsafe variant of [_transferOwnership](#Ownable-_transferOwnership). - Ownership transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. + Ownership transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Ownership privileges sent to a contract address may become uncallable. Once contract-to-contract calls are supported, this circuit may be deprecated. @@ -209,48 +238,12 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; * Contract is initialized. -#### _computeAccountId [toc] [#Ownable-_computeAccountId] -#### [!toc] [#Ownable-_computeAccountId] - - Computes the caller's account identifier from the `wit_OwnableSK` witness. - - ID Derivation: `accountId = persistentHash(secretKey)` - - The result is a 32-byte commitment that uniquely identifies the caller. - - -#### computeAccountId [toc] [#Ownable-computeAccountId] -#### [!toc] [#Ownable-computeAccountId] - - Computes an account identifier without on-chain state, allowing a user to derive - their identity commitment before submitting it in an ownership transfer. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. - - ID Derivation: `accountId = persistentHash(secretKey)` - - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the security of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss. - A lost key cannot be recovered. - - - --- ## ZOwnablePK [toc] [#ZOwnablePK] ```ts @@ -259,8 +252,20 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; --- -### Ledger [toc] [#ZOwnablePK-ledger] -### Ledger [!toc] [#ZOwnablePK-ledger] +### Ledger [toc] [#ZOwnablePK-Ledger] +### Ledger [!toc] [#ZOwnablePK-Ledger] + +#### _isInitialized [toc] [#ZOwnablePK-_isInitialized] +#### [!toc] [#ZOwnablePK-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + #### _ownerCommitment [toc] [#ZOwnablePK-_ownerCommitment] #### [!toc] [#ZOwnablePK-_ownerCommitment] @@ -273,7 +278,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; This commitment is derived from the public identifier (e.g., `SHA256(pk, nonce)`), the `instanceSalt`, the transfer `counter`, and a domain separator. - A commitment of `default>` (i.e. zero) indicates the contract is unowned. + A commitment of `default>` (i.e., zero) indicates the contract is unowned. #### _counter [toc] [#ZOwnablePK-_counter] @@ -285,8 +290,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; > Internal transfer counter used to prevent commitment reuse. - Increments by 1 on every successful ownership transfer. - Combined with `id` and `instanceSalt` to compute unique owner commitments over time. + Increments by 1 on every successful ownership transfer. Combined with `id` and + `instanceSalt` to compute unique owner commitments over time. #### _instanceSalt [toc] [#ZOwnablePK-_instanceSalt] @@ -296,59 +301,63 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; kind="sealed ledger" id="ZOwnablePK-_instanceSalt" > - A per-instance value provided at initialization used to namespace commitments for this contract instance. + A per-instance value provided at initialization used to namespace + commitments for this contract instance. - This salt prevents commitment collisions across contracts that might otherwise use the same owner identifiers or domain parameters. - It is immutable after initialization. + This salt prevents commitment collisions across contracts that might otherwise use + the same owner identifiers or domain parameters. It is immutable after initialization. -### Witnesses [toc] [#ZOwnablePK-witnesses] -### Witnesses [!toc] [#ZOwnablePK-witnesses] +### Witnesses [toc] [#ZOwnablePK-Witnesses] +### Witnesses [!toc] [#ZOwnablePK-Witnesses] + +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. #### wit_secretNonce [toc] [#ZOwnablePK-wit_secretNonce] #### [!toc] [#ZOwnablePK-wit_secretNonce] A private per-user nonce used in deriving the shielded owner identifier. - Combined with the user's public key as `SHA256(pk, nonce)` to produce an obfuscated, unlinkable identity commitment. - Users are encouraged to rotate this value on ownership changes. + Combined with the user's public key as `SHA256(pk, nonce)` to produce an obfuscated, + unlinkable identity commitment. Users are encouraged to rotate this value on ownership changes. -### Circuits [toc] [#ZOwnablePK-circuits] -### Circuits [!toc] [#ZOwnablePK-circuits] +### Circuits [toc] [#ZOwnablePK-Circuits] +### Circuits [!toc] [#ZOwnablePK-Circuits] #### initialize [toc] [#ZOwnablePK-initialize] #### [!toc] [#ZOwnablePK-initialize] Initializes the contract by setting the initial owner via `ownerId` - and storing the `instanceSalt` that acts as a privacy additive - for preventing duplicate commitments among other contracts implementing ZOwnablePK. + and storing the `instanceSalt` that acts as a privacy additive for preventing + duplicate commitments among other contracts implementing ZOwnablePK. The `ownerId` must be calculated prior to contract deployment using the SHA256 hashing algorithm. Using any other algorithm will result in a permanent loss of contract access. - See [_computeOwnerId](#ZOwnablePK-_computeOwnerId). Requirements: * Contract is not initialized. * `ownerId` is not zero. + * `instanceSalt` is not zero. #### owner [toc] [#ZOwnablePK-owner] #### [!toc] [#ZOwnablePK-owner] Leaves the contract without an owner. - It will not be possible to call `assertOnlyOwner` circuits anymore. + It will not be possible to call [assertOnlyOwner](#ZOwnablePK-assertOnlyOwner) circuits anymore. Can only be called by the current owner. Requirements: @@ -400,7 +409,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; #### assertOnlyOwner [toc] [#ZOwnablePK-assertOnlyOwner] #### [!toc] [#ZOwnablePK-assertOnlyOwner] #### _computeOwnerCommitment [toc] [#ZOwnablePK-_computeOwnerCommitment] #### [!toc] [#ZOwnablePK-_computeOwnerCommitment] Computes the owner commitment from the given `id` and `counter`. - The commitment derivation follows: - `commitment = SHA256(id, instanceSalt, counter, "ZOwnablePK:shield:")` + ## Owner ID (`id`) + The `id` is expected to be computed off-chain as: + `id = SHA256(pk, nonce)` + + * `pk`: The owner's public key. + * `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. + + ## Commitment Derivation + `commitment = SHA256(id, instanceSalt, counter, domain)` - Where: - - `id`: The unique identifier `SHA256(pk, nonce)`. - - `instanceSalt`: Per-deployment salt to prevent collisions. - - `counter`: Incremented with each transfer for uniqueness. Cast to `Field` then `Bytes<32>` for hashing. - - Domain separator: `"ZOwnablePK:shield:"` padded to 32 bytes. + * `id`: See above. + * `instanceSalt`: A unique per-deployment salt, stored during initialization. + This prevents commitment collisions across deployments. + * `counter`: Incremented with each ownership transfer, ensuring uniqueness + even with repeated `id` values. Cast to `Field` then `Bytes<32>` for hashing. + * `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent + hash collisions when extending the module or using similar commitment schemes. Requirements: @@ -442,16 +461,33 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; #### _computeOwnerId [toc] [#ZOwnablePK-_computeOwnerId] #### [!toc] [#ZOwnablePK-_computeOwnerId] - Computes the unique identifier (`id`) of the owner from their public key and a secret nonce. + Computes the unique identifier (`id`) of the owner from their + public key and a secret nonce. - The ID derivation follows: `id = SHA256(pk, nonce)` + ## ID Derivation + `id = SHA256(pk, nonce)` + + * `pk`: The public key of the caller. This is passed explicitly to allow + for off-chain derivation, testing, or scenarios where the caller is + different from the subject of the computation. + We recommend using an Air-Gapped Public Key. + * `nonce`: A secret nonce tied to the identity. The generation strategy is + left to the user, offering different security/convenience trade-offs. + + The result is a 32-byte commitment that uniquely identifies the owner. + This value is later used in owner commitment hashing, + and acts as a privacy-preserving alternative to a raw public key. - Currently only supports `ZswapCoinPublicKey`. Contract address owners are not yet supported. + This module allows ownership to be tied to an identity commitment derived + from a public key and secret nonce. + While typically used with user public keys, this mechanism may also + support contract addresses as identifiers in future contract-to-contract + interactions. Both are treated as 32-byte values (`Bytes<32>`). Requirements: @@ -462,26 +498,15 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; #### _transferOwnership [toc] [#ZOwnablePK-_transferOwnership] #### [!toc] [#ZOwnablePK-_transferOwnership] - Transfers ownership to owner id `newOwnerId` without enforcing permission checks on the caller. - - This is an internal function that increments the counter and updates the owner commitment. + Transfers ownership to owner id `newOwnerId` without + enforcing permission checks on the caller. Requirements: * Contract is initialized. - -#### _isTargetZero [toc] [#Ownable-_isTargetZero] -#### [!toc] [#Ownable-_isTargetZero] - - Returns `true` if `target`'s active branch (as indicated by `is_left`) holds the zero value. - diff --git a/content/contracts-compact/api/security.mdx b/content/contracts-compact/api/security.mdx index 5920fea5..7321d7e0 100644 --- a/content/contracts-compact/api/security.mdx +++ b/content/contracts-compact/api/security.mdx @@ -8,16 +8,31 @@ This package provides the API for all Security modules. For an overview of the module, read the [Security guide](../security). +The page covers the following modules: + +- [Initializable](#Initializable): a one-time initialization guard. +- [Pausable](#Pausable): an emergency stop mechanism. +- [Allowlist](#Allowlist): a permit-list keyed by account identifier. +- [Blocklist](#Blocklist): a deny-list keyed by account identifier. + ## Initializable [toc] [#Initializable] ```ts import "./node_modules/@openzeppelin/compact-contracts/security/Initializable"; ``` + + Use this module only at the top contract layer, and import it into at most one module per contract. + When two modules in the same directory both import Initializable, the compiler collapses their + `_isInitialized` flags into a single shared ledger slot + ([compact#270](https://github.com/LFDT-Minokawa/compact/issues/270)): initializing one marks the other + as initialized. The OpenZeppelin modules do not import this module; each tracks its own `_isInitialized` flag inline. + + --- ### Ledger [toc] [#Initializable-Ledger] @@ -91,7 +106,7 @@ None. ## Pausable [toc] [#Pausable] ```ts @@ -191,3 +206,181 @@ None. * Contract must be paused. + +--- + +## Allowlist [toc] [#Allowlist] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/security/Allowlist"; +``` + +A minimal permit-list keyed by a `Bytes<32>` account identifier. +The mutators `_allow` and `_disallow` are ungated: the implementing contract must protect them with its own access control +and decides where `assertAllowed` applies. +Membership is public and the approved set is publicly enumerable. + +--- + +### Ledger [toc] [#Allowlist-Ledger] +### Ledger [!toc] [#Allowlist-Ledger] + +#### _allowed [toc] [#Allowlist-_allowed] +#### [!toc] [#Allowlist-_allowed] + + Approved accounts. Absent means not allowed. + + +### Witnesses [toc] [#Allowlist-Witnesses] +### Witnesses [!toc] [#Allowlist-Witnesses] + +None. + +### Circuits [toc] [#Allowlist-Circuits] +### Circuits [!toc] [#Allowlist-Circuits] + +#### isAllowed [toc] [#Allowlist-isAllowed] +#### [!toc] [#Allowlist-isAllowed] + + Returns whether `account` is currently allowed. + + +#### assertAllowed [toc] [#Allowlist-assertAllowed] +#### [!toc] [#Allowlist-assertAllowed] + + Asserts that `account` is allowed. The composing contract + decides when this gate applies. + + Requirements: + + * `account` is a member of the allowlist. + + +#### _allow [toc] [#Allowlist-_allow] +#### [!toc] [#Allowlist-_allow] + + Adds `account` to the allowlist. + + +#### _disallow [toc] [#Allowlist-_disallow] +#### [!toc] [#Allowlist-_disallow] + + Removes `account` from the allowlist. + + +--- + +## Blocklist [toc] [#Blocklist] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/security/Blocklist"; +``` + +A minimal deny-list keyed by a `Bytes<32>` account identifier. +The mutators `_block` and `_unblock` are ungated: the implementing contract must protect them with its own access control +and decides where `assertNotBlocked` applies. +Membership is public and the blocked set is publicly enumerable. + +--- + +### Ledger [toc] [#Blocklist-Ledger] +### Ledger [!toc] [#Blocklist-Ledger] + +#### _blocked [toc] [#Blocklist-_blocked] +#### [!toc] [#Blocklist-_blocked] + + Blocked accounts. Absent means not blocked. + + +### Witnesses [toc] [#Blocklist-Witnesses] +### Witnesses [!toc] [#Blocklist-Witnesses] + +None. + +### Circuits [toc] [#Blocklist-Circuits] +### Circuits [!toc] [#Blocklist-Circuits] + +#### isBlocked [toc] [#Blocklist-isBlocked] +#### [!toc] [#Blocklist-isBlocked] + + Returns whether `account` is currently blocked. + + +#### assertNotBlocked [toc] [#Blocklist-assertNotBlocked] +#### [!toc] [#Blocklist-assertNotBlocked] + + Asserts that `account` is not blocked. The composing contract + decides when this gate applies. + + Requirements: + + * `account` is not a member of the blocklist. + + +#### _block [toc] [#Blocklist-_block] +#### [!toc] [#Blocklist-_block] + + Adds `account` to the blocklist. + + +#### _unblock [toc] [#Blocklist-_unblock] +#### [!toc] [#Blocklist-_unblock] + + Removes `account` from the blocklist. + diff --git a/content/contracts-compact/api/shielded-access-control.mdx b/content/contracts-compact/api/shielded-access-control.mdx index d9cf7ced..f17fdce3 100644 --- a/content/contracts-compact/api/shielded-access-control.mdx +++ b/content/contracts-compact/api/shielded-access-control.mdx @@ -8,14 +8,14 @@ This module provides a shielded role-based access control (RBAC) mechanism, where roles can be used to represent a set of permissions. Roles are stored as Merkle tree commitments to avoid disclosing information about role holders. Role commitments are created with the following hashing scheme, -where `‖` denotes concatenation and all values are `Bytes<32>`: +where `||` denotes concatenation and all values are `Bytes<32>`: ``` -roleCommitment := SHA256( role ‖ accountId ‖ instanceSalt ‖ commitmentDomain ) +roleCommitment := SHA256( role || accountId || instanceSalt || commitmentDomain ) -accountId := SHA256( secretKey ‖ instanceSalt ‖ accountIdDomain ) +accountId := SHA256( secretKey || instanceSalt || accountIdDomain ) -roleNullifier := SHA256( roleCommitment ‖ nullifierDomain ) +roleNullifier := SHA256( roleCommitment || nullifierDomain ) commitmentDomain := pad(32, "ShieldedAccessControl:commitment") accountIdDomain := pad(32, "ShieldedAccessControl:accountId") @@ -77,11 +77,11 @@ More complex role relationships can be created by using [_setRoleAdmin](#Shielde ## ShieldedAccessControl [toc] [#ShieldedAccessControl] ```ts -import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl" +import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl"; ``` --- @@ -106,7 +106,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="type" id="ShieldedAccessControl-RoleCommitment" > - A Merkle tree leaf committing a `(role, accountId)` pairing. Computed as `SHA256(role, accountId, instanceSalt, commitmentDomain)`. + A Merkle tree leaf committing a `(role, accountId)` pairing. Computed as `SHA256(role || accountId || instanceSalt || commitmentDomain)`. #### RoleIdentifier [toc] [#ShieldedAccessControl-RoleIdentifier] @@ -126,7 +126,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="type" id="ShieldedAccessControl-AccountIdentifier" > - A privacy-preserving identity commitment. Computed as `SHA256(secretKey, instanceSalt, accountIdDomain)`. + A privacy-preserving identity commitment. Computed as `SHA256(secretKey || instanceSalt || accountIdDomain)`. #### RoleNullifier [toc] [#ShieldedAccessControl-RoleNullifier] @@ -136,12 +136,24 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="type" id="ShieldedAccessControl-RoleNullifier" > - A one-time burn token that permanently invalidates a role commitment on revocation. Computed as `SHA256(roleCommitment, nullifierDomain)`. + A one-time burn token that permanently invalidates a role commitment on revocation. Computed as `SHA256(roleCommitment || nullifierDomain)`. ### Ledger [toc] [#ShieldedAccessControl-Ledger] ### Ledger [!toc] [#ShieldedAccessControl-Ledger] +#### _isInitialized [toc] [#ShieldedAccessControl-_isInitialized] +#### [!toc] [#ShieldedAccessControl-_isInitialized] + + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. + + #### _operatorRoles [toc] [#ShieldedAccessControl-_operatorRoles] #### [!toc] [#ShieldedAccessControl-_operatorRoles] - A Merkle tree of role commitments stored as `SHA256(role | accountId | instanceSalt | commitmentDomain)`. - Has a fixed capacity of 2^20 leaf slots. + A Merkle tree of role commitments stored as SHA256(role | accountId | instanceSalt | commitmentDomain) + Role commitments are derived from a public role identifier (e.g., `persistentHash>(pad(32, "MY_ROLE")`), + an account identifier (e.g., `SHA256(secretKey, instanceSalt, accountIdDomain)`), the `instanceSalt`, and a domain separator. #### _adminRoles [toc] [#ShieldedAccessControl-_adminRoles] @@ -170,7 +183,19 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="ledger" id="ShieldedAccessControl-_roleCommitmentNullifiers" > - A set of nullifiers used to prove a role has been revoked. + A set of nullifiers used to prove a role has been revoked + + +#### _grantedRoleNullifiers [toc] [#ShieldedAccessControl-_grantedRoleNullifiers] +#### [!toc] [#ShieldedAccessControl-_grantedRoleNullifiers] + + Nullifiers of every pairing ever granted. Makes a repeated grant a no-op so + one pairing can never consume more than one `_operatorRoles` slot, and gates revocation + so a nullifier can only exist for a granted pairing. #### _instanceSalt [toc] [#ShieldedAccessControl-_instanceSalt] @@ -180,15 +205,24 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="sealed ledger" id="ShieldedAccessControl-_instanceSalt" > - A per-instance value provided at initialization used to namespace commitments for this contract instance. - - This salt prevents commitment collisions across contracts that might otherwise use the same identifiers or domain parameters. - It should be a cryptographically strong random value. It is immutable after initialization. + A per-instance value provided at initialization used to namespace + commitments for this contract instance. + + This salt prevents commitment collisions across contracts that might otherwise use + the same identifiers or domain parameters. It should be a cryptographically strong random value. + If two deployments share the same instanceSalt and a user reuses their secretKey across both, + the resulting accountId and all role commitments will be identical in both contracts. + An observer who sees the same nullifier appear in both contracts' nullifier sets can conclude + that it's the same user. + It is immutable after initialization. ### Witnesses [toc] [#ShieldedAccessControl-Witnesses] ### Witnesses [!toc] [#ShieldedAccessControl-Witnesses] +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + #### wit_getRoleCommitmentPath [toc] [#ShieldedAccessControl-wit_getRoleCommitmentPath] #### [!toc] [#ShieldedAccessControl-wit_getRoleCommitmentPath] - Returns a path to a role commitment in the `_operatorRoles` Merkle tree if one exists. - Otherwise, returns an invalid path. + Returns a path to a role commitment in the `_operatorRoles` Merkle tree if one exists. Otherwise, returns an invalid path. #### wit_secretKey [toc] [#ShieldedAccessControl-wit_secretKey] @@ -228,13 +261,12 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont The `instanceSalt` must be calculated prior to contract deployment using a cryptographically - secure random number generator (e.g. `crypto.getRandomValues()`) to maintain strong privacy guarantees. + secure random number generator e.g. crypto.getRandomValues() to maintain strong privacy guarantees Requirements: * Contract is not initialized. - * `instanceSalt` must not be zero. #### DEFAULT_ADMIN_ROLE [toc] [#ShieldedAccessControl-DEFAULT_ADMIN_ROLE] @@ -244,9 +276,11 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="pure" modulePrefix="ShieldedAccessControl" > - The default admin role for all roles. Returns zero bytes (`default>`). - Only accounts with this role will be able to grant or revoke other roles - unless custom admin roles are created via [_setRoleAdmin](#ShieldedAccessControl-_setRoleAdmin). + The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles + unless custom admin roles are created. + + The Compact language does not support constant declarations, + so DEFAULT_ADMIN_ROLE is implemented as a circuit that returns a constant value by necessity. #### assertOnlyRole [toc] [#ShieldedAccessControl-assertOnlyRole] @@ -256,15 +290,19 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="circuit" modulePrefix="ShieldedAccessControl" > - Reverts if the caller cannot provide a valid proof of ownership for `role`. - - Disclosures: a Merkle tree path to a role commitment, a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. + Reverts if caller cannot provide a valid proof of ownership for `role`. Requirements: + * caller must prove ownership of `role`. + * caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * Contract is initialized. - * Caller must prove ownership of `role`. - * Caller must not provide a valid Merkle tree path for a different `(role, accountId)` pairing. + + Disclosures: + + * A Merkle tree path to a role commitment. + * A role commitment corresponding to a `(role, accountId)` pairing. + * A nullifier for the respective role commitment. #### canProveRole [toc] [#ShieldedAccessControl-canProveRole] @@ -274,36 +312,20 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="circuit" modulePrefix="ShieldedAccessControl" > - Returns `true` if the caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed + Returns `true` if a caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an unauthorized caller. - Disclosures: a Merkle tree path to a role commitment, a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. - Requirements: + * caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * Contract is initialized. - * Caller must not provide a valid Merkle tree path for a different `(role, accountId)` pairing. - - -#### _uncheckedCanProveRole [toc] [#ShieldedAccessControl-_uncheckedCanProveRole] -#### [!toc] [#ShieldedAccessControl-_uncheckedCanProveRole] - - Returns `true` if the caller proves ownership of `role` and is not revoked. - MAY return false for a legitimately credentialed caller if the proving environment supplies an invalid Merkle path. - This circuit will never return true for an unauthorized caller. - Disclosures: a Merkle tree path to a role commitment, a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. + Disclosures: - - This circuit does not perform an initialization check. - It is only meant to be used as an internal helper. - Using this circuit outside of the module may cause undefined behavior and break security guarantees. - + * A Merkle tree path to a role commitment. + * A role commitment corresponding to a `(role, accountId)` pairing. + * A nullifier for the respective role commitment. #### grantRole [toc] [#ShieldedAccessControl-grantRole] @@ -315,18 +337,23 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont modulePrefix="ShieldedAccessControl" > Grants `role` to `accountId` by inserting a role commitment unique to the - `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued - so long as they remain unrevoked. Once revoked, a role cannot be re-granted to the same `accountId`. A new + `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Granting an already active + pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new `accountId` must be generated to be re-authorized for a revoked `role`. - Disclosures: a Merkle tree path to a role commitment, a role commitment, a nullifier, and a role identifier. - Requirements: - * Contract is initialized. - * Caller must prove they are an admin for `role`. - * Caller must not provide a valid Merkle tree path for a different `(role, accountId)` pairing. + * caller must prove they're an admin for `role`. + * caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. * The `(role, accountId)` pairing must not be already revoked. + * Contract is initialized. + + Disclosures: + + * A Merkle tree path to a role commitment. + * A role commitment corresponding to a `(role, accountId)` pairing. + * A nullifier for the respective role commitment. + * A role identifier. #### revokeRole [toc] [#ShieldedAccessControl-revokeRole] @@ -341,19 +368,20 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for `role`. - - At this time, proofs of non-membership on values in the Merkle tree are not available, - so a `(role, accountId)` pairing that does not exist can still be revoked. - - - Disclosures: a Merkle tree path to a role commitment, a role commitment, a nullifier, and a role identifier. - Requirements: - * Contract is initialized. - * Caller must prove they are an admin for `role`. - * Caller must not provide a valid Merkle tree path for a different `(role, accountId)` pairing. + * caller must prove they're an admin for `role`. + * caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * The `(role, accountId)` pairing must have been granted. * The `(role, accountId)` pairing must not be already revoked. + * Contract is initialized. + + Disclosures: + + * A Merkle tree path to a role commitment. + * A role commitment corresponding to a `(role, accountId)` pairing. + * A nullifier for the respective role commitment. + * A role identifier. #### renounceRole [toc] [#ShieldedAccessControl-renounceRole] @@ -368,44 +396,28 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for `role`. - Roles are often managed via [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole): - this circuit's purpose is to provide a mechanism for accounts to lose their privileges - if they are compromised (such as when a trusted device is misplaced). - - Disclosures: a nullifier for the respective role commitment. + + Roles are often managed via [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole): this circuit's + purpose is to provide a mechanism for accounts to lose their privileges + if they are compromised (such as when a trusted device is misplaced). + Consumers with no need for self-revocation should not export this circuit. + Outside observers may be able to use timing and pattern analysis to weaken pseudonymity - guarantees if `renounceRole` is used in tandem with other on-chain actions. + guarantees if renounceRole is used in tandem with other on-chain actions. Requirements: - * Contract is initialized. * The caller must provide a valid `accountId` for the role. + * The `(role, accountId)` pairing must have been granted. * The `(role, accountId)` pairing must not be already revoked. - - -#### _updateRole [toc] [#ShieldedAccessControl-_updateRole] -#### [!toc] [#ShieldedAccessControl-_updateRole] - - Core business logic for the grant/revoke role circuits. - Asserts that the `(role, accountId)` pairing has not already been revoked. - On success, dispatches on `updateType`: a `Grant` inserts the role commitment into `_operatorRoles`, and - a `Revoke` inserts the nullifier into `_roleCommitmentNullifiers`. + * Contract is initialized. - Disclosures: a nullifier for the respective role commitment, and a role commitment (on Grant only). + Disclosures: - - The nullifier is disclosed via `_roleCommitmentNullifiers.member` on every call, - regardless of the update type. - This enables observers to correlate grant and revocation transactions for the same `(role, accountId)` pairing. - + * A nullifier for the respective role commitment. #### getRoleAdmin [toc] [#ShieldedAccessControl-getRoleAdmin] @@ -415,14 +427,16 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="circuit" modulePrefix="ShieldedAccessControl" > - Returns the admin role that controls `role`. Returns `DEFAULT_ADMIN_ROLE` for - roles with no explicitly set admin. Since `DEFAULT_ADMIN_ROLE` is the zero byte array, - there is no distinction between a nonexistent `role` and one whose admin is `DEFAULT_ADMIN_ROLE`. + Returns the admin role that controls `role`. Returns [DEFAULT_ADMIN_ROLE](#ShieldedAccessControl-DEFAULT_ADMIN_ROLE) for + roles with no explicitly set admin. Since [DEFAULT_ADMIN_ROLE](#ShieldedAccessControl-DEFAULT_ADMIN_ROLE) is the zero byte array, + there is no distinction between a nonexistent `role` and one whose admin is [DEFAULT_ADMIN_ROLE](#ShieldedAccessControl-DEFAULT_ADMIN_ROLE). See [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole). To change a role's admin use [_setRoleAdmin](#ShieldedAccessControl-_setRoleAdmin). - Disclosures: a role identifier. + Disclosures: + + * A role identifier. #### _grantRole [toc] [#ShieldedAccessControl-_grantRole] @@ -434,28 +448,31 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont modulePrefix="ShieldedAccessControl" > Grants `role` to `accountId` by inserting a role commitment unique to the - `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. + `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Granting an already active + pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new + `accountId` must be generated to be re-authorized for a revoked `role`. Internal circuit without access restriction. - Disclosures: a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. - Exposing this circuit directly in an implementing contract would allow anyone to grant roles without authorization. It must be wrapped with appropriate access control. - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots. - Deployers should monitor slot consumption off-chain. Duplicate grants waste tree capacity - but are otherwise benign. A single nullifier invalidates all duplicate commitments. - Implementing contracts are responsible for mitigating tree exhaustion risk. + The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots, one per + distinct pairing. Deployers should monitor slot consumption off-chain. Requirements: - * Contract is initialized. * The `(role, accountId)` pairing must not be already revoked. + * Contract is initialized. + + Disclosures: + + * A role commitment corresponding to a `(role, accountId)` pairing. + * A nullifier for the respective role commitment. #### _revokeRole [toc] [#ShieldedAccessControl-_revokeRole] @@ -472,8 +489,6 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont Internal circuit without access restriction. - Disclosures: a nullifier for the respective role commitment. - Exposing this circuit directly in an implementing contract would allow anyone to revoke roles without authorization. It must be wrapped with appropriate access control. @@ -481,8 +496,13 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont Requirements: - * Contract is initialized. + * The `(role, accountId)` pairing must have been granted. * The `(role, accountId)` pairing must not be already revoked. + * Contract is initialized. + + Disclosures: + + * A nullifier for the respective role commitment. #### _setRoleAdmin [toc] [#ShieldedAccessControl-_setRoleAdmin] @@ -495,42 +515,14 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont Sets `adminId` as `role`'s admin identifier. Users with valid admin identifiers may grant and revoke access to the specified `role`. Internal circuit without access restriction. - Disclosures: the role identifier and the admin identifier. - Exposing this circuit directly in an implementing contract would allow anyone to assign arbitrary admin roles without authorization. It must be wrapped with appropriate access control. - - - Requirements: - * Contract is initialized. - + Disclosures: -#### _validateRole [toc] [#ShieldedAccessControl-_validateRole] -#### [!toc] [#ShieldedAccessControl-_validateRole] - - Verifies whether `accountId` holds `role`. - MAY return false for a legitimately credentialed account - if the proving environment supplies an invalid Merkle path. - - Disclosures: a Merkle tree path to a role commitment, and a nullifier for the respective role commitment. - - - This circuit does not perform an initialization check. - It is only meant to be used as an internal helper. - Using this circuit outside of the module may cause undefined behavior and break security guarantees. - - - The nullifier is disclosed via `_roleCommitmentNullifiers.member` on every call. - Since this circuit is invoked by `assertOnlyRole` and `canProveRole`, - every protected operation discloses the same nullifier, - allowing observers to link all actions performed under the same role instance across time. + * The role identifier + * The admin identifier @@ -542,14 +534,28 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont stackParams="true" modulePrefix="ShieldedAccessControl" > - Computes the role commitment from the given `role` and `accountId`. - - Role Commitment Derivation: `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)` + Computes the role commitment from the given `accountId` and `role`. - This circuit does not perform an initialization check. It is only meant to be used as - an internal helper. Using this circuit outside of the module may cause undefined behavior - and break security guarantees. + This circuit does not perform an initialization check. It's only meant to be used as + an internal helper in the Shielded Access Control module. Using this circuit outside of the + module may cause undefined behavior and break security guarantees. + + ## Account ID (`accountId`) + The `accountId` is expected to be computed as: + `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)` + + * `secretKey`: A 32-byte cryptographically secure random value. + + ## Role Commitment Derivation + `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)` + + * `accountId`: See above. + * `role`: A unique role identifier. + * `instanceSalt`: A unique per-deployment salt, stored during initialization. + This prevents commitment collisions across deployments. + * `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent + hash collisions when extending the module or using similar commitment schemes. @@ -557,32 +563,17 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont #### [!toc] [#ShieldedAccessControl-computeNullifier] Computes the role nullifier for a given `roleCommitment`. - Role Nullifier Derivation: `roleNullifier = SHA256(roleCommitment, nullifierDomain)` - - -#### _computeAccountId [toc] [#ShieldedAccessControl-_computeAccountId] -#### [!toc] [#ShieldedAccessControl-_computeAccountId] - - Computes the caller's account identifier from the `wit_secretKey` witness and the `_instanceSalt`. + ## Role Nullifier Derivation + `roleNullifier = SHA256(roleCommitment, nullifierDomain)` - ID Derivation: `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)` - - The result is a 32-byte commitment that uniquely identifies the account. - - - This circuit does not perform an initialization check. - It is only meant to be used as an internal helper. - Using this circuit outside of the module may cause undefined behavior and break security guarantees. - + * `roleCommitment`: See [computeRoleCommitment](#ShieldedAccessControl-computeRoleCommitment). + * `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent + hash collisions when extending the module or using similar commitment schemes. #### computeAccountId [toc] [#ShieldedAccessControl-computeAccountId] @@ -595,15 +586,28 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont > Computes an `accountId` without on-chain state, allowing a user to derive their shielded identity commitment before submitting it in a grant or revoke operation. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result + This is the off-chain counterpart to `_computeAccountId` and produces an identical result given the same inputs. - ID Derivation: `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)` - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the privacy guarantees of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss. - A lost key cannot be recovered. + OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can + permanently compromise the privacy guarantees of this system: + + * **Never log or persist** the `secretKey` in plaintext — avoid browser devtools, + application logs, analytics pipelines, or any observable side-channel. + * **Store offline or in secure enclaves** — hardware security modules (HSMs), + air-gapped devices, or encrypted vaults are strongly preferred over hot storage. + * **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()` + or equivalent; weak or predictable keys can be brute-forced to reveal your identity. + * **Treat key loss as identity loss** — a lost key cannot be recovered. Back up + keys securely before using them in role commitments. + * **Avoid calling this circuit in untrusted environments** — executing this in an + unverified browser extension, compromised runtime, or shared machine may expose + the key to a malicious observer. + + ## ID Derivation + `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)` + + See `_computeAccountId` for further details. diff --git a/content/contracts-compact/api/utils.mdx b/content/contracts-compact/api/utils.mdx index be5c264a..6304705a 100644 --- a/content/contracts-compact/api/utils.mdx +++ b/content/contracts-compact/api/utils.mdx @@ -11,7 +11,7 @@ This package provides the API for all Utils modules. ## Utils [toc] [#Utils] ```ts @@ -20,10 +20,6 @@ import "./node_modules/@openzeppelin/compact-contracts/utils/Utils"; --- - - There's no easy way to get the constraints of circuits at this time so the constraints of the circuits listed below have been omitted. - - ### Ledger [toc] [#Utils-Ledger] ### Ledger [!toc] [#Utils-Ledger] @@ -41,15 +37,15 @@ None. #### [!toc] [#Utils-isKeyOrAddressZero] Returns whether `keyOrAddress` is the zero address. - - Midnight's burn address is represented as `left(default)` in Compact, - so we've chosen to represent the zero address as this structure as well. + + Midnight's burn address is represented as left\(default\) + in Compact, so we've chosen to represent the zero address as this structure as well. @@ -57,7 +53,7 @@ None. #### [!toc] [#Utils-isKeyZero] Returns whether `key` is the zero address. @@ -67,41 +63,137 @@ None. #### [!toc] [#Utils-isKeyOrAddressEqual] - Returns whether `keyOrAddress` is equal to `other`. - Assumes that a `ZswapCoinPublicKey` and a `ContractAddress` can never be equal. + Returns whether `keyOrAddress` is equal to `other`. Assumes that a ZswapCoinPublicKey + and a ContractAddress can never be equal #### isContractAddress [toc] [#Utils-isContractAddress] #### [!toc] [#Utils-isContractAddress] - Returns whether `keyOrAddress` is a `ContractAddress` type. + Returns whether `keyOrAddress` is a ContractAddress type. #### emptyString [toc] [#Utils-emptyString] #### [!toc] [#Utils-emptyString] - A helper function that returns the empty string: `""`. + A helper function that returns the empty string: "". #### canonicalize [toc] [#Utils-canonicalize] #### [!toc] [#Utils-canonicalize] + Zeroes out the unused side of an `Either` value. Prevents crafted + inputs where both `left` and `right` fields carry data from bypassing checks that + only inspect the active side. + + +#### zeroAccount [toc] [#Utils-zeroAccount] +#### [!toc] [#Utils-zeroAccount] + + Returns the canonical zero `Either, ContractAddress>` value: the + left variant (`Bytes<32>`) set to zero. Used as the sentinel for mint/burn operations + (a zero `from` signals a mint, a zero `to` signals a burn) and for cleared approvals. + + The left variant is chosen so that [isTargetZero](#Utils-isTargetZero) checks against `default>`, + and to avoid triggering contract-address guards (e.g. circuits that check `!to.is_left` + before delegating to an `_unsafe` variant). A right variant with `default` + would also pass [isTargetZero](#Utils-isTargetZero), but the left variant keeps guards and error messages consistent. + + +#### isTargetZero [toc] [#Utils-isTargetZero] +#### [!toc] [#Utils-isTargetZero] + + Returns `true` if `target`'s active branch (as indicated by `is_left`) + holds the zero value. + + +#### computeAccountId [toc] [#Utils-computeAccountId] +#### [!toc] [#Utils-computeAccountId] + + Computes an account identifier without on-chain state, allowing a user to derive + their identity commitment before submitting an operation. + + + OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can + permanently compromise the security of this system: + + * **Never log or persist** the `secretKey` in plaintext — avoid browser devtools, + application logs, analytics pipelines, or any observable side-channel. + * **Store offline or in secure enclaves** — hardware security modules (HSMs), + air-gapped devices, or encrypted vaults are strongly preferred over hot storage. + * **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()` + or equivalent; weak or predictable keys can be brute-forced to reveal your identity. + * **Treat key loss as identity loss** — a lost key cannot be recovered. + * **Avoid calling this circuit in untrusted environments** — executing this in an + unverified browser extension, compromised runtime, or shared machine may expose + the key to a malicious observer. + + ## ID Derivation + `accountId = persistentHash(secretKey)` + + + The absence of a domain-separation tag is DELIBERATE. This identifier + is global by design: the same key material yields the same identity in + every module deriving one, so a user who wishes to carry one identity + across modules can. Modules wanting a per-deployment, unlinkable identity + should not use this circuit. + + NOT FOR PRIVATE DERIVATION. This returns an identifier, not a secret, + and the construction is untagged. A circuit deriving a PRIVATE value from a + secret MUST use its own domain-separation tag. + + +#### selfAsRecipient [toc] [#Utils-selfAsRecipient] +#### [!toc] [#Utils-selfAsRecipient] + - Zeroes out the unused side of an `Either` value. Prevents crafted inputs where both `left` and `right` fields carry data from bypassing checks that only inspect the active side. + Returns the current contract's address as an + `Either` for use as a + recipient in shielded send operations (deposits and receiving change). + + +#### UINT128_MAX [toc] [#Utils-UINT128_MAX] +#### [!toc] [#Utils-UINT128_MAX] + + The maximum value representable by a `Uint<128>`. + + + TODO: Remove once a math module providing this constant is available. + diff --git a/content/contracts-compact/confidential-fungible-token.mdx b/content/contracts-compact/confidential-fungible-token.mdx new file mode 100644 index 00000000..0c2b324e --- /dev/null +++ b/content/contracts-compact/confidential-fungible-token.mdx @@ -0,0 +1,407 @@ +--- +title: ConfidentialFungibleToken +--- + +{/* links */} +[FungibleToken]: ./fungible-token.mdx +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern +[Ownable]: ./ownable.mdx +[crypto guide]: ./crypto +[ElGamal]: api/crypto#ElGamal +[EcdhMask]: api/crypto#EcdhMask +[ConfidentialFungibleToken API]: api/confidential-fungible-token +[EscrowEntry]: api/confidential-fungible-token#ConfidentialFungibleToken-EscrowEntry +[wit_ConfidentialTokenSK]: api/confidential-fungible-token#ConfidentialFungibleToken-wit_ConfidentialTokenSK +[wit_ConfidentialTokenEK]: api/confidential-fungible-token#ConfidentialFungibleToken-wit_ConfidentialTokenEK +[wit_PlaintextBalance]: api/confidential-fungible-token#ConfidentialFungibleToken-wit_PlaintextBalance +[wit_RandomnessSeed]: api/confidential-fungible-token#ConfidentialFungibleToken-wit_RandomnessSeed +[MAX_TRANSFER_VALUE]: api/confidential-fungible-token#ConfidentialFungibleToken-MAX_TRANSFER_VALUE +[initialize]: api/confidential-fungible-token#ConfidentialFungibleToken-initialize +[balanceOf]: api/confidential-fungible-token#ConfidentialFungibleToken-balanceOf +[pendingOf]: api/confidential-fungible-token#ConfidentialFungibleToken-pendingOf +[allowance]: api/confidential-fungible-token#ConfidentialFungibleToken-allowance +[isRegistered]: api/confidential-fungible-token#ConfidentialFungibleToken-isRegistered +[register]: api/confidential-fungible-token#ConfidentialFungibleToken-register +[_mint]: api/confidential-fungible-token#ConfidentialFungibleToken-_mint +[_burn]: api/confidential-fungible-token#ConfidentialFungibleToken-_burn +[_burnFrom]: api/confidential-fungible-token#ConfidentialFungibleToken-_burnFrom +[transfer]: api/confidential-fungible-token#ConfidentialFungibleToken-transfer +[_move]: api/confidential-fungible-token#ConfidentialFungibleToken-_move +[sweep]: api/confidential-fungible-token#ConfidentialFungibleToken-sweep +[approve]: api/confidential-fungible-token#ConfidentialFungibleToken-approve +[transferFrom]: api/confidential-fungible-token#ConfidentialFungibleToken-transferFrom +[clearMemos]: api/confidential-fungible-token#ConfidentialFungibleToken-clearMemos +[computeAccountId]: api/confidential-fungible-token#ConfidentialFungibleToken-computeAccountId +[ConfidentialFungibleTokenPublicSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply +[_addSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply-_addSupply +[_subSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply-_subSupply +[totalSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply-totalSupply + +ConfidentialFungibleToken is an account-based fungible token whose balances live on the public ledger +as [ElGamal] ciphertexts over the Jubjub curve, so no amount is ever written in the clear. +Accounts are identified the same way as in [FungibleToken], by a 32-byte `accountId` derived from a secret key, +but every value a circuit moves is encrypted under the account holder's own encryption key, +and the caller proves in zero knowledge that it knows the plaintext of the balance it spends from. +Transfers hide the amount; the (sender, recipient) pair is public. +Total-supply tracking is optional and lives in the separate [ConfidentialFungibleTokenPublicSupply] extension. + +The encryption primitives (`ElGamal` and `EcdhMask`) are documented in the [crypto guide]. +This page covers how the token composes them. + +## The account model + +### What is on the public ledger + +Every ledger field of the module is exported, so an observer can read all of the following directly from state: + +* **Encryption keys** (`_encryptionKeys`): `accountId` to the account's ElGamal public key, published by [register]. +* **Spendable balances** (`_balances`): one ElGamal ciphertext per registered account. + Debits (transfers out, approvals, burns) draw from this cell. +* **Pending balances** (`_pending`): a second ciphertext per account where incoming credits land. + Only the owner moves pending into spendable, with [sweep]. +* **Memos** (`_memos`): per account, a list of [EcdhMask] ciphertexts, one per credit received. + Each one carries the credited amount encrypted to the recipient's key; this is how a recipient learns what it was sent. +* **Escrow entries** (`_escrow`): per (owner, spender), an [EscrowEntry] holding the encrypted allowance. +* **Credit counters** (`_creditEpochs`): per account, the lifetime number of credits received. It never resets. +* **Escrow-spend counters** (`_escrowSpendEpochs`): per (owner, spender), the number of times the escrow was drawn on. It never resets. +* **Metadata** (`_name`, `_symbol`, `_decimals`) and the `_isInitialized` flag. + +The module keeps no nullifier sets; the two counters above are the only monotonic state it writes, +and both exist to keep per-operation randomness distinct (see below), not to prevent replay. + +What an observer learns from this is the set of registered `accountId`s, which accounts transact with which, +how many credits each account has received, and how often each escrow has been spent from. +What an observer does not learn is any balance, any transferred amount, or any allowance cap, +unless the deployment also tracks public supply (see [Minting, burning and supply](#minting-burning-and-supply)). + +### What the user holds privately + +Four witnesses feed the circuits. Each is listed with how (or whether) the circuit verifies it. + +* [wit_ConfidentialTokenSK] returns the **account secret**. The circuit computes `accountId = H(SK)` + and uses that id for every ledger lookup, so a wrong secret resolves to a different account and the call fails. +* [wit_ConfidentialTokenEK] returns the **encryption secret**. The circuit re-derives the ElGamal public key from it + and compares against the key stored at registration, either through the balance proof or directly on + [sweep] and [clearMemos], which have no plaintext to prove. +* [wit_PlaintextBalance] returns the wallet's **cached plaintext** for a given ciphertext. + The ciphertext is passed in as the argument so the wallet can route by ciphertext identity rather than by call order. + The circuit verifies the claim with `ElGamal_assertDecryptsTo`; a wrong value fails the proof. +* [wit_RandomnessSeed] returns a 32-byte **randomness seed** from which every per-operation random scalar + and every memo ephemeral is expanded in-circuit. Unlike the other three, this witness is **not** verified. + +The same key material may back both the account secret and the encryption secret, or both may be derived from one master secret: +the account id is an untagged hash of the SK, while the encryption scalar is a domain-separated hash of the EK, +so publishing the id does not reveal the encryption key. + + + Seed freshness is load-bearing for confidentiality on every path. The seed-to-randomness expansion is deterministic + and public, so anyone who knows a seed can strip the memo mask and read the delivered amount, and can recover a balance + or escrow amount from its public ciphertext. Reusing a seed across two operations leaks the difference between their amounts, + and the circuit does not prevent reuse. A wallet must supply a fresh, high-entropy, secret seed on every invocation. + On `transferFrom` and `burnFrom` the seed is the spender's but the ciphertexts it randomizes belong to the owner, + so a careless spender leaks a counterparty's balance, not their own. + + +The TypeScript witnesses in the repository are test-only and are not published with the package; +in particular the test seed witness returns a fixed value. A consumer authors its own witnesses, +backed by the user's wallet, and the seed witness must draw from a cryptographically secure random source. + +### Balance claims and the per-operation bound + +Every value-bearing circuit takes a `Uint<128>` amount and checks it against [MAX_TRANSFER_VALUE]. +A balance, however, is the homomorphic sum of every credit it has ever received and has no running total to bound it, +so the plaintext a wallet claims through [wit_PlaintextBalance] is a `Uint<248>`, wider than any single amount. +A balance accumulated past the per-operation bound therefore stays spendable. +Escrow claims are narrowed back to `Uint<128>`, since an allowance is set by one `approve` and only ever reduced. + +## Registration + +[register] derives the caller's ElGamal public key from the encryption secret, stores it under the caller's `accountId`, +and initializes both the spendable and the pending balance to an encryption of zero. +It discloses the `accountId` and the public key. Registration is required before an account can send **or** receive: +a credit needs the recipient's public key to encrypt under. + +Before any on-chain call, a wallet derives its own id with the pure [computeAccountId] circuit (no proof needed): + +```ts +const accountId = Token_computeAccountId(secretKey); +``` + +The id is global: the same secret produces the same `accountId` in every module that derives it this way +(`Utils_computeAccountId` is the shared implementation). + +## Transfers, pending balances and memos + +[transfer] does three things in one proof: + +1. **Debit.** The caller proves the plaintext of its spendable balance, the circuit checks it is at least `value`, + and the balance ciphertext is replaced by a homomorphic subtraction of `value` under fresh randomness. +2. **Credit.** The recipient's *pending* ciphertext is replaced by a homomorphic addition of `value`. + The recipient's credit counter is incremented and folded into the randomness, so two credits never share randomness + even if a sender reuses a seed. +3. **Memo.** An [EcdhMask] ciphertext carrying `value`, encrypted to the recipient's public key, is pushed to the front of + the recipient's memo list. + +What reaches the ledger: the sender's `accountId`, the recipient's `accountId`, two new ciphertexts and one memo. +The amount is not disclosed. Self-transfer is rejected. + +### How a recipient learns of incoming funds + +The on-chain balance ciphertext is exponential ElGamal (it encrypts `g^value`), so a wallet cannot cheaply decrypt it. +Instead the wallet scans its own `_memos` list off-chain, decrypts each new entry directly with its encryption secret +(no discrete-log search and no bounded-value table), and adds the amounts to a local plaintext cache. + +Memos record credits only. Debits (sends, approvals, burns) move value with no memo, so summing memos overcounts an account +that has ever spent. The wallet's running cache, not memo replay, is the authoritative balance, +and it is what the wallet returns from [wit_PlaintextBalance]. + +### Sweeping pending into spendable + +Credits land in `_pending`, not in `_balances`, so a third party cannot change the ciphertext an owner is about to prove against. +Without this split an attacker could send dust to invalidate a victim's in-flight spend proofs. +The owner calls [sweep] when ready: it homomorphically adds the two ciphertexts, resets pending to an encryption of zero, +and needs no plaintext (the wallet already learned the pending amounts from its memos). +Only the owner can sweep, since the circuit checks both the account secret and the encryption secret. +A wallet's total holding is `balanceOf + pendingOf`. + +### The conserving primitive + +[_move] is [transfer] without the self-transfer guard: it debits the caller and credits `to` by the same amount, net zero, +and never changes supply. It is the building block for custom value flows (fees, splits, settlement), +and it stays permissive so a composing contract can set its own policy. + +### Pruning memos + +A wallet that has folded its memos into a durable cache can call [clearMemos] to empty its list on chain. +The call takes the caller's view of its credit counter and is rejected if a credit landed in between, +so a memo the wallet has not yet seen is never discarded. + + + Clearing memos is destructive. Spending requires proving the balance plaintext, and the plaintext can only be + reconstructed from the memos or from the wallet's cache. Clear only memos already folded into a durable, backed-up cache; + otherwise the balance becomes unspendable, not merely undisplayable. + + +## Approvals and escrow + +An allowance is not a public cap. [approve] moves `value` out of the caller's spendable balance into an escrow entry +that holds two encryptions of the same amount, one under the spender's key and one under the owner's, +plus an owner-readable memo of the current remaining allowance: + +``` +EscrowEntry { spenderCt, ownerCt, ownerMemo } +``` + +Re-approving first refunds any prior escrow for that spender back to the owner's balance (a homomorphic add of `ownerCt`, +no plaintext needed) and then escrows the new amount, so allowances replace rather than stack. +Revoking is `approve(spender, 0)`. What reaches the ledger: the owner's and spender's ids, the owner's new balance ciphertext, +and the entry; the cap is not disclosed. + +[transferFrom] is called by the spender. It proves the plaintext of `spenderCt`, checks it is at least `value`, +reduces **both** escrow copies by `value`, refreshes `ownerMemo` with the new remaining amount +(computed in-circuit from the verified plaintext, so the spender cannot forge it), increments the (owner, spender) spend counter, +and credits `to` exactly as [transfer] does. [_burnFrom] is the same escrow spend with no credit. + +### What a partial spend leaves behind + +After a spender draws part of an allowance, the ledger holds a reduced `spenderCt` and `ownerCt` that still encrypt the same +remaining amount, a fresh `ownerMemo` stating that remainder, and an incremented spend counter. +The owner receives no balance memo (its spendable balance did not change) and instead decrypts `ownerMemo` +to learn how much is left in escrow. That is what lets the owner prove its post-refund balance on the next `approve`, +when the remainder flows back into `_balances`. Without the memo a partial spend would strand the escrow. + +[allowance] returns the entry, or a well-formed encrypted-zero entry when none exists. "No allowance" and "an allowance of zero" +read the same through the getter; a caller that must distinguish them reads `_escrow` membership off chain. +Granting nothing is safe because the spend path gates on escrow membership independently. + +## Minting, burning and supply + +The base module ships three supply-changing building blocks and no supply accounting: + +* [_mint] credits `value` to `account` (with a memo), exactly like the credit half of a transfer. +* [_burn] debits `value` from the caller's spendable balance. +* [_burnFrom] consumes `value` from an escrow the owner granted the caller. + +None of them touches a total. Pair them with [ConfidentialFungibleTokenPublicSupply] when the deployment tracks supply: +its `_totalSupply` ledger field is a public `Uint<128>`, and [_addSupply] / [_subSupply] disclose each mint and burn amount +through the delta on that value. Balances stay confidential; only the aggregate is visible. This is the right default +for issuers who must attest to a circulating supply. The extension imports no token module, needs no initialization, +and the pairing of each token operation with its accounting block is the composing contract's responsibility. + + + Never re-export `_mint`, `_burn`, or `_burnFrom` from a deployed contract. Compact cannot restrict a re-export, + so a raw `_mint` is a permissionless mint, a raw `_burn` a permissionless burn, and a raw `_burnFrom` destroys escrowed + value with no supply decrement. Expose only the conserving surface (`transfer`, `_move`, `approve`, `transferFrom`, `sweep`) + plus your own gated `mint` / `burn` / `burnFrom` wrappers. + + +## Gating on the caller + +A composing contract that enforces policy on the caller (freeze lists, KYC, pause) cannot read the caller's identity itself: +the caller is derived from a witness inside the module, and a second witness call in the wrapper could return a different secret. +For that reason every caller-authenticating circuit ([register], [_burn], [transfer], [_move], [approve], [transferFrom], +[sweep], [_burnFrom]) returns the `accountId` it authenticated. Gate on the returned value after the call; +an assertion that fails anywhere in a circuit invalidates the whole transaction, so a post-call check still blocks the operation. +The returned id is always the caller (sender, approver, spender, or registrant), never `to` or `fromAddress`, +which are explicit arguments a wrapper gates directly. + +Accounts are externally owned. A contract has no private state in which to hold an encryption secret, +so it could be credited but could never prove a balance or scan memos; the module therefore keys everything by a `Bytes<32>` +account id and offers no `ContractAddress` variant. A protocol that needs to hold these tokens uses an account it controls +through `approve` / `transferFrom`. + +## Usage + +The following contract composes the token with the public-supply extension and [Ownable], +pairing every supply change with its accounting block and restricting `mint` to the contract owner. +It re-exports the module's ledger so wallets can read ciphertexts and memos off chain, +and it exposes the conserving surface plus gated supply wrappers. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibleToken" + prefix Token_; +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/ConfidentialFungibleTokenPublicSupply" + prefix Supply_; +import "./node_modules/@openzeppelin/compact-contracts/crypto/ElGamal" + prefix ElGamal_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +// Types wallets need to read balances and escrow entries. +export { Token_EscrowEntry, ElGamal_Ciphertext }; + +// Surface the module's ledger so wallets can scan memos and read ciphertexts +// without a proof. +export { + Token__balances, + Token__pending, + Token__encryptionKeys, + Token__memos, + Token__escrow, + Token__creditEpochs, + Token__name, + Token__symbol, + Token__decimals, + Supply__totalSupply +}; + +constructor( + name_: Opaque<"string">, + symbol_: Opaque<"string">, + decimals_: Uint<8>, + issuer: Either, ContractAddress> +) { + Token_initialize(name_, symbol_, decimals_); + Ownable_initialize(issuer); +} + +// Views + +export circuit totalSupply(): Uint<128> { + return Supply_totalSupply(); +} + +export circuit balanceOf(account: Bytes<32>): ElGamal_Ciphertext { + return Token_balanceOf(account); +} + +export circuit pendingOf(account: Bytes<32>): ElGamal_Ciphertext { + return Token_pendingOf(account); +} + +export circuit allowance(owner: Bytes<32>, spender: Bytes<32>): Token_EscrowEntry { + return Token_allowance(owner, spender); +} + +export circuit isRegistered(account: Bytes<32>): Boolean { + return Token_isRegistered(account); +} + +export pure circuit computeAccountId(sk: Bytes<32>): Bytes<32> { + return Token_computeAccountId(sk); +} + +// Registration and wallet maintenance + +export circuit register(): Bytes<32> { + return Token_register(); +} + +export circuit sweep(): Bytes<32> { + return Token_sweep(); +} + +export circuit clearMemos(expectedEpoch: Uint<64>): [] { + Token_clearMemos(expectedEpoch); +} + +// Conserving value operations + +export circuit transfer(to: Bytes<32>, value: Uint<128>): Bytes<32> { + return Token_transfer(to, value); +} + +export circuit approve(spender: Bytes<32>, value: Uint<128>): Bytes<32> { + return Token_approve(spender, value); +} + +export circuit transferFrom( + fromAddress: Bytes<32>, + to: Bytes<32>, + value: Uint<128> +): Bytes<32> { + return Token_transferFrom(fromAddress, to, value); +} + +// Supply operations: each token building block is paired with its +// accounting block, so sum(balances) == totalSupply holds by construction. + +export circuit mint(account: Bytes<32>, value: Uint<128>): [] { + Ownable_assertOnlyOwner(); + Supply__addSupply(value); + Token__mint(account, value); +} + +export circuit burn(value: Uint<128>): Bytes<32> { + const accountId = Token__burn(value); + Supply__subSupply(value); + return accountId; +} + +export circuit burnFrom(fromAddress: Bytes<32>, value: Uint<128>): Bytes<32> { + const spenderId = Token__burnFrom(fromAddress, value); + Supply__subSupply(value); + return spenderId; +} +``` + +A few points about this layout: + +* `mint` is gated on the contract owner. `burn` and `burnFrom` need no extra gate: the module already authenticates + the caller as the account (or the approved spender) whose value is destroyed, and both return that `accountId` + for any further policy check. +* `name`, `symbol`, and `decimals` are read from the exported ledger rather than through getter circuits. +* The raw `_mint`, `_burn`, and `_burnFrom` building blocks are not re-exported; see the callout above. + +### Wallet-side flow + +A wallet integrating this contract typically: + +1. Derives `accountId` with `computeAccountId(sk)` and calls `register()` once. +2. Scans `_memos[accountId]` after each block, decrypts new entries with the encryption secret, and updates its plaintext cache. +3. Calls `sweep()` to make pending credits spendable. +4. Supplies, through its witnesses, the account secret, the encryption secret, the cached plaintext for whichever ciphertext + the circuit asks about, and a fresh seed on every call. +5. After a partial `transferFrom` against an allowance it granted, decrypts the escrow entry's `ownerMemo` to learn the remainder. + +## Known limitations + +* Two credits to the same recipient in the same block conflict, because each credit reads and advances the recipient's + credit counter. Credits to one account serialize. +* Anyone can append memos to any registered account by sending it dust, and only the account owner can prune the list. +* The value circuits are large. A deployment that exposes the full surface may exceed a network's per-transaction byte budget; + confirm deployability against the target network and expose only the operations the contract needs. +* Contract-to-contract holdings are out of scope, as described under [Gating on the caller](#gating-on-the-caller). diff --git a/content/contracts-compact/crypto.mdx b/content/contracts-compact/crypto.mdx new file mode 100644 index 00000000..602e1867 --- /dev/null +++ b/content/contracts-compact/crypto.mdx @@ -0,0 +1,264 @@ +--- +title: Crypto +--- + +{/* links */} +[ConfidentialFungibleToken guide]: ./confidential-fungible-token +[ElGamal API]: api/crypto#ElGamal +[EcdhMask API]: api/crypto#EcdhMask +[secretToScalar]: api/crypto#ElGamal-secretToScalar +[derivePk]: api/crypto#ElGamal-derivePk +[expandRandomness]: api/crypto#ElGamal-expandRandomness +[encrypt]: api/crypto#ElGamal-encrypt +[encryptPoint]: api/crypto#ElGamal-encryptPoint +[encryptZero]: api/crypto#ElGamal-encryptZero +[add]: api/crypto#ElGamal-add +[sub]: api/crypto#ElGamal-sub +[scalarMul]: api/crypto#ElGamal-scalarMul +[addEncrypted]: api/crypto#ElGamal-addEncrypted +[subEncrypted]: api/crypto#ElGamal-subEncrypted +[rerandomize]: api/crypto#ElGamal-rerandomize +[assertKeyPair]: api/crypto#ElGamal-assertKeyPair +[assertDecryptsTo]: api/crypto#ElGamal-assertDecryptsTo +[assertDecryptsToPoint]: api/crypto#ElGamal-assertDecryptsToPoint +[kdf]: api/crypto#EcdhMask-kdf +[EcdhMask.encrypt]: api/crypto#EcdhMask-encrypt +[EcdhMask.decrypt]: api/crypto#EcdhMask-decrypt + +The Crypto directory provides two stateless encryption primitives over Midnight's embedded Jubjub curve: +ElGamal, an additively homomorphic encryption of small integers, and EcdhMask, +a one-time pad derived from an ECDH key exchange that delivers a value to a recipient directly. +Both are pure modules: they hold no ledger state, declare no witnesses, +and take their keys, plaintexts, and randomness as explicit arguments. +The importing contract decides what is stored, what is disclosed, and where its secrets come from. + +The [ConfidentialFungibleToken guide] shows the two primitives working together end to end: +ElGamal keeps every balance as a ciphertext that the contract can update without decrypting it, +and EcdhMask hands each recipient the plaintext amount of an incoming transfer. + +## ElGamal + +The [ElGamal API] implements single-receiver ElGamal encryption over Jubjub. +A ciphertext is a pair of curve points `(c1, c2)` with `c1 = g^r` and `c2 = pk^r * m`, +where `g` is the Jubjub generator, `pk` is the recipient's public key, `r` is the encryption randomness, +and `m` is the message point. + +The module offers the scheme in two forms. + +* ***Exponential (lifted) form*** ([encrypt], [assertDecryptsTo]): a `Uint<128>` value `v` is encrypted as the point `g^v`. +Because the message sits in the exponent, ciphertexts combine arithmetically: +[add] turns ciphertexts of `a` and `b` into a ciphertext of `a + b`, [sub] yields `a - b`, +and [scalarMul] multiplies the plaintext by a public scalar. +[addEncrypted] and [subEncrypted] fold a known plaintext into an existing ciphertext in one call. +Recovering `v` from `g^v` is a discrete-log search, so the lifted form only works for values small enough to search; +the caller is responsible for keeping every encrypted value inside that bound. +* ***General point form*** ([encryptPoint], [assertDecryptsToPoint]): an arbitrary Jubjub point is encrypted as is. +There is no bound to respect, at the cost of decryption recovering only a point, never a scalar. + +[rerandomize] produces a fresh ciphertext of the same plaintext under the same key +(it homomorphically adds an encryption of zero), so a stored ciphertext can be made unlinkable to its previous form +without changing what it encrypts. +[encryptZero] returns the fixed pair of identity points `(O, O)`, a convenient initial value for a fresh slot. + + +`encryptZero()` is a constant, not a hiding encryption. +A slot still holding it is publicly recognizable as never written. +Credit it with fresh randomness, or rerandomize it, before treating it as private. + + +### Decryption by assertion + +No circuit returns a plaintext: a circuit cannot run the discrete-log search that recovers `v` from `g^v`. +Instead, the owner's wallet keeps the plaintext of every ciphertext it holds +and supplies it to the circuit through a witness, and the circuit calls [assertDecryptsTo] +to prove that the claimed plaintext is correct: it re-derives `pk` from the secret `ek`, +computes `c2 - ek * c1`, and asserts that the result equals `g^claimedValue`. +A wrong secret fails the key check; a wrong plaintext fails the decryption check. +[assertKeyPair] performs the key check alone, which lets a caller prove ownership of a public key +without touching any ciphertext. + +`claimedValue` is a `Uint<248>` so that a balance accumulated through many homomorphic additions +never outgrows the claim. The comparison happens on the curve, so two claims that are congruent modulo +the Jubjub subgroup order `ℓ` are indistinguishable; `Uint<248>` stays below `ℓ`, so the claim domain is sound. + +### Keys and randomness + +The secret in this scheme is a 32-byte value `ek`. Everything else is derived from it. + +* [secretToScalar] maps the secret to a Jubjub scalar: `degradeToTransient(H("ElGamal:secretToScalar" || ek))`, +where the tag is the 32-byte padded string and `H` is `persistentHash`. +`degradeToTransient` truncates the hash to 31 bytes, which guarantees a valid Jubjub scalar. +The tag is placed first so this derivation can never coincide with a call to [expandRandomness]. +It also keeps the encryption scalar unrelated to an account identifier computed as `H(secret)`, +so a wallet may derive both roles from common key material. +* [derivePk] computes the public key `pk = g^secretToScalar(ek)`. The public key is safe to publish +and is normally stored on the ledger next to the ciphertexts encrypted under it. +* [expandRandomness] derives encryption randomness from a seed: `degradeToTransient(H(seed || tag))`. +One secret seed per circuit invocation can therefore yield as many distinct scalars as the circuit needs, +one per `tag`. + +Every encryption takes its randomness `r` as a `Field` argument, and the module holds no record of which values have been used. +The caller owns three obligations that no circuit can check: + +* `r` must be a valid Jubjub scalar (strictly less than `ℓ`); larger values fault `ecMul`. +[expandRandomness] and [secretToScalar] always satisfy this. +* `r` must be secret. Anyone who learns `r` for a ciphertext computes `pk^r` and strips the mask. +* `r` must be fresh for every ciphertext. Reusing `r` under the same `pk` repeats `c1`, +and `c2 - c2'` then reveals the difference of the two plaintexts. +In practice this means the witness seed must be fresh per invocation and the tag distinct per encryption within an invocation. + +The module does reject the two inputs that would zero the mask altogether: `pk` equal to the identity point and `r = 0`. +Both are asserted in [encryptPoint], which every encryption path goes through. + +## EcdhMask + +The [EcdhMask API] is the direct-decrypt counterpart to ElGamal. Where exponential ElGamal lets a contract add ciphertexts +but makes recovery a search, EcdhMask delivers a `Uint<128>` value to whoever holds the secret behind a Jubjub public key, +with no bound on the value and no search to recover it. The construction, for a recipient key `pk = g^ek` and a fresh ephemeral scalar `e`: + +``` +E = g^e (ephemeral public key, published with the ciphertext) +S = pk^e (ECDH shared secret point) +mask = kdf(S, domain) +ct = value + mask (one-time pad in the field) +``` + +[kdf] hashes the shared point and the consumer's domain tag: `degradeToTransient(H(H(S) || domain))`, +with `H` again `persistentHash`. The consumer chooses `domain` and must use the same value on both sides. +[EcdhMask.encrypt] returns the `Ciphertext { ephemeralPk: E, ct }`; the recipient recovers `S = E^ek` and subtracts the mask with [EcdhMask.decrypt], +passing its secret as the scalar produced by ElGamal's [secretToScalar]. +`decrypt` is a pure circuit meant for wallets and off-chain code: reading a received value needs no proof, +and the on-chain path only ever calls `encrypt`. + +The same Jubjub key may serve both primitives, as the confidential token does. +The schemes share only the public key, the KDF is domain-separated and keyed by the secret point, +and neither exposes a decryption oracle. + + +The ephemeral scalar `e` is a one-time pad key. It must be unique per ciphertext AND secret. +Reusing `e` to the same recipient repeats the mask, and subtracting the two ciphertexts recovers both values. +A unique but predictable `e` (a counter, a public nonce) is just as fatal, because `pk` is public and anyone can recompute `pk^e`. +Derive `e` the way you derive ElGamal randomness: from a secret, per-invocation seed, with a distinct tag per memo. + + +The pad is malleable and the module adds no authentication. That is safe when the ciphertext is computed inside the same circuit +that commits to the value elsewhere and is immutable once written, which is the intended on-chain use. +A consumer with mutable storage or an in-transit attacker in its threat model must add its own authentication. +As with ElGamal, the weak inputs that would zero the shared secret are rejected: `encrypt` asserts that `recipientPk` is not the identity +and that the ephemeral point `g^e` is not the identity (which covers `e = 0` and any `e` congruent to zero modulo `ℓ`). + +## Jubjub facts a consumer must respect + +Both modules rely on two properties of Midnight's embedded-curve arithmetic. + +* ***Every point is constrained into the prime-order subgroup.*** The runtime assigns each `JubjubPoint` reaching a curve operation +by cofactor clearing at the constraint level, so an off-curve, low-order, or mixed-order point makes the circuit unsatisfiable. +This holds for points a caller passes as arguments or witnesses (a recipient `pk`, the message point of `encryptPoint`), +which is why the modules perform no in-circuit membership check and why the homomorphic algebra is sound. +The identity point is a valid subgroup member, so it is NOT caught by this guarantee; that is why the modules assert against it explicitly. +This is a substantiated-but-unaudited assumption: +it was verified in the source of the pinned Midnight runtime and is regression-tested by the library's runtime canary test, +but compactc and midnight-ledger are unaudited, so re-verify it before using the modules on a newer runtime. +* ***Negation is scalar multiplication by `ℓ - 1`, never by `-1`.*** A `Field` literal `-1` is the BLS scalar modulus minus one, +which exceeds the Jubjub scalar order and faults `ecMul`. For any `P` in the subgroup, `(ℓ - 1) * P = -P`, and that identity is what +`negate`, `sub`, and the decryption check build on. Do not multiply by `ℓ` itself either: it is out of range and faults as well. +A consumer that needs to negate a point of its own should follow the same pattern. + +## Usage + +Both modules are imported like any other module; prefix them with `ElGamal_` and `EcdhMask_` to avoid circuit signature clashes. +Each module exports a `Ciphertext` struct, which the prefix turns into `ElGamal_Ciphertext` and `EcdhMask_Ciphertext`. + +The witnesses below are declared by the importing contract; this library ships no TypeScript witness implementations +(the ones in the repository are test-only), so consumers author their own. + +### An encrypted single-owner balance + +The contract stores the owner's public key and one ElGamal ciphertext on the ledger. +The owner's wallet holds the encryption secret, the current plaintext, and a fresh randomness seed per call, +and supplies them through witnesses. +`credit` takes a public `value` and adds it to the ciphertext without knowing the balance; +`debit` proves the claimed balance is correct before subtracting. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/crypto/ElGamal" + prefix ElGamal_; + +export ledger _ownerPk: JubjubPoint; +export ledger _balance: ElGamal_Ciphertext; + +// The owner's 32-byte encryption secret. +witness wit_EncryptionKey(): Bytes<32>; +// The wallet's cached plaintext for `ct`. +witness wit_Plaintext(ct: ElGamal_Ciphertext): Uint<248>; +// A fresh, secret 32-byte seed for every call. +witness wit_RandomnessSeed(): Bytes<32>; + +export circuit register(): [] { + const pk = ElGamal_derivePk(wit_EncryptionKey()); + _ownerPk = disclose(pk); + _balance = ElGamal_encryptZero(); +} + +export circuit credit(value: Uint<128>): [] { + const r = ElGamal_expandRandomness(wit_RandomnessSeed(), pad(32, "credit")); + _balance = disclose(ElGamal_addEncrypted(_balance, _ownerPk, value, r)); +} + +export circuit debit(value: Uint<128>): [] { + const ek = wit_EncryptionKey(); + const ct = _balance; + const plaintext = wit_Plaintext(ct); + // Binds the private `ek` and `plaintext` to the public `_ownerPk` and `ct`. + ElGamal_assertDecryptsTo(ct, _ownerPk, ek, plaintext); + assert(plaintext >= value, "Balance: insufficient balance"); + + const r = ElGamal_expandRandomness(wit_RandomnessSeed(), pad(32, "debit")); + _balance = disclose(ElGamal_subEncrypted(ct, _ownerPk, value, r)); +} +``` + +What reaches the ledger: the public key and each new ciphertext. The `value` argument of an exported circuit is public, +so these circuits hide the running balance, not the individual amounts; +the confidential token layers escrow and memos on top of this pattern to hide amounts as well. +The secret, the plaintext, and the seed never leave the witness, and `assertDecryptsTo` is what makes a dishonest plaintext unprovable. + +### Delivering a value with a memo + +A sender who credits a recipient's ciphertext has no way to tell the recipient how much arrived, since the recipient would +otherwise face a discrete-log search over its own balance. An EcdhMask memo solves that: the circuit encrypts the amount +to the recipient's public key and appends it to a per-recipient list the recipient's wallet reads and decrypts off chain. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/crypto/ElGamal" + prefix ElGamal_; +import "./node_modules/@openzeppelin/compact-contracts/crypto/EcdhMask" + prefix EcdhMask_; + +export ledger _recipientPks: Map, JubjubPoint>; +export ledger _memos: Map, List>; + +witness wit_RandomnessSeed(): Bytes<32>; + +export circuit notify(recipient: Bytes<32>, value: Uint<128>): [] { + const pk = _recipientPks.lookup(disclose(recipient)); + // Secret, fresh per call, distinct tag per memo within a call. + const e = ElGamal_expandRandomness(wit_RandomnessSeed(), pad(32, "memo")); + const memo = EcdhMask_encrypt(pk, value, e, pad(32, "MyApp:memo:v1")); + + if (!_memos.member(disclose(recipient))) { + _memos.insert(disclose(recipient), default>); + } + _memos.lookup(disclose(recipient)).pushFront(disclose(memo)); +} +``` + +The recipient's wallet recovers the amount with the pure circuit +`EcdhMask_decrypt(memo, ElGamal_secretToScalar(ek), pad(32, "MyApp:memo:v1"))`, using the same domain tag. +Only the memo (the ephemeral point and the masked field element) reaches the ledger. diff --git a/content/contracts-compact/extensibility.mdx b/content/contracts-compact/extensibility.mdx index 08531c39..f030f54e 100644 --- a/content/contracts-compact/extensibility.mdx +++ b/content/contracts-compact/extensibility.mdx @@ -62,9 +62,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" prefix FungibleToken_; constructor( - _name: Maybe>, - _symbol: Maybe>, - _decimals:Uint<8>, + _name: Opaque<"string">, + _symbol: Opaque<"string">, + _decimals: Uint<8>, _owner: Either, ContractAddress> ) { FungibleToken_initialize(_name, _symbol, _decimals); @@ -113,12 +113,12 @@ export circuit transfer( } export circuit transferFrom( - from: Either, ContractAddress>, + fromAddress: Either, ContractAddress>, to: Either, ContractAddress>, value: Uint<128> ): Boolean { Pausable_assertNotPaused(); - return FungibleToken_transferFrom(from, to, value); + return FungibleToken_transferFrom(fromAddress, to, value); } export circuit approve( diff --git a/content/contracts-compact/fungible-token.mdx b/content/contracts-compact/fungible-token.mdx index 645f2777..ffedf221 100644 --- a/content/contracts-compact/fungible-token.mdx +++ b/content/contracts-compact/fungible-token.mdx @@ -5,8 +5,13 @@ title: FungibleToken {/* links */} [fungible tokens]: ../contracts/5.x/tokens.mdx#different-kinds-of-tokens [EIP-20]: https://eips.ethereum.org/EIPS/eip-20 -[Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [_mint]: api/fungible-token#FungibleToken-_mint +[transfer]: api/fungible-token#FungibleToken-transfer +[approve]: api/fungible-token#FungibleToken-approve +[transferFrom]: api/fungible-token#FungibleToken-transferFrom +[wit_FungibleTokenSK]: api/fungible-token#FungibleToken-wit_FungibleTokenSK +[computeAccountId]: api/utils#Utils-computeAccountId FungibleToken is a specification for [fungible tokens], a type of token where all the units are exactly equal to each other. @@ -43,6 +48,53 @@ The `unsafe` circuits will eventually be deprecated after Compact supports contr `transfer`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. +## Identity Model + +FungibleToken uses a witness-derived identity scheme. Each caller proves knowledge of a secret key +by injecting it through the [wit_FungibleTokenSK] witness. The module computes the caller's account identifier +as `persistentHash(secretKey)`, a commitment that hides the key while providing a stable, pseudonymous on-chain identity. +That identifier is the `Bytes<32>` (left) branch of the `Either, ContractAddress>` account type +used by [transfer], [approve], and [transferFrom] to locate the caller's balance and allowances. + +What is public on the ledger: + +* `_balances`, keyed by account identifier (or contract address), and `_totalSupply`. +* `_allowances`, keyed by owner and then spender. +* `_name`, `_symbol`, and `_decimals`. + +What the caller holds privately: + +* The 32-byte `secretKey`. It never reaches the ledger; only its hash does. + +A transfer discloses the caller's account identifier, the recipient, and the amount, because the balance map is updated with those values. +An approval discloses the owner's identifier, the spender, and the allowance. + +Because the identifier is `H(secretKey)` with no per-deployment salt, the same key produces the same identity in every contract. +Users who want cross-contract unlinkability can use a different secret key per contract at the wallet layer. + +To derive an account identifier off-chain (for example, to pass as the `recipient` of a mint), use [computeAccountId] from the Utils module: + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} +``` + +A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side. +The [Utils guide](./utils#deriving-an-identifier-off-chain) shows the equivalent computation with the Compact runtime. + +The TypeScript witness implementation lives with the DApp, not with the library. +The witnesses in the `compact-contracts` repository are test-only and are not published; +consumers author their own `wit_FungibleTokenSK` that returns the caller's 32-byte secret key. + + + The `secretKey` must never be logged or persisted in plaintext. Loss of the key means loss of the tokens held under that identity, + and key exposure allows impersonation. Use `crypto.getRandomValues()` or equivalent to generate keys. + + ## Usage Import the FungibleToken module into the implementing contract. diff --git a/content/contracts-compact/index.mdx b/content/contracts-compact/index.mdx index 1963a214..ae64ef1b 100644 --- a/content/contracts-compact/index.mdx +++ b/content/contracts-compact/index.mdx @@ -12,8 +12,10 @@ title: Contracts for Compact This library consists of modules to build custom smart contracts. -This repo contains highly experimental code. +Pre-release versions (`-alpha`, `-rc`) contain experimental, unaudited code. Expect rapid iteration. **Use at your own risk.** +For production, use a stable release (no `-alpha` or `-rc` suffix) on the Midnight ledger it targets. +See the [release and audit table](https://github.com/OpenZeppelin/compact-contracts/blob/v0.3.0/README.md#security) in the repository. ## Usage @@ -159,17 +161,20 @@ In the project root, compile the contract. ```bash % compact compile MyContract.compact artifacts/MyContract -Compiling 11 circuits: +Compiling 14 circuits: circuit "allowance" (k=11, rows=1352) - circuit "approve" (k=13, rows=3075) + circuit "approve" (k=13, rows=3080) circuit "balanceOf" (k=10, rows=673) circuit "decimals" (k=6, rows=28) circuit "name" (k=6, rows=28) + circuit "owner" (k=7, rows=76) circuit "pause" (k=13, rows=2365) + circuit "renounceOwnership" (k=13, rows=2364) circuit "symbol" (k=6, rows=28) circuit "totalSupply" (k=6, rows=28) circuit "transfer" (k=13, rows=3990) - circuit "transferFrom" (k=13, rows=4972) + circuit "transferFrom" (k=13, rows=4977) + circuit "transferOwnership" (k=13, rows=2959) circuit "unpause" (k=13, rows=2362) -Overall progress [====================] 11/11 +Overall progress [====================] 14/14 ``` diff --git a/content/contracts-compact/latest-version.js b/content/contracts-compact/latest-version.js index 52662a23..32018183 100644 --- a/content/contracts-compact/latest-version.js +++ b/content/contracts-compact/latest-version.js @@ -1 +1 @@ -export const latestStable = "0.1.x"; +export const latestStable = "0.3.x"; diff --git a/content/contracts-compact/multi-token.mdx b/content/contracts-compact/multi-token.mdx index d4123f2b..4a567e74 100644 --- a/content/contracts-compact/multi-token.mdx +++ b/content/contracts-compact/multi-token.mdx @@ -5,8 +5,12 @@ title: MultiToken {/* links */} [EIP-1155]: https://eips.ethereum.org/EIPS/eip-1155 [ERC165]: https://eips.ethereum.org/EIPS/eip-165 -[Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [_mint]: ./api/multi-token#MultiToken-_mint +[setApprovalForAll]: ./api/multi-token#MultiToken-setApprovalForAll +[transferFrom]: ./api/multi-token#MultiToken-transferFrom +[wit_MultiTokenSK]: ./api/multi-token#MultiToken-wit_MultiTokenSK +[computeAccountId]: ./api/utils#Utils-computeAccountId MultiToken is a specification for contracts that manage multiple token types. This module is an approximation of [EIP-1155] written in the Compact programming language for the Midnight network. @@ -62,6 +66,53 @@ The `unsafe` circuits will eventually be deprecated after Compact supports contr `transferFrom`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. +## Identity Model + +MultiToken uses a witness-derived identity scheme. Each caller proves knowledge of a secret key +by injecting it through the [wit_MultiTokenSK] witness. The module computes the caller's account identifier +as `persistentHash(secretKey)`, a commitment that hides the key while providing a stable, pseudonymous on-chain identity. +That identifier is the `Bytes<32>` (left) branch of the `Either, ContractAddress>` account type +and is what [setApprovalForAll] and [transferFrom] check ownership and operator rights against. + +What is public on the ledger: + +* `_balances`, keyed by token id and then by account identifier (or contract address). +* `_operatorApprovals`, keyed by owner and then operator. +* `_uri`. + +What the caller holds privately: + +* The 32-byte `secretKey`. It never reaches the ledger; only its hash does. + +A transfer discloses the sender, the recipient, the token id, and the amount, because the balance map is updated with those values. +An operator approval discloses the caller's identifier, the operator, and the flag. + +Because the identifier is `H(secretKey)` with no per-deployment salt, the same key produces the same identity in every contract. +Users who want cross-contract unlinkability can use a different secret key per contract at the wallet layer. + +To derive an account identifier off-chain (for example, to pass as the `recipient` of a mint), use [computeAccountId] from the Utils module: + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} +``` + +A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side. +The [Utils guide](./utils#deriving-an-identifier-off-chain) shows the equivalent computation with the Compact runtime. + +The TypeScript witness implementation lives with the DApp, not with the library. +The witnesses in the `compact-contracts` repository are test-only and are not published; +consumers author their own `wit_MultiTokenSK` that returns the caller's 32-byte secret key. + + + The `secretKey` must never be logged or persisted in plaintext. Loss of the key means loss of the tokens held under that identity, + and key exposure allows impersonation. Use `crypto.getRandomValues()` or equivalent to generate keys. + + ## Usage Import the MultiToken module into the implementing contract. diff --git a/content/contracts-compact/multisig.mdx b/content/contracts-compact/multisig.mdx new file mode 100644 index 00000000..bfe4077c --- /dev/null +++ b/content/contracts-compact/multisig.mdx @@ -0,0 +1,355 @@ +--- +title: Multisig +--- + +{/* links */} +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern +[Ownable]: ./ownable +[Signer API]: api/multisig#Signer +[initialize]: api/multisig#Signer-initialize +[assertSigner]: api/multisig#Signer-assertSigner +[assertThresholdMet]: api/multisig#Signer-assertThresholdMet +[isSigner]: api/multisig#Signer-isSigner +[_addSigner]: api/multisig#Signer-_addSigner +[_removeSigner]: api/multisig#Signer-_removeSigner +[_changeThreshold]: api/multisig#Signer-_changeThreshold +[_setThreshold]: api/multisig#Signer-_setThreshold +[ProposalManager API]: api/multisig#ProposalManager +[_createProposal]: api/multisig#ProposalManager-_createProposal +[_cancelProposal]: api/multisig#ProposalManager-_cancelProposal +[_markExecuted]: api/multisig#ProposalManager-_markExecuted +[toShieldedRecipient]: api/multisig#ProposalManager-toShieldedRecipient +[toUnshieldedRecipient]: api/multisig#ProposalManager-toUnshieldedRecipient +[ShieldedTreasury API]: api/multisig#ShieldedTreasury +[ShieldedTreasury _deposit]: api/multisig#ShieldedTreasury-_deposit +[ShieldedTreasury _send]: api/multisig#ShieldedTreasury-_send +[ShieldedTreasuryStateless API]: api/multisig#ShieldedTreasuryStateless +[ShieldedTreasuryStateless _send]: api/multisig#ShieldedTreasuryStateless-_send +[UnshieldedTreasury API]: api/multisig#UnshieldedTreasury +[UnshieldedTreasury _deposit]: api/multisig#UnshieldedTreasury-_deposit +[UnshieldedTreasury _send]: api/multisig#UnshieldedTreasury-_send +[ForwarderShielded API]: api/multisig#ForwarderShielded +[ForwarderUnshielded API]: api/multisig#ForwarderUnshielded +[ForwarderPrivate API]: api/multisig#ForwarderPrivate +[_drain]: api/multisig#ForwarderPrivate-_drain +[_calculateParentCommitment]: api/multisig#ForwarderPrivate-_calculateParentCommitment +[presets directory]: https://github.com/OpenZeppelin/compact-contracts/tree/v0.3.0/contracts/src/multisig/presets + +The multisig directory provides building blocks for M-of-N governance over funds held by a contract: +a `Signer` registry with an approval threshold, a `ProposalManager` that tracks transfer proposals, +three treasuries that hold and move shielded or unshielded tokens, +and three forwarders that pass every deposit on to a parent. +The modules are independent of one another. +A contract composes the ones it needs, following the [Module/Contract Pattern], +and supplies its own authorization policy on top. +The package also ships reference compositions of these modules; see [Presets](#presets). + +None of the multisig modules declares a witness. +Every input is a circuit parameter, so what reaches the public ledger is decided by the `disclose` calls inside each circuit. +The sections below name what each module publishes. + +## Signer + +The [Signer API] keeps the registry of who may approve and how many approvals are required. +It is generic over the signer identity type `T`, which the consuming contract chooses at import time: +`Bytes<32>` for a commitment such as the hash of a public key, +`JubjubPoint` for an aggregated key, +or `Either` for a caller identity. +The module never resolves who the caller is. +The contract passes an identity in, and Signer checks it against the registry. + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/multisig/Signer"> + prefix Signer_; +``` + +### Public ledger state + +Everything the module keeps is public: +`_signers` (the full set of registered identities), `_signerCount`, `_threshold`, and `_isInitialized`. +[isSigner], [_addSigner], and [_removeSigner] disclose the identity they are given, since it is the key of the set lookup or insertion, +and [_setThreshold] discloses the new threshold. +The signer count and threshold are `Uint<8>`, so a registry holds at most 255 signers. + +### Who may call what + +* [initialize] registers an initial signer vector and sets the threshold. + It runs once and MUST only be called from the constructor; calling it later would corrupt the signer set. + A contract may skip it entirely and configure signers through its own flow with [_addSigner] and [_setThreshold]. +* The guards [assertSigner] and [assertThresholdMet] and the views `getSignerCount`, `getThreshold`, and [isSigner] are open to anyone + and deliberately do not check initialization. + Safety comes from the state rather than from a flag: + an empty registry makes [isSigner] false for every input, so [assertSigner] rejects every caller, + and a zero threshold makes [assertThresholdMet] reject before it compares any approval count. + A contract that needs a "configured" signal beyond that tracks one itself. +* [_addSigner], [_removeSigner], [_changeThreshold], and [_setThreshold] have no access control. + The consuming contract must gate them behind its own authorization policy. + Left ungated, the first caller registers their own signer set and takes control of the contract. + +[assertThresholdMet] compares the supplied count against the threshold and nothing else; it never consults the registry. +The contract's own approval accounting must be gated by [assertSigner] and must count distinct signers. +The count is intentionally not bounded by `_signerCount`, +so approvals collected before a signer was removed still count toward a threshold the remaining signers satisfy. + +[_changeThreshold] refuses a threshold above the current signer count. +[_setThreshold] skips that check so a custom setup flow can set the threshold before any signer is registered. +A contract must not gate operations on [assertThresholdMet] while such a setup is incomplete. + +Reconfigurations that take several transactions (removing a signer, then lowering the threshold) pass through intermediate states +where the module's checks hold but the contract's intended configuration is incomplete. +Perform them atomically in a single circuit, or keep a configuration nonce that invalidates proposals created under a stale signer set. + +### Usage + +The example registers three `Bytes<32>` signer identities at deployment and lets an owner manage the registry afterwards. +The [Ownable] module authenticates the owner through its `wit_OwnableSK` witness, +which the consuming contract supplies in its own TypeScript +(the witness implementations in the library repository are test-only and are not published). + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/multisig/Signer"> + prefix Signer_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +constructor( + initialOwner: Either, ContractAddress>, + signers: Vector<3, Bytes<32>>, + thresh: Uint<8> +) { + Ownable_initialize(initialOwner); + Signer_initialize<3>(signers, thresh); +} + +export circuit addSigner(signer: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Signer__addSigner(signer); +} + +export circuit removeSigner(signer: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Signer__removeSigner(signer); +} + +export circuit changeThreshold(newThreshold: Uint<8>): [] { + Ownable_assertOnlyOwner(); + Signer__changeThreshold(newThreshold); +} + +export circuit isSigner(account: Bytes<32>): Boolean { + return Signer_isSigner(account); +} + +export circuit getThreshold(): Uint<8> { + return Signer_getThreshold(); +} +``` + +## ProposalManager + +The [ProposalManager API] tracks the lifecycle of transfer proposals independently of the token kind. +A `Proposal` records a `Recipient`, a token `color`, an `amount`, and a `ProposalStatus` (`Active`, `Executed`, or `Cancelled`; +`Inactive` is the zero value and is never stored). +A `Recipient` pairs 32 address bytes with a `RecipientKind` tag (`ShieldedUser`, `UnshieldedUser`, or `Contract`) +that says how those bytes are to be read. +Build recipients with `shieldedUserRecipient`, `unshieldedUserRecipient`, and `contractRecipient`, +and convert them at execution time with [toShieldedRecipient] (accepts `ShieldedUser` and `Contract`) +or [toUnshieldedRecipient] (accepts `UnshieldedUser` and `Contract`). +Because `Contract` is accepted by both, the tag alone does not decide the rail; +a contract composing both treasuries chooses which one a proposal executes on. + +### Public ledger state + +Proposals are public. +`_proposals` is exported ledger state, so every proposal's recipient address, color, amount, and status is readable off-chain, +and `_nextProposalId` reveals how many have been created. +This holds whichever treasury the contract composes: pairing the module with `ShieldedTreasuryStateless` still publishes +the full intent of every transfer. +The transfer is private; the plan is not. + +### Who may call what + +[_createProposal], [_cancelProposal], and [_markExecuted] have no access control and must be gated by the contract. +The guards `assertProposalExists` and `assertProposalActive`, the recipient helpers, and the getters are open. +[_createProposal] increments the counter and reads it back as the map key, +so two transactions that create proposals against the same ledger state conflict and only one of them lands. + +[_createProposal] validates nothing about the recipient beyond its type. +A kind the execution rail cannot accept (an `UnshieldedUser` recipient on a shielded treasury) +creates a proposal that stays `Active` and unexecutable until [_cancelProposal] retires it, +and a zero or otherwise unspendable address is accepted, so funds sent to it on execution are unrecoverable. + +## Treasuries + +Each treasury exposes an ungated `_deposit` and `_send` pair that the contract gates with its own policy. +They differ in what they keep on the ledger and in who handles the change of a shielded spend. + +### ShieldedTreasury + +The [ShieldedTreasury API] stores one coin per token color in `_coins`, a map from color to `QualifiedShieldedCoinInfo`, +together with cumulative `_received` and `_sent` totals per color. +[ShieldedTreasury _deposit] claims the incoming coin with `receiveShielded` +and merges it into the stored coin of the same color (`mergeCoinImmediate`), or inserts it when the color is new, +so the contract never has to choose between several UTXOs of one color at spend time. +[ShieldedTreasury _send] looks the coin up by color, checks it covers the amount, and calls `sendShielded`, +which routes any change back to the contract as a self-owned output. +The treasury retains that change: it is written back to `_coins` for the next spend, +and the color's entry is removed when the send leaves none. + +The holdings of this treasury are public. +`_coins` is exported, so every coin it holds (nonce, color, value, and Merkle index) is readable in plaintext, +as are the received and sent totals. +"Shielded" refers to the token rail: individual transfers are shielded, but what the contract holds is not. +Deposits disclose the incoming coin; sends disclose the color, recipient, and amount. + + + `_send` returns a `ShieldedSendResult` whose `sent` and `change` coins are in plaintext. + Consume it inside the circuit or drop it; returning it from an entry point, writing it to the ledger, + or passing it to another contract publishes the coin details. + The change coin already belongs to the treasury and is recorded in `_coins`: + spending it again in the same transaction double-spends the recorded coin. + + +### ShieldedTreasuryStateless + +The [ShieldedTreasuryStateless API] keeps no ledger state at all: no coin map, no balances, no totals. +`_deposit` calls `receiveShielded` and nothing else. +The operator discovers the coin's Merkle index from `ZswapOutput` events through the indexer, +builds the `QualifiedShieldedCoinInfo` off-chain, and passes it to [ShieldedTreasuryStateless _send] at spend time, +so coin data never reaches the public ledger and there is no on-chain balance to query. +Sufficiency is enforced by `sendShielded` rather than by the module, +and a double spend or a coin the contract does not own is rejected by the protocol when the transaction is applied. + +`sendShielded` routes change back to the contract, and [ShieldedTreasuryStateless _send] returns it untouched in `result.change` +as a live, spendable coin. +The contract decides what to do with it: keep it off-chain and pass it back as the `coin` of a later spend, +or route it onward to a different recipient in the same transaction with `sendImmediateShielded`. +The only rule is that the change must never be re-spent and still treated as held. +Writing it to the ledger would publish its nonce, color, and value. + +Deposits disclose the incoming coin; sends disclose the spent coin, recipient, and amount. +A coin commitment is a hash of the coin's data, so a receive can be linked to its later spend. + +### UnshieldedTreasury + +The [UnshieldedTreasury API] tracks a per-color balance in `_balances`. +[UnshieldedTreasury _deposit] calls `receiveUnshielded` and adds to the balance; +[UnshieldedTreasury _send] subtracts and calls `sendUnshielded`. +The sufficiency and overflow guards read `_balances`, not the protocol balance, +which is fixed at the start of execution and cannot see a deposit made earlier in the same transaction. +Route every unshielded credit and debit through the module: +a direct `receiveUnshielded` or `sendUnshielded` leaves `_balances` out of step and `getTokenBalance` misreports. + +Unshielded transfers are fully public: depositor, recipient, color, and amount all appear on the transcript. +Recipients are not validated, so an unspendable recipient permanently removes the funds, +and a send to the contract's own address zeroes the tracked balance while the funds stay held at the protocol level. + +### Usage + +The example composes `ShieldedTreasury` with [Ownable]: anyone may fund the treasury, only the owner may spend from it. +The send result is dropped because the change is already recorded in the treasury's coin map. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasury" + prefix Treasury_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +constructor(initialOwner: Either, ContractAddress>) { + Ownable_initialize(initialOwner); +} + +export circuit deposit(coin: ShieldedCoinInfo): [] { + Treasury__deposit(coin); +} + +export circuit withdraw( + recipient: Either, + color: Bytes<32>, + amount: Uint<128> +): [] { + Ownable_assertOnlyOwner(); + Treasury__send(recipient, color, amount); +} + +export circuit getTokenBalance(color: Bytes<32>): Uint<128> { + return Treasury_getTokenBalance(color); +} + +export circuit getReceivedTotal(color: Bytes<32>): Uint<128> { + return Treasury_getReceivedTotal(color); +} + +export circuit getSentTotal(color: Bytes<32>): Uint<128> { + return Treasury_getSentTotal(color); +} +``` + +## Forwarders + +A forwarder is bound to a single parent at deployment and passes every deposit on to it. +The modules are permissionless: the recipient is fixed, so any caller may deposit without compromising the parent. +None of them exposes a setter; a contract that needs to change the parent adds its own circuit, or deploys a new forwarder. + +### ForwarderShielded and ForwarderUnshielded + +The [ForwarderShielded API] and [ForwarderUnshielded API] forward atomically: +`_deposit` claims the incoming value (`receiveShielded` or `receiveUnshielded`) and re-sends it to the parent in the same transaction +(`sendImmediateShielded` or `sendUnshielded`). +Each keeps two ledger fields, `_parent` and `_isInitialized`, and every circuit except `initialize` asserts initialization. +The parent is stored as a generic `Either` so the ledger layout can later accept a contract address without a state migration, +but `initialize` accepts only a `ZswapCoinPublicKey` (shielded, stored in the `left` arm) +or a `UserAddress` (unshielded, stored in the `right` arm) today: +an atomic forward can only reach a recipient that needs no in-transaction claim, +and a third-party contract would never claim the output. + +`initialize` rejects only the all-zero key or address. +The deployer must set a parent that can actually spend the forwarded coins; forwarding to a key no one controls strands every deposit. +The shielded forwarder discloses the incoming coin; the unshielded forwarder discloses depositor, recipient, color, and amount. + +### ForwarderPrivate + +The [ForwarderPrivate API] hides the parent. +The ledger holds `_parentCommitment`, computed off-chain by the deployer as +`H(pad(32, "ForwarderPrivate:commitment") || parent || opSecret)` with the pure [_calculateParentCommitment], +plus `_isInitialized`. +`_deposit` claims the coin and writes nothing; coins dwell at the contract address until drained. +[_drain] takes the coin, the parent coin public key, the operational secret, and the value to send: +it checks that the pair hashes to the stored commitment, then sends the value to the parent with `sendShielded`. +The parent is a `ZswapCoinPublicKey` and never a `ContractAddress`: +a coin-public-key recipient is encrypted inside the Zswap output and never appears on the public transcript, +whereas a shielded send to a contract publishes that contract's address. +Any change is routed back to the contract and returned untouched in `result.change`, +to leave dwelling for a future drain or to route onward with `sendImmediateShielded`. + +Knowledge of the preimage is the sole authorization gate. +Two forwarders bound to the same parent with different operational secrets produce different commitments and are unlinkable on-chain. +Deposits disclose the incoming coin; drains disclose the spent coin and the value. + + + Losing the operational secret is permanent fund loss. + It is the only drain authorization, with no rotation, revocation, or recovery path: + every coin accumulated at the contract becomes inaccessible. + Back it up offline before the first deposit and treat it with the hygiene of a signing key. + + +## Presets + +The package ships six standalone contracts under its [presets directory]: +`ShieldedMultiSig`, `ShieldedMultiSigV2`, `ShieldedMultiSigV3`, and one forwarder preset per forwarder module. +They are reference compositions that show how the modules above fit together, and they are compiled and deployed as-is rather than imported. +They are not covered by the API reference. + + + The three `ShieldedMultiSig` presets do not verify real signatures. + `ShieldedMultiSigV2` and `ShieldedMultiSigV3` call a `stubVerifySignature` that accepts every signature + (to be replaced with `ecdsaVerify` and `keccak256` once those primitives are available), + and `ShieldedMultiSig` identifies the caller by `ownPublicKey()` alone. + Do not deploy them as multisigs. + diff --git a/content/contracts-compact/native-shielded-token.mdx b/content/contracts-compact/native-shielded-token.mdx new file mode 100644 index 00000000..a7b867b2 --- /dev/null +++ b/content/contracts-compact/native-shielded-token.mdx @@ -0,0 +1,375 @@ +--- +title: NativeShieldedToken +--- + +{/* links */} +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern +[ZOwnablePK]: ./ownable +[wit_secretNonce]: api/ownable#ZOwnablePK-wit_secretNonce +[ZOwnablePK__computeOwnerId]: api/ownable#ZOwnablePK-_computeOwnerId +[NativeShieldedToken]: api/native-shielded-token#NativeShieldedToken +[NativeShieldedTokenFamily]: api/native-shielded-token#NativeShieldedTokenFamily +[NativeShieldedTokenCore]: api/native-shielded-token#NativeShieldedTokenCore +[tokenColor]: api/native-shielded-token#NativeShieldedToken-tokenColor +[_mint]: api/native-shielded-token#NativeShieldedToken-_mint +[_mintToSelf]: api/native-shielded-token#NativeShieldedToken-_mintToSelf +[_burn]: api/native-shielded-token#NativeShieldedToken-_burn +[_burnFromSelf]: api/native-shielded-token#NativeShieldedToken-_burnFromSelf +[Core__mint]: api/native-shielded-token#NativeShieldedTokenCore-_mint +[NativeShieldedTokenPublicSupply]: api/native-shielded-token#NativeShieldedTokenPublicSupply +[NativeShieldedTokenFamilyPublicSupply]: api/native-shielded-token#NativeShieldedTokenFamilyPublicSupply +[_addMinted]: api/native-shielded-token#NativeShieldedTokenPublicSupply-_addMinted +[_addBurned]: api/native-shielded-token#NativeShieldedTokenPublicSupply-_addBurned +[totalMinted]: api/native-shielded-token#NativeShieldedTokenPublicSupply-totalMinted +[totalBurned]: api/native-shielded-token#NativeShieldedTokenPublicSupply-totalBurned +[totalSupply]: api/native-shielded-token#NativeShieldedTokenPublicSupply-totalSupply +[NativeShieldedTokenDerivedNonce]: api/native-shielded-token#NativeShieldedTokenDerivedNonce +[_deriveNonce]: api/native-shielded-token#NativeShieldedTokenDerivedNonce-_deriveNonce +[ZOwnableNativeShieldedToken]: api/native-shielded-token#ZOwnableNativeShieldedToken +[mint]: api/native-shielded-token#ZOwnableNativeShieldedToken-mint +[burn]: api/native-shielded-token#ZOwnableNativeShieldedToken-burn + +A native shielded token is a token whose units are Midnight's own shielded coins (Zswap UTXOs) +rather than entries in a contract-owned map. +The issuing contract mints coins of its own color and burns them; +holding, spending, and transferring happen in the holder's wallet through Zswap, outside the contract. +This family of modules implements the MIP-0011 native shielded token standard for the Midnight network: +a single-token flavor ([NativeShieldedToken]), a multi-token flavor ([NativeShieldedTokenFamily]), +the shared core both are built on ([NativeShieldedTokenCore]), opt-in supply accounting, +an opt-in derived-nonce helper, and an owner-gated preset ([ZOwnableNativeShieldedToken]). + +## How a native shielded token works + +**Coins, not balances.** A unit of the token is a shielded coin with a `color`, a `value`, and a `nonce`. +The ledger stores a commitment to each coin and a nullifier once it is spent; it never stores who holds what. +There is no `balanceOf`, `allowance`, or contract-mediated `transfer`: +once a user holds a coin, the contract cannot observe or restrict its movement, +and holders move coins between wallets with ordinary Zswap transactions. + +**Color.** Every token type is identified by its coin color, `tokenType(domain, contractAddress)`, +where `domain` is a 32-byte discriminator and `contractAddress` is the issuing contract. +Because the contract address is part of the derivation, only the issuing contract can mint coins of its colors. +The modules compute the color at call time with [tokenColor] and never store it. + +**Minting.** The contract calls `mintShieldedToken`, which creates a new coin for a recipient. +The protocol caps a single mint at `Uint<64>`. + +**Burning.** The contract burns by sending value to `shieldedBurnAddress()`. +Burn amounts are `Uint<128>`, because `sendShielded` accepts that width; the asymmetry with mints is a protocol property. + +**Metadata.** `name`, `symbol`, and `decimals` are contract-wide, fixed at construction, and public. +`decimals` is a display convention only; the protocol operates on integer values. + +## What is public and what is private + +The token modules declare no witnesses; the only secrets are the ones that travel inside coins. + +Public on the ledger as state: + +* `_name`, `_symbol`, `_decimals`, and `_isInitialized` (and `_domain` for the single-token flavor). +* The supply counters, if a supply extension is composed. + +Public in every transaction: + +* Each mint's color and amount, through the protocol's shielded-mint effect. +* The coin commitments created and the nullifiers spent. +* Which circuit ran. + +Private: + +* The recipient and the nonce of a mint, provided the nonce is secret and random. + Anyone who knows the nonce can recompute the commitment for candidate recipient keys. +* The amount, the coin, and the refund recipient of a burn, as long as the observer does not know the coin's nonce. + A change output does reveal that a burn was partial. + + +A contract-initiated mint or refund carries a coin ciphertext only when the DApp can resolve the recipient's +encryption key at proving time (its own, or one supplied through `additionalCoinEncPublicKeyMappings`). +Without that, the recipient's wallet cannot discover the coin by scanning the chain, and the `ShieldedCoinInfo` +a circuit returns is the only copy of the coin: the DApp must capture it and deliver it to the recipient +(or persist it, when the contract is the recipient). Dropping it strands the value irrecoverably. + + +## Single token or a family + +Both user-facing modules are thin wrappers over [NativeShieldedTokenCore], which owns the initialization flag, +the metadata, and the mint and burn logic. They differ only in where the domain comes from. + +* [NativeShieldedToken] issues one token type. The domain is passed to `initialize`, stored as `sealed ledger _domain`, + and forwarded to every core operation, so no circuit takes a domain parameter and a caller cannot misuse one. +* [NativeShieldedTokenFamily] issues many token types from one contract. Every circuit takes a `domain` parameter, + and `tokenType(domain, kernel.self())` discriminates the types. + Midnight has no contract-to-contract calls and no way for a contract to deploy another, + so a protocol that issues many related tokens (one liquidity-share token per pair, for instance) + must do so from a single contract. Metadata is family-wide; per-type identity belongs in the consumer's own state. + + +Pick one flavor per contract. Both wrap the same core, so importing both into one contract shares a single +initialization flag and one set of `name`, `symbol`, and `decimals`. + + +The core itself is not meant to be composed directly. Its `_mint` and `_burn` take an +`Either` recipient; the flavors narrow that to a `ZswapCoinPublicKey` +and expose [_mintToSelf] for the contract's own coins. + +## Minting and who can receive a coin + +[_mint] takes a `ZswapCoinPublicKey` recipient, a `Uint<64>` amount, and a caller-supplied `Bytes<32>` nonce, +and returns the new coin's `ShieldedCoinInfo`. The recipient must not be the zero key. + +**Recipients.** A coin can be minted to any user's coin public key, or to the issuing contract itself through [_mintToSelf], +which mints to `kernel.self()` and claims the coin with `receiveShielded` in the same call. +No other contract can receive a mint: the ledger only accepts a contract-addressed commitment when that contract claims it +in the same transaction, and no primitive can claim a receive on another contract's behalf. +The core enforces this directly ([Core__mint] rejects any `ContractAddress` other than the contract's own), +and the flavors do not expose the address arm at all. +A coin minted to the contract is the contract's holding: persist the returned coin info in your own ledger state, +and spend it later with [_burnFromSelf] once it carries a Merkle-tree index. + +**Nonces.** The caller is responsible for nonce uniqueness. Reusing a nonce for the same (domain, value, recipient) +produces a duplicate commitment, which the ledger rejects. +With a secret, cryptographically random nonce the mint is recipient-private. +If you do not want to manage nonces, compose [NativeShieldedTokenDerivedNonce] and pass its [_deriveNonce] output, +at the cost of making those mints recipient-public (see below). + +The modules include no access control. Gate `_mint` and `_mintToSelf` with an access-control module +following the [Module/Contract Pattern]; exposing them as-is lets anyone mint. + +## Burning and change + +There are two burn paths, chosen by where the coin comes from. + +[_burn] destroys value from a coin that the caller's wallet pays into the current transaction. +The contract receives the coin and spends `amount` of it to the burn address through `sendImmediateShielded`, +the required path for a coin that was created in the same transaction. +If `amount` is less than `coin.value`, the remainder is sent to `refundTo` as a new coin and returned as `some(refund)`; +pass `coin.value` as `amount` to burn the coin in full, in which case `none` is returned. +`refundTo` must always be supplied and non-zero, even on a full burn: the zero key is the burn address, +so a zero `refundTo` would silently burn the change as well. + +[_burnFromSelf] destroys value from a coin the contract already holds, identified by a `QualifiedShieldedCoinInfo` +(a coin with a valid Merkle-tree index). It spends through `sendShielded`; any change is received by the contract +automatically and returned so the consumer can persist it. The change coin replaces the original as the contract's holding, +and its info is not otherwise recoverable. + +Both paths put the spent coin's nullifier and the new output commitments on the ledger. +Neither puts the burned amount on the ledger: the `disclose()` calls in the module are compiler permission markers +for the coin primitives, not disclosure sinks. + +## Supply accounting (optional) + +The token modules track no supply totals. [NativeShieldedTokenPublicSupply] (for the single-token flavor) and +[NativeShieldedTokenFamilyPublicSupply] (for the family) add on-chain counters. +They import no token module: the consuming contract pairs every mint with [_addMinted] and every burn with [_addBurned], +under the same domain for the family. The three getters carry different guarantees: + +* [totalMinted] is exact. Every mint of this contract's colors goes through the contract. +* [totalBurned] is a lower bound. A holder can send coins to the burn address without calling the contract, and those burns are invisible. +* [totalSupply] is `totalMinted - totalBurned`, an upper bound on circulating supply. + +`_addBurned` enforces `burned <= minted`, so an unpaired, duplicate, or wrong-domain burn reverts instead of pushing +the supply negative. Nothing catches drift on the mint side: a missing `_addMinted` undercounts, after which +`_addBurned` rejects legitimate burns, and a duplicate `_addMinted` lets burns exceed what was minted. +The pairing is an invariant of the consuming contract. + + +Composing a supply extension makes contract-mediated burn amounts public: the `_addBurned` write is what puts the +burned amount into the public transcript. Mint amounts are already public through the shielded-mint effect, so +`_addMinted` adds accounting, not disclosure. Compose supply tracking only when on-chain auditability is worth more +than burn-amount privacy. + + +## Derived nonces (optional) + +[NativeShieldedTokenDerivedNonce] owns a monotonic `_counter` and exposes [_deriveNonce], which increments the counter +and returns `evolveNonce(_counter, pad(32, "NativeShieldedToken:nonce"))`. +Uniqueness comes from the counter; the fixed tag separates these nonces from any other `evolveNonce` use in the contract. +One counter serves the whole contract, so derived nonces never repeat across token types. +It needs no initialization and pairs with either flavor. + +Derived nonces are predictable from public state, so mints that use them are recipient-public: +anyone can recompute the commitment for candidate recipient keys. +They are also open to collision griefing: an actor with access to a caller-nonce mint can pre-mint a colliding commitment +and make the matching derived-nonce mint fail. Gate both mint paths at the same trust level, and do not rely on retrying: +the nonce is a pure function of the counter, and a reverted mint does not advance it, so an identical retry collides again. +A mint with different inputs advances the counter and clears the way. + +## Usage + +### An owner-gated single token + +[ZOwnableNativeShieldedToken] composes [ZOwnablePK] (a shielded owner), [NativeShieldedToken] (one color), and +[NativeShieldedTokenPublicSupply] (a public minted total). Only the owner can [mint] and [burn]; +the preset deliberately never calls `_addBurned`, so burn amounts stay private and `_totalBurned` stays empty. +The following contract is the published `ZOwnableNativeShieldedTokenExample`. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/presets/ZOwnableNativeShieldedToken" + prefix ZOwnableNativeShieldedToken_; + +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe, ShieldedCoinInfo }; + +// Surface the preset's state unprefixed so `ledger()` readers use the keys the +// modules declare. +import { + _ownerCommitment, + _counter, + _instanceSalt, + _ownableIsInitialized, + _domain, + _name, + _symbol, + _decimals, + _isInitialized, + _totalMinted, + _totalBurned, +} from "./node_modules/@openzeppelin/compact-contracts/token/presets/ZOwnableNativeShieldedToken"; +export { + _ownerCommitment, + _counter, + _instanceSalt, + _ownableIsInitialized, + _domain, + _name, + _symbol, + _decimals, + _isInitialized, + _totalMinted, + _totalBurned, +}; + +constructor( + ownerId: Bytes<32>, + instanceSalt: Bytes<32>, + tokenDomain: Bytes<32>, + name_: Opaque<"string">, + symbol_: Opaque<"string">, + decimals_: Uint<8>, +) { + ZOwnableNativeShieldedToken_initialize( + ownerId, + instanceSalt, + tokenDomain, + name_, + symbol_, + decimals_, + ); +} + +export circuit mint(recipient: ZswapCoinPublicKey, amount: Uint<64>, nonce: Bytes<32>): ShieldedCoinInfo { + return ZOwnableNativeShieldedToken_mint(recipient, amount, nonce); +} + +export circuit burn(coin: ShieldedCoinInfo, amount: Uint<64>, refundTo: ZswapCoinPublicKey): Maybe { + return ZOwnableNativeShieldedToken_burn(coin, amount, refundTo); +} + +export circuit transferOwnership(newOwnerId: Bytes<32>): [] { + ZOwnableNativeShieldedToken_transferOwnership(newOwnerId); +} + +export circuit renounceOwnership(): [] { + ZOwnableNativeShieldedToken_renounceOwnership(); +} + +export circuit owner(): Bytes<32> { + return ZOwnableNativeShieldedToken_owner(); +} + +export pure circuit _computeOwnerId(pk: Either, nonce: Bytes<32>): Bytes<32> { + return ZOwnableNativeShieldedToken__computeOwnerId(pk, nonce); +} + +export circuit totalMinted(): Uint<128> { + return ZOwnableNativeShieldedToken_totalMinted(); +} + +export circuit tokenColor(): Bytes<32> { + return ZOwnableNativeShieldedToken_tokenColor(); +} +``` + +The owner is identified by a commitment, not a key. Before deploying, compute +`ownerId = _computeOwnerId(pk, secretNonce)` off-chain, where `pk` is the coin public key the owner's wallet +will supply as `ownPublicKey()` and `secretNonce` is fresh random bytes the owner keeps private. +The contract cannot check this value at deploy time; any other value leaves the contract with no usable owner and no recovery. +Every owner transaction then proves knowledge of the pair: the wallet supplies the coin public key, and the +[wit_secretNonce] witness (inherited from [ZOwnablePK]) supplies the nonce. +The TypeScript witness implementations in the library repository are test-only and are not published; +you author the witness for your own DApp. +The proof server sees `wit_secretNonce`, so run it where the owner controls it. + +On the client side, pass fresh random bytes as `nonce` on every `mint`, and persist the `ShieldedCoinInfo` values that +`mint` and `burn` return: midnight-js encrypts an output to its recipient only when it can resolve the recipient's +encryption key (its own, or one supplied in `additionalCoinEncPublicKeyMappings`). + +`initialize` writes sealed fields, so it belongs in the constructor and must not be exposed as a post-deploy circuit. +`transferOwnership` is one step and `renounceOwnership` stops minting and burning forever. + +### A token family with supply tracking and derived nonces + +This contract issues one token type per `domain`, counts supply per domain, and lets the owner mint without +managing nonces. Burns are open to any holder, since a holder can always burn by sending to the burn address directly; +routing burns through the contract keeps `totalBurned` closer to the truth. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK" + prefix ZOwnablePK_; +import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedTokenFamily" + prefix Family_; +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenFamilyPublicSupply" + prefix Supply_; +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenDerivedNonce" + prefix Derived_; + +export { ZswapCoinPublicKey, Maybe, ShieldedCoinInfo }; + +constructor( + ownerId: Bytes<32>, + instanceSalt: Bytes<32>, + name_: Opaque<"string">, + symbol_: Opaque<"string">, + decimals_: Uint<8>, +) { + ZOwnablePK_initialize(ownerId, instanceSalt); + Family_initialize(name_, symbol_, decimals_); +} + +// Owner only. Derived nonces make these mints recipient-public. +export circuit mint(domain: Bytes<32>, recipient: ZswapCoinPublicKey, amount: Uint<64>): ShieldedCoinInfo { + ZOwnablePK_assertOnlyOwner(); + const coin = Family__mint(domain, recipient, amount, Derived__deriveNonce()); + Supply__addMinted(domain, amount); + return coin; +} + +// Any holder. `coin` is paid into the transaction by the caller's wallet; +// the change, if any, goes to `refundTo`. Publishes `amount`. +export circuit burn( + domain: Bytes<32>, + coin: ShieldedCoinInfo, + amount: Uint<128>, + refundTo: ZswapCoinPublicKey +): Maybe { + const refund = Family__burn(domain, coin, amount, refundTo); + Supply__addBurned(domain, amount); + return refund; +} + +export circuit tokenColor(domain: Bytes<32>): Bytes<32> { + return Family_tokenColor(domain); +} + +export circuit totalSupply(domain: Bytes<32>): Uint<128> { + return Supply_totalSupply(domain); +} +``` + +A `mint` and a `burn` under the same `domain` both touch that domain's supply entry, so only one of them lands per block; +operations on different domains do not conflict. diff --git a/content/contracts-compact/non-fungible-token.mdx b/content/contracts-compact/non-fungible-token.mdx index a70985ac..0d31dd42 100644 --- a/content/contracts-compact/non-fungible-token.mdx +++ b/content/contracts-compact/non-fungible-token.mdx @@ -8,6 +8,11 @@ title: NonFungibleToken [ERC-165]: https://eips.ethereum.org/EIPS/eip-165 [Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [_mint]: ./api/non-fungible-token#NonFungibleToken-_mint +[approve]: ./api/non-fungible-token#NonFungibleToken-approve +[setApprovalForAll]: ./api/non-fungible-token#NonFungibleToken-setApprovalForAll +[transferFrom]: ./api/non-fungible-token#NonFungibleToken-transferFrom +[wit_NonFungibleTokenSK]: ./api/non-fungible-token#NonFungibleToken-wit_NonFungibleTokenSK +[computeAccountId]: ./api/utils#Utils-computeAccountId NonFungibleToken is a specification for [non-fungible tokens], a type of token where all the units are unique and distinct from each other. @@ -52,6 +57,53 @@ The `unsafe` circuits will eventually be deprecated after Compact supports contr `_transfer`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. +## Identity Model + +NonFungibleToken uses a witness-derived identity scheme. Each caller proves knowledge of a secret key +by injecting it through the [wit_NonFungibleTokenSK] witness. The module computes the caller's account identifier +as `persistentHash(secretKey)`, a commitment that hides the key while providing a stable, pseudonymous on-chain identity. +That identifier is the `Bytes<32>` (left) branch of the `Either, ContractAddress>` account type +and is what [approve], [setApprovalForAll], and [transferFrom] check ownership and operator rights against. + +What is public on the ledger: + +* `_owners`, keyed by token id, and `_balances`, keyed by account identifier (or contract address). +* `_tokenApprovals`, keyed by token id, and `_operatorApprovals`, keyed by owner and then operator. +* `_tokenURIs`, `_name`, and `_symbol`. + +What the caller holds privately: + +* The 32-byte `secretKey`. It never reaches the ledger; only its hash does. + +A transfer discloses the token id, the previous owner, and the recipient, because the owner and balance maps are updated with those values. +A single-token approval discloses the token id and the approved account; an operator approval discloses the caller's identifier, the operator, and the flag. + +Because the identifier is `H(secretKey)` with no per-deployment salt, the same key produces the same identity in every contract. +Users who want cross-contract unlinkability can use a different secret key per contract at the wallet layer. + +To derive an account identifier off-chain (for example, to pass as the `recipient` of a mint), use [computeAccountId] from the Utils module: + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} +``` + +A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side. +The [Utils guide](./utils#deriving-an-identifier-off-chain) shows the equivalent computation with the Compact runtime. + +The TypeScript witness implementation lives with the DApp, not with the library. +The witnesses in the `compact-contracts` repository are test-only and are not published; +consumers author their own `wit_NonFungibleTokenSK` that returns the caller's 32-byte secret key. + + + The `secretKey` must never be logged or persisted in plaintext. Loss of the key means loss of the tokens held under that identity, + and key exposure allows impersonation. Use `crypto.getRandomValues()` or equivalent to generate keys. + + ## Usage Import the NonFungibleToken module into the implementing contract. diff --git a/content/contracts-compact/ownable.mdx b/content/contracts-compact/ownable.mdx index 5deb387f..2c25eb5b 100644 --- a/content/contracts-compact/ownable.mdx +++ b/content/contracts-compact/ownable.mdx @@ -8,7 +8,7 @@ title: Ownable [_transferOwnership]: api/ownable#Ownable-_transferOwnership [_unsafeTransferOwnership]: api/ownable#Ownable-_unsafeTransferOwnership [_unsafeUncheckedTransferOwnership]: api/ownable#Ownable-_unsafeUncheckedTransferOwnership -[computeAccountId]: api/ownable#Ownable-computeAccountId +[computeAccountId]: api/utils#Utils-computeAccountId [assertOnlyOwner]: api/ownable#Ownable-assertOnlyOwner [Ownable API]: api/ownable#Ownable @@ -16,10 +16,12 @@ title: Ownable [ZOwnablePK_transferOwnership]: api/ownable#ZOwnablePK-transferOwnership [ZOwnablePK_assertOnlyOwner]: api/ownable#ZOwnablePK-assertOnlyOwner [_computeOwnerId]: api/ownable#ZOwnablePK-_computeOwnerId +[wit_secretNonce]: api/ownable#ZOwnablePK-wit_secretNonce [ed25519]: https://ed25519.cr.yp.to/ [rfc6979]: https://datatracker.ietf.org/doc/html/rfc6979 [AccessControl]: ./access-control.mdx +[Utils]: ./utils.mdx ## Access Control Defined @@ -46,17 +48,49 @@ This can later be changed with [transferOwnership]. Ownable uses a witness-derived identity scheme. The owner proves knowledge of a secret key by injecting it via the `wit_OwnableSK` witness. The module computes an account identifier -as `persistentHash(secretKey)`, which is a commitment that hides the key while providing a stable, +as `H(secretKey)` (a `persistentHash` of the key), which is a commitment that hides the key while providing a stable, pseudonymous on-chain identity. +What is public and what is private: + +* **On the ledger**: `_owner` holds the account identifier `H(secretKey)` (or a contract address), and `_isInitialized`. +* **Held privately by the owner**: the 32-byte `secretKey`, supplied through `wit_OwnableSK` at proving time. + The key never reaches the ledger; [assertOnlyOwner] recomputes the identifier inside the proof and compares it against `_owner`. + +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + To derive an account identifier off-chain (e.g. for the `initialOwner` parameter during construction), -use [computeAccountId]: +use [computeAccountId] from the [Utils] module: ```typescript -const accountId = Ownable_computeAccountId(secretKey); +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} +``` + +A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side. +The same value can be computed directly in TypeScript with the Compact runtime: + +```typescript +import { + CompactTypeBytes, + CompactTypeVector, + persistentHash, +} from '@midnight-ntwrk/compact-runtime'; + +const computeAccountId = (secretKey: Uint8Array): Uint8Array => { + const rt_type = new CompactTypeVector(1, new CompactTypeBytes(32)); + return persistentHash(rt_type, [secretKey]); +}; ``` The `secretKey` is a 32-byte cryptographically secure random value that the owner keeps private. +Because the identifier has no per-deployment salt, the same key produces the same identity across all contracts. +Owners who want cross-contract unlinkability should use a different key per contract. The `secretKey` must never be logged or persisted in plaintext. Loss of the key means loss of ownership. @@ -72,12 +106,12 @@ In other words, ownership transfers to contract addresses are disallowed through This is because Compact currently does not support contract-to-contract calls, which means if a contract is granted ownership, the owner contract cannot directly call the protected circuit. -### Experimental Features +### Unsafe Circuits -This module offers experimental circuits that allow ownership to be granted to contract addresses +This module offers circuits that allow ownership to be granted to contract addresses ([_unsafeTransferOwnership] and [_unsafeUncheckedTransferOwnership]). -Note that the circuit names are very explicit ("unsafe") with these experimental circuits. +Note that the circuit names are very explicit ("unsafe"). Until contract-to-contract calls are supported, there is no direct way for a contract to call circuits of other contracts or transfer ownership back to a user. @@ -90,7 +124,7 @@ there is no direct way for a contract to call circuits of other contracts or tra Import the Ownable module into the implementing contract. It's recommended to prefix the module with `Ownable_` to avoid circuit signature clashes. -The `initialOwner` should be an account identifier derived off-chain using `computeAccountId(secretKey)`: +The `initialOwner` should be an account identifier derived off-chain using `Utils_computeAccountId(secretKey)`: ```ts pragma language_version >= {{compact_language_version}}; @@ -118,7 +152,7 @@ export circuit mySensitiveCircuit(): [] { ``` Contracts may expose [transferOwnership] to allow the owner to transfer ownership. -The `newOwner` should be an account identifier derived by the new owner using `computeAccountId(secretKey)`: +The `newOwner` should be an account identifier derived by the new owner using `Utils_computeAccountId(secretKey)`: ```ts export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { @@ -139,7 +173,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" /** * Set `initialOwner` as the owner of the contract. - * `initialOwner` should be derived off-chain using computeAccountId(secretKey). + * `initialOwner` should be derived off-chain using Utils_computeAccountId(secretKey). */ constructor(initialOwner: Either, ContractAddress>) { Ownable_initialize(initialOwner); @@ -155,7 +189,7 @@ export circuit owner(): Either, ContractAddress> { /** * Transfers ownership of the contract. * Can only be called by the current owner. - * `newOwner` should be derived off-chain by the new owner using computeAccountId(secretKey). + * `newOwner` should be derived off-chain by the new owner using Utils_computeAccountId(secretKey). */ export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { Ownable_transferOwnership(newOwner); @@ -202,6 +236,17 @@ instead, the contract stores only a cryptographic commitment that proves ownersh This is useful in scenarios such as private treasuries, anonymous governance, or any application where revealing the administrator's identity may compromise the system's confidentiality. +What is public and what is private: + +* **On the ledger**: `_ownerCommitment`, the transfer `_counter`, the sealed `_instanceSalt`, and `_isInitialized`. + Ownership changes disclose only the new commitment, never the owner identifier that produced it. +* **Held privately by the owner**: the secret nonce, supplied through the [wit_secretNonce] witness, + and the public key, supplied through `ownPublicKey()`. [ZOwnablePK_assertOnlyOwner] recomputes the commitment + from both inside the proof and compares it against `_ownerCommitment`. + +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + ### Commitment Scheme The `ZOwnablePK` module employs a two-layer cryptographic commitment scheme designed to provide privacy, @@ -212,7 +257,7 @@ unlinkability, and collision resistance across deployments and ownership transfe The foundation of the system is the owner identifier, computed as: ```typescript -id = SHA256(pk, nonce) +id = SHA256(pk || nonce) ``` Where `pk` is the owner's public key and `nonce` is a secret value that may be either randomly generated for maximum privacy @@ -225,14 +270,15 @@ ensuring the owner's identity remains confidential. The final ownership commitment stored on-chain is computed as: ```typescript -commitment = SHA256(id, instanceSalt, counter, pad(32, "ZOwnablePK:shield:")) +commitment = SHA256(id || instanceSalt || counter || pad(32, "ZOwnablePK:shield:")) ``` This multi-element hash provides several security properties: * `id`: The privacy-preserving owner identifier described above. * `instanceSalt`: A unique per-deployment salt that prevents commitment collisions across different contract instances, -even when the same owner and nonce are used. +even when the same owner and nonce are used. It must be nonzero, and it should be a fresh random value per deployment: +a reused salt links the same owner across deployments. * `counter`: Incremented with each ownership transfer to ensure unlinkability. Each transfer produces a completely different commitment even with the same underlying owner. * `pad(32, "ZOwnablePK:shield:")`: A domain separator padded to 32 bytes that prevents hash collisions @@ -246,6 +292,13 @@ This commitment scheme ensures that: * Observers cannot correlate past and future ownership. * Cross-contract collisions are prevented through instance-specific salting. + + `ownPublicKey()` is a witness: it returns the caller's Zswap coin public key, and nothing in the circuit proves + that the caller holds the matching private key. The secret nonce is therefore the owner credential: + anyone holding the nonce and the owner's public key can pass [ZOwnablePK_assertOnlyOwner]. + Generate the nonce as 32 bytes from a CSPRNG (or a derivation with equivalent entropy) and keep it private. + + ### Nonce Generation Strategies The choice of nonce generation strategy represents a fundamental trade-off between simplicity/security and recoverability. @@ -261,7 +314,7 @@ const ownerId = ZOwnablePK._computeOwnerId(publicKey, randomNonce); ``` This approach is easy to generate and ensures maximum unlinkability. -However, it requires secure backup of both the private key and the nonce. +However, it requires secure backup of both the public key and the nonce. **Loss of either component results in permanent, irrecoverable loss of ownership.** #### Deterministic Nonce @@ -318,18 +371,18 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK" prefix ZOwnablePK_; constructor( - initOwnerCommitment: Bytes<32>, + initOwnerId: Bytes<32>, instanceSalt: Bytes<32>, ) { - ZOwnablePK_initialize(initOwnerCommitment, instanceSalt); + ZOwnablePK_initialize(initOwnerId, instanceSalt); } export circuit owner(): Bytes<32> { return ZOwnablePK_owner(); } -export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { - return ZOwnablePK_transferOwnership(disclose(newOwnerCommitment)); +export circuit transferOwnership(newOwnerId: Bytes<32>): [] { + return ZOwnablePK_transferOwnership(disclose(newOwnerId)); } export circuit renounceOwnership(): [] { @@ -337,6 +390,8 @@ export circuit renounceOwnership(): [] { } ``` +[ZOwnablePK_initialize] rejects a zero `ownerId` and a zero `instanceSalt`. + Similar to the [Ownable](#usage) module, circuits can be protected so that only the contract owner may call them by adding `assertOnlyOwner` as the first line in the circuit body: @@ -373,7 +428,7 @@ const generateId = ( return persistentHash(rt_type, [pk, nonce]); }; -// Instance salt for the constructor +// Instance salt for the constructor (must be nonzero) const generateInstanceSalt = (): Uint8Array => { return getRandomValues(new Uint8Array(32)); } diff --git a/content/contracts-compact/security.mdx b/content/contracts-compact/security.mdx index e8419e1b..b2824704 100644 --- a/content/contracts-compact/security.mdx +++ b/content/contracts-compact/security.mdx @@ -2,6 +2,24 @@ title: Security --- +{/* links */} +[Ownable]: ./ownable +[Utils guide]: ./utils#account-identifiers +[assertPaused]: ./api/security#Pausable-assertPaused +[assertNotPaused]: ./api/security#Pausable-assertNotPaused +[Initializable API]: ./api/security#Initializable +[Allowlist API]: ./api/security#Allowlist +[Blocklist API]: ./api/security#Blocklist +[isAllowed]: ./api/security#Allowlist-isAllowed +[assertAllowed]: ./api/security#Allowlist-assertAllowed +[_allow]: ./api/security#Allowlist-_allow +[_disallow]: ./api/security#Allowlist-_disallow +[isBlocked]: ./api/security#Blocklist-isBlocked +[assertNotBlocked]: ./api/security#Blocklist-assertNotBlocked +[_block]: ./api/security#Blocklist-_block +[_unblock]: ./api/security#Blocklist-_unblock +[compact#270]: https://github.com/LFDT-Minokawa/compact/issues/270 + The following documentation provides context, reasoning, and examples of modules found in the Security directory. ## Initializable @@ -11,11 +29,21 @@ More specifically, it enables logic to be performed once and only once to set up This is useful when a constructor cannot be used, for example when there are circular dependencies at construction time. -Many modules also use the initializable pattern which ensures that implementing contracts: +The initializable pattern ensures that implementing contracts: - Don't allow circuit calls until the contract is initialized. - Can only initialize the contract once. + + Use this module only at the top contract layer, and import it into at most one module per contract. + The Compact compiler collapses a shared import's ledger state inconsistently depending on directory layout + ([compact#270]): when two modules in the same directory both import Initializable, + their `_isInitialized` flags share a single ledger slot, so initializing one marks the other as initialized + and the other can never be initialized on its own. + For this reason the OpenZeppelin modules do not import Initializable; each one tracks its own + `_isInitialized` ledger flag inline. + + ### Usage ```ts @@ -38,7 +66,7 @@ export circuit doSomethingBeforeInitialized(): [] { export circuit setFieldAfterDeployment(f: Field): [] { // Initialize so the circuit cannot be called again Initializable_initialize(); - _fieldAfterDeployment = f; + _fieldAfterDeployment = disclose(f); } export circuit checkFieldAfterDeployment(): Field { @@ -48,11 +76,9 @@ export circuit checkFieldAfterDeployment(): Field { } ``` -## Pausable +See the [Initializable API] for the full list of circuits. -[Ownable]: ./ownable -[assertPaused]: ./api/security#Pausable-assertPaused -[assertNotPaused]: ./api/security#Pausable-assertNotPaused +## Pausable The Pausable module allows contracts to implement an emergency stop mechanism. This can be useful for scenarios such as preventing trades until the end of an evaluation period @@ -101,3 +127,156 @@ export circuit whenPaused(): [] { // Do something } ``` + +## Allowlist + +The Allowlist module is a minimal, reusable permit-list keyed by a `Bytes<32>` account identifier. +It answers a single question: is this account a member? +It backs use cases such as KYC allowlists, permissioned mints, and membership gates. + +The module is a membership mechanism, not a policy. +It holds no access control and no notion of when the list is enforced: + +- The mutators [_allow] and [_disallow] are ungated building blocks. + The implementing contract must protect them with its own access control module, + in the same way a token protects `_mint` and `_burn`. +- The implementing contract decides where [assertAllowed] applies, + for example only on transfers, only on mints, or only while an "eligibility required" flag is set. +- [isAllowed] reads membership without asserting it. + +### What is public + +The approved set lives in the public ledger field `_allowed: Set>`. +Membership is publicly readable and the approved set is publicly enumerable. +Every circuit discloses the `account` it is called with, so the identifier reaches the ledger as a public input. +This is the right posture for transparent or public-graph assets. +An asset whose transaction graph is meant to stay private should not use a public allowlist, +since the published candidate set would bound its anonymity set. + +The `Bytes<32>` keys are account identifiers as derived by `Utils_computeAccountId`; +see the [Utils guide] for how to derive one off-chain. + +### Usage + +The following example is a token whose recipients must be approved by the owner before they can receive tokens. +Membership changes are gated behind [Ownable], and the gate is enforced on `transfer`: + +```ts +// PermissionedToken.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/security/Allowlist" + prefix Allowlist_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + initialOwner: Either, ContractAddress>, + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, +) { + Ownable_initialize(initialOwner); + FungibleToken_initialize(name, symbol, decimals); +} + +// Membership changes are protected by the owner. +export circuit allow(account: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Allowlist__allow(account); +} + +export circuit disallow(account: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Allowlist__disallow(account); +} + +export circuit isAllowed(account: Bytes<32>): Boolean { + return Allowlist_isAllowed(account); +} + +// The implementing contract decides where the gate applies. +export circuit transfer(to: Either, ContractAddress>, value: Uint<128>): Boolean { + assert(to.is_left, "PermissionedToken: recipient must be an account identifier"); + Allowlist_assertAllowed(to.left); + return FungibleToken_transfer(to, value); +} +``` + +See the [Allowlist API] for the full list of circuits. + +## Blocklist + +The Blocklist module is a minimal, reusable deny-list keyed by a `Bytes<32>` account identifier. +It answers a single question: is this account blocked? +It backs use cases such as sanctions lists, account freezes, and abuse denials. + +As with Allowlist, the module is a membership mechanism, not a policy: + +- The mutators [_block] and [_unblock] are ungated building blocks that the implementing contract + must protect with its own access control module. +- The implementing contract decides where [assertNotBlocked] applies. +- [isBlocked] reads membership without asserting it. + +### What is public + +The blocked set lives in the public ledger field `_blocked: Set>`. +Membership is publicly readable and the blocked set is publicly enumerable, +and every circuit discloses the `account` it is called with. +As with Allowlist, this is the right posture for transparent or public-graph assets only. + +### Usage + +The following example lets the owner freeze accounts, and refuses transfers to a frozen recipient: + +```ts +// FreezableToken.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/security/Blocklist" + prefix Blocklist_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + initialOwner: Either, ContractAddress>, + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, +) { + Ownable_initialize(initialOwner); + FungibleToken_initialize(name, symbol, decimals); +} + +// Freezing and unfreezing are protected by the owner. +export circuit block(account: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Blocklist__block(account); +} + +export circuit unblock(account: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Blocklist__unblock(account); +} + +export circuit isBlocked(account: Bytes<32>): Boolean { + return Blocklist_isBlocked(account); +} + +// The implementing contract decides where the gate applies. +export circuit transfer(to: Either, ContractAddress>, value: Uint<128>): Boolean { + assert(to.is_left, "FreezableToken: recipient must be an account identifier"); + Blocklist_assertNotBlocked(to.left); + return FungibleToken_transfer(to, value); +} +``` + +See the [Blocklist API] for the full list of circuits. diff --git a/content/contracts-compact/shielded-access-control.mdx b/content/contracts-compact/shielded-access-control.mdx index e3a3d34c..578933c0 100644 --- a/content/contracts-compact/shielded-access-control.mdx +++ b/content/contracts-compact/shielded-access-control.mdx @@ -18,6 +18,11 @@ title: ShieldedAccessControl [getRoleAdmin]: api/shielded-access-control#ShieldedAccessControl-getRoleAdmin [computeAccountId]: api/shielded-access-control#ShieldedAccessControl-computeAccountId [DEFAULT_ADMIN_ROLE]: api/shielded-access-control#ShieldedAccessControl-DEFAULT_ADMIN_ROLE +[_operatorRoles]: api/shielded-access-control#ShieldedAccessControl-_operatorRoles +[_grantedRoleNullifiers]: api/shielded-access-control#ShieldedAccessControl-_grantedRoleNullifiers +[_roleCommitmentNullifiers]: api/shielded-access-control#ShieldedAccessControl-_roleCommitmentNullifiers +[wit_secretKey]: api/shielded-access-control#ShieldedAccessControl-wit_secretKey +[wit_getRoleCommitmentPath]: api/shielded-access-control#ShieldedAccessControl-wit_getRoleCommitmentPath This module provides a **privacy-preserving** role-based access control mechanism. Unlike [AccessControl], which stores role assignments in a public map, @@ -47,26 +52,44 @@ ShieldedAccessControl is built on three cryptographic primitives: When a role is granted, a commitment is inserted into a Merkle tree: ``` -roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain) +roleCommitment = SHA256(role || accountId || instanceSalt || commitmentDomain) ``` **Account identifiers** are privacy-preserving identity commitments derived from a secret key and a per-deployment instance salt. A single secret key can be used across multiple roles within the same contract: ``` -accountId = SHA256(secretKey, instanceSalt, accountIdDomain) +accountId = SHA256(secretKey || instanceSalt || accountIdDomain) ``` **Role nullifiers** are one-time burn tokens. When a role is revoked, its nullifier is inserted into a set, permanently invalidating the corresponding commitment: ``` -roleNullifier = SHA256(roleCommitment, nullifierDomain) +roleNullifier = SHA256(roleCommitment || nullifierDomain) ``` The `instanceSalt` is a per-deployment random value that prevents commitment collisions across contracts and ensures that the same secret key produces different identities in different deployments. +### Public ledger and private witnesses + +What is on the ledger: + +- [_operatorRoles]: the Merkle tree of role commitments. Inserts expose only the internal leaf hash, not the commitment value. +- [_grantedRoleNullifiers]: the nullifier of every `(role, accountId)` pairing ever granted. +- [_roleCommitmentNullifiers]: the nullifier of every pairing that has been revoked. +- `_adminRoles`, the sealed `_instanceSalt`, and `_isInitialized`. + +What each role holder keeps private, and supplies through witnesses at proving time: + +- The `secretKey`, via [wit_secretKey]. It is hashed with `_instanceSalt` inside the proof and never reaches the ledger. +- The Merkle path to their role commitment, via [wit_getRoleCommitmentPath]. + The consumer's witness implementation builds this path from the exported `_operatorRoles` state. + +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + ## Using `ShieldedAccessControl` Usage follows the same pattern as [AccessControl] which is to define role identifiers, @@ -103,7 +126,7 @@ export circuit mint(/* ... */): [] { ``` - The `instanceSalt` must be generated using a cryptographically secure random number generator + The `instanceSalt` must be nonzero and generated using a cryptographically secure random number generator (e.g. `crypto.getRandomValues()`) prior to deployment. Weak or predictable salts weaken privacy guarantees. @@ -132,6 +155,14 @@ Only accounts that can prove they hold the admin role may call [grantRole] and [ By default, all roles are administered by [DEFAULT_ADMIN_ROLE]. Custom admin relationships can be set using [_setRoleAdmin]. +Each `(role, accountId)` pairing has exactly one lifecycle: + +- **Grant**: the first grant inserts the role commitment into the Merkle tree and records the pairing's nullifier + in [_grantedRoleNullifiers]. Granting a pairing that is already active is a no-op, so a pairing never consumes + more than one tree slot. +- **Revoke**: revoking (or renouncing) inserts the nullifier into [_roleCommitmentNullifiers]. + A pairing that was never granted cannot be revoked; the call reverts with `role was never granted`. + There is one important difference from unshielded AccessControl: **revocation is permanent for a given identity**. When a role is revoked, its nullifier is burned, and the same `(role, accountId)` pairing can never be granted again. @@ -144,7 +175,8 @@ until all roles under the old key are no longer needed. [renounceRole] allows a role holder to voluntarily give up their own role. This is useful if a trusted device is compromised and the holder wants to immediately invalidate their credentials. -The caller must confirm their `accountId` to prevent accidental renunciation. +The caller must confirm their `accountId` to prevent accidental renunciation, +and the pairing must have been granted. Consumers with no need for self-revocation should not export this circuit. ### Proving Role Ownership @@ -168,12 +200,13 @@ ShieldedAccessControl provides strong privacy guarantees, but it's important to - The same nullifier across calls, allowing them to link all actions performed under the same role instance over time (pseudonymity, not anonymity) - Correlation between a grant and its revocation (the same nullifier appears in both transactions) - Which role is being granted or revoked when the admin-checked circuits ([grantRole], [revokeRole]) are used, since those disclose the role identifier in the same transaction -- The cumulative number of role grants across all roles; per-role counts are also inferable when the admin-checked circuits are used +- The number of distinct `(role, accountId)` pairings ever granted across all roles; per-role counts are also inferable when the admin-checked circuits are used **What observers CANNOT see:** - The identity of any role holder, so long as secret keys are kept private and generated using cryptographically secure random values - The value of role commitments stored in the Merkle tree (MerkleTree inserts hash the value internally; only the internal leaf hash appears as a public input, unlike Set operations where values are directly visible) -- Users can be retroactively deanonymized if their secret key is exposed + +Users can be retroactively deanonymized if their secret key is exposed. The internal circuits ([_grantRole], [_revokeRole]) do not disclose the role identifier, offering stronger privacy @@ -183,10 +216,10 @@ ShieldedAccessControl provides strong privacy guarantees, but it's important to ## Security Considerations -- The `_operatorRoles` Merkle tree has a **fixed capacity of 2^20 leaf slots** and slots cannot be reclaimed after revocation. Monitor slot consumption off-chain and plan capacity accordingly. -- A single secret key compromise exposes **all roles** held by that user within the contract instance. Users requiring compartmentalization between roles should use separate keys per role. -- Admins can revoke `(role, accountId)` pairings that were never granted (non-membership proofs are not available for the Merkle tree). Admin trust is a fundamental assumption. Off-chain validation should confirm a pairing was actually granted before submitting a revocation. -- The SHA256 hashing function is used throughout. A migration to a ZK-friendly hashing function is planned when an implementation becomes available. +- The `_operatorRoles` Merkle tree has a **fixed capacity of 2^20 leaf slots**. Each distinct `(role, accountId)` pairing consumes one slot, re-granting an active pairing is a no-op, and slots cannot be reclaimed after revocation. Once exhausted, the contract can never grant a new role. Monitor slot consumption off-chain (`firstFree()` on the exported tree in TypeScript, `isFull()` in a circuit) and plan capacity accordingly. +- A single secret key compromise exposes **all roles** held by that user within the contract instance. Users requiring compartmentalization between roles should use separate keys per role. On compromise, coordinate with admins to revoke every role under the compromised `accountId` and re-grant under a new identity. +- A pairing can only be revoked after it was granted, so the nullifier set holds at most one entry per granted pairing. Admin trust is still a fundamental assumption: an admin can revoke any granted pairing, and revocation is irreversible. +- The SHA256 hashing function is used throughout. For the full API reference, see the [ShieldedAccessControl API]. diff --git a/content/contracts-compact/utils.mdx b/content/contracts-compact/utils.mdx index 1437ed3e..a7589eaa 100644 --- a/content/contracts-compact/utils.mdx +++ b/content/contracts-compact/utils.mdx @@ -2,9 +2,88 @@ title: Utils --- +{/* links */} +[Utils API]: ./api/utils#Utils +[computeAccountId]: ./api/utils#Utils-computeAccountId +[zeroAccount]: ./api/utils#Utils-zeroAccount +[isTargetZero]: ./api/utils#Utils-isTargetZero +[selfAsRecipient]: ./api/utils#Utils-selfAsRecipient +[Ownable]: ./ownable +[AccessControl]: ./access-control + The Utils module provides miscellaneous circuits and common utilities for Compact contract development. +Most of them are pure circuits with no ledger state, so they can be called from a circuit, +from a constructor, or off-chain through the compiled contract. + +## Account identifiers + +The [Ownable] and [AccessControl] modules, and the token modules, identify a caller by a `Bytes<32>` account identifier +rather than by a public key. +The caller proves knowledge of a 32-byte secret key through a witness, and the module derives the identifier as: + +``` +accountId = H(secretKey) +``` + +where `H` is `persistentHash`. +[computeAccountId] is the shared derivation: it is a pure circuit, so the same function that the modules +evaluate inside a proof can be evaluated ahead of time to produce the identifier a contract expects as input, +for example the `initialOwner` of an Ownable contract, the `defaultAdmin` of an AccessControl contract, +or the recipient of a token mint. -### Usage +The witnesses that supply the secret key are authored by the implementing contract's TypeScript side. +The witnesses in the library repository are test-only and are not published with the package. + +### Deriving an identifier off-chain + +Any of the following produce the same identifier for the same key: + +* Call the pure circuit through the compiled contract: `pureCircuits.computeAccountId(secretKey)` + (exposed by any contract that re-exports it as a `pure circuit`). +* Compute it directly with `@midnight-ntwrk/compact-runtime`: + +```typescript +import { + CompactTypeBytes, + CompactTypeVector, + persistentHash, +} from '@midnight-ntwrk/compact-runtime'; + +const accountIdOf = (secretKey: Uint8Array): Uint8Array => + persistentHash(new CompactTypeVector(1, new CompactTypeBytes(32)), [secretKey]); +``` + +The secret key must be a 32-byte cryptographically secure random value (`crypto.getRandomValues()` or equivalent). + + + The secret key is the identity. Never log or persist it in plaintext, keep it in offline or enclave storage + where possible, and avoid evaluating the derivation in an untrusted runtime. A lost key cannot be recovered, + and an exposed key allows impersonation. + + +### Scope of the identifier + +The derivation is deliberately untagged and global: the same secret key yields the same identifier in every +module and every contract that derives one, so a user who wants to carry a single identity across contracts can. +Users who want unlinkable identities across contracts should use a different secret key per contract. +A module that needs a per-deployment, unlinkable identity should not use this circuit. + +The output is an identifier, not a secret. Because the construction carries no domain-separation tag, +it must not be reused to derive private values: a circuit that derives a private value from a secret +must use its own domain-separation tag. + +## Zero accounts + +[zeroAccount] returns the canonical zero `Either, ContractAddress>` (the left variant set to zero), +which the token modules use as the mint and burn sentinel and for cleared approvals. +[isTargetZero] checks whether the active side of such a value is zero. + +## Contract as recipient + +[selfAsRecipient] wraps the executing contract's own address as an `Either`, +the recipient shape expected by shielded send operations, for deposits into the contract and for receiving change. + +## Usage ```typescript pragma language_version >= {{compact_language_version}}; @@ -24,4 +103,12 @@ export circuit performActionWhenEqual( // Do something else } } + +// Re-export the derivation so the identifier can be computed off-chain +// through the compiled contract. +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} ``` + +See the [Utils API] for the full list of circuits. diff --git a/content/contracts-compact/utils/constants.js b/content/contracts-compact/utils/constants.js index 27c5d08b..fb6add47 100644 --- a/content/contracts-compact/utils/constants.js +++ b/content/contracts-compact/utils/constants.js @@ -1,2 +1,2 @@ -export const COMPACT_COMPILER_VERSION = "0.29.0"; -export const COMPACT_LANGUAGE_VERSION = "0.21.0"; +export const COMPACT_COMPILER_VERSION = "0.31.0"; +export const COMPACT_LANGUAGE_VERSION = "0.23.0"; diff --git a/src/navigation/midnight.json b/src/navigation/midnight.json index 793f569e..b24a8fe9 100644 --- a/src/navigation/midnight.json +++ b/src/navigation/midnight.json @@ -73,9 +73,29 @@ "type": "page", "name": "MultiToken", "url": "/contracts-compact/multi-token" + }, + { + "type": "page", + "name": "NativeShieldedToken", + "url": "/contracts-compact/native-shielded-token" + }, + { + "type": "page", + "name": "ConfidentialFungibleToken", + "url": "/contracts-compact/confidential-fungible-token" } ] }, + { + "type": "page", + "name": "Crypto", + "url": "/contracts-compact/crypto" + }, + { + "type": "page", + "name": "Multisig", + "url": "/contracts-compact/multisig" + }, { "type": "page", "name": "Utils", @@ -92,6 +112,16 @@ "name": "AccessControl API", "url": "/contracts-compact/api/access-control" }, + { + "type": "page", + "name": "ConfidentialFungibleToken API", + "url": "/contracts-compact/api/confidential-fungible-token" + }, + { + "type": "page", + "name": "Crypto API", + "url": "/contracts-compact/api/crypto" + }, { "type": "page", "name": "FungibleToken API", @@ -102,6 +132,16 @@ "name": "MultiToken API", "url": "/contracts-compact/api/multi-token" }, + { + "type": "page", + "name": "Multisig API", + "url": "/contracts-compact/api/multisig" + }, + { + "type": "page", + "name": "NativeShieldedToken API", + "url": "/contracts-compact/api/native-shielded-token" + }, { "type": "page", "name": "NonFungibleToken API",