--- eip: 8091 title: Privacy Address Format description: A client-side address format specification for privacy-preserving tokens using zk-SNARK optimized cryptography author: Rowan (@0xRowan) discussions-to: https://ethereum-magicians.org/t/erc-8091-privacy-address-format/26689 status: Draft type: Standards Track category: ERC created: 2025-11-24 --- ## Abstract This EIP defines a standardized client-side privacy address format for privacy-preserving tokens on Ethereum. The format uses a versioned prefix (`pv` + version number) to support cryptographic evolution and future upgrades. **Privacy Version 1 (pv1)** is the initial version defined by this standard, using the Baby Jubjub elliptic curve for zk-SNARK optimization. Future versions (pv2, pv3, etc.) MAY adopt different cryptographic schemes, such as post-quantum resistant algorithms. The format is designed for privacy-preserving token protocols, including native privacy tokens, dual-mode tokens, and wrapper protocols that add privacy capabilities to existing [ERC-20](./eip-20) tokens. **Key Characteristics**: - **Client-side specification**: Address generation and parsing are performed entirely off-chain without smart contract interaction - **Version support**: The "pv" prefix followed by version number (pv1, pv2, pv3) allows future upgrades for new cryptographic schemes or post-quantum resistance - **Three-key architecture**: Separate spend, scan, and encryption public keys for fine-grained permission control - **zk-SNARK optimization**: Baby Jubjub elliptic curve for efficient zero-knowledge proof generation - **Multi-chain support**: Single-character network codes supporting 58+ EVM-compatible chains - **Compact encoding**: Base58 compression with FNV-1a checksum for error detection - **Wrapper protocol compatible**: Applicable to wrapper protocol that adds privacy capabilities to existing [ERC-20](./eip-20) tokens (e.g., DAI → zDAI) By standardizing the privacy address format at the client level, this proposal enables interoperability between privacy-preserving dApps and seamless privacy asset transfers across the Ethereum ecosystem. ## Motivation ### Completing the Privacy Ecosystem Privacy token protocols typically define interfaces for commitments, nullifiers, and note encryption, but leave the **address format** unspecified. Without a standardized privacy address format: - Each dApp implements custom address encoding, leading to ecosystem fragmentation - Privacy assets cannot flow between different privacy dApps - Users need different addresses for different protocols - Wallets must implement custom logic for each privacy implementation ### The Interoperability Problem Consider these real-world scenarios that are currently impossible: **Scenario 1: Cross-dApp Privacy Transfers** ``` User has privacy assets in dApp_A, wants to use them in dApp_B Without pv1: Each dApp uses incompatible address formats With pv1: Unified address format enables seamless transfers ``` **Scenario 2: ENS Privacy Payments** ``` Alice wants to receive private payments at alice.eth With pv1: alice.eth can be associated with pv1MSxxxxxxxx ``` ### Why Not Use [ERC-5564](./eip-5564)? [ERC-5564](./eip-5564) (Stealth Addresses) is a valuable standard for general-purpose stealth addresses. While ERC-5564's schemeId mechanism could theoretically support different elliptic curves, the fundamental architectural differences make pv1 better suited for native privacy asset ecosystems: | Aspect | ERC-5564 | pv1 | | ------------------------ | --------------------------------- | ------------------------------------- | | Key Structure | 2 keys (viewing + spending) | 3 keys (spend + scan + encryption) | | Permission Granularity | Binary (view all or nothing) | Granular (scan, decrypt, spend separately) | | Encoding | Hex (st:eth:0x...) | Base58 with checksum (pv1...) | | Checksum | None | FNV-1a (error detection) | | Version Support | schemeId (on-chain routing) | Prefix-based (pv1/pv2/pv3 in address) | | Designed For | General stealth addresses | Native privacy assets (IZRC20) | **The three-key architecture is the core differentiator**: it enables granular permission control by selectively sharing private keys: - **Audit-only access**: Share scanPrivateKey only (can detect transactions, but not amounts) - **Accounting access**: Share scanPrivateKey + encryptionPrivateKey (full read-only visibility) - **Full control**: All three private keys (can spend funds) This granularity is not possible with ERC-5564's two-key model. For protocols using zero-knowledge proofs, pv1 uses the Baby Jubjub curve which provides better performance in zk-SNARK circuits due to its native compatibility with the BN254 scalar field. ### Why Version Support Matters The "pv" prefix stands for "Privacy Version", with "pv1" indicating version 1 of the format. This versioning enables: 1. **Elliptic curve upgrades**: pv1 uses Baby Jubjub; future versions can adopt different curves 2. **Post-quantum migration**: When quantum-resistant cryptography matures, pv2 could use lattice-based schemes 3. **Backward compatibility**: Wallets can support multiple versions simultaneously 4. **Gradual ecosystem migration**: No forced upgrades; users choose when to migrate ### Design Philosophy This standard embraces the principle: **"Privacy addresses should be as usable as regular addresses."** Key design goals: 1. **Human-readable**: Compact enough to share via messaging apps 2. **Error-resistant**: Checksum prevents typos from losing funds 3. **Permission-granular**: Three-key architecture enables selective disclosure 4. **Future-proof**: Versioned format and extensible codes 5. **Off-chain first**: No blockchain interaction required for address operations ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. ### Definitions - **pv1 Address**: A privacy address string in the format `pv[V][N][CompressedData][Checksum]` - **Privacy Version**: The "pv" prefix followed by version number (1, 2, 3, etc.) - **Spend Public Key**: Used by senders to derive stealth addresses for recipients - **Scan Public Key**: Used by senders to compute shared secrets and view tags - **Encryption Public Key**: Used by senders to encrypt note data for recipients - **Network Code**: Single Base58 character identifying the blockchain network - **Compressed Data**: Base58-encoded, point-compressed public keys - **Checksum**: 4-character FNV-1a hash for error detection ### Address Format ``` pv[V][N][CompressedData][Checksum] ┌──────┬─────────┬─────────┬──────────────────┬──────────┐ │ pv │ V │ N │ CompressedData │ Checksum │ ├──────┼─────────┼─────────┼──────────────────┼──────────┤ │ 2 ch │ 1 char │ 1 char │ Variable Base58 │ 4 char │ │Prefix│ Version │ Network │ Public Keys │ Base58 │ └──────┴─────────┴─────────┴──────────────────┴──────────┘ - pv: Privacy Version prefix - V: Version number (1, 2, 3, etc.) - pv1 uses Baby Jubjub curve - N: Network code (M=mainnet, P=polygon, etc.) ``` ### Network Codes Implementations MUST support at least the following network codes: | Network | Code | Chain ID | Description | | ---------------- | ----- | ---------- | ------------------ | | Ethereum Mainnet | `M` | 1 | Primary deployment | | Base Sepolia | `T` | 84532 | Testing networks | | Polygon | `P` | 137 | Polygon PoS | | Arbitrum | `A` | 42161 | Arbitrum One | | Base | `B` | 8453 | Base L2 | | Optimism | `O` | 10 | Optimism L2 | | Avalanche | `V` | 43114 | Avalanche C-Chain | | BNB Chain | `S` | 56 | BNB Smart Chain | | Gnosis | `G` | 100 | Gnosis Chain | Additional network codes MAY be defined. The Base58 character set allows up to 58 unique network codes. ### Cryptographic Parameters #### Elliptic Curve For pv1, implementations MUST use the Baby Jubjub curve defined over the BN254 scalar field: ``` Curve: Twisted Edwards Equation: ax² + y² = 1 + dx²y² Parameters: a = 168700 d = 168696 p = 21888242871839275222246405745257275088548364400416034343698204186575808495617 Subgroup order (for scalars): l = 2736030358979909402780800718157159386076813972158567259200215660948447373041 ``` This curve has cofactor 8. Implementations using circomlibjs SHOULD use `babyJub.Base8` as the generator point, which generates the prime-order subgroup of l points and avoids small subgroup attacks. This curve is compatible with circomlib and other zk-SNARK tooling. Future versions (pv2, pv3, etc.) MAY adopt different elliptic curves as cryptographic needs evolve, such as post-quantum resistant curves or curves optimized for different proof systems. #### Point Compression Public keys MUST be compressed using the following algorithm: 1. For point (x, y), store only x-coordinate (256 bits) 2. Store y-coordinate parity as a single bit (odd = 1, even = 0) 3. Pack three keys with parity flags into a single integer: ``` packedData = (flags << 768) | (spendX << 512) | (scanX << 256) | encryptionX Where flags = (spendIsOdd ? 1 : 0) | (scanIsOdd ? 2 : 0) | (encryptionIsOdd ? 4 : 0) ``` #### Base58 Encoding Implementations MUST use the Bitcoin Base58 alphabet (excludes 0, O, I, l): ``` 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz ``` #### Checksum Calculation Implementations MUST use FNV-1a hash for checksum: ```javascript function calculateChecksum(data) { let hash = 0x811c9dc5; // FNV-1a initial value for (let i = 0; i < data.length; i++) { hash ^= data.charCodeAt(i); hash = (hash * 0x01000193) >>> 0; // FNV-1a prime } // Clamp to 4-character Base58 range const maxHash = 58**4 - 1; return toBase58(hash % maxHash, 4); } ``` ### Client-Side Operations All pv1 address operations are performed client-side without blockchain interaction: #### Address Generation ```javascript function generatePv1Address(spendPubKey, scanPubKey, encryptPubKey, network) { // 1. Validate inputs validateNetwork(network); validatePoint(spendPubKey); validatePoint(scanPubKey); validatePoint(encryptPubKey); // 2. Compress public keys const compressedData = compressPublicKeys(spendPubKey, scanPubKey, encryptPubKey); // 3. Assemble address const networkCode = NETWORK_CODES[network]; const baseData = `pv1${networkCode}${compressedData}`; // 4. Calculate checksum const checksum = calculateChecksum(baseData); return baseData + checksum; } ``` #### Address Parsing ```javascript function parsePv1Address(pv1Address) { // 1. Validate prefix and length if (!pv1Address.startsWith('pv1') || pv1Address.length < 10) { throw new Error('Invalid pv1 address format'); } // 2. Extract components const networkCode = pv1Address[3]; const checksum = pv1Address.slice(-4); const compressedData = pv1Address.slice(4, -4); // 3. Verify checksum const baseData = pv1Address.slice(0, -4); const expectedChecksum = calculateChecksum(baseData); if (checksum !== expectedChecksum) { throw new Error('Invalid checksum'); } // 4. Validate network code if (!REVERSE_NETWORK_CODES[networkCode]) { throw new Error('Invalid network code'); } // 5. Decompress public keys const publicKeys = decompressPublicKeys(compressedData); return { version: 1, network: REVERSE_NETWORK_CODES[networkCode], ...publicKeys }; } ``` ### Validation Rules Implementations MUST validate pv1 addresses according to these rules: | Check | Rule | Error | | ------------ | ----------------------------- | ----------------- | | Prefix | Must start with "pv1" | Invalid prefix | | Length | Between 135 and 150 characters | Invalid length | | Network code | Must be in defined set | Unknown network | | Characters | All must be valid Base58 | Invalid character | | Checksum | Must match calculated value | Checksum mismatch | | Points | Must be on Baby Jubjub curve | Invalid point | ## Rationale ### Why Three Keys Instead of Two? [ERC-5564](./eip-5564) uses two keys (viewing + spending). We add a third key for encryption: **Benefits of three-key architecture**: 1. **Granular Permission Control** (by selectively sharing private keys) - Share `scanPrivateKey` only: Detect transaction existence (audit) - Share `scanPrivateKey` + `encryptionPrivateKey`: Full read-only visibility (accounting) - Keep `spendPrivateKey` secure: Only owner can spend 2. **Enhanced Security** - Compromised scan key doesn't reveal amounts - Compromised encryption key doesn't enable spending - Defense in depth 3. **Use Case Support** - Auditors: scanPrivateKey only (knows you received, not how much) - Accountants: scanPrivateKey + encryptionPrivateKey (full visibility, no spending) - Owner: all three private keys 4. **Potential Extended Use Cases** The encryption public key, being stable and publicly accessible as part of the pv-address, could potentially support use cases beyond note encryption: - **Private messaging**: Direct encrypted communication between users - **Encrypted metadata**: Payment references, invoice numbers, or custom notes attached to transactions - **Cross-protocol notifications**: Privacy-preserving alerts like "payment received" or "note available" - **Wallet-to-wallet communication**: Encrypted payment requests, refund addresses, or coordination messages This standard does not restrict how implementations utilize the encryption key. Developers are free to innovate and extend its usage beyond the core privacy token protocol, enabling richer privacy-preserving applications. ### Why Baby Jubjub Instead of secp256k1? | Consideration | secp256k1 | Baby Jubjub | | ---------------------- | -------------------------- | ------------------------------ | | Ethereum compatibility | Native | Requires field conversion | | zk-SNARK efficiency | Low (non-native field) | High (native to BN254) | | Proof generation | Slow | Fast | | Circuit complexity | High | Low | For privacy protocols using zk-SNARKs, the significant performance improvement justifies the additional complexity of using a non-native curve. ### Why Base58 Instead of Hex? 1. **Shorter addresses**: ~145 chars vs ~200+ chars in hex 2. **Human-readable**: Avoids confusing characters (0/O, 1/l/I) 3. **Established**: Used by Bitcoin addresses 4. **Checksum-friendly**: Easy to append 4-char checksum ### Why Single-Character Network Codes? 1. **Compactness**: Saves ~10 characters vs full names 2. **Sufficient capacity**: 58 codes cover all major chains 3. **Unambiguous**: No parsing needed for network 4. **Extensible**: New networks can be added ### Why Client-Side Only? 1. **No gas costs**: Address generation is free 2. **Instant**: No blockchain confirmation needed 3. **Privacy**: No on-chain footprint for address creation 4. **Offline capable**: Works without network connection 5. **Simplicity**: No smart contract deployment or interaction needed ## Backwards Compatibility This standard introduces a new client-side address format and does not modify existing standards or require smart contract changes. ## Reference Implementation A reference implementation in JavaScript: ```javascript class Pv1AddressManager { constructor() { this.version = 1; this.prefix = "pv1"; this.base58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; this.networkCodes = { 'mainnet': 'M', 'testnet': 'T', 'polygon': 'P', 'arbitrum': 'A', 'base': 'B', 'optimism': 'O', 'avalanche': 'V', 'bsc': 'S', 'fantom': 'F', 'gnosis': 'G' }; this.reverseNetworkCodes = Object.fromEntries( Object.entries(this.networkCodes).map(([k, v]) => [v, k]) ); } generatePv1Address(spendPubKey, scanPubKey, encryptPubKey, network) { // Validate inputs if (!this.networkCodes[network]) throw new Error(`Invalid network: ${network}`); // Compress public keys const compressedData = this._compressPublicKeys(spendPubKey, scanPubKey, encryptPubKey); // Assemble address const networkCode = this.networkCodes[network]; const baseData = `${this.prefix}${networkCode}${compressedData}`; // Calculate checksum const checksum = this._calculateChecksum(baseData); return baseData + checksum; } parsePv1Address(pv1Address) { // Validate format if (!pv1Address.startsWith('pv1') || pv1Address.length < 135) { throw new Error('Invalid pv1 address format'); } // Extract components const networkCode = pv1Address[3]; const checksum = pv1Address.slice(-4); const compressedData = pv1Address.slice(4, -4); // Verify checksum const baseData = pv1Address.slice(0, -4); const expectedChecksum = this._calculateChecksum(baseData); if (checksum !== expectedChecksum) { throw new Error(`Invalid checksum: expected ${expectedChecksum}, got ${checksum}`); } // Validate network code const network = this.reverseNetworkCodes[networkCode]; if (!network) throw new Error(`Invalid network code: ${networkCode}`); // Decompress public keys const publicKeys = this._decompressPublicKeys(compressedData); return { version: this.version, network, ...publicKeys }; } _compressPublicKeys(spendPubKey, scanPubKey, encryptPubKey) { const compress = (point) => ({ x: BigInt(point[0]), isOdd: BigInt(point[1]) % 2n === 1n }); const spend = compress(spendPubKey); const scan = compress(scanPubKey); const encrypt = compress(encryptPubKey); const flags = (spend.isOdd ? 1 : 0) | (scan.isOdd ? 2 : 0) | (encrypt.isOdd ? 4 : 0); const packedData = (BigInt(flags) << 768n) | (spend.x << 512n) | (scan.x << 256n) | encrypt.x; return this._bigIntToBase58(packedData); } _decompressPublicKeys(compressedBase58) { const packedData = this._base58ToBigInt(compressedBase58); const flags = Number(packedData >> 768n); const spendX = (packedData >> 512n) & ((1n << 256n) - 1n); const scanX = (packedData >> 256n) & ((1n << 256n) - 1n); const encryptX = packedData & ((1n << 256n) - 1n); return { spendPublicKey: [spendX.toString(), this._recoverY(spendX, (flags & 1) !== 0).toString()], scanPublicKey: [scanX.toString(), this._recoverY(scanX, (flags & 2) !== 0).toString()], encryptionPublicKey: [encryptX.toString(), this._recoverY(encryptX, (flags & 4) !== 0).toString()] }; } _recoverY(x, isOdd) { // Baby Jubjub curve parameters const p = 21888242871839275222246405745257275088548364400416034343698204186575808495617n; const a = 168700n; const d = 168696n; // Edwards curve: ax² + y² = 1 + dx²y² // Solve for y²: y² = (1 - ax²) / (1 - dx²) const x2 = (x * x) % p; const numerator = (1n + p - (a * x2) % p) % p; const denominator = (1n + p - (d * x2) % p) % p; const denominatorInv = this._modPow(denominator, p - 2n, p); const y2 = (numerator * denominatorInv) % p; let y = this._tonelliShanks(y2, p); // Adjust parity if (((y & 1n) === 1n) !== isOdd) { y = p - y; } return y; } _calculateChecksum(data) { let hash = 0x811c9dc5; for (let i = 0; i < data.length; i++) { hash ^= data.charCodeAt(i); hash = (hash * 0x01000193) >>> 0; } return this._numberToBase58(hash % (58**4 - 1), 4); } _bigIntToBase58(bigint) { if (bigint === 0n) return this.base58Chars[0]; let result = ''; while (bigint > 0n) { result = this.base58Chars[Number(bigint % 58n)] + result; bigint = bigint / 58n; } return result; } _base58ToBigInt(encoded) { let result = 0n; for (const char of encoded) { const digit = this.base58Chars.indexOf(char); if (digit === -1) throw new Error(`Invalid Base58 character: ${char}`); result = result * 58n + BigInt(digit); } return result; } _numberToBase58(number, length) { let result = ''; while (number > 0) { result = this.base58Chars[number % 58] + result; number = Math.floor(number / 58); } return result.padStart(length, this.base58Chars[0]); } _modPow(base, exp, mod) { let result = 1n; base = base % mod; while (exp > 0n) { if (exp % 2n === 1n) result = (result * base) % mod; exp = exp / 2n; base = (base * base) % mod; } return result; } _tonelliShanks(n, p) { // Tonelli-Shanks algorithm for modular square root // Full implementation in reference code repository if (this._modPow(n, (p - 1n) / 2n, p) !== 1n) { throw new Error('No square root exists'); } let Q = p - 1n; let S = 0n; while (Q % 2n === 0n) { Q /= 2n; S++; } if (S === 1n) { return this._modPow(n, (p + 1n) / 4n, p); } let z = 2n; while (this._modPow(z, (p - 1n) / 2n, p) !== p - 1n) { z++; } let M = S; let c = this._modPow(z, Q, p); let t = this._modPow(n, Q, p); let R = this._modPow(n, (Q + 1n) / 2n, p); while (t !== 1n) { let i = 0n; let temp = t; while (temp !== 1n) { temp = (temp * temp) % p; i++; } let b = this._modPow(c, this._modPow(2n, M - i - 1n, p - 1n), p); M = i; c = (b * b) % p; t = (t * c) % p; R = (R * b) % p; } return R; } } ``` ## Security Considerations ### Private Key Security **Attack Vector**: Compromise of private keys enables unauthorized spending or surveillance. **Mitigation**: - `spendPrivateKey`: MUST be stored with highest security (hardware wallet recommended) - `scanPrivateKey`: Can be shared with trusted parties for monitoring only - `encryptionPrivateKey`: Compromise reveals amounts but not spending capability **Recommendation**: Use hierarchical deterministic (HD) key derivation to generate all three keys from a single master seed. ### Address Parsing Vulnerabilities **Attack Vector**: Malformed addresses could cause parsing errors or incorrect key extraction. **Mitigation**: - MUST validate checksum before processing - MUST verify extracted points are on the Baby Jubjub curve - MUST reject addresses with invalid network codes - SHOULD implement length bounds checking ### Replay Attacks Across Networks **Attack Vector**: A pv1 address generated for one network could be used on another. **Mitigation**: - Network code is embedded in the address - Implementations MUST verify the network code matches the current chain - Client applications SHOULD warn users of network mismatches ### Encryption Key Extended Usage While this standard defines the encryption public key primarily for encrypting note data in privacy token protocols, implementations MAY use it for additional purposes such as private messaging, encrypted metadata, or other privacy-preserving communications. **Privacy Considerations for Extended Usage**: - **Linkability**: Using the same encryption key for multiple purposes (e.g., both transactions and messaging) may create linkability between different types of activities associated with the same pv-address - **Key Compromise Impact**: If the encryption private key is compromised, it affects all encrypted data across all use cases, not just transaction notes - **Use Case Evaluation**: Implementations SHOULD evaluate privacy implications specific to their use case when extending encryption key usage beyond note encryption The decision to use the encryption key for extended purposes is left to individual implementations. This flexibility enables innovation while maintaining the core address format standard. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).