From bccf705a427c51fce916c97b3a31b67a5de55f54 Mon Sep 17 00:00:00 2001 From: Tiancheng Xie Date: Tue, 24 Mar 2026 09:30:54 +0800 Subject: [PATCH] BAP-674: Privacy-Preserving Token Transfer Protocol Add BAP-674 specifying a standard interface for privacy-preserving token transfers on BNB Chain using a UTXO model with zk-SNARK (Groth16) proofs. Key features: - Shielded deposit/transfer/freeze/withdraw operations - Poseidon2 commitments and nullifiers over BN254 - Amortized Merkle tree with buffer array and frontier-based insertion - ElGamal viewer encryption and freezer key compliance - Relayer model with per-operation binding input construction - Complete event specification (21 events) - Poseidon2 round constants and test vectors (Appendix A) Co-Authored-By: Claude Opus 4.6 (1M context) --- BAPs/BAP-674.md | 773 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 773 insertions(+) create mode 100644 BAPs/BAP-674.md diff --git a/BAPs/BAP-674.md b/BAPs/BAP-674.md new file mode 100644 index 00000000..a62e67b2 --- /dev/null +++ b/BAPs/BAP-674.md @@ -0,0 +1,773 @@ +``` +BAP: 674 +Title: Privacy-Preserving Token Transfer Protocol +Status: Draft +Type: Application +Created: 2026-02-09 +``` + +# BAP-674: Privacy-Preserving Token Transfer Protocol + +- [BAP-674: Privacy-Preserving Token Transfer Protocol](#bap-674-privacy-preserving-token-transfer-protocol) + - [1. Summary](#1-summary) + - [2. Abstract](#2-abstract) + - [3. Motivation](#3-motivation) + - [4. Specification](#4-specification) + - [5. Rationale](#5-rationale) + - [6. Backwards Compatibility](#6-backwards-compatibility) + - [7. Security Considerations](#7-security-considerations) + - [8. Reference Implementations](#8-reference-implementations) + - [9. License](#9-license) + - [Appendix A: Poseidon2 Round Constants](#appendix-a-poseidon2-round-constants) + +--- + +## 1. Summary + +This proposal establishes a standard interface for privacy-preserving token transfers on BNB Chain, enabling deposit, transfer, and withdraw of BEP-20 tokens (and native BNB) with on-chain confidentiality using a UTXO-based model. + +--- + +## 2. Abstract + +The BAP defines an application-layer privacy protocol for BNB Smart Chain where users shield tokens into a pool, conduct private transfers via zk-SNARK proofs, and unshield to any address. The on-chain state contains only cryptographic commitments, nullifiers, and Merkle roots — no amounts, senders, or recipients are revealed during transfers. The protocol supports multiple token pools, a compliance layer with viewer and freezer keys, and relayer-based submission for sender privacy. + +--- + +## 3. Motivation + +### 3.1 The Privacy Gap on BNB Chain + +All transactions on BNB Smart Chain are fully transparent. This creates three primary issues: + +1. **Financial surveillance**: Users' transaction history, balances, and counterparties are publicly linkable. +2. **Front-running**: Visible pending transactions enable MEV extraction. +3. **Business confidentiality**: Enterprises cannot use public blockchains for sensitive payments without exposing financial data. + +### 3.2 Why a Standard? + +Without standardization, privacy protocol deployments become siloed — each defining its own contract interface, event format, and key derivation. A BAP-level standard enables four capabilities: + +- Wallet interoperability +- Indexer compatibility +- Relayer market development +- SDK portability across compliant deployments + +### 3.3 Regulatory Compatibility + +The protocol includes built-in compliance mechanisms: each pool is configured with a viewer key (for auditing) and a freezer key (for asset freezing), enabling deployment in regulated environments where selective disclosure and law enforcement cooperation are required. + +--- + +## 4. Specification + +The keywords "MUST", "MUST NOT", "SHOULD", "MAY" follow RFC 2119 interpretation. + +### 4.1 Overview + +The protocol operates as a shielded UTXO pool on an EVM-compatible chain: + +| Operation | Inputs | Outputs | Proof Required | Token Flow | +|-----------|--------|---------|----------------|------------| +| **Deposit** | Public amount | 1 commitment | No | User → Contract | +| **Transfer** | 1–3 nullifiers | 1–3 commitments | Groth16 | None | +| **Freeze** | 1–3 nullifiers | 1–3 commitments | Groth16 | None | +| **Withdraw** | 1 nullifier | 0–1 commitment | Groth16 | Contract → Recipient | + +### 4.2 Cryptographic Primitives + +#### 4.2.1 Finite Field + +All field arithmetic uses the BN254 scalar field: + +``` +p = 21888242871839275222246405745257275088548364400416034343698204186575808495617 +``` + +All commitments, nullifiers, Merkle nodes, and pool IDs must be elements of F_p. + +#### 4.2.2 Poseidon2 Hash + +The protocol uses Poseidon2 with sponge construction over BN254 with these parameters: + +| Parameter | Value | +|-----------|-------| +| State width (t) | 3 | +| Rate | 2 | +| Capacity | 1 | +| Full rounds | 8 (4 first + 4 last) | +| Partial rounds | 56 | +| S-box | x^5 | + +The hash function is defined as `Poseidon2.hashDomain(a, b, domain) → F_p`, where state is initialized as `[a, b, domain]`, a single permutation (64 rounds) is applied, and the output is `state[0]`. + +**Domain Tags** (for domain separation): + +| Domain | Tag (hex) | Usage | +|--------|-----------|-------| +| Record | `0x5245434F52440000` | Commitment computation | +| Nullifier | `0x4E554C4C49464945` | Nullifier derivation | +| Merkle | `0x4D45524B4C450000` | Merkle tree nodes | +| Policy | `0x504F4C4943590000` | Pool ID derivation | +| Array | `0x4152524159000000` | Commitment buffer hash chain | +| KeyDerivation | `0x4B45594445520000` | Nullifier key derivation (DH) | + +#### 4.2.3 BabyJubjub Curve + +User keys are points on the BabyJubjub twisted Edwards curve: + +``` +-x² + y² = 1 + d·x²·y² +``` + +| Parameter | Value | +|-----------|-------| +| d | `12181644023421730124874158521699555681764249180949974110617291017600649128846` | +| Subgroup order (l) | `2736030358979909402780800718157159386076813972158567259200215660948447373041` | +| Cofactor | 8 | + +A public key is a point `(pk.x, pk.y)` on the prime-order subgroup. The secret key `sk` is a scalar in `[1, l)`. + +#### 4.2.4 Key Derivation + +From a user seed (UTF-8 string, minimum 16 characters): + +``` +ikm = UTF8(seed) +sk_bytes = HKDF-SHA256(ikm, salt=None, info="OCash.KeyGen", len=32) +sk = sk_bytes mod l +pk = sk · G (BabyJubjub base point multiplication) +``` + +For derived keys with a nonce: + +``` +sk_bytes = HKDF-SHA256(ikm, salt=None, info="OCash.KeyGen:" || nonce, len=32) +``` + +#### 4.2.5 Memo Encryption + +Encrypted memos allow recipients to decode UTXO details. The encryption scheme: + +1. Generate ephemeral BabyJubjub key pair `(eph_sk, eph_pk)` +2. Compute shared secret via ECDH: `shared = eph_sk · recipient_pk` +3. Derive symmetric key: x-coordinate of `shared`, encoded as 32 bytes (big-endian) +4. Derive nonce: `keccak256(compress(eph_pk) || compress(recipient_pk))[0:24]` +5. Encrypt with NaCl SecretBox (XSalsa20-Poly1305): `ciphertext = secretbox(plaintext, nonce, key)` +6. Output: `compress(eph_pk) || ciphertext` + +Point compression uses 32 bytes, little-endian y-coordinate, with the sign of x in the MSB of byte 31. + +#### 4.2.6 Zero-Knowledge Proofs + +All proofs use the Groth16 proof system over the BN254 pairing curve. A proof consists of 8 field elements `[a₀, a₁, b₀, b₁, b₂, b₃, c₀, c₁]` representing points on BN254 G1 and G2. + +### 4.3 UTXO Record Model + +#### 4.3.1 Commitment + +A UTXO is stored on-chain as a Poseidon2 commitment computed as a sequential hash chain: + +``` +h₀ = Poseidon2.hashDomain(pk.x, pk.y, Record) +h₁ = Poseidon2.hashDomain(h₀, blinding, Record) +h₂ = Poseidon2.hashDomain(h₁, poolId, Record) +commitment = Poseidon2.hashDomain(h₂, amount, Record) +``` + +| Field | Type | Description | +|-------|------|-------------| +| `pk.x`, `pk.y` | F_p | Owner's BabyJubjub public key | +| `blinding` | F_p | Random blinding factor (CSPRNG) | +| `poolId` | F_p | Pool identifier | +| `amount` | uint128 | Token amount in base units | + +The commitment hides all inputs — only the holder of `sk` and knowledge of `blinding` can reconstruct it. + +#### 4.3.2 Deposit Commitment + +For deposits (no proof required), the contract computes the commitment from public inputs. The nonce replaces the blinding factor: + +``` +h₀ = Poseidon2.hashDomain(pk.x, pk.y, Record) +h₁ = Poseidon2.hashDomain(h₀, nonce, Record) +h₂ = Poseidon2.hashDomain(h₁, poolId, Record) +commitment = Poseidon2.hashDomain(h₂, amount, Record) +``` + +#### 4.3.3 Nullifier + +When a UTXO is spent, a deterministic nullifier is published. The nullifier derivation depends on the pool's freezer public key. + +**Step 1 — Derive nullifier key:** + +``` +if freezerPk == (0, 1): // default (no freezer) + nullifierKey = sk +else: + shared = sk · freezerPk // BabyJubjub scalar multiplication (ECDH) + nullifierKey = Poseidon2.hashDomain(shared.x, shared.y, KeyDerivation) +``` + +**Step 2 — Compute nullifier:** + +``` +nullifier = Poseidon2.hashDomain(nullifierKey, commitment, Nullifier) +``` + +When `freezerPk` is the identity point `(0, 1)`, the nullifier key reduces to the secret key directly. When a non-default freezer is configured, the nullifier key is derived via Diffie-Hellman between the user's secret key and the freezer's public key, ensuring that the freezer authority can independently compute the same nullifier key (using its own secret key and the user's public key) for freeze operations. + +The same UTXO always produces the same nullifier (preventing double-spend), but the link from commitment to nullifier is hidden by the nullifier key. + +#### 4.3.4 Amortized Merkle Tree + +All commitments are managed by an **amortized Merkle tree** consisting of two structures: + +**Buffer Array (size 32):** + +New commitments are first inserted into a fixed-size buffer array of 32 elements. Each insertion updates a hash chain: + +``` +if arrayIndex == 0: + arrayHash = Poseidon2.hashDomain(0, commitment, Array) +else: + arrayHash = Poseidon2.hashDomain(prevArrayHash, commitment, Array) +``` + +The contract maintains up to 2048 array hash slots (`arrayHashes[0..2047]`), providing a rolling hash chain of recent commitments. + +**Frontier Merkle Tree (depth 32):** + +When the buffer array fills (32 elements), it is merged into a permanent Poseidon2 Merkle tree: + +1. The 32 buffer elements are built into a subtree of depth 5 +2. The subtree root is merged into the main tree using a frontier-based insertion (depth 5 through 31) +3. A new Merkle root is stored on-chain + +The tree uses `Poseidon2.hashDomain(left, right, Merkle)` for internal nodes. Zero-tree values (precomputed hashes of empty subtrees at each level) fill unpopulated branches. + +**Dual-Root Verification:** + +Proofs reference both a historical Merkle root and a historical array hash. A commitment's membership is proven either via a standard Merkle path (for commitments already merged into the tree) or via the array hash chain (for commitments still in the buffer). The public inputs for transfer, freeze, and withdraw include both `merkleRoot` and `arrayHash`. + +**State:** + +| State Variable | Description | +|---|---| +| `array[32]` | Current buffer of uncommitted commitments | +| `arrayHashes[2048]` | Rolling hash chain of buffer insertions | +| `frontier[27]` | Frontier nodes for levels 5–31 | +| `merkleRoots[mapping]` | Historical Merkle roots (indexed by batch) | +| `totalElements` | Total commitments inserted | + +Historical Merkle roots and array hashes are stored on-chain. Proofs MUST reference a valid (non-zero) historical root and array hash. + +### 4.4 Pool Model + +Each deployment supports multiple pools. A pool is defined by a token address and a pair of compliance keys (viewer and freezer). + +```solidity +struct PoolInfo { + address token; // BEP-20 address (or 0xEeee...eE for native) + uint16 depositFeeBPS; // Protocol fee on deposit (basis points, < 10000) + uint16 withdrawFeeBPS; // Protocol fee on withdrawal + uint128 accumulatedFee; // Unclaimed protocol fees + uint256[2] viewerPK; // Viewer public key (BabyJubjub) + uint256[2] freezerPK; // Freezer public key (BabyJubjub) + uint128 transferMaxAmount; // Per-transfer amount cap + uint128 withdrawMaxAmount; // Per-withdraw amount cap +} +``` + +The native token address MUST be `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. + +Pool ID is deterministically derived from the token address and compliance keys: + +``` +id = Poseidon2.hashDomain(uint160(token), viewerPK.x, Policy) +id = Poseidon2.hashDomain(id, viewerPK.y, Policy) +id = Poseidon2.hashDomain(id, freezerPK.x, Policy) +id = Poseidon2.hashDomain(id, freezerPK.y, Policy) +``` + +Fee parameters and amount limits are NOT part of the pool ID derivation — they can be updated without changing the pool identity. + +### 4.5 Core Operations + +#### 4.5.1 Deposit + +Converts public tokens into a shielded UTXO. The user provides their public key and a nonce; the contract computes the commitment on-chain and holds the tokens. + +The commitment is **queued** in a `pendingDepositsQueue` rather than inserted directly into the Merkle tree. A relayer later calls `processPendingDeposits(maxBatchSize, relayer)` to insert queued commitments into the amortized Merkle tree structure (buffer array → tree). The relayer earns a per-deposit relay fee (set globally via `depositRelayerFee`) for processing the queue. + +For native token pools, the full amount (deposit amount + protocol fee + relayer fee) is sent as `msg.value`. For BEP-20 pools, the deposit amount + protocol fee is transferred via `safeTransferFrom`, and the relayer fee is sent as `msg.value`. + +#### 4.5.2 Transfer + +Moves value between shielded UTXOs. Consumes 1–3 inputs and creates 1–3 outputs. The Groth16 proof (29 public inputs) attests that: + +- Each input exists in the Merkle tree or buffer array and the prover knows its secret key +- Nullifiers are correctly derived (including freezer key derivation when applicable) +- Output commitments are well-formed +- Sum of inputs = sum of outputs + relayerFee +- The sum of all non-change output amounts plus the relayer fee MUST NOT exceed `transferMaxAmount`. A non-change output is defined as an output whose owner public key differs from the first input's owner public key (i.e., it is not sent back to the sender). Dummy outputs are excluded from this sum. Formally: `Σ(non-change output amounts) + relayerFee ≤ transferMaxAmount` +- All inputs and outputs MUST NOT be frozen (`isFrozen == 0`) +- Viewer data is correctly encrypted for the pool's viewer key + +**Public inputs layout (29 elements):** + +| Index | Input | +|-------|-------| +| 0 | poolId | +| 1 | merkleRoot | +| 2 | arrayHash | +| 3–5 | inputNullifiers[0..2] | +| 6–8 | outputs[0..2] | +| 9 | relayerFee | +| 10 | transferMaxAmount | +| 11–27 | viewerData[0..16] | +| 28 | bindingInput = `keccak256(abi.encode(relayer, extraData)) % p` | + +Contract-side validation: + +- `inputNullifiers[0]` MUST be non-zero; remaining nullifiers MAY be zero (unused) +- `outputs[0]` MUST be non-zero; remaining outputs MAY be zero +- Each non-zero nullifier MUST NOT have been published before +- Each non-zero output MUST NOT be a duplicate commitment + +#### 4.5.3 Freeze + +Allows the freezer authority to **toggle the frozen state** of UTXOs — both freezing and unfreezing are performed through the same `freeze()` function. The Groth16 proof (27 public inputs) attests that: + +- The prover knows the freezer secret key (not the UTXO owner's key) +- For each non-dummy input-output pair: `input.isFrozen + output.isFrozen == 1` (exactly one is frozen) + - If `input.isFrozen == 0` and `output.isFrozen == 1` → **freeze** + - If `input.isFrozen == 1` and `output.isFrozen == 0` → **unfreeze** +- For dummy pairs: both `isFrozen` flags MUST be 0 +- Each input-output pair preserves: amount, asset ID, and owner public key +- No relayer fee (fee is always 0) +- No balance check is needed since amounts are unchanged +- Viewer data is correctly encrypted for the pool's viewer key + +Frozen UTXOs cannot be spent via `transfer()` or withdrawn via `withdraw()` — both circuits enforce `isFrozen == 0` for all inputs and outputs. + +**Public inputs layout (27 elements):** + +| Index | Input | +|-------|-------| +| 0 | poolId | +| 1 | merkleRoot | +| 2 | arrayHash | +| 3–5 | inputNullifiers[0..2] | +| 6–8 | outputs[0..2] | +| 9–25 | viewerData[0..16] | +| 26 | bindingInput = `keccak256(abi.encode(extraData)) % p` | + +#### 4.5.4 Withdraw + +Converts a shielded UTXO back to public tokens. Consumes 1 input and optionally creates 1 change output. + +**Public inputs layout (14 elements):** + +| Index | Input | +|-------|-------| +| 0 | poolId | +| 1 | merkleRoot | +| 2 | arrayHash | +| 3 | inputNullifier | +| 4 | output (change UTXO, or 0) | +| 5 | bindingInput = `keccak256(abi.encode(recipient, amount, relayer, relayerFee, gasDropValue, extraData)) % p` | +| 6 | amountWithFee = amount + protocolFee + relayerFee | +| 7–13 | viewerData[0..6] | + +The contract verifies the proof, publishes the nullifier, releases tokens to the recipient, accumulates fees, and optionally sends `gasDropValue` ETH to the recipient (BEP-20 pools only). + +`amount + relayerFee` MUST NOT exceed `pool.withdrawMaxAmount`. + +### 4.6 Relayer Model + +To preserve sender privacy, transfers and withdrawals are submitted through a relayer that broadcasts the transaction and pays gas on behalf of the user. + +The `bindingInput` construction differs by operation, preventing the relayer from altering transaction parameters: + +| Operation | bindingInput formula | relayerFee handling | +|---|---|---| +| **Transfer** | `keccak256(abi.encode(relayer, extraData)) % p` | Separate public input (index 9) | +| **Freeze** | `keccak256(abi.encode(extraData)) % p` | No relayer, no fee | +| **Withdraw** | `keccak256(abi.encode(recipient, amount, relayer, relayerFee, gasDropValue, extraData)) % p` | Packed into bindingInput | + +Relayer fees are accumulated in the contract and claimed via `claimRelayerFees(token)`. + +Transfer and withdraw support self-relay mode: if `relayer == address(0)`, then `relayerFee` MUST be 0. Freeze has no relayer parameter. Relayers are not a mandatory protocol component — users MAY submit transactions directly. + +### 4.7 Compliance: Viewer and Freezer + +Each pool is configured with two BabyJubjub key pairs: + +**Viewer**: The zk-SNARK circuit encrypts transaction metadata for the viewer key using **ElGamal encryption on the BabyJubjub curve**. For each operation (transfer, freeze, withdraw), the circuit: + +1. Constructs a plaintext message containing input commitments and output record details (owner public key, amount combined with freeze flag, blinding factor) +2. Generates an ephemeral BabyJubjub key pair `(r, r·G)` using a random scalar `r` +3. Computes the ElGamal ciphertext: ephemeral public key `r·G` plus encrypted payload using the shared point `r·viewerPK` +4. The ciphertext is output as the `viewerData` public input array (17 elements for transfer/freeze, 7 elements for withdraw) + +The circuit proves in zero knowledge that the `viewerData` is correctly encrypted for the pool's viewer public key. The viewer can decrypt using their secret key to recover all transaction details. The viewer can decrypt and audit all transactions in the pool but cannot spend or freeze UTXOs. + +**Freezer**: The freezer authority can toggle the frozen state of UTXOs by proving knowledge of the freezer secret key. Frozen UTXOs cannot be spent or withdrawn by the owner. The freezer can both freeze and unfreeze UTXOs through the same `freeze()` function (see §4.5.3). + +### 4.8 Contract Interface + +A compliant implementation MUST expose the following functions: + +```solidity +// --- Core Operations --- + +function deposit( + uint256 poolId, uint128 amount, + uint256[2] calldata userPK, uint256 nonce, + bytes calldata extraData +) external payable; + +function transfer( + uint256 poolId, uint256 merkleRootIndex, uint256 arrayHashIndex, + uint256[3] calldata inputNullifiers, uint256[3] calldata outputs, + uint256[8] calldata proof, uint256[17] calldata viewerData, + bytes calldata extraData, address relayer, uint128 relayerFee +) external; + +function freeze( + uint256 poolId, uint256 merkleRootIndex, uint256 arrayHashIndex, + uint256[3] calldata inputNullifiers, uint256[3] calldata outputs, + uint256[8] calldata proof, uint256[17] calldata viewerData, + bytes calldata extraData +) external; + +struct WithdrawInput { + uint256 poolId; + uint256 merkleRootIndex; + uint256 arrayHashIndex; + uint256 inputNullifier; + uint256 output; + address recipient; + uint128 amount; + uint256[8] proof; + uint256[7] viewerData; + bytes extraData; + address relayer; + uint128 relayerFee; + uint128 gasDropValue; +} + +function withdraw(WithdrawInput calldata inp) external payable; + +function processPendingDeposits(uint256 maxBatchSize, address relayer) external; + +// --- Pool Management (owner only) --- + +function registerPool( + address token, uint16 depositFeeBPS, uint16 withdrawFeeBPS, + uint128 transferMaxAmount, uint128 withdrawMaxAmount, + uint256[2] calldata viewerPK, uint256[2] calldata freezerPK +) external returns (uint256 poolId); + +function setPoolFees(uint256 poolId, uint16 depositFeeBPS, uint16 withdrawFeeBPS) external; +function setPoolLimits(uint256 poolId, uint128 transferMaxAmount, uint128 withdrawMaxAmount) external; + +// --- Fee Claims --- + +function claimProtocolFees(uint256 poolId) external; +function claimRelayerFees(address token) external; + +// --- Query --- + +function getPoolInfo(uint256 poolId) external view returns (PoolInfo memory); +function nullifiers(uint256) external view returns (bool); +function commitments(uint256) external view returns (bool); +``` + +### 4.9 Events + +A compliant implementation MUST emit the following events: + +```solidity +// --- Core Operations --- + +event Deposit( + uint256 indexed poolId, address indexed from, + uint128 amount, uint128 fee, + uint256[2] userPK, uint256 nonce, uint128 relayerFee +); + +event Transfer( + uint256 indexed poolId, uint256 merkleRoot, uint256 arrayHash, + uint256[3] inputs, uint256[3] outputs, + address indexed relayer, uint128 relayerFee +); + +event Freeze( + uint256 indexed poolId, uint256 merkleRoot, uint256 arrayHash, + uint256[3] inputs, uint256[3] outputs +); + +event Withdraw( + uint256 indexed poolId, uint256 merkleRoot, uint256 arrayHash, + uint256 input, uint256 output, + address indexed recipient, uint128 amount, uint128 protocolFee, + address indexed relayer, uint128 relayerFee, uint128 gasDropValue +); + +// --- Encrypted Data --- + +event TransferMemo(uint256 indexed poolId, bytes extraData); +event FreezeMemo(uint256 indexed poolId, bytes extraData); +event WithdrawMemo(uint256 indexed poolId, bytes extraData); +event TransferViewerData(uint256 indexed poolId, uint256[17] viewerData); +event FreezeViewerData(uint256 indexed poolId, uint256[17] viewerData); +event WithdrawViewerData(uint256 indexed poolId, uint256[7] viewerData); + +// --- Pool Lifecycle --- + +event PoolRegistered( + uint256 indexed poolId, address indexed token, + uint256[2] viewerPK, uint256[2] freezerPK +); +event PoolFeesUpdated(uint256 indexed poolId, uint16 depositFeeBPS, uint16 withdrawFeeBPS); +event PoolLimitsUpdated(uint256 indexed poolId, uint128 transferMaxAmount, uint128 withdrawMaxAmount); + +// --- Fee Claims --- + +event ProtocolFeesClaimed(uint256 indexed poolId, address indexed recipient, uint128 amount); +event RelayerFeesClaimed(address indexed relayer, address indexed token, uint128 amount); + +// --- Amortized Merkle Tree --- + +event ElementInserted(uint256 indexed element, uint256 indexed globalIndex); +event ArrayMergedToTree(uint256 indexed batchIndex, uint256 indexed newRoot); +event DepositQueued(uint256 indexed queueIndex, uint256 indexed leaf, uint128 relayerFee); +event DepositsProcessed(uint256 indexed fromIndex, uint256 indexed toIndex); + +// --- Deposit Relayer Fee --- + +event DepositRelayerFeeUpdated(uint128 depositRelayerFee); + +// --- User Registration --- + +event UserRegistered(address indexed user, bytes userKey); +``` + +--- + +## 5. Rationale + +### 5.1 UTXO vs Account Model + +The UTXO model provides natural parallelism (multiple UTXOs can be spent independently), simple state (each UTXO is a single commitment), and flexible denominations. Account-based privacy (e.g., homomorphic encrypted balances) requires more complex state management and limits concurrency. + +### 5.2 Pool-Based Architecture + +One pool per token-viewer-freezer combination isolates risk, enables per-pool fee schedules and compliance keys, and produces deterministic pool IDs from the token address and compliance key configuration. + +### 5.3 Relayer-Based Submission + +Direct on-chain submission links the user's address to the shielded operation. Relayers break this link. The binding input in the zk-SNARK proof prevents relayer tampering. + +### 5.4 Viewer/Freezer Compliance + +Embedding compliance keys at the pool level makes the protocol deployable in regulated environments without compromising privacy for ordinary transfers. The viewer can audit without spending; the freezer can freeze without viewing transaction history. + +--- + +## 6. Backwards Compatibility + +This BAP defines a new application-layer protocol. It does not modify any existing BEP-20 token behavior or BSC protocol rules. Compliant contracts accept standard BEP-20 tokens and native BNB, and are fully compatible with existing BSC infrastructure. + +--- + +## 7. Security Considerations + +### 7.1 Smart Contract Security + +- ERC-20 interactions MUST use `SafeERC20`. +- The contract MUST reject duplicate nullifiers and duplicate commitments. +- All public inputs MUST be validated to be in the BN254 scalar field (`< p`). +- State-modifying functions use reentrancy guards as follows: + +| Function | Contract | `nonReentrant` | +|---|---|---| +| `deposit` | Ocash | Yes | +| `transfer` | Ocash | Yes | +| `freeze` | Ocash | Yes | +| `withdraw` | Ocash | Yes | +| `claimProtocolFees` | Ocash | Yes | +| `claimRelayerFees` | Ocash | Yes | +| `registerPool` | PoolRegistry | Yes | +| `setPoolFees` | PoolRegistry | Yes | +| `setPoolLimits` | PoolRegistry | Yes | +| `setDepositRelayerFee` | RelayerFees | Yes | +| `processPendingDeposits` | AmortizedMerkleTree | No | +| `registerUser` | Ocash | No | + +`processPendingDeposits` and `registerUser` do not use reentrancy guards as they do not involve external calls or token transfers that could trigger reentrant behavior. + +### 7.2 Cryptographic Security + +- **Groth16 trusted setup**: A compromised setup allows proof forgery. Multi-party computation (MPC) ceremonies mitigate this risk. +- **BabyJubjub cofactor**: Implementations MUST use the prime-order subgroup for all key operations. +- **Poseidon2 parameters**: Round constants and MDS matrix MUST exactly match the reference implementation. Incorrect parameters will produce incompatible commitments and nullifiers. + +### 7.3 Privacy Considerations + +- Deposits and withdrawals are partially public (amounts and addresses visible). Transfers within the pool provide the strongest privacy guarantees — amounts, senders, and recipients are hidden — but nullifiers, output commitments, relayer metadata, viewer payloads, and timing information remain public. +- Timing and amount correlation between deposit and withdrawal MAY reduce the anonymity set. Users SHOULD avoid immediate deposit-then-withdraw patterns. +- Relayers can selectively censor but cannot alter transactions. A competitive relayer market mitigates censorship risk. + +### 7.4 Compliance Risks + +- Viewer/freezer key compromise deanonymizes or enables unauthorized freezing of UTXOs in that pool. These keys SHOULD be protected with HSM or multi-party threshold schemes. + +--- + +## 8. Reference Implementations + +| Component | Description | +|---|---| +| Smart Contracts | Solidity implementation of pool registry, amortized Merkle tree, deposit/transfer/freeze/withdraw with Groth16 verification. Deployed on Ethereum, BNB Chain, and Base. | +| ZK Circuits | Groth16 circuits (Go/gnark) for transfer, withdraw, and freeze policies, including Poseidon2 hashing, BabyJubjub key operations, and ElGamal viewer memo encryption. | +| Proving System | Go WASM bridge for in-browser and Node.js proof generation. | +| Client SDK | TypeScript SDK (`@ocash/sdk`) providing the full deposit/transfer/withdraw pipeline: key derivation, commitment/nullifier computation, Merkle proof assembly, witness generation, proof generation (via WASM), and relayer request building. | +| Backend Services | Indexer (memo/nullifier sync), Merkle proof service, and relayer service. | + +--- + +## 9. License + +The content of this specification document is licensed under [CC0](https://creativecommons.org/publicdomain/zero/1.0/). + +Note: This license applies to the specification text only. Reference implementations may be distributed under different licenses. Consult the respective repository for applicable license terms. + +--- + +## Appendix A: Poseidon2 Round Constants + +### A.1 MDS Matrices + +**External matrix** (used in full rounds): + +``` +M_E = [[2, 1, 1], + [1, 2, 1], + [1, 1, 2]] +``` + +**Internal matrix** (used in partial rounds): + +``` +M_I = [[2, 1, 1], + [1, 2, 1], + [1, 1, 3]] +``` + +### A.2 Permutation Structure + +Each permutation consists of 64 rounds: + +- **Rounds 0–3** (full): S-box applied to all 3 state elements, followed by external MDS +- **Rounds 4–59** (partial): S-box applied to `state[0]` only, followed by internal MDS +- **Rounds 60–63** (full): S-box applied to all 3 state elements, followed by external MDS + +Before round 0, an initial external MDS multiplication is applied to the state. + +### A.3 Round Constants + +All values are elements of the BN254 scalar field, given in hexadecimal. + +#### Full Rounds 0–3 (3 constants per round: C₀, C₁, C₂) + +| Round | C₀ | C₁ | C₂ | +|-------|----|----|-----| +| 0 | `0x2ba117aea05b03e08d3e8cdc3441e489710b7eae2127240261f1161a4c375ec3` | `0x13d62b66e9d5236b1c4349076bc462097eca577bcd980e3e5262986898001a95` | `0x2ceb56ddb7d8c8886771c2f12a458edd58886a852e29ea9a157cb6c3ba8201a2` | +| 1 | `0x0ba9383b6a5ba188031f7377b152f8df895115269e8437f9eccdc767ecaf458f` | `0x188b8a2dd4baa4aeda8cf74c2cb3f5dfa482de9987f03fdeafd832f6c3be19c6` | `0x2672744cbbe045c930be1dcaae5b38cf4f0b9673514cbe5129908164ef7d7b58` | +| 2 | `0x1e0365a9b92d37b502579f6a3c3236df558f8417be56e58908897fa5cfbf15bb` | `0x2060426d53c6386a3f2f4e29d886bfc8e1be0ddafbdb50a9fd0be33143d1004a` | `0x1b917ac39485d49545e20d21e06735af839b1360e077daa4dfc2938ff91ce4d0` | +| 3 | `0x2065aa0d75c8773cd397593ca429c21ad2d10c066a09dea04378fed619021786` | `0x04767c771c63b9efcaee16d3463c0457ba7f029dd533e5c7f4b3ccef3677db6b` | `0x2b632ce28c5d4908c11b68f4ed9e3da0dd104c018d2376eaa0abd28f9cf8bd76` | + +#### Partial Rounds 4–59 (1 constant per round: C₀) + +| Round | C₀ | +|-------|-----| +| 4 | `0x122bd8150e3bf5129ed1f41b201d3881fe41c68ed194ffe6b414de857f03765d` | +| 5 | `0x23d4440906f4412f8994c3fa4cc08e849c0fbd10dc12518a07c0e8d77562c13f` | +| 6 | `0x2c5e99b87c743de13935855afed6cf836d6dd62ce31dfdca21efcfe197c9e321` | +| 7 | `0x06fba87a3924cbbb4117b782aa697bbc23900de6bf31a28ecc2f6a9225aebfe4` | +| 8 | `0x0c954d8f108f43ece97439775cfa22e1343a6cacae91604c76601eb6c7e90e1b` | +| 9 | `0x20980b82aa1ac356a0a48bc8101468c74f1efd47cd29ea01a852d6af93836a44` | +| 10 | `0x07e9df3ac21d190f9281b2ac56bf9dcce410bf95bd7fc196f4cfbb86acb60ec6` | +| 11 | `0x01e7459f591496f37d759e6eb427fa073eb923d9a67b066271dabe8e793ad796` | +| 12 | `0x0c1b5194e4c1af42dc01dadde54c73624ce1b8a0302d25ad499b2036f768e6e8` | +| 13 | `0x0cfd0f94030d285ffb85c8aa9f0675ac7077133b5a329c78b74656932fac8a27` | +| 14 | `0x0212ea73cc21625d7f1e361ad3df28c9f9cfd57fec66fb1bf69f1ab7cd11c55c` | +| 15 | `0x25fbc0b1fa13ea08b022f853e9b07a5c0fe9d5fb23c26eec54599100e60d57f6` | +| 16 | `0x074521adcc4a9387f4d6feeac681b1115b92f5e98e35d6d591d79b75d61d204c` | +| 17 | `0x267f9f5e6eea2a9d8816d7b683ab95d8121adeeaae66990bd24be95b6f0a0cd9` | +| 18 | `0x2fdf445c73cde6a7f4f23bef9bf520ceb72f08dabd391b118a253630f2878ade` | +| 19 | `0x02645e68b2890d258fa7eaffbc587c5ca8f7099cc4ddf923e23672c6153a8ae7` | +| 20 | `0x0c9e3d4841852fcf02818cdd86c3d86dcae4c1f7c140c3b17edc1f17b2652079` | +| 21 | `0x0a42e90f71ff44221ec000e0ff81b6f229292b0cb4470ff7c66c1fe06d9e69aa` | +| 22 | `0x1fb9a7d91fcf3173a1d80d3749192ed7d8a5b50cfd631571dc15154d0e71d7a2` | +| 23 | `0x10bff373cf04aca27c90792eaa545000503d6f118f4d9c5a906203aefe316d42` | +| 24 | `0x00956799581ce2c42ed5b55a130fb853683014e7cb9c3f32b9dfa4cf5c53127a` | +| 25 | `0x2b0bd2da61cae5f4f442b449cb1e9cc6af7a6d126b02ffd65aa887278741ab07` | +| 26 | `0x01c76af7e47ec30b4139081219fd7d173d498ba2e2ca928fb2b26019b16e5c64` | +| 27 | `0x2d9e586bd3c8cde82932cd1397db8564cbdebfc4f5c970e28a2d9f559db9d696` | +| 28 | `0x2c4b2a625ac29f468cc94f6a3ebdb7bb962f245568676073b829b95ace6d1ccc` | +| 29 | `0x027299c22883e4d52b8251a0724083c063e7be0a7f0070fba1c8d4d206841e6a` | +| 30 | `0x2af17121feea81979d98fa13cfdb5cf7f1f1717168ee7bf2da3709e589c381e7` | +| 31 | `0x09ad5501e4c9db7fee67f2fda8ce71162e6b2e0fc252f03c3d40470168ed4ea7` | +| 32 | `0x276bf230a40c51dac71697a84d603ac0423e3d8f23cc9330a23306976f7f902d` | +| 33 | `0x0b40af0d626b972c04b83a3897031c9bd0b4acc3b138fc505e15fdb6b60ba5f8` | +| 34 | `0x15c6033f97a1337ce18e37d0d22cf07f6c80f96af620c4d67c351e7210d688cf` | +| 35 | `0x27a5134eeea854449d10ae3dd3e17cbfc0f24c21a4265bb1e99982af48eb3966` | +| 36 | `0x0a3f27bafac251bbc63797868e84434a412400913e2e11616cb18f3bd01eb0d7` | +| 37 | `0x09409ff82de14430d5f1f16dd157c8175372a4f922b3563550230390c4476c59` | +| 38 | `0x1b6b39381a0b663344ee9a8cff259b84c593b709cf543014996ec33c7a00008b` | +| 39 | `0x16ac5b58d45468a298e60cbb92055daa665f29dd7194c77cac679c35f6f64552` | +| 40 | `0x121fb0f41bab603e46a4f4cb110d0a56bceff1f3af5577e7715e3777a5cfe7d8` | +| 41 | `0x056f262099a9d3e1d0060799732486358ad8b7bd2f515dd8767c2d19917d282d` | +| 42 | `0x0626740e4ff0fe7b8df127d56310c0c1fc47a07f630983bd55800ee8e24911d8` | +| 43 | `0x0b2b0b1213bed0c4b40fe2c938d076c65f22fe21eef4767b507561a63eea2874` | +| 44 | `0x1674784dcc6d6b3ef6467ee673c85311d1375aa39122ecf4b942caba565a6982` | +| 45 | `0x0690678b4bc42090fdbed7a334b323db5441a24c92b5b234f54ec16cff367db3` | +| 46 | `0x186719b1d7d0fb0087396c72ba57f53a5b67dc1077b82caadc62d5cdf7cd8db4` | +| 47 | `0x0178ed1e5ce3430020a30f0684fb01c60136e731a9a8c6afcbad139af2e8fcf7` | +| 48 | `0x1f31dc123a2384c71b57678dcf5a2fa6294f88a21a333cbec5facbc69424306c` | +| 49 | `0x017d928d2e3dbbe3a273f0bec79f881f8b75f4d333002b528fb1ae737cbf13eb` | +| 50 | `0x2f4fb0605668c045469510611c0137828be267709c0fa9392c28c2d95f9504bb` | +| 51 | `0x2ee2627a181d62b24501da3efccba9b4a9b61e6d9a7cdaa152c39347bdebe481` | +| 52 | `0x254cd2d79997885ca82e0ec5998aab8de0b09a02d04f54dcbd1a6f8776fd537b` | +| 53 | `0x2aa675a61643b83ad60d88b16c574a4695fc1b463dd44f8bbd674d1a1294dbfe` | +| 54 | `0x2dbc70b7e86794439ebd7d10cee37147e51769ed7a441187f6e22e644a003a51` | +| 55 | `0x019fc425ab24feca173ddab7070ebb4a2eeb9b82bee3a399ebedef2affe3ecd96` | +| 56 | `0x1b7a37f7ef7ce586df66295e955aba1b9b15052673534d4c13e02c19f02959e2` | +| 57 | `0x0772f989bc7bc4361340c9887a0225b92a192c14a85dc3ade21f6135b9239341` | +| 58 | `0x13f24e0e97fad4c45866626b9a1b9f3cc46f4ab2a018f0bda5bdade2087a07cf` | +| 59 | `0x1976c62d2c2c4ba095ff81bef054fe0757d7301950ede83426a34dd6cc12a4a5` | + +#### Full Rounds 60–63 (3 constants per round: C₀, C₁, C₂) + +| Round | C₀ | C₁ | C₂ | +|-------|----|----|-----| +| 60 | `0x1ea7aeca90530805e5fa1b676a6f12ace24c1c0f5b6cd68bf01558be11bb864a` | `0x070249ba94928b35fe02f56b12590e86f21a8a19e949ec10b62a5fcefea5c2b3` | `0x02cd4b5f5d87caaac64f78c44a62c408211c2e1d70a69549f9f1d36bd8a46073` | +| 61 | `0x07f4c9774540f9f81fa29a73910899ad91d950e8f83a4f52d37ccc35a982f152` | `0x02d8b931d897f634fd9cdae140a7b3f4d4bab1814e009fe84e754c4a23ae23cc` | `0x2b9e86726e0cfec43981d9898da6ddb631ae469a473aa73e570274ecd2376899` | +| 62 | `0x0c96c00773943b1de5a3dfb5959f30975f85adc57cc641bc2cea037837447191` | `0x258a43226d21462808593a8701f2dce2aaa28668f8fe35647a706fa4a81d5d47` | `0x26688ac841f42286102d1494db773e91760d8cad9cfb1a654284ed630a9bee42` | +| 63 | `0x0b39f30858ad21e1805c8ced014837777cfdd776fc2d4c07a97b2351f21764b1` | `0x0b114bc66867e038d6648a6ab3556243a5f78ea3db7aa997ba13961735792377` | `0x0c08b1719426f8ff2dee487f9f41ac785ffdb8a7be5fc869754689cb02999e51` | + +### A.4 Test Vectors + +Implementations MUST produce the following outputs: + +| Input | Domain | Expected Output | +|-------|--------|-----------------| +| `(0, 0)` | `0` (None) | `0x1fecb4beb3e5523b63e61f3f89216a71f3d686bcba6f3e35ce240b2404ae300a` | +| `(1, 2)` | `0` (None) | `0x1bb27765b122dcd5e531fc44bd05257b6c167523f492f8afe8c3a68683097af3` | +| `(1, 2)` | `0x5245434f52440000` (Record) | `0x3048d4e7ac8b75e96fa5e9f1d683d0e87ccfbeb2a99edc32e30ceee98c769278` | +| `(1, 2)` | `0x4e554c4c49464945` (Nullifier) | `0x11fc11249d578f0b73a542a51ae2484c42059e6ab9cf272096dd38f1b65a416a` | +| `(1, 2)` | `0x4d45524b4c450000` (Merkle) | `0x057be847fd6dafd3f9e563a05923bb6c0c31cc1dd69ee74b318ac4f1aec77632` |