Discussion topic for ERC-8091:
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 (ERC-8086) , dual-mode tokens(ERC-8085), and wrapper protocols that add privacy capabilities to existing ERC-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 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?
ERC-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:
- Elliptic curve upgrades: pv1 uses Baby Jubjub; future versions can adopt different curves
- Post-quantum migration: When quantum-resistant cryptography matures, pv2 could use lattice-based schemes
- Backward compatibility: Wallets can support multiple versions simultaneously
- 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:
- Human-readable: Compact enough to share via messaging apps
- Error-resistant: Checksum prevents typos from losing funds
- Permission-granular: Three-key architecture enables selective disclosure
- Future-proof: Versioned format and extensible codes
- 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:
- For point (x, y), store only x-coordinate (256 bits)
- Store y-coordinate parity as a single bit (odd = 1, even = 0)
- 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:
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
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
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
};
}