|
| 1 | +# Token Shielded Pools |
| 2 | + |
| 3 | +From protocol version 14 a token can own a shielded pool: an Orchard pool that holds that token |
| 4 | +instead of credits. Holders move tokens between their identity balance and the pool with three |
| 5 | +token transitions inside a batch, and transfer inside the pool without revealing amounts or |
| 6 | +counterparties. This chapter describes the storage, the configuration flag, the transitions, the |
| 7 | +validation rules, the block end bookkeeping, the queries and the client builders. |
| 8 | + |
| 9 | +## Why one pool per token |
| 10 | + |
| 11 | +The Orchard construction Platform uses for credits has no asset base: a note carries a value |
| 12 | +but not an asset id, and the value balance the circuit proves is a single number. Mixing tokens |
| 13 | +in one pool would let a spend of token A create a note of token B. A token therefore gets its |
| 14 | +own pool, and each pool is a copy of the credit pool's layout rooted under the token. |
| 15 | + |
| 16 | +## Storage layout |
| 17 | + |
| 18 | +The credit shielded pool lives at `[ShieldedBalances(52)]/"M"`. Token pools live under the |
| 19 | +tokens tree: |
| 20 | + |
| 21 | +```text |
| 22 | +[Tokens(16)] |
| 23 | + [TOKEN_SHIELDED_POOLS_KEY(224)] BigSumTree |
| 24 | + [token_id] SumTree (the pool) |
| 25 | + NOTES[128] CommitmentTree, chunk power 11 (the note commitments and ciphertexts) |
| 26 | + NULLIFIERS[64] ProvableCountTree (spent nullifiers) |
| 27 | + ANCHORS_IN_POOL[192] anchor -> block height |
| 28 | + TOTAL_BALANCE[32] SumItem (tokens currently shielded) |
| 29 | + ANCHORS_BY_HEIGHT[96] block height -> anchor (for pruning) |
| 30 | +``` |
| 31 | + |
| 32 | +The five children use the same keys as the credit pool, so the drive primitives that insert |
| 33 | +notes and nullifiers, read balances and record anchors are shared: every credit pool method has |
| 34 | +a pool-agnostic form taking the pool path, and a token twin that supplies the token's path. The |
| 35 | +root of all token pools is a BigSumTree so the amount of every token that is shielded is one |
| 36 | +sum, which the token conservation check reads. |
| 37 | + |
| 38 | +The root tree is created by the version 14 upgrade transition (`transition_to_version_14`) on |
| 39 | +an existing chain and by `create_initial_state_structure` version 4 on a new one. A pool's five |
| 40 | +trees are created when a contract with the flag is inserted or updated. |
| 41 | + |
| 42 | +## Configuration |
| 43 | + |
| 44 | +`TokenConfiguration` gains a format version 1 whose only addition over version 0 is |
| 45 | +`hasShieldedPool: bool`. A version 0 configuration behaves as `hasShieldedPool: false`. |
| 46 | +The format version is admitted by |
| 47 | +`dpp.contract_versions.token_versions.token_configuration_format`: protocol versions 13 and |
| 48 | +below allow only version 0, protocol version 14 allows versions 0 and 1. Contract create and |
| 49 | +update reject a token configuration outside the bounds with `UnsupportedVersionError`, so a |
| 50 | +pre-14 network never stores the flag. |
| 51 | + |
| 52 | +The flag is immutable. A contract update that changes it is rejected with |
| 53 | +`DataContractTokenConfigurationUpdateError` for `hasShieldedPool`, because a pool that was |
| 54 | +enabled can hold notes that would become unspendable, and a pool that is enabled late would |
| 55 | +need a tree created under an existing token. |
| 56 | + |
| 57 | +## Transitions |
| 58 | + |
| 59 | +The three operations are `TokenTransition` variants inside a `Batch` transition, like every |
| 60 | +other token operation. The identity signs the batch and pays the fee in credits. Tokens cannot |
| 61 | +pay fees, so unlike the credit pool nothing is carved from the bundle's value balance. |
| 62 | + |
| 63 | +| Transition | Flags | Value balance | Extra sighash data | Effect | |
| 64 | +|---|---|---|---|---| |
| 65 | +| `TokenShield` | outputs only | `-amount` | none | `amount` leaves the owner's balance and enters the pool as new notes. | |
| 66 | +| `TokenUnshield` | spends and outputs | `+amount` | `token_id, owner_id, recipient_id, amount` | Notes are spent; `amount` is credited to `recipient_id`; change comes back as new notes. | |
| 67 | +| `TokenShieldedTransfer` | spends and outputs | `0` | `token_id, owner_id` | Notes are spent and recreated; the pool balance is unchanged. | |
| 68 | + |
| 69 | +Each transition carries the Orchard bundle (`actions`, `anchor`, `proof`, `binding_signature`) |
| 70 | +next to the token base transition (`token_id`, contract id, contract position, identity |
| 71 | +contract nonce). The extra sighash data is bound into the Orchard sighash by the client and |
| 72 | +recomputed by consensus from the transition's own fields, so a bundle proven for one token, |
| 73 | +owner, recipient or amount cannot be replayed with another. The layouts are in |
| 74 | +`dpp::shielded::sighash` (`token_unshield_extra_sighash_data`, |
| 75 | +`token_shielded_transfer_extra_sighash_data`). |
| 76 | + |
| 77 | +The shield bundle has no spends, so its anchor is not checked against the pool; the client |
| 78 | +builds it against the empty tree. Spending bundles must name an anchor the pool has recorded. |
| 79 | + |
| 80 | +## Validation |
| 81 | + |
| 82 | +Structure validation checks the amount bounds, the action count against |
| 83 | +`SystemLimits::max_shielded_transition_actions`, the encrypted note sizes, a non-empty proof and |
| 84 | +a non-zero anchor. |
| 85 | + |
| 86 | +State validation runs in this order, and the first failure is returned: |
| 87 | + |
| 88 | +1. The token base transition (contract exists, position valid, nonce). |
| 89 | +2. `hasShieldedPool` on the token's configuration, else `TokenShieldedPoolNotEnabledError`. |
| 90 | +3. Shield: the owner holds `amount`, the owner's account is not frozen, the token is not |
| 91 | + paused. Unshield: the token is not paused, the recipient identity exists, the recipient's |
| 92 | + account is not frozen unless the token allows transfers to frozen balances. Shielded |
| 93 | + transfer: the token is not paused. |
| 94 | +4. Spending bundles: the anchor is recorded in the pool (`InvalidAnchorError`), no nullifier |
| 95 | + repeats within the bundle or is already spent (`NullifierAlreadySpentError`), and for an |
| 96 | + unshield the pool holds `amount`. |
| 97 | +5. Proof verification. The fee for it, `compute_shielded_verification_fee(actions)`, is added |
| 98 | + as a precalculated operation before the Halo 2 proof and binding signature are checked, so a |
| 99 | + failed proof is a paid failure: the identity is charged, its nonce advances, and nothing |
| 100 | + else moves. |
| 101 | + |
| 102 | +Batch state validation does not run in `CheckTx`. The mempool admission path |
| 103 | +(`CheckTxProofVerifier`) verifies the proofs of batch token transitions keyed by the identity |
| 104 | +contract nonce, so a proof is verified once per nonce before the block and not again for the |
| 105 | +same submission. |
| 106 | + |
| 107 | +The transitions are gated on the protocol version: below 14 `validate_is_allowed` rejects a |
| 108 | +batch carrying any of them with `StateTransitionNotActiveError`. |
| 109 | + |
| 110 | +## Execution and conservation |
| 111 | + |
| 112 | +The drive operations are composites of the credit pool primitives re-rooted under the token: |
| 113 | + |
| 114 | +- shield: remove `amount` from the owner's token balance, append the notes, add `amount` to |
| 115 | + the pool's `TOTAL_BALANCE`; |
| 116 | +- unshield: insert the nullifiers, append the notes, subtract `amount` from the pool balance, |
| 117 | + add `amount` to the recipient's token balance; |
| 118 | +- shielded transfer: insert the nullifiers, append the notes. |
| 119 | + |
| 120 | +The total supply of a token never changes. `calculate_total_tokens_balance` version 1 reads |
| 121 | +the token pools BigSumTree and the block end conservation check requires |
| 122 | +`identity balances + pool balances == total supply`. |
| 123 | + |
| 124 | +## Block end |
| 125 | + |
| 126 | +Every successful or paid token pool transition records its pool in |
| 127 | +`StateTransitionsProcessingResult::token_shielded_pools_touched`. At block end |
| 128 | +`record_token_shielded_pool_anchors` (enabled by `DRIVE_ABCI_METHOD_VERSIONS_V10`) records |
| 129 | +each touched pool's current anchor if the commitment tree changed and prunes that pool's |
| 130 | +anchors older than `shielded_anchor_retention_blocks`, always keeping the newest one. Pruning |
| 131 | +is driven by touches rather than by an interval because there can be many pools; an idle pool |
| 132 | +keeps a valid anchor to spend against. |
| 133 | + |
| 134 | +## Queries |
| 135 | + |
| 136 | +The six shielded pool queries (`getShieldedPoolState`, `getShieldedNotesCount`, |
| 137 | +`getShieldedAnchors`, `getMostRecentShieldedAnchor`, `getShieldedEncryptedNotes`, |
| 138 | +`getShieldedNullifiers`) take an optional `token_id`. Without it they target the credit pool; |
| 139 | +with a 32-byte token id they target that token's pool and answer with the same response shape. |
| 140 | +A token id is rejected with `InvalidArgument` before protocol version 14 or when it is not 32 |
| 141 | +bytes. The proof verifier routes on the same field to the token twins of the verify functions |
| 142 | +(`verify_token_shielded_pool_state` and the rest), and the Rust SDK exposes |
| 143 | +`TokenShieldedPoolQuery`, `TokenShieldedEncryptedNotesQuery` and `TokenShieldedNullifiersQuery`. |
| 144 | + |
| 145 | +A token pool transition proves its execution like the token transfer it resembles: a shield |
| 146 | +proves the owner's balance, an unshield proves the recipient's balance, and a shielded transfer |
| 147 | +proves the spent nullifiers in the token's pool. |
| 148 | + |
| 149 | +## Client builders |
| 150 | + |
| 151 | +`dpp::shielded::builder` provides `build_token_shield_transition`, |
| 152 | +`build_token_unshield_transition` and `build_token_shielded_transfer_transition`. They prove |
| 153 | +the bundle, bind the extra sighash data, and call the batch constructors |
| 154 | +(`new_token_shield_transition` and siblings) which sign with the identity key. The wasm |
| 155 | +bindings expose `TokenShieldTransition`, `TokenUnshieldTransition` and |
| 156 | +`TokenShieldedTransferTransition`, and `TokenConfiguration` accepts `hasShieldedPool` and |
| 157 | +reports `formatVersion`. |
| 158 | + |
| 159 | +## Fees |
| 160 | + |
| 161 | +See [Shielded Transaction Fees](../fees/shielded-fees.md#token-shielded-pool-fees). In short: |
| 162 | +the identity pays the metered cost of the writes plus the proof verification fee, exactly like |
| 163 | +`ShieldFromIdentity`, for all three transitions. |
0 commit comments