Skip to content

Commit 3a0edfd

Browse files
decofe0xKitsune
andauthored
docs: restore privacy specs to docs/specs/privacy/ (#375)
Co-authored-by: 0xKitsune <[email protected]>
1 parent c32d27d commit 3a0edfd

5 files changed

Lines changed: 3286 additions & 0 deletions

File tree

docs/specs/privacy/execution.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Privacy Zone Execution Environment (Draft)
2+
3+
This document specifies how the EVM execution environment of a privacy zone differs from a standard Tempo zone. These changes are enforced at the execution level (inside the zone's TIP-20 precompile, gas accounting, and EVM configuration), not at the RPC layer. They apply to all code paths — user transactions, sequencer system calls, `eth_call` simulations, and prover re-execution.
4+
5+
For RPC-level access controls (authentication, method filtering, event scoping), see the [Zone RPC Specification](./rpc).
6+
7+
A reference Solidity specification of all TIP-20 modifications is available at [`PrivateZoneToken.sol`](/specs/src/zone/PrivateZoneToken.sol).
8+
9+
## TIP-20 modifications
10+
11+
Privacy zones modify the zone token's TIP-20 precompile in four areas: balance privacy, allowance privacy, fixed gas accounting, and system mint/burn permissions.
12+
13+
### Balance privacy: `balanceOf` access control
14+
15+
On a standard zone (and on Tempo), `balanceOf(address account)` is a public view function — any caller can read any account's balance. On a privacy zone, the function enforces caller restrictions:
16+
17+
- If `msg.sender == account`, the call succeeds and returns the balance.
18+
- If `msg.sender` is the sequencer (as read from `ZoneConfig.sequencer()`), the call succeeds.
19+
- Otherwise, the call reverts with `Unauthorized()`.
20+
21+
This means:
22+
23+
- **User transactions**: A contract calling `balanceOf(someOtherAddress)` will revert. Only self-queries succeed.
24+
- **`eth_call` simulations**: The RPC server sets `from` to the authenticated account, so `balanceOf` only works for the caller's own address. See [RPC spec](./rpc).
25+
- **Sequencer and system calls**: The sequencer retains full read access, which is required for block production, deposit processing, and fee accounting.
26+
27+
**Rationale**: On a public chain, anyone can read any balance. On a privacy zone, balances are private. Enforcing this at the EVM level (not just at the RPC layer) ensures that even on-chain composition cannot leak balances — a contract deployed on the zone cannot be used to read and re-emit another account's balance.
28+
29+
### Allowance privacy: `allowance` access control
30+
31+
The `allowance(address owner, address spender)` function is similarly restricted:
32+
33+
- If `msg.sender == owner` or `msg.sender == spender`, the call succeeds and returns the allowance.
34+
- If `msg.sender` is the sequencer, the call succeeds.
35+
- Otherwise, the call reverts with `Unauthorized()`.
36+
37+
**Rationale**: A non-zero allowance reveals that `owner` has interacted with `spender` — a relationship that should be private on a privacy zone. Restricting reads to the two parties involved preserves standard ERC-20 approval flows (both the owner and the spender can check the allowance) without leaking relationship information to third parties.
38+
39+
**Unchanged views**: `totalSupply()`, `name()`, `symbol()`, `decimals()`, and other non-per-account view functions remain unrestricted.
40+
41+
### Fixed gas: constant transfer cost
42+
43+
All TIP-20 transfer operations on a privacy zone charge a fixed gas cost of **100,000 gas**, regardless of execution-dependent factors:
44+
45+
| Function | Gas cost |
46+
|----------|----------|
47+
| `transfer(to, amount)` | 100,000 |
48+
| `transferFrom(from, to, amount)` | 100,000 |
49+
| `transferWithMemo(to, amount, memo)` | 100,000 |
50+
| `transferFromWithMemo(from, to, amount, memo)` | 100,000 |
51+
| `approve(spender, amount)` | 100,000 |
52+
53+
On a standard EVM chain, gas cost varies depending on whether a transfer writes to a previously empty storage slot (zero → non-zero costs 20,000 gas more than non-zero → non-zero). This difference reveals whether the recipient has previously received tokens — a binary signal about account existence.
54+
55+
By fixing the gas cost:
56+
57+
- All transfer receipts have identical `gasUsed` for the TIP-20 portion, removing the side channel.
58+
- Observers (including the sender, who sees their own receipt) cannot distinguish transfers to new vs. existing accounts.
59+
- The fixed cost of 100,000 gas matches the zone's `FIXED_DEPOSIT_GAS` constant, providing a consistent gas unit across deposits and transfers.
60+
61+
**Implementation**: The zone's TIP-20 precompile always charges exactly 100,000 gas for any transfer-family call, regardless of the actual storage operations required. If the transaction provides less than 100,000 gas to the precompile call, it reverts with out-of-gas. Excess gas beyond 100,000 is returned to the caller as usual.
62+
63+
**Unchanged operations**: System functions (`systemTransferFrom`, `transferFeePreTx`, `transferFeePostTx`) retain their standard gas costs. These are restricted to precompile-only callers where the gas side channel is not exploitable.
64+
65+
### System mint and burn permissions
66+
67+
On Tempo, `mint()` and `burn()` on a TIP-20 require the caller to hold `ISSUER_ROLE`. On a zone, the token is a bridged representation — tokens are minted when deposits arrive from Tempo and burned when withdrawals are requested. `ISSUER_ROLE` is not used on zones. The sole mint/burn authorities are the zone system contracts:
68+
69+
| Operation | Standard TIP-20 (Tempo) | Zone access |
70+
|-----------|------------------------|-------------|
71+
| `mint(to, amount)` | `ISSUER_ROLE` only | ZoneInbox (`0x1c...0001`) only |
72+
| `burn(from, amount)` | `ISSUER_ROLE` only | ZoneOutbox (`0x1c...0002`) only |
73+
74+
Authorization is **operation-specific**: ZoneInbox access applies to `mint` only, and ZoneOutbox access applies to `burn` only. Implementations MUST NOT use a shared "inbox-or-outbox" check for both operations.
75+
76+
**ZoneInbox mints** during deposit processing in `advanceTempo()`:
77+
78+
- Regular deposit: `mint(deposit.to, deposit.amount)` — credits the recipient on the zone.
79+
- Encrypted deposit (decryption succeeded): `mint(decrypted.to, deposit.amount)` — credits the decrypted recipient on the zone.
80+
- Encrypted deposit (decryption failed): `mint(deposit.sender, deposit.amount)` — credits the depositor's address on the zone (same address as their L1 account). The L1 funds remain escrowed in the portal.
81+
82+
**ZoneOutbox burns** during withdrawal requests in `requestWithdrawal()`:
83+
84+
- The user approves the ZoneOutbox to spend `amount + fee`.
85+
- ZoneOutbox calls `transferFrom(user, self, amount + fee)`, then `burn(self, amount + fee)`.
86+
- The burned tokens are released on Tempo when the sequencer processes the withdrawal.
87+
88+
**Gas costs**: `mint` and `burn` retain standard variable gas costs (not the fixed 100,000). These functions are only called by system contracts during sequencer operations, so there is no user-exploitable gas side channel.
89+
90+
## EVM restrictions
91+
92+
### Contract creation disabled
93+
94+
Privacy zones disable the `CREATE` and `CREATE2` opcodes. The zone runs a fixed set of predeploys (system contracts and the zone token); user-deployed contracts are not supported. Any transaction or call that attempts contract creation reverts.
95+
96+
**Rationale**: Arbitrary contract deployment would allow users to deploy contracts that circumvent execution-level privacy protections — for example, a contract that calls `balanceOf` on behalf of a third party and emits the result as an event.
97+
98+
## Interaction with RPC
99+
100+
These execution-level changes are the first line of defense. The [RPC specification](./rpc) adds a second layer of access control (authentication, method restrictions, event filtering). Both layers are required:
101+
102+
- **Execution alone is insufficient**: Without RPC restrictions, a caller could use `eth_getStorageAt` to read TIP-20 balance mapping slots directly, bypassing the `balanceOf` access control entirely.
103+
- **RPC alone is insufficient**: Without execution-level changes, a caller could deploy or call a contract via `eth_call` that reads another account's balance and returns it, bypassing RPC-level filtering.
104+
105+
The two layers are complementary: execution-level changes protect against in-EVM information leaks, and RPC-level changes protect against raw state inspection.

0 commit comments

Comments
 (0)