# Specter Developer Documentation > Developer documentation for the Specter L1 blockchain - Site: https://docs.specterchain.com - Source: https://github.com/Specter-Foundation/docs-website - Generated: 2026-04-03 ## Table of Contents - [Specter Developer Docs](https://docs.specterchain.com/) - [What is Specter](https://docs.specterchain.com/getting-started/what-is-specter) - [Quickstart](https://docs.specterchain.com/getting-started/quickstart) - [Network Details](https://docs.specterchain.com/getting-started/network-details) - [Running a Node](https://docs.specterchain.com/chain/running-a-node) - [specterd CLI Reference](https://docs.specterchain.com/chain/specterd-cli) - [Node Configuration](https://docs.specterchain.com/chain/configuration) - [Genesis Parameters](https://docs.specterchain.com/chain/genesis-params) - [EVM Development](https://docs.specterchain.com/evm/overview) - [Connecting MetaMask](https://docs.specterchain.com/evm/connecting-metamask) - [Deploying Contracts](https://docs.specterchain.com/evm/deploying-contracts) - [Foundry Setup](https://docs.specterchain.com/evm/foundry-setup) - [RPC Endpoints](https://docs.specterchain.com/evm/rpc-endpoints) - [Ghost Protocol](https://docs.specterchain.com/ghost-protocol/overview) - [Commit Flow](https://docs.specterchain.com/ghost-protocol/commit-flow) - [Reveal Flow](https://docs.specterchain.com/ghost-protocol/reveal-flow) - [Commitment Structure](https://docs.specterchain.com/ghost-protocol/commitment-structure) - [Ghost Protocol Integration Guide](https://docs.specterchain.com/ghost-protocol/integration-guide) - [Smart Contracts Overview](https://docs.specterchain.com/contracts/overview) - [CommitRevealVault](https://docs.specterchain.com/contracts/commit-reveal-vault) - [CommitmentTree](https://docs.specterchain.com/contracts/commitment-tree) - [NullifierRegistry](https://docs.specterchain.com/contracts/nullifier-registry) - [NativeAssetHandler](https://docs.specterchain.com/contracts/native-asset-handler) - [AssetGuard](https://docs.specterchain.com/contracts/asset-guard) - [GhostERC20](https://docs.specterchain.com/contracts/ghost-erc20) - [GhostERC20Factory](https://docs.specterchain.com/contracts/ghost-erc20-factory) - [Deployed Addresses](https://docs.specterchain.com/contracts/deployed-addresses) - [Ghostmint Precompile](https://docs.specterchain.com/ghostmint/overview) - [Ghostmint ABI Reference](https://docs.specterchain.com/ghostmint/abi-reference) - [Ghostmint Authorization](https://docs.specterchain.com/ghostmint/authorization) - [Ghostmint Usage Examples](https://docs.specterchain.com/ghostmint/usage-examples) - [Zero-Knowledge Proofs](https://docs.specterchain.com/zk-proofs/overview) - [Redemption Circuit](https://docs.specterchain.com/zk-proofs/redemption-circuit) - [Access Proof Circuit](https://docs.specterchain.com/zk-proofs/access-proof-circuit) - [Generating Proofs](https://docs.specterchain.com/zk-proofs/generating-proofs) - [Circuit Inputs & Outputs](https://docs.specterchain.com/zk-proofs/circuit-inputs-outputs) - [Policy System](https://docs.specterchain.com/policies/overview) - [IRevealPolicy Interface](https://docs.specterchain.com/policies/ireveal-policy) - [TimelockExpiry](https://docs.specterchain.com/policies/timelock-expiry) - [DestinationRestriction](https://docs.specterchain.com/policies/destination-restriction) - [ThresholdWitness](https://docs.specterchain.com/policies/threshold-witness) - [PolicyRegistry](https://docs.specterchain.com/policies/policy-registry) - [Writing Custom Policies](https://docs.specterchain.com/policies/custom-policies) - [Relayer Services](https://docs.specterchain.com/relayer/overview) - [Root Updater](https://docs.specterchain.com/relayer/root-updater) - [Commitment Relayer](https://docs.specterchain.com/relayer/commitment-relayer) - [Proof Relayer](https://docs.specterchain.com/relayer/proof-relayer) - [Faucet Service](https://docs.specterchain.com/relayer/faucet) - [Batch Root Updater](https://docs.specterchain.com/relayer/batch-root-updater) - [Open Ghost Root Updater](https://docs.specterchain.com/relayer/open-ghost-root-updater) - [Stale Monitor](https://docs.specterchain.com/relayer/stale-monitor) - [Frontend Integration](https://docs.specterchain.com/frontend/overview) - [Chain Configuration](https://docs.specterchain.com/frontend/chain-config) - [Contract Interaction with viem & ethers.js](https://docs.specterchain.com/frontend/viem-ethers) - [Client-Side ZK Proof Generation](https://docs.specterchain.com/frontend/client-side-zk) - [Building dApps on Specter](https://docs.specterchain.com/frontend/building-dapps) - [GHOST Token](https://docs.specterchain.com/token/overview) - [Denominations](https://docs.specterchain.com/token/denominations) - [Wrapping as GhostERC20](https://docs.specterchain.com/token/wrapping-ghost-erc20) - [Gas and Fees](https://docs.specterchain.com/token/gas-and-fees) - [Staking](https://docs.specterchain.com/token/staking) - [Open Protocol](https://docs.specterchain.com/open-protocol/overview) - [OpenGhostVault](https://docs.specterchain.com/open-protocol/open-ghost-vault) - [Key Vault](https://docs.specterchain.com/open-protocol/key-vault) - [Open vs Private: When to Use Each](https://docs.specterchain.com/open-protocol/use-cases) - [Scaling](https://docs.specterchain.com/scaling/overview) - [Batch Operations](https://docs.specterchain.com/scaling/batch-operations) - [Session Vaults](https://docs.specterchain.com/scaling/session-vaults) - [Sharded Trees](https://docs.specterchain.com/scaling/sharded-trees) - [Testnet](https://docs.specterchain.com/testnet/overview) - [Faucet](https://docs.specterchain.com/testnet/faucet) - [Block Explorer](https://docs.specterchain.com/testnet/block-explorer) - [Troubleshooting](https://docs.specterchain.com/testnet/troubleshooting) - [Security Best Practices](https://docs.specterchain.com/security/best-practices) - [Threat Model](https://docs.specterchain.com/security/threat-model) - [Security Audits](https://docs.specterchain.com/security/audits) - [Glossary](https://docs.specterchain.com/glossary) - [Reference](https://docs.specterchain.com/reference) --- # Specter Developer Docs Build on a privacy-first L1 blockchain Deploy smart contracts, integrate the Ghost Protocol commit-reveal system, generate zero-knowledge proofs, and build privacy-preserving dApps on Specter — a sovereign Layer 1 built on Cosmos SDK with full EVM compatibility. ### Deploy Contracts Full EVM compatibility with Solidity 0.8.24 and Foundry. Deploy standard smart contracts or integrate with Ghost Protocol for privacy-enabled token operations. Chain ID `5445`, connect with MetaMask, Foundry, or any Ethereum tooling. ### Ghost Protocol Commit-reveal privacy system powered by Groth16 zero-knowledge proofs. Burn tokens into Poseidon hash commitments, then mint fresh tokens from a ZK proof — with no link between commit and reveal. Build privacy into any application. ### Relayer APIs Seven production services handle Merkle root updates, Poseidon hash computation, ZK proof generation, and testnet faucet distribution. REST APIs with HMAC authentication for seamless integration. | Parameter | Value | |---|---| | Chain ID (EVM) | 5445 | | Chain ID (Cosmos) | specter-testnet-1 | | RPC | https://testnet.specterchain.com | | WebSocket | wss://testnet.specterchain.com/ws | | Token | GHOST (18 decimals) | | Max Supply | 1,000,000,000 | | Framework | Cosmos SDK v0.53.2 | | Consensus | CometBFT BFT | | ZK System | Groth16 on BN254 | | Precompile | 0x0808 (Ghostmint) | ## Quick Links ### [Getting Started](/getting-started/quickstart) Add Specter to MetaMask, get testnet GHOST from the faucet, and deploy your first contract in under 5 minutes. ### [EVM Development](/evm/overview) Foundry setup, contract deployment, RPC endpoints, and everything you need for standard EVM development on Specter. ### [Ghost Protocol](/ghost-protocol/overview) Understand the commit-reveal system, learn how to integrate privacy into your smart contracts, and follow the end-to-end integration guide. ### [Contract Reference](/contracts/overview) Complete documentation for every deployed smart contract — interfaces, addresses, and usage examples. ### [ZK Proofs](/zk-proofs/overview) Groth16 proof generation, circuit inputs and outputs, client-side proof generation with snarkjs, and the proof relayer API. ### [Testnet](/testnet/overview) Faucet, block explorer, deployed contract addresses, and troubleshooting for the Specter testnet. --- # What is Specter Specter is a sovereign Layer 1 blockchain purpose-built for data privacy. It runs on **Cosmos SDK v0.53.2** with **CometBFT** consensus and provides **full EVM compatibility**, meaning any Solidity smart contract, Ethereum wallet, or dApp framework works out of the box. ## What makes Specter different Specter's core innovation is the **Ghost Protocol** — a commit-reveal system that enables private data operations directly at the consensus layer. ### How it works in one sentence You destroy value by committing it (tokens are actually burned), and you mint fresh value when you reveal it with a zero-knowledge proof — with no link between the two operations. ### Key architecture decisions | Decision | What it means | |---|---| | **Burn-and-mint model** | No liquidity pool or escrow contract. Tokens are destroyed on commit and created fresh on reveal via the chain's BankKeeper. This eliminates pool-based attack vectors. | | **Ghostmint precompile at `0x0808`** | Smart contracts can call the chain's native mint/burn operations directly through an EVM precompile. Only governance-authorized contracts can invoke it. | | **Poseidon hashing** | Commitments use the ZK-friendly Poseidon hash function. The PoseidonT3 library is deployed on-chain (~55KB bytecode, enabled by a custom MaxCodeSize of 64KB). | | **Groth16 on BN254** | Zero-knowledge proofs are 256 bytes, verified in a single pairing check using Ethereum's `ecPairing` precompile (~200K gas). | | **Off-chain tree, on-chain roots** | The Merkle tree is built off-chain by relayer services (gas-efficient), with roots committed on-chain. A history window of the last 100 roots allows reveals against recent state. | | **Programmable policies** | Reveal conditions (timelocks, destination restrictions, threshold witnesses) are enforced on-chain without compromising privacy. | ## The tech stack ``` ┌─────────────────────────────────────────┐ │ Applications │ │ (Solidity contracts, dApps, wallets) │ ├─────────────────────────────────────────┤ │ EVM Layer │ │ (go-ethereum fork, MaxCode 64KB) │ ├─────────────────────────────────────────┤ │ Ghostmint Precompile │ │ (0x0808 — native mint/burn bridge) │ ├─────────────────────────────────────────┤ │ Cosmos SDK v0.53.2 │ │ (BankKeeper, staking, governance) │ ├─────────────────────────────────────────┤ │ CometBFT Consensus │ │ (BFT PoS, instant finality, 1-2s) │ └─────────────────────────────────────────┘ ``` ## Who this documentation is for - **Smart contract developers** — deploy Solidity contracts, integrate with Ghost Protocol - **dApp builders** — build frontends that interact with the chain via standard Ethereum tooling - **Infrastructure operators** — run nodes, validators, and relayer services - **Protocol integrators** — use the commit-reveal system for privacy-preserving applications beyond tokens ## Next steps - [Quickstart](/getting-started/quickstart) — deploy your first contract in 5 minutes - [Network Details](/getting-started/network-details) — all endpoints, chain IDs, and configuration - [Ghost Protocol Overview](/ghost-protocol/overview) — understand the commit-reveal system --- # Quickstart Get up and running on Specter testnet in under 5 minutes. You'll add the network to MetaMask, get test GHOST from the faucet, and deploy a smart contract. ## 1. Add Specter to MetaMask Open MetaMask and add a custom network with these settings: | Field | Value | |---|---| | Network Name | Specter Testnet | | RPC URL | `https://testnet.specterchain.com` | | Chain ID | `5445` | | Currency Symbol | `GHOST` | | Block Explorer | `https://explorer.specterchain.com` | Or add it programmatically from your dApp: ```javascript await window.ethereum.request({ method: 'wallet_addEthereumChain', params: [{ chainId: '0x1545', // 5445 in hex chainName: 'Specter Testnet', nativeCurrency: { name: 'GHOST', symbol: 'GHOST', decimals: 18 }, rpcUrls: ['https://testnet.specterchain.com'], blockExplorerUrls: ['https://explorer.specterchain.com'], }], }); ``` ## 2. Get testnet GHOST Request test tokens from the faucet: ```bash curl -X POST https://relayer.specterchain.com/api/faucet/claim \ -H "Content-Type: application/json" \ -d '{"address": "0xYOUR_ADDRESS_HERE"}' ``` The faucet distributes **100 GHOST** per address (one claim per address). Check your claim status: ```bash curl "https://relayer.specterchain.com/api/faucet/status?address=0xYOUR_ADDRESS_HERE" ``` ## 3. Deploy a contract with Foundry ### Install Foundry ```bash curl -L https://foundry.paradigm.xyz | bash foundryup ``` ### Create a project ```bash forge init hello-specter cd hello-specter ``` ### Deploy ```bash forge create src/Counter.sol:Counter \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` Never use a private key with real funds for testnet. Create a dedicated testnet wallet. ### Interact with your contract ```bash # Read a value cast call $CONTRACT_ADDRESS "number()(uint256)" \ --rpc-url https://testnet.specterchain.com # Send a transaction cast send $CONTRACT_ADDRESS "increment()" \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` ## 4. Verify it works Check the transaction on the block explorer: ```bash cast receipt $TX_HASH --rpc-url https://testnet.specterchain.com ``` ## Next steps - [EVM Development](/evm/overview) — deploying contracts, Foundry setup, RPC reference - [Ghost Protocol](/ghost-protocol/overview) — integrate privacy features - [Smart Contract Reference](/contracts/overview) — all deployed contract interfaces - [Testnet](/testnet/overview) — explorer, faucet details, troubleshooting --- # Network Details Complete reference for all Specter network endpoints, identifiers, and configuration values. ## Chain identifiers | Parameter | Value | |---|---| | Chain ID (EVM) | `5445` | | Chain ID (Cosmos) | `specter-testnet-1` | | Bech32 prefix | `specter` | | Binary | `specterd` | ## RPC endpoints | Service | URL | |---|---| | EVM JSON-RPC (HTTPS) | `https://testnet.specterchain.com` | | EVM WebSocket | `wss://testnet.specterchain.com/ws` | | CometBFT RPC | `https://testnet.specterchain.com/rpc/` | | Relayer API | `https://relayer.specterchain.com` | The JSON-RPC endpoint supports all standard Ethereum methods: `eth_blockNumber`, `eth_sendTransaction`, `eth_call`, `eth_getBalance`, `eth_getLogs`, etc. ## Native token | Parameter | Value | |---|---| | Name | GHOST | | Base unit | aghost | | Decimals | 18 | | Max supply | 1,000,000,000 GHOST | | Inflation | 0% | | Gas price | 1 gwei minimum | 1 GHOST = 10^18 aghost (same relationship as ETH to wei). ## Relayer services | Service | Port | URL | |---|---|---| | Root Updater | 3001 | `https://relayer.specterchain.com` | | Commitment Relayer | 3002 | `https://relayer.specterchain.com` | | Proof Relayer | 3003 | `https://relayer.specterchain.com` | | Faucet | 3005 | `https://relayer.specterchain.com` | | Batch Root Updater | 3030 | Internal | | Open Ghost Root Updater | 3008 | Internal | | Stale Monitor | — | Internal | ## Key addresses | Role | Address | |---|---| | Deployer | `0x2bBd88eA213c76f2cA3BAeBaA20F3FA9c4B522e7` | | Relayer Operator | `0x42f5f839fA29b9e9AEF0d66A3D2aAc89C4CBF1f1` | | Ghostmint Precompile | `0x0000000000000000000000000000000000000808` | ## Infrastructure | Component | Details | |---|---| | Validator | DigitalOcean droplet, CometBFT + EVM RPC | | Relayer | DigitalOcean droplet, 7 PM2 services | | Block explorer | Blockscout | | Block time | 1–2 seconds | | Finality | Instant (BFT) | ## Supported GhostERC20 tokens These privacy-enabled ERC20 tokens are deployed on testnet: | Token | Address | |---|---| | gUSDC | `0x65c9091a6A45Db302a343AF460657C298FAA222D` | | gWETH | `0x923295a3e3bE5eDe29Fc408A507dA057ee044E81` | | gLABS | `0x062f8a68f6386c1b448b3379abd369825bec9aa2` | See [Deployed Addresses](/contracts/deployed-addresses) for the complete contract address table. --- # Running a Node Run a Specter full node or validator using the `specterd` binary. ## Build from source ```bash # Clone the repository git clone https://github.com/Specter-Foundation/specterchain.git cd specterchain/chain # Build the binary go build -o specterd ./cmd/specterd # Verify ./specterd version ``` ## Initialize ```bash ./specterd init my-node --chain-id specter-testnet-1 ``` This creates the node's home directory at `~/.specter` with default configuration files. ## Start ```bash ./specterd start ``` ## Systemd service For production nodes, run as a systemd service: ```ini # /etc/systemd/system/specterd.service [Unit] Description=Specter Node After=network.target [Service] Type=simple User=specter ExecStart=/opt/specter/bin/specterd start Restart=always RestartSec=5 Environment=GHOSTMINT_AUTHORIZED_CALLERS=0x35cdaE691037fcBb3ff9D0518725F1ae98d502b7 [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl enable specterd sudo systemctl start specterd sudo journalctl -u specterd -f # view logs ``` ## Data directory structure ``` ~/.specter/ ├── config/ │ ├── config.toml # CometBFT configuration │ ├── app.toml # Application configuration │ ├── genesis.json # Chain genesis │ └── node_key.json # Node identity ├── data/ # Chain data └── keyring-* # Key storage ``` ## Ports | Port | Service | |---|---| | 26656 | CometBFT P2P | | 26657 | CometBFT RPC | | 8545 | EVM JSON-RPC | | 8546 | EVM WebSocket | | 1317 | Cosmos REST API | | 9090 | gRPC | ## Hardware requirements | Component | Minimum | Recommended | |---|---|---| | CPU | 4 cores | 8 cores | | RAM | 8 GB | 16 GB | | Disk | 100 GB SSD | 500 GB NVMe | | Network | 100 Mbps | 1 Gbps | --- # specterd CLI Reference The `specterd` binary is the all-in-one CLI for running nodes, managing keys, and querying the chain. ## Node operations ```bash # Initialize node specterd init --chain-id specter-testnet-1 # Start node specterd start # Show node status specterd status ``` ## Key management ```bash # Create a new key specterd keys add # List keys specterd keys list # Show a key's address specterd keys show --bech val # validator address specterd keys show # account address # Export private key specterd keys export ``` ## Bank module (transfers) ```bash # Send tokens specterd tx bank send aghost --chain-id specter-testnet-1 # Query balance specterd query bank balance
aghost ``` ## Staking ```bash # Delegate specterd tx staking delegate aghost --from # Undelegate specterd tx staking unbond aghost --from # Query validators specterd query staking validators ``` ## Governance ```bash # Submit proposal specterd tx gov submit-proposal --from # Vote specterd tx gov vote yes --from # Query proposals specterd query gov proposals ``` ## EVM queries ```bash # Query EVM parameters specterd q evm params # Get EVM account info specterd q evm account ``` ## Useful flags | Flag | Description | |---|---| | `--chain-id` | Chain identifier (e.g., `specter-testnet-1`) | | `--node` | RPC endpoint (default: `tcp://localhost:26657`) | | `--from` | Key name to sign transactions with | | `--gas` | Gas limit (or `auto` for estimation) | | `--gas-prices` | Gas price (e.g., `1000000000aghost`) | | `--broadcast-mode` | `sync`, `async`, or `block` | --- # Node Configuration Specter nodes use two primary configuration files: `config.toml` (CometBFT) and `app.toml` (application). ## config.toml (CometBFT) Located at `~/.specter/config/config.toml`. Key settings: ```toml [p2p] # P2P listen address laddr = "tcp://0.0.0.0:26656" # Persistent peers (comma-separated) persistent_peers = "node-id@ip:26656" [consensus] # Block time target timeout_commit = "1s" [rpc] # CometBFT RPC laddr = "tcp://0.0.0.0:26657" ``` ## app.toml (Application) Located at `~/.specter/config/app.toml`. Key settings: ```toml # Minimum gas price minimum-gas-prices = "0.000001aghost" # Pruning strategy pruning = "default" # EVM JSON-RPC [json-rpc] enable = true address = "0.0.0.0:8545" ws-address = "0.0.0.0:8546" # API [api] enable = true address = "tcp://0.0.0.0:1317" # gRPC [grpc] enable = true address = "0.0.0.0:9090" # EVM configuration [evm] max-tx-gas-wanted = 10000000 ``` ## Environment variables | Variable | Description | |---|---| | `GHOSTMINT_AUTHORIZED_CALLERS` | Comma-separated list of addresses authorized to call the Ghostmint precompile. Takes precedence over KVStore. | ## Pruning strategies | Strategy | Description | |---|---| | `default` | Keep last 362,880 states (about 10 days at 1 block/s) | | `nothing` | Keep all states (archive node) | | `everything` | Keep only the latest state (minimal disk usage) | | `custom` | Set `pruning-keep-recent` and `pruning-interval` | --- # Genesis Parameters Key chain parameters set at genesis. ## Chain identity | Parameter | Value | |---|---| | Chain ID (Cosmos) | `specter-testnet-1` | | Chain ID (EVM) | `5445` (testnet), `5447` (mainnet) | | Bech32 prefix | `specter` | | Binary | `specterd` | ## Token economics | Parameter | Value | |---|---| | Bond denomination | `aghost` | | Display denomination | GHOST | | Decimals | 18 | | Max supply | 1,000,000,000 GHOST | | Inflation | 0% (no inflation module) | ## Consensus | Parameter | Value | |---|---| | Consensus | CometBFT (BFT Proof of Stake) | | Block time | 1–2 seconds | | Finality | Instant (BFT) | | Max validators | Configurable via governance | ## EVM | Parameter | Value | |---|---| | MaxCodeSize | 64 KB (custom, increased from Ethereum's 24 KB) | | ChainID | 5445 | | Supported precompiles | Standard Ethereum + Ghostmint (0x0808) | ## Governance | Parameter | Default | |---|---| | Min deposit | Configurable | | Voting period | Configurable | | Quorum | Configurable | | Threshold | Configurable | All governance parameters can be modified via on-chain governance proposals. --- # EVM Development Specter provides full Ethereum Virtual Machine compatibility through the Cosmos SDK EVM module. Any Solidity smart contract that runs on Ethereum will run on Specter without modification. ## What's the same as Ethereum - Solidity, Vyper, and any EVM-compatible language - Standard Ethereum JSON-RPC methods - MetaMask, WalletConnect, and all Ethereum wallets - Foundry, Hardhat, Remix, and all development frameworks - ethers.js, viem, web3.js, and all client libraries - OpenZeppelin and all Solidity libraries ## What's different | Feature | Ethereum | Specter | |---|---|---| | Chain ID | 1 | 5445 (testnet) | | Gas token | ETH | GHOST | | Block time | ~12 seconds | 1–2 seconds | | Finality | Probabilistic (~12 min) | Instant (BFT) | | MaxCodeSize | 24 KB | 64 KB | | Consensus | Proof of Stake | CometBFT BFT | | Native mint/burn | Not possible | Via `0x0808` precompile | ### 64 KB code size limit Specter uses a custom go-ethereum fork that increases `MaxCodeSize` from 24 KB to 64 KB. This accommodates the PoseidonT3 library (~55 KB bytecode), which is required for ZK-friendly hashing in smart contracts. Standard contracts are unaffected. ### Ghostmint precompile The precompile at address `0x0808` lets authorized smart contracts mint and burn native GHOST tokens. This is how the Ghost Protocol commit-reveal system works at the consensus layer. See [Ghostmint Precompile](/ghostmint/overview) for details. ### Gas price behavior The Cosmos EVM module may return `0` from `eth_gasPrice`, but the chain enforces a minimum gas price of **1 gwei**. Always set gas price explicitly: ```javascript const tx = await contract.someFunction({ gasPrice: 1_000_000_000n, // 1 gwei }); ``` ## Dual address format Specter supports both Ethereum hex addresses and Cosmos Bech32 addresses: - **Ethereum format**: `0x2bBd88eA213c76f2cA3BAeBaA20F3FA9c4B522e7` - **Cosmos format**: `specter1...` (Bech32 with `specter` prefix) Both formats refer to the same underlying account. Use the Ethereum format for EVM interactions and the Cosmos format for staking, governance, and IBC operations. ## Next steps - [Connecting MetaMask](/evm/connecting-metamask) — wallet setup step-by-step - [Deploying Contracts](/evm/deploying-contracts) — deploy with Foundry - [Foundry Setup](/evm/foundry-setup) — configure `foundry.toml` for Specter - [RPC Endpoints](/evm/rpc-endpoints) — complete JSON-RPC reference --- # Connecting MetaMask ## Manual setup 1. Open MetaMask and click the network selector 2. Click **Add Network** then **Add a network manually** 3. Enter the following: | Field | Value | |---|---| | Network Name | `Specter Testnet` | | New RPC URL | `https://testnet.specterchain.com` | | Chain ID | `5445` | | Currency Symbol | `GHOST` | | Block Explorer URL | `https://explorer.specterchain.com` | 4. Click **Save** ## Programmatic setup Add Specter to MetaMask from your dApp: ```javascript async function addSpecterNetwork() { await window.ethereum.request({ method: 'wallet_addEthereumChain', params: [{ chainId: '0x1545', chainName: 'Specter Testnet', nativeCurrency: { name: 'GHOST', symbol: 'GHOST', decimals: 18, }, rpcUrls: ['https://testnet.specterchain.com'], blockExplorerUrls: ['https://explorer.specterchain.com'], }], }); } ``` ## Adding GhostERC20 tokens To see GhostERC20 token balances in MetaMask, import them as custom tokens: | Token | Contract Address | |---|---| | gUSDC | `0x65c9091a6A45Db302a343AF460657C298FAA222D` | | gWETH | `0x923295a3e3bE5eDe29Fc408A507dA057ee044E81` | | gLABS | `0x062f8a68f6386c1b448b3379abd369825bec9aa2` | All GhostERC20 tokens use 18 decimals and are standard ERC20-compatible. ## WalletConnect For WalletConnect integration, register a project at [WalletConnect Cloud](https://cloud.walletconnect.com/) and use your project ID: ```javascript const projectId = 'YOUR_WALLETCONNECT_PROJECT_ID'; ``` ## Troubleshooting **MetaMask shows 0 gas price**: This is expected. The Cosmos EVM module may report 0, but the chain enforces 1 gwei minimum. Set gas price explicitly in your transactions. **Transaction stuck pending**: Ensure you're using the correct chain ID (`5445`) and RPC URL. Try resetting the account in MetaMask (Settings > Advanced > Clear activity tab data). **Nonce too low**: If you've been testing and restarting, MetaMask may have a stale nonce. Reset the account to fix this. --- # Deploying Contracts Specter supports any EVM-compatible deployment method. This guide covers Foundry (recommended) and direct RPC deployment. ## With Foundry ### Basic deployment ```bash forge create src/MyContract.sol:MyContract \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` ### With constructor arguments ```bash forge create src/MyToken.sol:MyToken \ --constructor-args "My Token" "MTK" 18 \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` ### With a keystore file ```bash KPWD=$(cat /path/to/password-file) PK=$(cast wallet decrypt-keystore /path/to/keystore \ --unsafe-password "$KPWD" | grep "private key") forge create src/MyContract.sol:MyContract \ --private-key $PK \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 ``` ### Deployment scripts ```solidity // script/Deploy.s.sol pragma solidity ^0.8.24; contract DeployScript is Script { function run() external { vm.startBroadcast(); new MyContract(); vm.stopBroadcast(); } } ``` ```bash forge script script/Deploy.s.sol \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --broadcast \ --private-key $PRIVATE_KEY ``` ## Interacting with deployed contracts ### Read a value ```bash cast call $CONTRACT_ADDRESS "balanceOf(address)(uint256)" $WALLET_ADDRESS \ --rpc-url https://testnet.specterchain.com ``` ### Send a transaction ```bash cast send $CONTRACT_ADDRESS "transfer(address,uint256)" $RECIPIENT $AMOUNT \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` ### Get a transaction receipt ```bash cast receipt $TX_HASH --rpc-url https://testnet.specterchain.com ``` ## Deploying Ghost Protocol contracts If your contract needs to interact with Ghost Protocol contracts (CommitRevealVault, GhostERC20, etc.), you'll need to link the PoseidonT3 library. See [Foundry Setup](/evm/foundry-setup) for the library linking configuration. Contracts that call the Ghostmint precompile at `0x0808` must be authorized via governance. Only the `NativeAssetHandler` contract is currently authorized. See [Ghostmint Authorization](/ghostmint/authorization). ## Gas considerations - **Minimum gas price**: 1 gwei (1,000,000,000 wei) - **ZK proof verification**: ~200K gas (Groth16 pairing check via `ecPairing` precompile) - **Commit operation**: ~150K gas (Merkle tree insertion + event emission) - **Reveal operation**: ~350K gas (proof verification + token mint + nullifier registration) --- # Foundry Setup Configure Foundry for Specter development, including the PoseidonT3 library linking required by Ghost Protocol contracts. ## Basic `foundry.toml` For standard contracts that don't use Ghost Protocol: ```toml [profile.default] src = "src" out = "out" libs = ["lib"] solc_version = "0.8.24" ``` ## Ghost Protocol `foundry.toml` For contracts that use Poseidon hashing or interact with Ghost Protocol: ```toml [profile.default] src = "src" out = "out" libs = ["lib"] solc_version = "0.8.24" via_ir = true remappings = [ "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", "poseidon-solidity/=lib/poseidon-solidity/contracts/", ] libraries = [ "lib/poseidon-solidity/contracts/PoseidonT3.sol:PoseidonT3:0xa786eDD407eb9EbaCA5E624B7Ee7C31E3b7f9521", ] [rpc_endpoints] specter-testnet = "https://testnet.specterchain.com" ``` The `libraries` directive is required. PoseidonT3 is a pre-deployed library at `0xa786eDD407eb9EbaCA5E624B7Ee7C31E3b7f9521` on testnet. Without this directive, Foundry will fail to link the library and deployment will fail with unresolved references. ### Why `via_ir = true` The `via_ir` flag enables the Yul intermediate representation pipeline. Ghost Protocol contracts use it for optimization. If you're building contracts that import from the Ghost Protocol contract source, you must enable this flag. ## Installing dependencies ```bash # OpenZeppelin contracts forge install OpenZeppelin/openzeppelin-contracts # Poseidon library (required for Ghost Protocol integration) forge install Specter-Foundation/poseidon-solidity ``` ## Building and testing ```bash # Compile forge build # Run tests forge test # Run tests with verbosity forge test -vvv # Gas report forge test --gas-report ``` ## Key deployed library | Library | Address | Size | |---|---|---| | PoseidonT3 | `0xa786eDD407eb9EbaCA5E624B7Ee7C31E3b7f9521` | ~55 KB | The PoseidonT3 library is the on-chain implementation of the Poseidon hash function (arity 2). It is used by CommitmentTree, CommitRevealVault, and all contracts that compute or verify Poseidon commitments. --- # RPC Endpoints Specter exposes three RPC interfaces: Ethereum JSON-RPC, Ethereum WebSocket, and CometBFT RPC. ## Ethereum JSON-RPC **URL**: `https://testnet.specterchain.com` Standard Ethereum JSON-RPC over HTTPS. Compatible with all Ethereum tooling. ### Supported methods All standard Ethereum JSON-RPC methods are supported: | Method | Description | |---|---| | `eth_blockNumber` | Current block number | | `eth_getBalance` | Account balance | | `eth_getTransactionCount` | Account nonce | | `eth_sendRawTransaction` | Submit signed transaction | | `eth_call` | Execute read-only call | | `eth_estimateGas` | Estimate gas for transaction | | `eth_getTransactionReceipt` | Get transaction receipt | | `eth_getLogs` | Query event logs | | `eth_getCode` | Get contract bytecode | | `eth_getStorageAt` | Read contract storage | | `eth_gasPrice` | Get current gas price | | `eth_chainId` | Returns `0x1545` (5445) | | `net_version` | Returns `5445` | ### Example requests ```bash # Get latest block number curl -X POST https://testnet.specterchain.com \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' # Get balance curl -X POST https://testnet.specterchain.com \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xYOUR_ADDRESS","latest"],"id":1}' ``` ## Ethereum WebSocket **URL**: `wss://testnet.specterchain.com/ws` Use WebSocket for real-time event subscriptions. ```javascript const client = createPublicClient({ transport: webSocket('wss://testnet.specterchain.com/ws'), }); // Subscribe to new blocks const unwatch = client.watchBlockNumber({ onBlockNumber: (blockNumber) => console.log(blockNumber), }); // Subscribe to contract events const unwatch2 = client.watchContractEvent({ address: '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', abi: commitRevealVaultAbi, eventName: 'Committed', onLogs: (logs) => console.log(logs), }); ``` ## CometBFT RPC **URL**: `https://testnet.specterchain.com/rpc/` The CometBFT RPC provides access to consensus-level data (validators, blocks, evidence) and Cosmos-specific queries. ```bash # Get node status curl https://testnet.specterchain.com/rpc/status # Get latest block curl https://testnet.specterchain.com/rpc/block # Get validators curl https://testnet.specterchain.com/rpc/validators ``` ## Rate limits There are no hard rate limits on the public testnet RPC, but excessive requests may be throttled. For production applications, consider running your own node. ## Gas price note `eth_gasPrice` may return `0` on Specter. This is a Cosmos EVM behavior. The chain enforces a minimum gas price of **1 gwei**. Always set gas price explicitly: ```bash cast send $CONTRACT "myFunction()" \ --gas-price 1000000000 \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` --- # Ghost Protocol The Ghost Protocol is Specter's core privacy primitive — a commit-reveal system that uses zero-knowledge proofs to break the link between depositing and withdrawing value. ## How it works ### The key insight Tokens are not transferred or escrowed — they are **destroyed** on commit and **created fresh** on reveal. There is no pool, no escrow contract, and no link between the two operations. The only thing connecting them is a zero-knowledge proof that says "I know the preimage of some commitment in the tree." ## The flow in detail ### Step 1: Commit (burn) The user calls `commit()` or `commitNative()` on the CommitRevealVault. The vault: 1. Burns the tokens via the Ghostmint precompile (`BankKeeper.BurnCoins()`) 2. Computes a Poseidon hash commitment from the user's secret inputs 3. Inserts the commitment into the Merkle tree 4. Emits a `Committed` event ### Step 2: Root update The root updater relayer service watches for `CommitmentAdded` events, rebuilds the off-chain Merkle tree, and submits the new root on-chain. This takes 5–15 seconds. ### Step 3: Reveal (mint) The user generates a Groth16 ZK proof proving they know the preimage of some commitment in the tree — without revealing which one. The vault: 1. Verifies the proof against the on-chain verifier 2. Checks the nullifier hasn't been spent (prevents double-reveal) 3. Registers the nullifier 4. Mints fresh tokens via the Ghostmint precompile (`BankKeeper.MintCoins()`) ## Privacy properties | Property | How it's achieved | |---|---| | **Sender privacy** | Tokens are burned, not sent. No outgoing transfer to trace. | | **Receiver privacy** | Tokens are minted fresh. No incoming transfer to trace. | | **Amount privacy** | Partial reveals with change commitments obscure amounts. | | **Unlinkability** | The ZK proof proves membership in the full tree without revealing which leaf. The anonymity set is all commitments in the tree. | | **Double-spend prevention** | Nullifiers derived from user secrets are registered on-chain. Same commitment cannot be revealed twice. | ## What can you commit? The Ghost Protocol is data-agnostic. The commitment structure stores: - **secret** — user's private key for this commitment - **nullifierSecret** — used to derive the unique nullifier - **amount** — the value being committed - **tokenId** — hash identifying the token type - **blinding** — random blinding factor - **policyId** — optional policy contract address - **policyParamsHash** — optional policy parameters Any data that fits this structure can be committed. Native GHOST, GhostERC20 tokens, and arbitrary data types all use the same circuit. ## Next steps - [Commit Flow](/ghost-protocol/commit-flow) — detailed commit process with code - [Reveal Flow](/ghost-protocol/reveal-flow) — proof generation and reveal - [Commitment Structure](/ghost-protocol/commitment-structure) — Poseidon hash fields - [Integration Guide](/ghost-protocol/integration-guide) — end-to-end tutorial --- # Commit Flow The commit operation destroys tokens and stores a cryptographic commitment in the Merkle tree. ## Committing native GHOST ```solidity interface ICommitRevealVault { function commitNative( bytes32 commitment, bytes32 quantumCommitment ) external payable; } ``` Send GHOST as `msg.value`. The vault burns the tokens and inserts the commitment. ### Example with Foundry ```bash # Commit 1 GHOST cast send 0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70 \ "commitNative(bytes32,bytes32)" \ $COMMITMENT \ 0x0000000000000000000000000000000000000000000000000000000000000000 \ --value 1ether \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` ### Example with ethers.js ```javascript const vault = new ethers.Contract( '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', ['function commitNative(bytes32 commitment, bytes32 quantumCommitment) payable'], signer ); const tx = await vault.commitNative( commitment, // Poseidon hash of your secret inputs ethers.ZeroHash, // quantum commitment (0x0 if not using quantum resistance) { value: ethers.parseEther('1'), gasPrice: 1_000_000_000n } ); await tx.wait(); ``` ## Committing ERC20 tokens ```solidity interface ICommitRevealVault { function commit( address token, uint256 amount, bytes32 commitment, bytes32 quantumCommitment ) external; } ``` First approve the vault to spend your tokens, then call `commit()`: ```javascript // Approve const token = new ethers.Contract(tokenAddress, ['function approve(address,uint256)'], signer); await (await token.approve('0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', amount)).wait(); // Commit const vault = new ethers.Contract( '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', ['function commit(address,uint256,bytes32,bytes32)'], signer ); await (await vault.commit(tokenAddress, amount, commitment, ethers.ZeroHash)).wait(); ``` ## Committing with a policy ```solidity interface ICommitRevealVault { function commitNativeWithPolicy( bytes32 commitment, bytes32 quantumCommitment, address policy, bytes32 policyParamsHash ) external payable; function commitWithPolicy( address token, uint256 amount, bytes32 commitment, bytes32 quantumCommitment, address policy, bytes32 policyParamsHash ) external; } ``` Policies bind enforceable conditions to the commitment. The policy contract's `validate()` function is called during reveal. See [Policy System](/policies/overview). ## Computing the commitment The commitment is a Poseidon hash of 5 or 7 fields: ``` commitment = Poseidon5(secret, nullifierSecret, tokenIdHash, amount, blinding) ``` Or with policy: ``` commitment = Poseidon7(secret, nullifierSecret, tokenIdHash, amount, blinding, policyId, policyParamsHash) ``` You can compute this client-side with snarkjs/circomlibjs, or via the commitment relayer: ```bash curl -X POST https://relayer.specterchain.com/api/commitment/compute \ -H "Content-Type: application/json" \ -H "X-HMAC-Signature: $HMAC_SIG" \ -d '{ "secret": "0x...", "nullifierSecret": "0x...", "blinding": "0x...", "tokenIdHash": "0x...", "amount": "1000000000000000000" }' ``` ## What happens on-chain 1. The vault verifies the token is authorized via AssetGuard 2. For ERC20: calls `token.burn(from, amount)`. For native: calls precompile `burnNativeFrom()` 3. Calls `commitmentTree.insert(commitment)` to add the leaf 4. Records `totalCommitted[token] += amount` 5. If policy is specified, stores `commitmentPolicies[commitment] = policy` 6. Emits `Committed(commitment, token, amount)` or `CommittedWithPolicy(...)` ## Rate limiting A cooldown period (default 5 seconds) prevents rapid sequential commits from the same address. This is enforced via `lastCommitTime` mapping and `commitCooldown` parameter. --- # Reveal Flow The reveal operation proves knowledge of a commitment's preimage using a Groth16 ZK proof and mints fresh tokens to the recipient. ## The reveal function ```solidity interface ICommitRevealVault { function reveal( address token, uint256[8] calldata proof, uint256[] calldata publicInputs, bytes32 commitment, bytes calldata quantumProof, bytes32 changeQuantumCommitment, bytes calldata policyParams ) external; } ``` ### Parameters | Parameter | Description | |---|---| | `token` | Token address (or `address(0)` for native GHOST) | | `proof` | Groth16 proof (8 uint256 values = 3 EC points) | | `publicInputs` | 8 public circuit inputs (see below) | | `commitment` | The original commitment being revealed | | `quantumProof` | Quantum resistance proof (empty bytes if unused) | | `changeQuantumCommitment` | Quantum commitment for change (zero hash if unused) | | `policyParams` | ABI-encoded policy parameters (empty if no policy) | ### Public inputs (8 values) | Index | Field | Description | |---|---|---| | 0 | `root` | Merkle tree root the proof was generated against | | 1 | `nullifier` | Derived nullifier (prevents double-reveal) | | 2 | `withdrawAmount` | Amount being withdrawn (in aghost) | | 3 | `recipient` | Address receiving the minted tokens | | 4 | `changeCommitment` | New commitment for remaining funds | | 5 | `tokenId` | Token identifier hash | | 6 | `policyId` | Policy contract address (0 if none) | | 7 | `policyParamsHash` | Hash of policy parameters (0 if none) | ## Step-by-step reveal process ### 1. Generate the ZK proof Using the proof relayer: ```bash curl -X POST https://relayer.specterchain.com/api/proof/generate \ -H "Content-Type: application/json" \ -H "X-HMAC-Signature: $HMAC_SIG" \ -d '{ "secret": "0x...", "nullifierSecret": "0x...", "amount": "1000000000000000000", "blinding": "0x...", "tokenIdHash": "0x...", "recipient": "0xRECIPIENT_ADDRESS", "withdrawAmount": "1000000000000000000", "newBlinding": "0x..." }' ``` Or generate client-side with snarkjs (see [Generating Proofs](/zk-proofs/generating-proofs)). ### 2. Submit the reveal transaction ```javascript const vault = new ethers.Contract( '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', ['function reveal(address,uint256[8],uint256[],bytes32,bytes,bytes32,bytes)'], signer ); const tx = await vault.reveal( ethers.ZeroAddress, // native GHOST proof, // 8 uint256 values from proof generation publicInputs, // 8 public inputs originalCommitment, // the commitment being revealed '0x', // no quantum proof ethers.ZeroHash, // no quantum change commitment '0x', // no policy params { gasPrice: 1_000_000_000n } ); await tx.wait(); ``` ## What happens on-chain 1. **Root check** — verifies `publicInputs[0]` (root) is in the commitment tree's history window (last 100 roots) 2. **Nullifier check** — verifies `publicInputs[1]` (nullifier) has not been spent 3. **Proof verification** — calls the Groth16ProofVerifier to verify the proof against public inputs 4. **Policy validation** — if the commitment has a policy, calls `policy.validate()` via staticcall with 100K gas cap 5. **Nullifier registration** — marks the nullifier as spent in the NullifierRegistry 6. **Token minting** — calls the Ghostmint precompile to mint fresh tokens to the recipient 7. **Change commitment** — if `changeCommitment != 0`, inserts it into the tree for the remaining balance 8. **Event emission** — emits `Revealed(commitment, token, amount, recipient)` ## Partial reveals and change You don't have to reveal the full committed amount. For example, if you committed 10 GHOST, you can reveal 3 GHOST and create a change commitment for 7 GHOST: - `withdrawAmount` = 3 GHOST - `changeCommitment` = Poseidon hash of a new commitment with 7 GHOST - The circuit verifies: `withdrawAmount + changeAmount == originalAmount` The change commitment goes back into the tree and can be revealed later with a new proof. ## Common errors | Error | Cause | |---|---| | `InvalidRoot` | The root used in the proof is no longer in the history window. Generate a fresh proof. | | `NullifierAlreadySpent` | This commitment has already been revealed. | | `InvalidProof` | The proof is invalid — check inputs match the commitment preimage exactly. | | `PolicyValidationFailed` | The reveal policy's `validate()` returned false. | | `InvalidAmount` | Withdraw amount doesn't match the proof's public inputs. | --- # Commitment Structure Every commitment in the Ghost Protocol is a Poseidon hash of a structured preimage. Understanding the fields is essential for generating valid proofs. ## Basic commitment (no policy) ``` commitment = Poseidon5(secret, nullifierSecret, tokenIdHash, amount, blinding) ``` | Field | Type | Description | |---|---|---| | `secret` | Field element | User's private key for this commitment. Only the user knows this. | | `nullifierSecret` | Field element | Used to derive the nullifier. Must be unique per commitment. | | `tokenIdHash` | Field element | `keccak256(tokenAddress)` truncated to fit the BN254 field. Identifies the token type. | | `amount` | uint256 | The amount committed, in base units (aghost for native GHOST). | | `blinding` | Field element | Random blinding factor. Prevents commitment grinding attacks. | ## Policy commitment ``` commitment = Poseidon7(secret, nullifierSecret, tokenIdHash, amount, blinding, policyId, policyParamsHash) ``` | Additional Field | Type | Description | |---|---|---| | `policyId` | address (as field) | The reveal policy contract address. | | `policyParamsHash` | Field element | Poseidon hash of the policy parameters. | These fields are bound inside the ZK circuit — you cannot change the policy after commit without invalidating the proof. ## Derived values ### Nullifier ``` nullifier = Poseidon2(nullifierSecret, leafIndex) ``` The nullifier uniquely identifies a spent commitment. It is derived from `nullifierSecret` and the leaf's position in the Merkle tree. Since the `nullifierSecret` is private, an observer cannot compute which commitment a nullifier corresponds to. ### Token ID hash ``` tokenIdHash = uint256(keccak256(tokenAddress)) % BN254_FIELD_MODULUS ``` For native GHOST, the token address is `address(0)`. ## Field constraints All field elements must be less than the BN254 scalar field modulus: ``` p = 21888242871839275222246405745257275088548364400416034343698204186575808495617 ``` Values exceeding this will cause the circuit to reject the proof. ## Generating commitment inputs ### Client-side (JavaScript) ```javascript const poseidon = await buildPoseidon(); // Generate random field elements const secret = BigInt('0x' + crypto.getRandomValues(new Uint8Array(31)).reduce((s, b) => s + b.toString(16).padStart(2, '0'), '')); const nullifierSecret = BigInt('0x' + crypto.getRandomValues(new Uint8Array(31)).reduce((s, b) => s + b.toString(16).padStart(2, '0'), '')); const blinding = BigInt('0x' + crypto.getRandomValues(new Uint8Array(31)).reduce((s, b) => s + b.toString(16).padStart(2, '0'), '')); const amount = BigInt('1000000000000000000'); // 1 GHOST const tokenIdHash = BigInt(0); // native GHOST // Compute commitment const commitment = poseidon([secret, nullifierSecret, tokenIdHash, amount, blinding]); ``` ### Via commitment relayer ```bash curl -X POST https://relayer.specterchain.com/api/commitment/compute \ -H "Content-Type: application/json" \ -H "X-HMAC-Signature: $HMAC_SIG" \ -d '{ "secret": "123456789", "nullifierSecret": "987654321", "blinding": "111222333", "tokenIdHash": "0", "amount": "1000000000000000000" }' ``` Never share your `secret` or `nullifierSecret`. These values are what prove ownership of the commitment. Anyone who has them can reveal your tokens. ## Storage - Commitments are stored as leaves in the CommitmentTree (Merkle tree, depth 20, ~1M capacity) - Spent nullifiers are stored in the NullifierRegistry (append-only) - Policy bindings are stored in the CommitRevealVault's `commitmentPolicies` mapping --- # Ghost Protocol Integration Guide End-to-end walkthrough: commit GHOST tokens, wait for the Merkle root update, generate a ZK proof, and reveal tokens to a fresh address. ## Prerequisites - A funded wallet on Specter testnet ([get GHOST from faucet](/testnet/faucet)) - Node.js 20+ for client-side proof generation - The commitment secrets must be stored securely — if you lose them, the committed tokens are unrecoverable ## Step 1: Generate commitment secrets ```javascript function randomFieldElement() { // Generate a random 31-byte value (fits within BN254 field) return BigInt('0x' + crypto.randomBytes(31).toString('hex')); } const secret = randomFieldElement(); const nullifierSecret = randomFieldElement(); const blinding = randomFieldElement(); const amount = BigInt('1000000000000000000'); // 1 GHOST const tokenIdHash = BigInt(0); // native GHOST = address(0) // SAVE THESE VALUES SECURELY — you need them for reveal console.log('secret:', secret.toString()); console.log('nullifierSecret:', nullifierSecret.toString()); console.log('blinding:', blinding.toString()); ``` ## Step 2: Compute the commitment hash ```javascript const poseidon = await buildPoseidon(); const F = poseidon.F; const commitment = poseidon([secret, nullifierSecret, tokenIdHash, amount, blinding]); const commitmentHex = '0x' + F.toString(commitment, 16).padStart(64, '0'); console.log('commitment:', commitmentHex); ``` ## Step 3: Submit the commit transaction ```javascript const provider = new ethers.JsonRpcProvider('https://testnet.specterchain.com'); const signer = new ethers.Wallet(PRIVATE_KEY, provider); const vault = new ethers.Contract( '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', ['function commitNative(bytes32,bytes32) payable'], signer ); const tx = await vault.commitNative( commitmentHex, ethers.ZeroHash, // no quantum commitment { value: amount, gasPrice: 1_000_000_000n } ); const receipt = await tx.wait(); console.log('Commit tx:', receipt.hash); ``` ## Step 4: Wait for root update The root updater relayer processes new commitments and updates the on-chain Merkle root. Poll until it's updated: ```javascript const tree = new ethers.Contract( '0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87', ['function getLastRoot() view returns (bytes32)'], provider ); // Wait for root to update (typically 5-15 seconds) let previousRoot = await tree.getLastRoot(); let newRoot; do { await new Promise(r => setTimeout(r, 5000)); newRoot = await tree.getLastRoot(); } while (newRoot === previousRoot); console.log('Root updated:', newRoot); ``` ## Step 5: Build the Merkle proof Fetch all commitments and build the tree locally to get the Merkle path: ```javascript // Fetch all CommitmentAdded events const treeContract = new ethers.Contract( '0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87', ['event CommitmentAdded(bytes32 indexed commitment, uint256 index)'], provider ); const events = await treeContract.queryFilter('CommitmentAdded'); const leaves = events.map(e => e.args.commitment); // Find your leaf index const leafIndex = leaves.findIndex(l => l === commitmentHex); ``` ## Step 6: Generate the ZK proof Using the proof relayer API: ```javascript const proofResponse = await fetch('https://relayer.specterchain.com/api/proof/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-HMAC-Signature': hmacSignature, }, body: JSON.stringify({ secret: secret.toString(), nullifierSecret: nullifierSecret.toString(), amount: amount.toString(), blinding: blinding.toString(), tokenIdHash: '0', recipient: RECIPIENT_ADDRESS, withdrawAmount: amount.toString(), newBlinding: '0', // full reveal, no change }), }); const { proof, publicInputs } = await proofResponse.json(); ``` ## Step 7: Submit the reveal transaction ```javascript const revealSigner = new ethers.Wallet(RECIPIENT_PRIVATE_KEY, provider); const revealVault = new ethers.Contract( '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', ['function reveal(address,uint256[8],uint256[],bytes32,bytes,bytes32,bytes)'], revealSigner ); const tx = await revealVault.reveal( ethers.ZeroAddress, // native GHOST proof, // 8 uint256 values publicInputs, // 8 public inputs commitmentHex, // original commitment '0x', // no quantum proof ethers.ZeroHash, // no quantum change commitment '0x', // no policy params { gasPrice: 1_000_000_000n } ); const receipt = await tx.wait(); console.log('Reveal tx:', receipt.hash); console.log('Tokens minted to:', RECIPIENT_ADDRESS); ``` ## What just happened 1. You **destroyed** 1 GHOST from your original address (commit) 2. You **created** 1 GHOST at a fresh recipient address (reveal) 3. No on-chain observer can link the two operations 4. The anonymity set is every commitment in the Merkle tree ## Security reminders - **Store secrets securely** — `secret`, `nullifierSecret`, and `blinding` are the only way to reveal committed tokens - **Use different addresses** — reveal to a fresh address for maximum privacy - **Don't reuse secrets** — every commitment must have unique `secret` and `nullifierSecret` values - **Verify the root** — ensure the root used in your proof is still in the on-chain history window (last 100 roots) --- # Smart Contracts Overview The Ghost Protocol is implemented as a set of interrelated Solidity smart contracts. All contracts use Solidity 0.8.24 and are built with Foundry. ## Architecture ## Core contracts | Contract | Purpose | Address | |---|---|---| | [CommitRevealVault](/contracts/commit-reveal-vault) | Orchestrates all commit and reveal operations | `0x443434...6c70` | | [CommitmentTree](/contracts/commitment-tree) | Stores commitments in a Merkle tree (depth 20) | `0xB7E37E...4D87` | | [NullifierRegistry](/contracts/nullifier-registry) | Tracks spent nullifiers to prevent double-reveals | `0x0987cc...1C58b` | | [NativeAssetHandler](/contracts/native-asset-handler) | Bridges between EVM contracts and the Ghostmint precompile | `0x35cdaE...d502b7` | | [AssetGuard](/contracts/asset-guard) | Whitelist of authorized tokens for commit/reveal | `0xa1E7d5...A8A1` | ## Token contracts | Contract | Purpose | Address | |---|---|---| | [GhostERC20](/contracts/ghost-erc20) | Privacy-enabled ERC20 with vault-controlled mint/burn | Various | | [GhostERC20Factory](/contracts/ghost-erc20-factory) | Deploys and registers new GhostERC20 tokens | `0x925B54...88C8E` | ## Verification | Contract | Purpose | Address | |---|---|---| | Groth16ProofVerifier | Verifies Groth16 ZK proofs on-chain | `0x2C8Fb6...E5bd2` | ## Policy contracts | Contract | Purpose | Address | |---|---|---| | [PolicyRegistry](/policies/policy-registry) | Maps commitments to their reveal policies | `0x54F039...396aB` | | [TimelockExpiry](/policies/timelock-expiry) | Time-locked reveals | `0xae2307...39d6` | | [DestinationRestriction](/policies/destination-restriction) | Restrict reveals to specific addresses | `0x899E9f...9A3ee00` | | [ThresholdWitness](/policies/threshold-witness) | Multi-party approval for reveals | `0xa89638...fEBD7` | See [Deployed Addresses](/contracts/deployed-addresses) for the full address table. --- # CommitRevealVault The central orchestrator for all Ghost Protocol operations. All commits and reveals flow through this contract. **Address**: `0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70` ## Interface ```solidity interface ICommitRevealVault { // Commit native GHOST function commitNative(bytes32 commitment, bytes32 quantumCommitment) external payable; // Commit ERC20 tokens function commit(address token, uint256 amount, bytes32 commitment, bytes32 quantumCommitment) external; // Commit with policy function commitNativeWithPolicy( bytes32 commitment, bytes32 quantumCommitment, address policy, bytes32 policyParamsHash ) external payable; function commitWithPolicy( address token, uint256 amount, bytes32 commitment, bytes32 quantumCommitment, address policy, bytes32 policyParamsHash ) external; // Reveal tokens function reveal( address token, uint256[8] calldata proof, uint256[] calldata publicInputs, bytes32 commitment, bytes calldata quantumProof, bytes32 changeQuantumCommitment, bytes calldata policyParams ) external; // View functions function commitmentTree() external view returns (address); function nullifierRegistry() external view returns (address); function proofVerifier() external view returns (address); function assetGuard() external view returns (address); function nativeHandler() external view returns (address); function paused() external view returns (bool); function totalCommitted(address token) external view returns (uint256); function tokenIdHashes(address token) external view returns (bytes32); function commitmentPolicies(bytes32 commitment) external view returns (address); // Events event Committed(bytes32 indexed commitment, address indexed token, uint256 amount); event Revealed(bytes32 indexed commitment, address indexed token, uint256 amount, address recipient); event CommittedWithPolicy(bytes32 indexed commitment, address indexed token, uint256 amount, address policy); } ``` ## Constants | Constant | Value | Description | |---|---|---| | `DEFAULT_COMMIT_COOLDOWN` | 5 seconds | Minimum time between commits from same address | | `PUBLIC_INPUT_COUNT` | 8 | Number of public inputs for the ZK circuit | ## Usage examples ### Query total committed ```bash cast call 0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70 \ "totalCommitted(address)(uint256)" 0x0000000000000000000000000000000000000000 \ --rpc-url https://testnet.specterchain.com ``` ### Check if vault is paused ```bash cast call 0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70 \ "paused()(bool)" \ --rpc-url https://testnet.specterchain.com ``` ### Get token ID hash ```bash cast call 0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70 \ "tokenIdHashes(address)(bytes32)" $TOKEN_ADDRESS \ --rpc-url https://testnet.specterchain.com ``` ## Security model - **ReentrancyGuard** — all state-changing functions are non-reentrant - **Ownable** — admin functions restricted to owner (deployer) - **Pausable** — owner can pause all operations in emergencies - **AssetGuard** — only whitelisted tokens can be committed/revealed - **Rate limiting** — cooldown between commits from the same address --- # CommitmentTree An append-only Merkle tree that stores all Ghost Protocol commitments. Uses Poseidon hashing for ZK-friendly proofs. **Address**: `0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87` ## Parameters | Parameter | Value | |---|---| | Tree depth | 20 | | Capacity | ~1,048,576 leaves | | Hash function | PoseidonT3 | | Root history | Last 100 roots | ## Key functions ```solidity // Insert a new commitment (only callable by vault) function insert(bytes32 commitment) external; // Get the most recent root function getLastRoot() external view returns (bytes32); // Check if a root is in the history window function isKnownRoot(bytes32 root) external view returns (bool); // Get the current leaf index (number of commitments) function nextIndex() external view returns (uint256); ``` ## Root history The tree maintains a circular buffer of the last 100 roots. When a new commitment is inserted, the root is recalculated and added to the buffer. Reveal proofs must use a root from this window — proofs against older roots will fail. ## Usage examples ```bash # Get current root cast call 0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87 \ "getLastRoot()(bytes32)" \ --rpc-url https://testnet.specterchain.com # Check if root is known cast call 0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87 \ "isKnownRoot(bytes32)(bool)" $ROOT \ --rpc-url https://testnet.specterchain.com # Get number of commitments cast call 0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87 \ "nextIndex()(uint256)" \ --rpc-url https://testnet.specterchain.com ``` ## Events ```solidity event CommitmentAdded(bytes32 indexed commitment, uint256 index); event RootUpdated(bytes32 indexed root); ``` The root updater relayer watches `CommitmentAdded` events to rebuild the off-chain tree and submit updated roots. ## Off-chain tree The full Merkle tree is maintained off-chain by relayer services. This design saves gas — inserting a leaf on-chain only requires storing the commitment, not recomputing the entire tree. The relayer rebuilds the tree, computes the new root, and calls `updateRoot()`. Clients building Merkle proofs for reveals must reconstruct the tree by indexing all `CommitmentAdded` events. --- # NullifierRegistry An append-only registry of spent nullifiers. Prevents double-reveals — each commitment can only be revealed once. **Address**: `0x0987cc3dE6f76c4c8834Dc6205De24968091C58b` ## How nullifiers work A nullifier is derived from the user's `nullifierSecret` and the leaf's position in the Merkle tree: ``` nullifier = Poseidon2(nullifierSecret, leafIndex) ``` Since `nullifierSecret` is private, no observer can determine which commitment a nullifier corresponds to. But if the same commitment is revealed twice, it would produce the same nullifier, which the registry rejects. ## Key functions ```solidity // Check if a nullifier has been spent function isSpent(bytes32 nullifier) external view returns (bool); // Register a nullifier (only callable by vault) function spend(bytes32 nullifier) external; ``` ## Usage examples ```bash # Check if a nullifier is spent cast call 0x0987cc3dE6f76c4c8834Dc6205De24968091C58b \ "isSpent(bytes32)(bool)" $NULLIFIER \ --rpc-url https://testnet.specterchain.com ``` ## Events ```solidity event NullifierSpent(bytes32 indexed nullifier); ``` --- # NativeAssetHandler The bridge between EVM smart contracts and the Ghostmint precompile at `0x0808`. Handles minting and burning native GHOST tokens on behalf of the CommitRevealVault. **Address**: `0x35cdaE691037fcBb3ff9D0518725F1ae98d502b7` ## Why it exists Smart contracts cannot directly call the Ghostmint precompile — only governance-authorized contracts are permitted. The NativeAssetHandler is the single authorized caller, and it restricts its own callers to the CommitRevealVault. ``` CommitRevealVault → NativeAssetHandler → Ghostmint Precompile (0x0808) → BankKeeper ``` ## Interface ```solidity contract NativeAssetHandler { address public constant MINT_PRECOMPILE = 0x0000000000000000000000000000000000000808; address public vault; address public owner; // Set the vault address (one-time, by owner) function setVault(address _vault) external; // Burn native GHOST (payable — receive GHOST as msg.value) function burnNativeFrom(address from, uint256 amount) external payable; // Mint native GHOST to recipient function mintNativeTo(address to, uint256 amount) external; // Accept native tokens from vault receive() external payable; } ``` ## Key behaviors - **Vault-only**: `burnNativeFrom` and `mintNativeTo` can only be called by the vault - **One-time vault setup**: `setVault()` can only be called once by the owner - **Native token flow**: On burn, tokens are forwarded to the precompile. On mint, the precompile creates fresh tokens. ## Usage The NativeAssetHandler is not called directly by developers. It's an internal component of the Ghost Protocol. The CommitRevealVault calls it during: - **Commit**: `burnNativeFrom(user, amount)` — destroys the user's GHOST - **Reveal**: `mintNativeTo(recipient, amount)` — creates fresh GHOST for the recipient ## Security - This is the **only** contract authorized to call the Ghostmint precompile - The authorization is set via the `GHOSTMINT_AUTHORIZED_CALLERS` environment variable on the validator node - Changing the authorized callers requires a governance proposal or validator restart --- # AssetGuard A whitelist contract that controls which tokens can be committed and revealed through the Ghost Protocol. **Address**: `0xa1E7d557333CE14D897510220442d0DEfCf5A8A1` ## Purpose Not every ERC20 token can be used with Ghost Protocol — tokens must be explicitly authorized. AssetGuard maintains the allowlist and is consulted by the CommitRevealVault before every commit operation. ## Key functions ```solidity // Check if a token is authorized function isAuthorized(address token) external view returns (bool); // Authorize a token (owner only) function authorize(address token) external; // Revoke authorization (owner only) function revoke(address token) external; ``` ## Usage ```bash # Check if a token is authorized cast call 0xa1E7d557333CE14D897510220442d0DEfCf5A8A1 \ "isAuthorized(address)(bool)" $TOKEN_ADDRESS \ --rpc-url https://testnet.specterchain.com ``` ## Authorized tokens on testnet - Native GHOST (`address(0)`) — always authorized - gUSDC (`0x65c9091a6A45Db302a343AF460657C298FAA222D`) - gWETH (`0x923295a3e3bE5eDe29Fc408A507dA057ee044E81`) - gLABS (`0x062f8a68f6386c1b448b3379abd369825bec9aa2`) All tokens deployed via GhostERC20Factory are automatically authorized by the factory during deployment. --- # GhostERC20 A privacy-enabled ERC20 token that supports Ghost Protocol commit and reveal operations. GhostERC20 tokens are standard ERC20 tokens with additional mint/burn functions restricted to the CommitRevealVault. ## Interface ```solidity interface IGhostERC20 is IERC20 { // View functions function vault() external view returns (address); function factory() external view returns (address); function tokenIdHash() external view returns (bytes32); function isGhostEnabled() external view returns (bool); // Vault-only functions function mint(address to, uint256 amount) external; function burn(address from, uint256 amount) external; // Factory-only function function enableGhost() external; } ``` ## Standard ERC20 operations GhostERC20 tokens behave like any ERC20 token for standard operations: ```javascript const token = new ethers.Contract(tokenAddress, [ 'function balanceOf(address) view returns (uint256)', 'function transfer(address to, uint256 amount) returns (bool)', 'function approve(address spender, uint256 amount) returns (bool)', 'function allowance(address owner, address spender) view returns (uint256)', ], signer); // Check balance const balance = await token.balanceOf(myAddress); // Transfer tokens await token.transfer(recipient, ethers.parseEther('10')); // Approve vault for commit await token.approve('0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', ethers.parseEther('10')); ``` ## Ghost Protocol operations When a GhostERC20 token is committed through the vault: 1. The vault calls `token.burn(from, amount)` — destroys the tokens 2. A Poseidon commitment is stored in the Merkle tree 3. The `tokenIdHash` identifies this token type in the circuit When revealing: 1. The vault calls `token.mint(recipient, amount)` — creates fresh tokens 2. The nullifier is registered to prevent double-reveal ## Token ID hash Each GhostERC20 has a unique `tokenIdHash` that identifies it in the ZK circuit: ```bash cast call $TOKEN_ADDRESS "tokenIdHash()(bytes32)" \ --rpc-url https://testnet.specterchain.com ``` This hash is used as one of the public inputs to the Groth16 proof, ensuring you can only reveal the same type of token that was committed. ## Deployed tokens | Token | Address | |---|---| | gUSDC | `0x65c9091a6A45Db302a343AF460657C298FAA222D` | | gWETH | `0x923295a3e3bE5eDe29Fc408A507dA057ee044E81` | | gLABS | `0x062f8a68f6386c1b448b3379abd369825bec9aa2` | --- # GhostERC20Factory Deploys new GhostERC20 tokens and registers them with the CommitRevealVault and AssetGuard. **Address**: `0x925B548F059C0B8B6CF7168Efb84881252F88C8E` ## Interface ```solidity interface IGhostERC20Factory { // Deploy a new GhostERC20 token function deployToken( string calldata name, string calldata symbol, uint8 decimals, bytes32 salt ) external returns (address token); // Predict deployment address (CREATE2) function computeTokenAddress( string calldata name, string calldata symbol, uint8 decimals, bytes32 salt ) external view returns (address); // View functions function vault() external view returns (address); function assetGuard() external view returns (address); function isFactoryDeployed(address token) external view returns (bool); function getTokenIdHash(address token) external view returns (bytes32); // Events event TokenDeployed(address indexed token, string name, string symbol, uint8 decimals); } ``` ## Deploying a token ```bash cast send 0x925B548F059C0B8B6CF7168Efb84881252F88C8E \ "deployToken(string,string,uint8,bytes32)(address)" \ "My Ghost Token" "gMTK" 18 \ 0x0000000000000000000000000000000000000000000000000000000000000001 \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` ## What deployment does 1. Deploys a new GhostERC20 contract using CREATE2 (deterministic address) 2. Registers the token's `tokenIdHash` with the CommitRevealVault 3. Authorizes the token in AssetGuard 4. Calls `enableGhost()` on the token to activate vault permissions 5. Emits `TokenDeployed` event ## Predicting the address Since deployment uses CREATE2, you can predict the token address before deploying: ```bash cast call 0x925B548F059C0B8B6CF7168Efb84881252F88C8E \ "computeTokenAddress(string,string,uint8,bytes32)(address)" \ "My Ghost Token" "gMTK" 18 \ 0x0000000000000000000000000000000000000000000000000000000000000001 \ --rpc-url https://testnet.specterchain.com ``` ## Access control Only authorized deployers can create new tokens. The factory owner manages the deployer list: ```solidity function authorizeDeployer(address deployer) external; // owner only function revokeDeployer(address deployer) external; // owner only ``` --- # Deployed Addresses All contract addresses on Specter testnet (Chain ID: 5445). ## Core contracts | Contract | Address | |---|---| | CommitRevealVault | `0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70` | | CommitmentTree | `0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87` | | NullifierRegistry | `0x0987cc3dE6f76c4c8834Dc6205De24968091C58b` | | NativeAssetHandler | `0x35cdaE691037fcBb3ff9D0518725F1ae98d502b7` | | AssetGuard | `0xa1E7d557333CE14D897510220442d0DEfCf5A8A1` | ## Proof verification | Contract | Address | |---|---| | Groth16ProofVerifier | `0x2C8Fb67874E5f380efB995a9Ab59b2Ef327E5bd2` | ## Token contracts | Contract | Address | |---|---| | GhostERC20Factory | `0x925B548F059C0B8B6CF7168Efb84881252F88C8E` | ## Policy contracts | Contract | Address | |---|---| | PolicyRegistry | `0x54F039F586922e1affaF4B812871f365048396aB` | | TimelockExpiry | `0xae2307620840916a06A862A61BF2101d694539d6` | | DestinationRestriction | `0x899E9fa69F092D4B30FC7d4A3896366159A3ee00` | | ThresholdWitness | `0xa89638B084B77965B278dF45EA6b7037C55fEBD7` | ## Open Protocol contracts | Contract | Address | |---|---| | OpenGhostVault | `0x45B022fEB169AF906CaBa8086c977AA7b15faAf1` | | OpenGhostReveal | `0x70BD6eE41507139285e868a46399104305dF1833` | | OpenGhostRevealVerifier | `0x3762d9ED2E7a31bE5043eaC9038c58662f7fD655` | | OpenCommitmentTree | `0xE2b33dB178d6201EDd854ED9163B30dcfECC0c48` | | OpenNullifierRegistry | `0xC1c32f5697d5cfD74c35D1Fd4CF05E1F6d2A90b2` | | OpenGhostKeyVault | `0x4943959c05e028884C1CDd9878c762D785332A67` | | PersistentKeyVault (v2) | `0x683B3ff7795D508Ff1e088a08981580e19af7496` | | AccessProofVerifier (v2) | `0x4C2cA5FFCE417A3914b6531C79b4946117B4aA21` | ## Libraries | Library | Address | |---|---| | PoseidonT3 | `0xa786eDD407eb9EbaCA5E624B7Ee7C31E3b7f9521` | ## Legacy contracts | Contract | Address | |---|---| | PersistentKeyVault (v1) | `0x8Ef4Fb6Aac386a524DE9605a2870414279CB811A` | | AccessProofVerifier (v1) | `0x08C3851f03D9A8200a1C01C4a88Dac2dB8E119B4` | ## GhostERC20 tokens | Token | Address | |---|---| | gUSDC | `0x65c9091a6A45Db302a343AF460657C298FAA222D` | | gWETH | `0x923295a3e3bE5eDe29Fc408A507dA057ee044E81` | | gLABS | `0x062f8a68f6386c1b448b3379abd369825bec9aa2` | ## Precompiles | Precompile | Address | |---|---| | Ghostmint | `0x0000000000000000000000000000000000000808` | ## Key addresses | Role | Address | |---|---| | Deployer | `0x2bBd88eA213c76f2cA3BAeBaA20F3FA9c4B522e7` | | Relayer Operator | `0x42f5f839fA29b9e9AEF0d66A3D2aAc89C4CBF1f1` | | Authorized Precompile Caller | `0x35cdaE691037fcBb3ff9D0518725F1ae98d502b7` (NativeAssetHandler) | --- # Ghostmint Precompile The Ghostmint precompile at address `0x0808` enables smart contracts to mint and burn native GHOST tokens by calling the chain's `BankKeeper` directly from the EVM. This is the consensus-layer mechanism that powers the Ghost Protocol's burn-and-mint model. ## Why a precompile On standard EVM chains, smart contracts cannot create or destroy the native token — they can only transfer it. Specter's custom precompile bridges this gap: ``` Smart Contract → Precompile (0x0808) → BankKeeper.MintCoins() / BurnCoins() ``` This means tokens are genuinely destroyed on commit (not locked in a contract) and genuinely created on reveal (not withdrawn from a pool). ## Address ``` 0x0000000000000000000000000000000000000808 ``` ## Key properties | Property | Value | |---|---| | Supply cap | 1,000,000,000 GHOST | | Authorization | Governance-controlled whitelist | | Invariant check | PreCommit ABCI hook verifies supply delta each block | | Currently authorized | NativeAssetHandler (`0x35cdaE...d502b7`) | ## Supply tracking The precompile tracks cumulative mints and burns: ``` Net supply = totalMinted - totalBurned ``` This value cannot exceed 1 billion GHOST. The chain's `PreCommit` hook verifies this invariant on every block — if a block would violate the cap, it is rejected. ## Next steps - [ABI Reference](/ghostmint/abi-reference) — function signatures and event definitions - [Authorization](/ghostmint/authorization) — how authorized callers are managed - [Usage Examples](/ghostmint/usage-examples) — calling the precompile from Solidity --- # Ghostmint ABI Reference The complete ABI for the Ghostmint precompile at `0x0808`. ## Functions ### `mintNativeTo` Mints native GHOST tokens to a recipient address. ```solidity function mintNativeTo(address recipient, uint256 amount) external returns (bool success); ``` | Parameter | Type | Description | |---|---|---| | `recipient` | `address` | Address to receive the minted tokens | | `amount` | `uint256` | Amount in aghost (base units) | | Returns | `bool` | `true` if successful | ### `burnNativeFrom` Burns native GHOST tokens. The tokens must be sent as `msg.value`. ```solidity function burnNativeFrom(address from, uint256 amount) external payable returns (bool success); ``` | Parameter | Type | Description | |---|---|---| | `from` | `address` | Address the tokens are being burned from | | `amount` | `uint256` | Amount in aghost (must match `msg.value`) | | Returns | `bool` | `true` if successful | ### `totalMinted` (view) Returns the cumulative amount of GHOST minted through the precompile. ```solidity function totalMinted() external view returns (uint256 total); ``` ### `totalBurned` (view) Returns the cumulative amount of GHOST burned through the precompile. ```solidity function totalBurned() external view returns (uint256 total); ``` ## Events ### `NativeMinted` ```solidity event NativeMinted(address indexed recipient, uint256 amount); ``` ### `NativeBurned` ```solidity event NativeBurned(address indexed from, uint256 amount); ``` ## Full ABI JSON ```json [ { "type": "function", "name": "mintNativeTo", "inputs": [ {"name": "recipient", "type": "address"}, {"name": "amount", "type": "uint256"} ], "outputs": [{"name": "success", "type": "bool"}], "stateMutability": "nonpayable" }, { "type": "function", "name": "burnNativeFrom", "inputs": [ {"name": "from", "type": "address"}, {"name": "amount", "type": "uint256"} ], "outputs": [{"name": "success", "type": "bool"}], "stateMutability": "payable" }, { "type": "function", "name": "totalMinted", "inputs": [], "outputs": [{"name": "total", "type": "uint256"}], "stateMutability": "view" }, { "type": "function", "name": "totalBurned", "inputs": [], "outputs": [{"name": "total", "type": "uint256"}], "stateMutability": "view" }, { "type": "event", "name": "NativeMinted", "inputs": [ {"name": "recipient", "type": "address", "indexed": true}, {"name": "amount", "type": "uint256", "indexed": false} ] }, { "type": "event", "name": "NativeBurned", "inputs": [ {"name": "from", "type": "address", "indexed": true}, {"name": "amount", "type": "uint256", "indexed": false} ] } ] ``` ## Querying supply stats ```bash # Total minted cast call 0x0000000000000000000000000000000000000808 \ "totalMinted()(uint256)" \ --rpc-url https://testnet.specterchain.com # Total burned cast call 0x0000000000000000000000000000000000000808 \ "totalBurned()(uint256)" \ --rpc-url https://testnet.specterchain.com ``` --- # Ghostmint Authorization Only governance-authorized contracts can call the Ghostmint precompile. Unauthorized calls revert. ## How authorization works The list of authorized callers is configured via the `GHOSTMINT_AUTHORIZED_CALLERS` environment variable on the validator node. The precompile checks this list on every call. ### Current authorized callers | Contract | Address | Purpose | |---|---|---| | NativeAssetHandler | `0x35cdaE691037fcBb3ff9D0518725F1ae98d502b7` | Commit/reveal bridge | ### Call chain ``` User → CommitRevealVault → NativeAssetHandler → Precompile (0x0808) (not authorized) (authorized) (checks caller) ``` The CommitRevealVault itself is **not** an authorized caller. It delegates to the NativeAssetHandler, which is the single authorized entry point. ## Updating authorized callers Authorization changes require either: 1. **Validator restart** — update the `GHOSTMINT_AUTHORIZED_CALLERS` environment variable and restart the `specterd` service 2. **Governance proposal** — submit an on-chain governance proposal to update the authorized callers stored in the KVStore The environment variable takes precedence over the KVStore. This allows emergency authorization changes without waiting for governance. ### Environment variable format ```bash GHOSTMINT_AUTHORIZED_CALLERS=0x35cdaE691037fcBb3ff9D0518725F1ae98d502b7 ``` Multiple callers can be comma-separated: ```bash GHOSTMINT_AUTHORIZED_CALLERS=0xAddress1,0xAddress2 ``` ## PreCommit invariant The chain's `PreCommit` ABCI hook verifies the supply invariant on every block: ``` totalMinted - totalBurned <= 1,000,000,000 GHOST ``` If a block would violate this constraint, it is rejected. This is a consensus-level safeguard independent of the smart contract logic. ## For developers You cannot directly call the precompile from your smart contracts unless your contract is authorized via governance. To interact with the mint/burn system: 1. Use the **CommitRevealVault** for standard commit/reveal operations 2. Use **GhostERC20** tokens for ERC20 privacy operations 3. If you need custom mint/burn logic, propose a governance authorization for your contract --- # Ghostmint Usage Examples ## Reading supply statistics These are view functions that anyone can call: ```bash # Total GHOST minted through precompile cast call 0x0000000000000000000000000000000000000808 \ "totalMinted()(uint256)" \ --rpc-url https://testnet.specterchain.com # Total GHOST burned through precompile cast call 0x0000000000000000000000000000000000000808 \ "totalBurned()(uint256)" \ --rpc-url https://testnet.specterchain.com ``` ```javascript const client = createPublicClient({ transport: http('https://testnet.specterchain.com'), }); const ghostmintAbi = [ { name: 'totalMinted', type: 'function', inputs: [], outputs: [{ type: 'uint256' }], stateMutability: 'view' }, { name: 'totalBurned', type: 'function', inputs: [], outputs: [{ type: 'uint256' }], stateMutability: 'view' }, ]; const totalMinted = await client.readContract({ address: '0x0000000000000000000000000000000000000808', abi: ghostmintAbi, functionName: 'totalMinted', }); const totalBurned = await client.readContract({ address: '0x0000000000000000000000000000000000000808', abi: ghostmintAbi, functionName: 'totalBurned', }); console.log('Net supply delta:', totalMinted - totalBurned); ``` ## How NativeAssetHandler calls the precompile The NativeAssetHandler is the reference implementation of an authorized precompile caller: ```solidity // Simplified from NativeAssetHandler.sol contract NativeAssetHandler { address public constant MINT_PRECOMPILE = 0x0000000000000000000000000000000000000808; function mintNativeTo(address to, uint256 amount) external onlyVault { (bool success,) = MINT_PRECOMPILE.call( abi.encodeWithSignature("mintNativeTo(address,uint256)", to, amount) ); require(success, "Mint failed"); } function burnNativeFrom(address from, uint256 amount) external payable onlyVault { (bool success,) = MINT_PRECOMPILE.call{value: msg.value}( abi.encodeWithSignature("burnNativeFrom(address,uint256)", from, amount) ); require(success, "Burn failed"); } } ``` ## Writing an authorized caller If your contract were authorized via governance, this is the pattern for calling the precompile: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; interface IGhostmint { function mintNativeTo(address recipient, uint256 amount) external returns (bool); function burnNativeFrom(address from, uint256 amount) external payable returns (bool); } contract MyAuthorizedContract { IGhostmint constant GHOSTMINT = IGhostmint(0x0000000000000000000000000000000000000808); function mintTokens(address to, uint256 amount) external { bool success = GHOSTMINT.mintNativeTo(to, amount); require(success, "Mint failed"); } function burnTokens(uint256 amount) external payable { bool success = GHOSTMINT.burnNativeFrom{value: msg.value}(msg.sender, amount); require(success, "Burn failed"); } } ``` This contract would need governance authorization before `mintNativeTo` or `burnNativeFrom` calls would succeed. Without authorization, these calls revert. --- # Zero-Knowledge Proofs Specter uses **Groth16** zero-knowledge proofs on the **BN254** curve to verify Ghost Protocol reveals. Proofs are 256 bytes, verified in a single pairing check (~200K gas), and can be generated client-side in a browser or server-side via the proof relayer. ## Proof system | Parameter | Value | |---|---| | Proof system | Groth16 | | Curve | BN254 (alt_bn128) | | Proof size | 256 bytes (3 EC points) | | Verification gas | ~200,000 (via `ecPairing` precompile) | | Circuit language | Circom 2.x | | Proving library | snarkjs (browser/Node.js) | ## Circuits Specter has two ZK circuits: ### Redemption circuit The primary circuit used for revealing (redeeming) committed tokens. Proves: - Knowledge of a commitment's preimage (secret, nullifierSecret, amount, etc.) - The commitment exists in the Merkle tree - The nullifier is correctly derived - Amount conservation (withdraw + change = committed amount) - Policy binding (if applicable) **Public inputs**: 8 | **Private inputs**: 7+ (including Merkle path) ### Access proof circuit A non-destructive proof used for persistent key operations. Proves knowledge of a commitment without revealing or spending it. Used for the Phantom Keys system. **Public inputs**: 4 | **Private inputs**: 5+ ## Proof generation options ### Client-side (snarkjs in browser) ```javascript const { proof, publicSignals } = await snarkjs.groth16.fullProve( circuitInputs, 'redemption.wasm', 'redemption_final.zkey' ); ``` ### Server-side (proof relayer) ```bash curl -X POST https://relayer.specterchain.com/api/proof/generate \ -H "Content-Type: application/json" \ -H "X-HMAC-Signature: $HMAC_SIG" \ -d '{ ... circuit inputs ... }' ``` ## On-chain verification The Groth16ProofVerifier contract at `0x2C8Fb67874E5f380efB995a9Ab59b2Ef327E5bd2` uses Ethereum's `ecPairing` precompile for verification. The verification key is hardcoded from the trusted setup. ## Next steps - [Redemption Circuit](/zk-proofs/redemption-circuit) — input/output specification - [Access Proof Circuit](/zk-proofs/access-proof-circuit) — non-destructive proofs - [Generating Proofs](/zk-proofs/generating-proofs) — client-side and server-side methods - [Circuit Inputs & Outputs](/zk-proofs/circuit-inputs-outputs) — complete reference tables --- # Redemption Circuit The redemption circuit is the primary ZK circuit in Ghost Protocol. It proves that the prover knows the preimage of a commitment in the Merkle tree and correctly derives the nullifier, all without revealing which commitment is being redeemed. ## Circuit definition ``` template Redemption(levels) { // Public inputs (8) signal input root; signal input nullifier; signal input withdrawAmount; signal input recipient; signal input changeCommitment; signal input tokenId; signal input policyId; signal input policyParamsHash; // Private inputs signal input secret; signal input nullifierSecret; signal input amount; signal input blinding; signal input pathElements[levels]; signal input pathIndices[levels]; signal input newBlinding; } ``` The circuit is instantiated with `levels = 20` (Merkle tree depth). ## What it proves 1. **Commitment preimage** — The prover knows `(secret, nullifierSecret, tokenId, amount, blinding)` such that: ``` commitment = Poseidon5(secret, nullifierSecret, tokenId, amount, blinding) ``` 2. **Merkle membership** — The commitment exists at some leaf position in the tree with the given `root`: ``` MerkleProof(commitment, pathElements, pathIndices) == root ``` 3. **Nullifier derivation** — The nullifier is correctly computed: ``` nullifier == Poseidon2(nullifierSecret, leafIndex) ``` where `leafIndex` is derived from `pathIndices`. 4. **Amount conservation** — The committed amount equals the sum of withdraw and change: ``` amount == withdrawAmount + changeAmount ``` where `changeAmount` is embedded in `changeCommitment` via `newBlinding`. 5. **Policy binding** — If `policyId != 0`, verifies that the commitment includes the policy: ``` commitment == Poseidon7(secret, nullifierSecret, tokenId, amount, blinding, policyId, policyParamsHash) ``` ## Partial reveals The circuit supports partial reveals. If you committed 10 GHOST, you can: - Withdraw 3 GHOST (`withdrawAmount = 3`) - Create a change commitment for 7 GHOST (`changeCommitment = Poseidon5(newSecret, newNullifierSecret, tokenId, 7, newBlinding)`) The change commitment is inserted into the tree and can be revealed later. For a full reveal (no change), set `changeCommitment = 0` and `withdrawAmount = amount`. ## Gas cost On-chain verification costs approximately **200,000 gas** via the `ecPairing` precompile at address `0x08`. --- # Access Proof Circuit The access proof circuit proves knowledge of a commitment's preimage without consuming it. Unlike the redemption circuit, it does **not** compute a nullifier, so the same commitment can be accessed multiple times. This powers the Persistent Phantom Keys system. ## Circuit definition ``` template AccessProof(levels) { // Public inputs (4) signal input root; signal input dataHash; signal input sessionNonce; signal input accessTag; // Private inputs signal input secret; signal input nullifierSecret; signal input blinding; signal input pathElements[levels]; signal input pathIndices[levels]; } ``` ## What it proves 1. **Commitment preimage** — The prover knows `(secret, nullifierSecret, blinding)` such that: ``` commitment = Poseidon5(secret, nullifierSecret, tokenId, amount, blinding) ``` 2. **Merkle membership** — The commitment exists in the tree with the given `root`. 3. **Access tag derivation** — The access tag is correctly computed: ``` accessTag == Poseidon2(nullifierSecret, sessionNonce) ``` ## Public inputs | Index | Field | Description | |---|---|---| | 0 | `root` | Merkle tree root | | 1 | `dataHash` | Hash of the sealed data being accessed | | 2 | `sessionNonce` | Random nonce for this session | | 3 | `accessTag` | Derived tag for anti-replay protection | ## Key differences from redemption circuit | Property | Redemption | Access Proof | |---|---|---| | Computes nullifier | Yes | No | | Consumes commitment | Yes (nullifier registered) | No (reusable) | | Public inputs | 8 | 4 | | Use case | Token reveals | Data access, persistent keys | | Anti-replay | Nullifier (permanent) | Access tag (session-scoped) | ## Use cases - **Persistent Phantom Keys** — Prove you own a key stored in the tree without revealing or consuming it - **Encrypted data access** — Prove authorization to access sealed data - **Identity verification** — Prove knowledge of a credential commitment without exposing it - **Session-based authentication** — The `sessionNonce` ensures proofs are bound to specific sessions ## Access tag The access tag serves as a session-scoped anti-replay mechanism: ``` accessTag = Poseidon2(nullifierSecret, sessionNonce) ``` Since `nullifierSecret` is private, different sessions produce different tags. An observer cannot link access tags across sessions. --- # Generating Proofs ZK proofs can be generated client-side (in the browser or Node.js using snarkjs) or server-side via the proof relayer API. ## Client-side with snarkjs ### Installation ```bash npm install snarkjs circomlibjs ``` ### Generating a redemption proof ```javascript // Prepare circuit inputs const input = { // Public inputs root: merkleRoot, nullifier: computedNullifier, withdrawAmount: withdrawAmount.toString(), recipient: BigInt(recipientAddress).toString(), changeCommitment: changeCommitment || '0', tokenId: tokenIdHash.toString(), policyId: '0', policyParamsHash: '0', // Private inputs secret: secret.toString(), nullifierSecret: nullifierSecret.toString(), amount: amount.toString(), blinding: blinding.toString(), pathElements: merkleProof.pathElements, pathIndices: merkleProof.pathIndices, newBlinding: newBlinding.toString(), }; // Generate proof const { proof, publicSignals } = await snarkjs.groth16.fullProve( input, '/path/to/redemption.wasm', // compiled circuit '/path/to/redemption_final.zkey' // proving key ); // Format proof for on-chain submission (8 uint256 values) const calldata = await snarkjs.groth16.exportSolidityCallData(proof, publicSignals); ``` ### Circuit artifacts Proof generation requires two files from the trusted setup: | File | Description | Typical size | |---|---|---| | `redemption.wasm` | Compiled circuit (WebAssembly) | ~5 MB | | `redemption_final.zkey` | Proving key | ~50 MB | These files should be hosted on a CDN and fetched by the client. They are not secret — only the private inputs are. ## Server-side via proof relayer The proof relayer at `https://relayer.specterchain.com` generates proofs server-side. This is useful when: - Client devices lack the compute power for proof generation - You want consistent proof generation times - The WASM circuit files are too large for client download ### API endpoint ``` POST https://relayer.specterchain.com/api/proof/generate ``` ### Request ```json { "secret": "123456789", "nullifierSecret": "987654321", "amount": "1000000000000000000", "blinding": "111222333", "tokenIdHash": "0", "recipient": "0xRecipientAddress", "withdrawAmount": "1000000000000000000", "newBlinding": "0" } ``` ### Response ```json { "proof": [ "0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x..." ], "publicInputs": [ "0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x..." ] } ``` ### Rate limits | Limit | Value | |---|---| | Requests per minute per IP | 5 | | Max concurrent proofs | 2 | Proof generation is CPU-intensive. The proof relayer has strict rate limits to prevent denial of service. For high-throughput applications, generate proofs client-side. ### Authentication The proof relayer requires HMAC authentication. Include the signature in the request headers: ``` X-HMAC-Signature: ``` ## Proof format for on-chain submission The Groth16 proof consists of 3 elliptic curve points (A, B, C) encoded as 8 `uint256` values: ``` proof[0], proof[1] → Point A (G1) proof[2], proof[3] → Point B x-coordinate (G2) proof[4], proof[5] → Point B y-coordinate (G2) proof[6], proof[7] → Point C (G1) ``` These map directly to the `uint256[8] proof` parameter in the `reveal()` function. --- # Circuit Inputs & Outputs Complete reference for all inputs and outputs of both ZK circuits. ## Redemption circuit ### Public inputs (8) | Index | Name | Type | Description | |---|---|---|---| | 0 | `root` | Field | Merkle tree root the proof is generated against | | 1 | `nullifier` | Field | Derived nullifier = `Poseidon2(nullifierSecret, leafIndex)` | | 2 | `withdrawAmount` | uint256 | Amount being withdrawn (in aghost) | | 3 | `recipient` | address (as Field) | Address receiving the minted tokens | | 4 | `changeCommitment` | Field | Poseidon hash of the change commitment (0 for full reveal) | | 5 | `tokenId` | Field | Token identifier = `keccak256(tokenAddress) % p` | | 6 | `policyId` | address (as Field) | Policy contract address (0 if no policy) | | 7 | `policyParamsHash` | Field | Poseidon hash of policy parameters (0 if no policy) | ### Private inputs | Name | Type | Description | |---|---|---| | `secret` | Field | User's private key for this commitment | | `nullifierSecret` | Field | Used to derive the nullifier | | `amount` | uint256 | Full committed amount | | `blinding` | Field | Random blinding factor | | `pathElements[20]` | Field[20] | Merkle proof sibling hashes | | `pathIndices[20]` | bit[20] | Merkle proof path directions (0=left, 1=right) | | `newBlinding` | Field | Blinding factor for the change commitment | ### Constraints verified ``` 1. commitment = Poseidon5(secret, nullifierSecret, tokenId, amount, blinding) 2. MerkleProof(commitment, pathElements, pathIndices) == root 3. leafIndex = binaryToDecimal(pathIndices) 4. nullifier == Poseidon2(nullifierSecret, leafIndex) 5. amount == withdrawAmount + changeAmount 6. If changeAmount > 0: changeCommitment == Poseidon5(secret, newNullifierSecret, tokenId, changeAmount, newBlinding) 7. If policyId != 0: commitment includes policyId and policyParamsHash ``` ## Access proof circuit ### Public inputs (4) | Index | Name | Type | Description | |---|---|---|---| | 0 | `root` | Field | Merkle tree root | | 1 | `dataHash` | Field | Hash of the sealed data being accessed | | 2 | `sessionNonce` | Field | Random nonce for this session | | 3 | `accessTag` | Field | `Poseidon2(nullifierSecret, sessionNonce)` | ### Private inputs | Name | Type | Description | |---|---|---| | `secret` | Field | User's private key | | `nullifierSecret` | Field | Used to derive the access tag | | `blinding` | Field | Blinding factor | | `pathElements[20]` | Field[20] | Merkle proof sibling hashes | | `pathIndices[20]` | bit[20] | Merkle proof path directions | ### Constraints verified ``` 1. commitment = Poseidon5(secret, nullifierSecret, tokenId, amount, blinding) 2. MerkleProof(commitment, pathElements, pathIndices) == root 3. accessTag == Poseidon2(nullifierSecret, sessionNonce) ``` ## Field modulus All field elements must be less than the BN254 scalar field modulus: ``` p = 21888242871839275222246405745257275088548364400416034343698204186575808495617 ``` This is approximately $2^{254}$. Values that exceed this modulus will wrap around, producing invalid proofs. ## Computing the leaf index The leaf index is derived from the `pathIndices` array (binary encoding): ``` leafIndex = pathIndices[0] * 2^0 + pathIndices[1] * 2^1 + ... + pathIndices[19] * 2^19 ``` This index determines the nullifier value through `Poseidon2(nullifierSecret, leafIndex)`. --- # Policy System The policy system lets you attach enforceable on-chain conditions to commitments. Policies are checked during reveal — if the policy's `validate()` function returns `false`, the reveal is rejected. ## How policies work 1. **Commit with policy** — When committing, specify a policy contract address and parameters hash 2. **Policy is bound** — The `policyId` and `policyParamsHash` are included in the Poseidon commitment hash and verified inside the ZK circuit. You cannot change the policy after commit. 3. **Reveal checks policy** — During reveal, the vault calls `policy.validate()` via `staticcall` with a 100K gas cap 4. **Policy validates** — The policy contract checks conditions (time, destination, witnesses, etc.) and returns `true` or `false` ## Built-in policies | Policy | Address | Description | |---|---|---| | [TimelockExpiry](/policies/timelock-expiry) | `0xae2307...39d6` | Reveals only allowed after/before certain timestamps | | [DestinationRestriction](/policies/destination-restriction) | `0x899E9f...9A3ee00` | Reveals restricted to specific recipient addresses | | [ThresholdWitness](/policies/threshold-witness) | `0xa89638...fEBD7` | Requires M-of-N witness signatures to reveal | ## Policy validation call The vault calls: ```solidity (bool valid) = policy.staticcall{gas: 100_000}( abi.encodeCall(IRevealPolicy.validate, ( commitment, nullifier, recipient, amount, token, policyParams )) ); ``` Key properties: - **Read-only** — `staticcall` prevents state changes - **Gas-limited** — 100K gas cap prevents griefing - **ABI-encoded params** — `policyParams` carries arbitrary encoded data ## Example: commit with a timelock ```javascript const timeLockPolicy = '0xae2307620840916a06A862A61BF2101d694539d6'; const unlockTime = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now const policyParams = ethers.AbiCoder.defaultAbiCoder().encode( ['uint256', 'uint256'], [unlockTime, 0] // [notBefore, notAfter] ); const policyParamsHash = poseidon([...policyParamsFields]); const tx = await vault.commitNativeWithPolicy( commitment, ethers.ZeroHash, timeLockPolicy, policyParamsHash, { value: amount, gasPrice: 1_000_000_000n } ); ``` ## Next steps - [IRevealPolicy Interface](/policies/ireveal-policy) — implement your own policy - [Custom Policies](/policies/custom-policies) — tutorial for writing a custom policy --- # IRevealPolicy Interface The interface that all reveal policy contracts must implement. ## Interface ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; interface IRevealPolicy { function validate( bytes32 commitment, bytes32 nullifier, address recipient, uint256 amount, address token, bytes calldata policyParams ) external view returns (bool); } ``` ## Parameters | Parameter | Type | Description | |---|---|---| | `commitment` | `bytes32` | The commitment being revealed | | `nullifier` | `bytes32` | The derived nullifier | | `recipient` | `address` | Address that will receive the minted tokens | | `amount` | `uint256` | Amount being revealed (in base units) | | `token` | `address` | Token contract address (`address(0)` for native GHOST) | | `policyParams` | `bytes` | ABI-encoded policy-specific parameters | ## Return value - `true` — reveal is allowed - `false` — reveal is rejected (transaction reverts with `PolicyValidationFailed`) ## Execution context - Called via `staticcall` — your contract **cannot modify state** - Gas limit: **100,000** — keep validation logic efficient - The function must be `view` or `pure` ## Policy params hash The `policyParamsHash` stored in the commitment must match the hash of the `policyParams` provided during reveal. This is verified inside the ZK circuit, preventing parameter tampering. The hash is computed as: ``` policyParamsHash = Poseidon(decodedParamFields...) ``` The exact encoding depends on the policy implementation. --- # TimelockExpiry A policy that restricts reveals to a specific time window. **Address**: `0xae2307620840916a06A862A61BF2101d694539d6` ## Parameters The policy params encode two timestamps: ```solidity (uint256 notBefore, uint256 notAfter) = abi.decode(policyParams, (uint256, uint256)); ``` | Parameter | Description | |---|---| | `notBefore` | Earliest timestamp for reveal (0 = no lower bound) | | `notAfter` | Latest timestamp for reveal (0 = no upper bound) | ## Validation logic ``` if (notBefore > 0) require(block.timestamp >= notBefore); if (notAfter > 0) require(block.timestamp <= notAfter); ``` ## Examples ### Reveal only after 1 hour ```javascript const notBefore = Math.floor(Date.now() / 1000) + 3600; const notAfter = 0; // no upper bound const policyParams = ethers.AbiCoder.defaultAbiCoder().encode( ['uint256', 'uint256'], [notBefore, notAfter] ); ``` ### Reveal within a 24-hour window ```javascript const now = Math.floor(Date.now() / 1000); const notBefore = now + 3600; // starts in 1 hour const notAfter = now + 86400; // expires in 24 hours ``` ### Reveal before a deadline (expiry) ```javascript const notBefore = 0; // immediate const notAfter = Math.floor(Date.now() / 1000) + 604800; // 7 days ``` --- # DestinationRestriction A policy that restricts reveals to specific recipient addresses. **Address**: `0x899E9fa69F092D4B30FC7d4A3896366159A3ee00` ## Parameters The policy params encode an allowed recipient address: ```solidity (address allowedRecipient) = abi.decode(policyParams, (address)); ``` ## Validation logic ``` require(recipient == allowedRecipient); ``` The reveal will only succeed if the `recipient` public input in the proof matches the `allowedRecipient` in the policy params. ## Use case Bind committed tokens to a specific withdrawal address. Useful for: - **Escrow** — commit tokens that can only be revealed to a designated party - **Payroll** — commit salary tokens that only the employee can reveal - **Grant distribution** — commit grant funds restricted to the grantee ## Example ```javascript const policy = '0x899E9fa69F092D4B30FC7d4A3896366159A3ee00'; const policyParams = ethers.AbiCoder.defaultAbiCoder().encode( ['address'], [recipientAddress] ); ``` --- # ThresholdWitness A policy that requires M-of-N witness signatures before a reveal is permitted. **Address**: `0xa89638B084B77965B278dF45EA6b7037C55fEBD7` ## Parameters The policy params encode: ```solidity ( address[] witnesses, uint256 threshold, bytes[] signatures ) = abi.decode(policyParams, (address[], uint256, bytes[])); ``` | Parameter | Description | |---|---| | `witnesses` | Array of authorized witness addresses | | `threshold` | Minimum number of witness signatures required | | `signatures` | Array of EIP-712 signatures from witnesses | ## Validation logic 1. Verifies at least `threshold` valid signatures are provided 2. Each signature must be from a unique witness in the `witnesses` array 3. Signatures are verified using `ecrecover` ## Use case Multi-party approval for high-value reveals. Examples: - **Corporate treasury** — 3-of-5 board member approval to reveal committed funds - **Multi-sig privacy** — multiple keyholders must sign off on a privacy operation - **Compliance gates** — require compliance officer approval before reveal --- # PolicyRegistry Manages the mapping between commitments and their associated reveal policies. **Address**: `0x54F039F586922e1affaF4B812871f365048396aB` ## Purpose When a commitment is made with a policy, the CommitRevealVault stores the policy binding in the PolicyRegistry. During reveal, the vault queries the registry to determine which policy to validate. ## Key functions ```solidity // Check if a commitment has a policy function getPolicy(bytes32 commitment) external view returns (address policy); // Check policy params hash function getPolicyParamsHash(bytes32 commitment) external view returns (bytes32); ``` ## Usage ```bash # Check if commitment has a policy cast call 0x54F039F586922e1affaF4B812871f365048396aB \ "getPolicy(bytes32)(address)" $COMMITMENT \ --rpc-url https://testnet.specterchain.com ``` ## How it integrates The policy binding is enforced both on-chain (via the registry) and in the ZK circuit (via `policyId` and `policyParamsHash` public inputs). --- # Writing Custom Policies You can write your own reveal policy by implementing the `IRevealPolicy` interface. ## Step 1: Implement the interface ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract MinAmountPolicy is IRevealPolicy { function validate( bytes32 commitment, bytes32 nullifier, address recipient, uint256 amount, address token, bytes calldata policyParams ) external view returns (bool) { uint256 minAmount = abi.decode(policyParams, (uint256)); return amount >= minAmount; } } ``` ## Step 2: Deploy ```bash forge create src/MinAmountPolicy.sol:MinAmountPolicy \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` ## Step 3: Commit with your policy ```javascript const policyAddress = '0xYourPolicyAddress'; const minAmount = ethers.parseEther('1'); // minimum 1 GHOST const policyParams = ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [minAmount]); // Compute policyParamsHash for the commitment const policyParamsHash = poseidon([minAmount]); const tx = await vault.commitNativeWithPolicy( commitment, ethers.ZeroHash, policyAddress, policyParamsHash, { value: amount, gasPrice: 1_000_000_000n } ); ``` ## Constraints - **Must be `view` or `pure`** — called via `staticcall`, no state changes allowed - **100K gas limit** — keep logic simple and efficient - **Must return `bool`** — `true` to allow, `false` to reject - **Policy params must match** — the `policyParamsHash` in the commitment must match the hash of the params provided at reveal time (verified in the ZK circuit) ## Design patterns ### Read external state Policies can read from other contracts (oracles, registries, etc.): ```solidity function validate(...) external view returns (bool) { uint256 price = IOracle(ORACLE).getPrice(token); return amount * price >= MIN_USD_VALUE; } ``` ### Combine conditions ```solidity function validate(...) external view returns (bool) { (uint256 notBefore, address allowedRecipient) = abi.decode(policyParams, (uint256, address)); return block.timestamp >= notBefore && recipient == allowedRecipient; } ``` ### Allowlist-based ```solidity mapping(address => bool) public allowed; function validate(...) external view returns (bool) { return allowed[recipient]; } ``` Remember the 100K gas limit. Complex logic, large loops, or extensive external calls may exceed this limit and cause the reveal to fail. --- # Relayer Services The Ghost Protocol relies on seven off-chain relayer services managed by PM2. These services handle Merkle tree maintenance, hash computation, proof generation, and testnet faucet operations. ## Architecture ## Services summary | Service | Port | Purpose | Public API | |---|---|---|---| | [Root Updater](/relayer/root-updater) | 3001 | Updates Merkle roots for CommitmentTree | No (internal) | | [Batch Root Updater](/relayer/batch-root-updater) | 3030 | Updates roots for 16 sharded trees | No (internal) | | [Open Ghost Root Updater](/relayer/open-ghost-root-updater) | 3008 | Updates roots for Open Protocol | No (internal) | | [Commitment Relayer](/relayer/commitment-relayer) | 3002 | Computes Poseidon commitment hashes | Yes (authenticated) | | [Proof Relayer](/relayer/proof-relayer) | 3003 | Generates Groth16 ZK proofs | Yes (authenticated) | | [Faucet](/relayer/faucet) | 3005 | Distributes testnet GHOST | Yes (public) | | [Stale Monitor](/relayer/stale-monitor) | — | Alerts if roots go stale | No (internal) | ## Common configuration All services share these environment variables: ```bash RPC_URL=http://localhost:8545 # Local JSON-RPC CHAIN_ID=5445 # Specter testnet ``` ## Authentication The commitment relayer and proof relayer require **HMAC authentication**. Requests must include an `X-HMAC-Signature` header with a computed signature. The faucet is publicly accessible with rate limiting. ## Rate limits | Service | Limit | |---|---| | Commitment Relayer | 10 requests/minute/IP | | Proof Relayer | 5 requests/minute/IP | | Faucet (claim) | 5 requests/minute/IP | | Faucet (status) | 15 requests/minute/IP | ## Process management All services run under PM2 with auto-restart, memory limits, and health check endpoints. ```bash # View all services pm2 list # View logs for a service pm2 logs ghost-root-updater # Restart a service pm2 restart ghost-proof-relayer ``` --- # Root Updater Watches for new commitments on-chain and updates the Merkle tree root. This is a critical infrastructure service — without it, reveals cannot succeed because proofs require an up-to-date root. ## How it works 1. Listens for `CommitmentAdded` events from the CommitmentTree contract 2. Rebuilds the off-chain incremental Merkle tree 3. Computes the new root 4. Submits the root on-chain via `updateRoot()` 5. Typically completes within 5–15 seconds of a new commitment ## Configuration | Parameter | Value | |---|---| | Port | 3001 | | Contract | CommitmentTree (`0xB7E37E...4D87`) | | RPC | `http://localhost:8545` | | Process name | `ghost-root-updater` | ## Health check ```bash curl http://localhost:3001/health ``` ## Why it matters The CommitmentTree maintains a history window of the last 100 roots. Reveal proofs must reference a root within this window. If the root updater falls behind, new commitments won't be provable until the root catches up. The [Stale Monitor](/relayer/stale-monitor) watches for this condition and alerts operators. --- # Commitment Relayer Computes Poseidon commitment hashes server-side for clients that cannot run the Poseidon hash function locally. ## Endpoint ``` POST /api/commitment/compute ``` **Port**: 3002 **Public URL**: `https://relayer.specterchain.com/api/commitment/compute` ## Request ```json { "secret": "123456789", "nullifierSecret": "987654321", "blinding": "111222333", "tokenIdHash": "0", "amount": "1000000000000000000", "policyId": "0", "policyParamsHash": "0" } ``` | Field | Type | Required | Description | |---|---|---|---| | `secret` | string | Yes | User's private key (as decimal string) | | `nullifierSecret` | string | Yes | Nullifier secret (as decimal string) | | `blinding` | string | Yes | Random blinding factor (as decimal string) | | `tokenIdHash` | string | Yes | Token ID hash ("0" for native GHOST) | | `amount` | string | Yes | Amount in aghost (as decimal string) | | `policyId` | string | No | Policy contract address ("0" if none) | | `policyParamsHash` | string | No | Policy params hash ("0" if none) | ## Response ```json { "commitment": "0x1a2b3c4d..." } ``` ## Authentication Requires HMAC signature in the request header: ``` X-HMAC-Signature: ``` ## Rate limit 10 requests/minute/IP ## Health check ``` GET /health ``` ## Security note The commitment relayer receives your `secret` and `nullifierSecret` in plaintext. If you're concerned about the relayer operator seeing these values, compute the Poseidon hash client-side using `circomlibjs` instead. ```javascript const poseidon = await buildPoseidon(); const commitment = poseidon([secret, nullifierSecret, tokenIdHash, amount, blinding]); ``` --- # Proof Relayer Generates Groth16 ZK proofs server-side. CPU-intensive — limited to 2 concurrent proof generations. ## Endpoint ``` POST /api/proof/generate ``` **Port**: 3003 **Public URL**: `https://relayer.specterchain.com/api/proof/generate` ## Request ```json { "secret": "123456789", "nullifierSecret": "987654321", "amount": "1000000000000000000", "blinding": "111222333", "tokenIdHash": "0", "recipient": "0xRecipientAddress", "withdrawAmount": "1000000000000000000", "newBlinding": "0" } ``` | Field | Type | Required | Description | |---|---|---|---| | `secret` | string | Yes | Commitment secret | | `nullifierSecret` | string | Yes | Nullifier secret | | `amount` | string | Yes | Full committed amount | | `blinding` | string | Yes | Commitment blinding factor | | `tokenIdHash` | string | Yes | Token ID hash | | `recipient` | string | Yes | Recipient Ethereum address | | `withdrawAmount` | string | Yes | Amount to withdraw | | `newBlinding` | string | Yes | Blinding for change commitment ("0" for full reveal) | ## Response ```json { "proof": [ "0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x..." ], "publicInputs": [ "0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x..." ] } ``` The `proof` array contains 8 `uint256` values (3 EC points) ready for the `reveal()` function. ## Limits | Parameter | Value | |---|---| | Rate limit | 5 requests/minute/IP | | Max concurrent proofs | 2 | | Commitment cache TTL | 5 minutes | ## Authentication Requires HMAC signature: ``` X-HMAC-Signature: ``` ## Status endpoint ``` GET /status ``` Returns service status including current load and queue depth. ## Security note Like the commitment relayer, this service receives your secret values in plaintext. For maximum privacy, generate proofs client-side using snarkjs. See [Generating Proofs](/zk-proofs/generating-proofs). ## Internal architecture The proof relayer: 1. Maintains an in-memory copy of the Merkle tree (rebuilt from `CommitmentAdded` events) 2. Computes the Merkle proof path for the requested commitment 3. Generates the Groth16 proof using snarkjs with the circuit WASM and proving key 4. Returns the formatted proof and public inputs --- # Faucet Service Distributes testnet GHOST tokens to developer addresses. One claim per address. ## Endpoints ### Claim tokens ``` POST /api/faucet/claim ``` ```json { "address": "0xYourEVMAddress" } ``` **Response (success)**: ```json { "success": true, "txHash": "0xabc123...", "amount": "100000000000000000000" } ``` **Response (already claimed)**: ```json { "error": "Address has already claimed", "claimedAt": "2025-01-15T10:30:00.000Z" } ``` ### Check status ``` GET /api/faucet/status?address=0xYourAddress ``` **Response**: ```json { "address": "0xYourAddress", "claimed": true, "claimedAt": "2025-01-15T10:30:00.000Z", "txHash": "0xabc123..." } ``` ### Health check ``` GET /health ``` ## Configuration | Parameter | Value | |---|---| | Port | 3005 | | Drip amount | 100 GHOST | | Claims per address | 1 | | Claim rate limit | 5 requests/minute/IP | | Status rate limit | 15 requests/minute/IP | ## No authentication required The faucet is publicly accessible. Rate limiting prevents abuse. ## Public URL ``` https://relayer.specterchain.com/api/faucet/claim https://relayer.specterchain.com/api/faucet/status?address=0x... ``` --- # Batch Root Updater Updates Merkle roots for the BatchCommitRevealVault, which distributes commitments across 16 shards for higher throughput. ## How it works The BatchCommitRevealVault uses a ShardedTreeRegistry with 16 separate Merkle trees. Each shard has its own root that must be updated independently. The batch root updater: 1. Watches for commitment events across all 16 shards 2. Rebuilds each shard's Merkle tree 3. Submits updated roots for each modified shard ## Configuration | Parameter | Value | |---|---| | Port | 3030 | | Shards | 16 | | Process name | `batch-root-updater` | ## When to use The batch system is designed for high-throughput scenarios where the single-tree CommitmentTree becomes a bottleneck. Most developers should use the standard CommitRevealVault — the batch system is for advanced use cases. See [Batch Operations](/scaling/batch-operations) for more details. --- # Open Ghost Root Updater Updates Merkle roots for the Open Protocol contracts (OpenGhostVault, OpenCommitmentTree). ## How it works The Open Protocol uses a separate Merkle tree from the standard Ghost Protocol. This updater: 1. Watches for commitment events from OpenCommitmentTree (`0xE2b33d...C0c48`) 2. Rebuilds the Open Protocol's Merkle tree 3. Submits the updated root on-chain ## Configuration | Parameter | Value | |---|---| | Port | 3008 | | Contract | OpenCommitmentTree (`0xE2b33dB178d6201EDd854ED9163B30dcfECC0c48`) | | Process name | `open-ghost-root-updater` | ## Difference from standard root updater The Open Protocol is for **public** data commitments — the data is revealed publicly, not privately. See [Open Protocol](/open-protocol/overview) for details. --- # Stale Monitor A liveness monitoring service that alerts operators if Merkle roots go stale (fall behind new commitments). ## What it watches - Time since last root update on the CommitmentTree - Whether new commitments have been made without a corresponding root update - Root updater service health ## Why it matters If the root updater service goes down: 1. New commitments are inserted but the root is not updated 2. Proofs generated against the old root remain valid (within the 100-root history window) 3. Once the history window is exceeded, older proofs become invalid 4. Users who committed but haven't revealed yet may be unable to do so The stale monitor detects this condition early and alerts operators to restart the root updater. ## Configuration | Parameter | Value | |---|---| | Process name | `ghost-stale-monitor` | | No port | Runs as a background job | ## Alert conditions - Root hasn't been updated in more than 60 seconds despite new commitments - Root updater health endpoint is unreachable - Root updater process has restarted unexpectedly --- # Frontend Integration Build dApps on Specter using standard Ethereum frontend libraries. Specter is fully EVM-compatible, so ethers.js, viem, wagmi, RainbowKit, and all Ethereum tooling work out of the box. ## Supported libraries | Library | Purpose | |---|---| | [viem](https://viem.sh) | TypeScript-first Ethereum client | | [ethers.js v6](https://docs.ethers.org/v6/) | Battle-tested Ethereum library | | [wagmi](https://wagmi.sh) | React hooks for Ethereum | | [RainbowKit](https://www.rainbowkit.com) | Wallet connection UI | | [snarkjs](https://github.com/iden3/snarkjs) | Client-side ZK proof generation | | [circomlibjs](https://github.com/iden3/circomlibjs) | Poseidon hashing in JavaScript | ## Quick setup ```bash npm install viem ethers snarkjs circomlibjs ``` ## Next steps - [Chain Config](/frontend/chain-config) — viem/ethers chain definitions - [viem & ethers.js](/frontend/viem-ethers) — contract interaction examples - [Client-Side ZK](/frontend/client-side-zk) — browser-based proof generation - [Building dApps](/frontend/building-dapps) — end-to-end dApp tutorial --- # Chain Configuration Define the Specter chain for your frontend library. ## viem chain definition ```typescript export const specterTestnet = defineChain({ id: 5445, name: 'Specter Testnet', nativeCurrency: { name: 'GHOST', symbol: 'GHOST', decimals: 18, }, rpcUrls: { default: { http: ['https://testnet.specterchain.com'], webSocket: ['wss://testnet.specterchain.com/ws'], }, }, blockExplorers: { default: { name: 'Blockscout', url: 'https://explorer.specterchain.com', }, }, }); ``` ## ethers.js provider ```typescript const provider = new ethers.JsonRpcProvider('https://testnet.specterchain.com', { name: 'specter-testnet', chainId: 5445, }); // With WebSocket const wsProvider = new ethers.WebSocketProvider('wss://testnet.specterchain.com/ws', { name: 'specter-testnet', chainId: 5445, }); ``` ## wagmi config ```typescript export const config = createConfig({ chains: [specterTestnet], transports: { [specterTestnet.id]: http('https://testnet.specterchain.com'), }, }); ``` ## RainbowKit ```typescript const config = getDefaultConfig({ appName: 'My Specter dApp', projectId: 'YOUR_WALLETCONNECT_PROJECT_ID', chains: [specterTestnet], }); ``` ## Contract addresses ```typescript export const CONTRACTS = { GHOST_VAULT: '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', COMMITMENT_TREE: '0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87', NULLIFIER_REGISTRY: '0x0987cc3dE6f76c4c8834Dc6205De24968091C58b', GHOST_ERC20_FACTORY: '0x925B548F059C0B8B6CF7168Efb84881252F88C8E', NATIVE_HANDLER: '0x35cdaE691037fcBb3ff9D0518725F1ae98d502b7', PROOF_VERIFIER: '0x2C8Fb67874E5f380efB995a9Ab59b2Ef327E5bd2', POSEIDON_T3: '0xa786eDD407eb9EbaCA5E624B7Ee7C31E3b7f9521', OPEN_GHOST_VAULT: '0x45B022fEB169AF906CaBa8086c977AA7b15faAf1', } as const; export const GAS_PRICE = 1_000_000_000n; // 1 gwei ``` ## Important: gas price Always set `gasPrice` explicitly. The Cosmos EVM module may return 0 from `eth_gasPrice`: ```typescript const tx = await contract.someFunction({ gasPrice: 1_000_000_000n, }); ``` --- # Contract Interaction with viem & ethers.js ## Reading contract state ### viem ```typescript const client = createPublicClient({ chain: specterTestnet, transport: http(), }); // Read the current Merkle root const root = await client.readContract({ address: '0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87', abi: [{ name: 'getLastRoot', type: 'function', inputs: [], outputs: [{ type: 'bytes32' }], stateMutability: 'view' }], functionName: 'getLastRoot', }); // Check if a nullifier is spent const isSpent = await client.readContract({ address: '0x0987cc3dE6f76c4c8834Dc6205De24968091C58b', abi: [{ name: 'isSpent', type: 'function', inputs: [{ type: 'bytes32' }], outputs: [{ type: 'bool' }], stateMutability: 'view' }], functionName: 'isSpent', args: [nullifier], }); ``` ### ethers.js ```typescript const provider = new ethers.JsonRpcProvider('https://testnet.specterchain.com'); const tree = new ethers.Contract( '0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87', ['function getLastRoot() view returns (bytes32)'], provider ); const root = await tree.getLastRoot(); ``` ## Writing transactions ### Commit native GHOST (viem) ```typescript const account = privateKeyToAccount(privateKey); const walletClient = createWalletClient({ account, chain: specterTestnet, transport: http(), }); const hash = await walletClient.writeContract({ address: '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', abi: [{ name: 'commitNative', type: 'function', inputs: [{ type: 'bytes32' }, { type: 'bytes32' }], stateMutability: 'payable' }], functionName: 'commitNative', args: [commitment, '0x' + '0'.repeat(64)], value: parseEther('1'), gasPrice: 1_000_000_000n, }); ``` ### Commit native GHOST (ethers.js) ```typescript const signer = new ethers.Wallet(privateKey, provider); const vault = new ethers.Contract( '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', ['function commitNative(bytes32,bytes32) payable'], signer ); const tx = await vault.commitNative(commitment, ethers.ZeroHash, { value: ethers.parseEther('1'), gasPrice: 1_000_000_000n, }); await tx.wait(); ``` ## Event subscriptions ### WebSocket (viem) ```typescript const wsClient = createPublicClient({ chain: specterTestnet, transport: webSocket('wss://testnet.specterchain.com/ws'), }); const unwatch = wsClient.watchContractEvent({ address: '0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87', abi: [{ name: 'CommitmentAdded', type: 'event', inputs: [{ name: 'commitment', type: 'bytes32', indexed: true }, { name: 'index', type: 'uint256' }] }], eventName: 'CommitmentAdded', onLogs: (logs) => console.log('New commitment:', logs), }); ``` ### Query historical events (ethers.js) ```typescript const filter = tree.filters.CommitmentAdded(); const events = await tree.queryFilter(filter, 0, 'latest'); console.log(`Total commitments: ${events.length}`); ``` --- # Client-Side ZK Proof Generation Generate Groth16 proofs directly in the browser using snarkjs. This is the most private option — your secret values never leave your device. ## Installation ```bash npm install snarkjs circomlibjs ``` ## Poseidon hashing ```javascript const poseidon = await buildPoseidon(); const F = poseidon.F; // Hash commitment preimage const commitment = poseidon([secret, nullifierSecret, tokenIdHash, amount, blinding]); const commitmentHex = '0x' + F.toString(commitment, 16).padStart(64, '0'); // Hash nullifier const leafIndex = BigInt(12345); // from Merkle tree const nullifier = poseidon([nullifierSecret, leafIndex]); ``` ## Building a Merkle tree ```javascript // Fetch all commitments from chain const events = await treeContract.queryFilter('CommitmentAdded'); const leaves = events.map(e => e.args.commitment); // Build tree in-memory class MerkleTree { constructor(depth, leaves, poseidon) { this.depth = depth; this.poseidon = poseidon; this.F = poseidon.F; this.layers = [leaves.map(l => BigInt(l))]; for (let i = 0; i < depth; i++) { const layer = this.layers[i]; const nextLayer = []; for (let j = 0; j < layer.length; j += 2) { const left = layer[j] || 0n; const right = layer[j + 1] || 0n; nextLayer.push(this.F.toObject(poseidon([left, right]))); } this.layers.push(nextLayer); } } getProof(index) { const pathElements = []; const pathIndices = []; for (let i = 0; i < this.depth; i++) { const siblingIndex = index % 2 === 0 ? index + 1 : index - 1; pathElements.push((this.layers[i][siblingIndex] || 0n).toString()); pathIndices.push(index % 2); index = Math.floor(index / 2); } return { pathElements, pathIndices }; } get root() { return this.layers[this.depth][0]; } } ``` ## Generating a proof ```javascript // Circuit artifacts (fetch from CDN) const wasmFile = await fetch('/circuits/redemption.wasm'); const zkeyFile = await fetch('/circuits/redemption_final.zkey'); const input = { root: tree.root.toString(), nullifier: nullifier.toString(), withdrawAmount: withdrawAmount.toString(), recipient: BigInt(recipientAddress).toString(), changeCommitment: '0', tokenId: tokenIdHash.toString(), policyId: '0', policyParamsHash: '0', secret: secret.toString(), nullifierSecret: nullifierSecret.toString(), amount: amount.toString(), blinding: blinding.toString(), pathElements: merkleProof.pathElements, pathIndices: merkleProof.pathIndices, newBlinding: '0', }; const { proof, publicSignals } = await snarkjs.groth16.fullProve( input, wasmBuffer, zkeyBuffer ); ``` ## Formatting for on-chain submission ```javascript // Convert proof to Solidity-compatible format const calldata = await snarkjs.groth16.exportSolidityCallData(proof, publicSignals); const [proofFormatted, publicInputsFormatted] = JSON.parse('[' + calldata + ']'); // proofFormatted is uint256[8] // publicInputsFormatted is uint256[] ``` ## Performance | Device | Proof generation time | |---|---| | Desktop (M1/M2) | 2–5 seconds | | Desktop (Intel i7) | 5–10 seconds | | Mobile (modern) | 10–30 seconds | | Mobile (older) | 30–60 seconds | Proof generation is CPU-intensive. Consider showing a loading indicator and generating proofs in a Web Worker to avoid blocking the UI thread. ## Web Worker pattern ```javascript // worker.js self.onmessage = async (e) => { const { input, wasmBuffer, zkeyBuffer } = e.data; const { proof, publicSignals } = await snarkjs.groth16.fullProve(input, wasmBuffer, zkeyBuffer); self.postMessage({ proof, publicSignals }); }; ``` --- # Building dApps on Specter End-to-end guide for building a privacy-enabled dApp using React, viem, and snarkjs. ## Project setup ```bash npm create vite@latest my-specter-dapp -- --template react-ts cd my-specter-dapp npm install viem snarkjs circomlibjs @rainbow-me/rainbowkit wagmi ``` ## Connect wallet ```typescript // src/App.tsx function App() { return ( {/* Your app components */} ); } ``` ## Read chain data ```typescript function VaultStatus() { const { data: totalCommitted } = useReadContract({ address: '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', abi: [{ name: 'totalCommitted', type: 'function', inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' }], functionName: 'totalCommitted', args: ['0x0000000000000000000000000000000000000000'], }); return
Total GHOST committed: {formatEther(totalCommitted || 0n)}
; } ``` ## Commit tokens ```typescript function CommitForm() { const { writeContract } = useWriteContract(); async function handleCommit(commitment: `0x${string}`, amount: string) { writeContract({ address: '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', abi: [{ name: 'commitNative', type: 'function', inputs: [{ type: 'bytes32' }, { type: 'bytes32' }], stateMutability: 'payable' }], functionName: 'commitNative', args: [commitment, '0x' + '0'.repeat(64) as `0x${string}`], value: parseEther(amount), gasPrice: 1_000_000_000n, }); } return (/* form UI */); } ``` ## Generate proof and reveal ```typescript async function handleReveal(secrets, recipientAddress) { // 1. Generate proof (in Web Worker for best UX) const { proof, publicInputs } = await generateProof(secrets, recipientAddress); // 2. Submit reveal transaction writeContract({ address: '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', abi: revealAbi, functionName: 'reveal', args: [ '0x0000000000000000000000000000000000000000', // native GHOST proof, publicInputs, secrets.commitment, '0x', // no quantum proof '0x' + '0'.repeat(64), // no quantum change '0x', // no policy params ], gasPrice: 1_000_000_000n, }); } ``` ## Secret management The commitment secrets (`secret`, `nullifierSecret`, `blinding`) must be stored securely. If lost, committed tokens are permanently unrecoverable. Consider: - **Browser storage**: `localStorage` or `IndexedDB` (cleared if browser data is deleted) - **Encrypted backup**: Let users download an encrypted file containing their secrets - **Key derivation**: Derive secrets deterministically from a seed phrase using a KDF ## Architecture recommendations 1. **Never store secrets on a server** — keep commitment secrets client-side only 2. **Use Web Workers for proof generation** — keeps the UI responsive during the 2–30 second proof generation 3. **Handle root staleness** — if a proof fails with `InvalidRoot`, regenerate against the latest root 4. **Show commitment status** — poll the CommitmentTree for root updates so users know when they can reveal 5. **Set gas price explicitly** — always use `gasPrice: 1_000_000_000n` --- # GHOST Token GHOST is the native gas and staking token of the Specter network. It is used for transaction fees, validator staking, governance, and as the primary asset for Ghost Protocol privacy operations. ## Key parameters | Parameter | Value | |---|---| | Name | GHOST | | Base unit | aghost | | Decimals | 18 | | Max supply | 1,000,000,000 GHOST | | Inflation | 0% (hard cap, no inflation module) | | Uses | Gas fees, staking, governance, privacy operations | ## Token lifecycle in Ghost Protocol GHOST has a unique lifecycle within the Ghost Protocol commit-reveal system: 1. **Commit (burn)** — User sends GHOST to the CommitRevealVault, which calls the Ghostmint precompile to burn the tokens via `BankKeeper.BurnCoins()`. The tokens are destroyed. 2. **Commitment stored** — A Poseidon hash commitment is inserted into the Merkle tree. No tokens exist anywhere — they were destroyed. 3. **Reveal (mint)** — User provides a Groth16 ZK proof. The vault calls the Ghostmint precompile to mint fresh GHOST via `BankKeeper.MintCoins()`. This burn-and-mint model means there is no escrow pool or liquidity contract. The supply temporarily decreases during commits and increases during reveals. ### Supply cap enforcement The Ghostmint precompile enforces a hard cap: cumulative mints minus cumulative burns cannot exceed 1 billion GHOST. A `PreCommit` hook in the chain's ABCI lifecycle verifies this invariant on every block. ## Next steps - [Denominations](/token/denominations) — GHOST vs aghost, conversion - [Wrapping as GhostERC20](/token/wrapping-ghost-erc20) — deploy privacy-enabled ERC20 tokens - [Gas and Fees](/token/gas-and-fees) — gas pricing on Specter - [Staking](/token/staking) — validator staking and governance --- # Denominations ## GHOST and aghost | Unit | Symbol | Relation | |---|---|---| | GHOST | GHOST | 1 GHOST = 10^18 aghost | | aghost | aghost | Base unit (like wei in Ethereum) | This follows the same pattern as ETH/wei. All on-chain values and smart contract interactions use aghost (the base unit). Display values use GHOST. ## Conversion examples ```javascript // JavaScript: convert GHOST to aghost const ghostAmount = 1.5; const aghostAmount = BigInt(ghostAmount * 1e18); // 1500000000000000000n // ethers.js const aghost = parseEther('1.5'); // 1500000000000000000n const ghost = formatEther(aghost); // "1.5" // viem const aghost = parseEther('1.5'); // 1500000000000000000n ``` ```bash # Foundry: convert using cast cast to-wei 1.5 # 1500000000000000000 cast from-wei 1500000000000000000 # 1.5 ``` ## In Cosmos SDK When interacting via the Cosmos side (staking, governance), amounts use the `aghost` denomination: ```bash # Send tokens via specterd specterd tx bank send 1000000000000000000aghost # Query balance specterd query bank balance
aghost ``` ## In smart contracts Solidity contracts always work with aghost (uint256): ```solidity // 1 GHOST = 1 ether in Solidity notation uint256 oneGhost = 1 ether; // 1000000000000000000 // Sending GHOST payable(recipient).transfer(1 ether); ``` --- # Wrapping as GhostERC20 GhostERC20 tokens are privacy-enabled ERC20 tokens that can be committed and revealed through the Ghost Protocol. They are standard ERC20 tokens with additional mint/burn capabilities for the CommitRevealVault. ## How it works 1. Deploy a GhostERC20 token via the factory 2. The factory registers the token's ID hash with the CommitRevealVault 3. Users can commit (burn) the token into the Merkle tree 4. Users can reveal (mint) the token back using a ZK proof ## Deploying a new GhostERC20 Use the GhostERC20Factory at `0x925B548F059C0B8B6CF7168Efb84881252F88C8E`: ```solidity interface IGhostERC20Factory { function deployToken( string calldata name, string calldata symbol, uint8 decimals, bytes32 salt ) external returns (address token); function computeTokenAddress( string calldata name, string calldata symbol, uint8 decimals, bytes32 salt ) external view returns (address); } ``` ### Deploy via Foundry ```bash cast send 0x925B548F059C0B8B6CF7168Efb84881252F88C8E \ "deployToken(string,string,uint8,bytes32)" \ "My Token" "MTK" 18 0x0000000000000000000000000000000000000000000000000000000000000001 \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` ### Predict the address ```bash cast call 0x925B548F059C0B8B6CF7168Efb84881252F88C8E \ "computeTokenAddress(string,string,uint8,bytes32)(address)" \ "My Token" "MTK" 18 0x0000000000000000000000000000000000000000000000000000000000000001 \ --rpc-url https://testnet.specterchain.com ``` ## GhostERC20 interface GhostERC20 tokens extend standard ERC20 with: ```solidity interface IGhostERC20 is IERC20 { function vault() external view returns (address); function factory() external view returns (address); function tokenIdHash() external view returns (bytes32); function isGhostEnabled() external view returns (bool); // Only callable by the vault function mint(address to, uint256 amount) external; function burn(address from, uint256 amount) external; } ``` The `mint` and `burn` functions are restricted to the CommitRevealVault contract. Users interact with the token normally (transfer, approve, etc.) and use the vault for privacy operations. ## Existing GhostERC20 tokens on testnet | Token | Address | |---|---| | gUSDC | `0x65c9091a6A45Db302a343AF460657C298FAA222D` | | gWETH | `0x923295a3e3bE5eDe29Fc408A507dA057ee044E81` | | gLABS | `0x062f8a68f6386c1b448b3379abd369825bec9aa2` | ## Committing GhostERC20 tokens To commit a GhostERC20 token, first approve the vault, then call `commit()`: ```solidity IERC20(tokenAddress).approve(vaultAddress, amount); ICommitRevealVault(vaultAddress).commit(tokenAddress, amount, commitment, quantumCommitment); ``` The vault will call `burn(from, amount)` on the token, destroying the tokens, and insert the commitment into the Merkle tree. --- # Gas and Fees ## Gas pricing Specter uses GHOST as the gas token. The chain enforces a minimum gas price of **1 gwei** (1,000,000,000 aghost). `eth_gasPrice` may return `0` on Specter. This is a known Cosmos EVM behavior. Always set gas price explicitly to avoid transaction failures. ### Setting gas price ```javascript // viem const hash = await walletClient.sendTransaction({ to: recipient, value: parseEther('1'), gasPrice: 1_000_000_000n, }); // ethers.js v6 const tx = await contract.myFunction({ gasPrice: 1_000_000_000n, }); ``` ```bash # Foundry cast send $CONTRACT "myFunction()" \ --gas-price 1000000000 \ --rpc-url https://testnet.specterchain.com ``` ## Typical gas costs | Operation | Approximate Gas | |---|---| | Simple transfer | ~21,000 | | ERC20 transfer | ~65,000 | | Contract deployment (small) | ~200,000 | | Ghost Protocol commit | ~150,000 | | Ghost Protocol reveal | ~350,000 | | Groth16 proof verification | ~200,000 | | GhostERC20 deployment | ~2,000,000 | ## Cost calculation ``` Cost (in GHOST) = gasUsed × gasPrice / 10^18 Example: Reveal operation = 350,000 × 1,000,000,000 / 10^18 = 0.00035 GHOST ``` At 1 gwei gas price, most operations cost fractions of a cent worth of GHOST. --- # Staking Specter uses CometBFT Byzantine Fault Tolerant consensus with a Proof of Stake validator set. GHOST tokens are staked to participate in consensus and governance. ## How staking works 1. **Validators** stake GHOST to participate in block production 2. **Delegators** can delegate GHOST to validators to share in rewards 3. Staking happens on the Cosmos side using the `specterd` CLI or Cosmos-compatible wallets 4. Staked tokens are bonded and subject to unbonding periods ## Staking via specterd ```bash # Delegate tokens to a validator specterd tx staking delegate 1000000000000000000aghost \ --from \ --chain-id specter-testnet-1 # Query delegations specterd query staking delegations # Undelegate tokens (begins unbonding period) specterd tx staking unbond 1000000000000000000aghost \ --from \ --chain-id specter-testnet-1 ``` ## Governance Staked GHOST grants governance voting power. Proposals can modify chain parameters, authorize Ghostmint precompile callers, and upgrade the protocol. ```bash # Submit a proposal specterd tx gov submit-proposal \ --from \ --chain-id specter-testnet-1 # Vote on a proposal specterd tx gov vote yes \ --from \ --chain-id specter-testnet-1 ``` ## Slashing Validators can be slashed for: - **Double signing** — signing two different blocks at the same height - **Downtime** — being offline for an extended period Slashing penalties apply to both the validator and their delegators. --- # Open Protocol The Open Protocol is a variant of Ghost Protocol for **public** data commitments. Unlike the standard protocol where reveals are private (unlinkable), Open Protocol reveals are public — the data is exposed during reveal. This is useful when you want the integrity guarantees of a Merkle tree commitment without the privacy layer. ## When to use Open Protocol | Use case | Standard Ghost Protocol | Open Protocol | |---|---|---| | Private token transfers | Yes | No | | Public data provenance | No | Yes | | Verifiable credentials | Sometimes | Yes | | Persistent key storage | No | Yes | | Sealed-then-revealed data | Yes (private reveal) | Yes (public reveal) | ## Contracts | Contract | Address | Purpose | |---|---|---| | OpenGhostVault | `0x45B022fEB169AF906CaBa8086c977AA7b15faAf1` | Commit and reveal orchestrator | | OpenGhostReveal | `0x70BD6eE41507139285e868a46399104305dF1833` | Reveal mechanism | | OpenCommitmentTree | `0xE2b33dB178d6201EDd854ED9163B30dcfECC0c48` | Merkle tree for open commitments | | OpenNullifierRegistry | `0xC1c32f5697d5cfD74c35D1Fd4CF05E1F6d2A90b2` | Spent nullifier tracking | | OpenGhostKeyVault | `0x4943959c05e028884C1CDd9878c762D785332A67` | Key management | | PersistentKeyVault (v2) | `0x683B3ff7795D508Ff1e088a08981580e19af7496` | Persistent key storage | ## Architecture The Open Protocol uses separate contracts and a separate Merkle tree from the standard Ghost Protocol. This means: - Open commitments don't mix with private commitments - The anonymity set is separate - A dedicated root updater service maintains the Open Protocol tree ## Next steps - [OpenGhostVault](/open-protocol/open-ghost-vault) — committing and revealing public data - [Key Vault](/open-protocol/key-vault) — persistent key storage - [Use Cases](/open-protocol/use-cases) — when to choose open vs private --- # OpenGhostVault The commit and reveal orchestrator for the Open Protocol. Functions similarly to CommitRevealVault but for public data commitments. **Address**: `0x45B022fEB169AF906CaBa8086c977AA7b15faAf1` ## Key differences from CommitRevealVault | Feature | CommitRevealVault | OpenGhostVault | |---|---|---| | Privacy | Unlinkable commits and reveals | Public reveals | | Token operations | Burns and mints tokens | Data commitments (no token movement) | | Merkle tree | CommitmentTree | OpenCommitmentTree | | Nullifier registry | NullifierRegistry | OpenNullifierRegistry | | Root updater | ghost-root-updater | open-ghost-root-updater | ## Usage The OpenGhostVault is used for data commitments where the data itself will be revealed publicly. The Merkle tree provides: - **Ordering** — commitments have a verifiable insertion order - **Integrity** — the root proves the complete set of commitments - **Existence proofs** — prove a specific commitment exists without revealing all data ## Example: query the open tree ```bash # Get current root of the open commitment tree cast call 0xE2b33dB178d6201EDd854ED9163B30dcfECC0c48 \ "getLastRoot()(bytes32)" \ --rpc-url https://testnet.specterchain.com # Get number of open commitments cast call 0xE2b33dB178d6201EDd854ED9163B30dcfECC0c48 \ "nextIndex()(uint256)" \ --rpc-url https://testnet.specterchain.com ``` --- # Key Vault The PersistentKeyVault stores encryption keys and other persistent data in the Open Protocol Merkle tree. Keys can be accessed repeatedly using access proofs without being consumed. **PersistentKeyVault (v2)**: `0x683B3ff7795D508Ff1e088a08981580e19af7496` **AccessProofVerifier (v2)**: `0x4C2cA5FFCE417A3914b6531C79b4946117B4aA21` ## How it works 1. **Store a key** — commit an encryption key (or any data) to the Open Protocol tree 2. **Access the key** — generate an access proof (non-destructive ZK proof) to prove you own the key 3. **Retrieve the data** — the verifier confirms your proof, and you can decrypt/use the key Unlike standard reveals, access proofs don't consume the commitment. The same key can be accessed unlimited times. ## Access proof vs redemption proof | Property | Redemption Proof | Access Proof | |---|---|---| | Consumes commitment | Yes (nullifier spent) | No (reusable) | | Circuit | redemption.circom | accessProof.circom | | Public inputs | 8 | 4 | | Use case | Token reveals | Key access, data retrieval | ## Usage ```bash # Check the access proof verifier cast call 0x4C2cA5FFCE417A3914b6531C79b4946117B4aA21 \ "verifyProof(uint256[8],uint256[4])(bool)" \ $PROOF $PUBLIC_INPUTS \ --rpc-url https://testnet.specterchain.com ``` See [Access Proof Circuit](/zk-proofs/access-proof-circuit) for the circuit specification. --- # Open vs Private: When to Use Each ## Choose Standard Ghost Protocol when: - **Privacy is required** — token transfers, confidential transactions - **Unlinkability matters** — commit and reveal should not be traceable - **Tokens are involved** — burn-and-mint operations for GHOST or GhostERC20 tokens ## Choose Open Protocol when: - **Data provenance** — you want to prove data existed at a certain time (timestamps via block inclusion) - **Public credentials** — verifiable credentials that will be revealed publicly - **Persistent keys** — encryption keys that need to be accessed repeatedly - **Sealed bids** — commit to a value, then reveal it publicly after a deadline - **Attestations** — on-chain attestations where the data is eventually public ## Combining both protocols You can use both protocols in the same application: 1. Use the **Open Protocol** to store a public commitment (e.g., a credential hash) 2. Use the **Standard Ghost Protocol** to privately transfer tokens as a reward for the credential 3. The two operations are on separate Merkle trees and don't interfere ## Summary table | Feature | Standard | Open | |---|---|---| | Token operations | Yes (burn/mint) | No | | Private reveals | Yes | No | | Persistent access | No | Yes (access proofs) | | Separate Merkle tree | CommitmentTree | OpenCommitmentTree | | Privacy guarantees | Full unlinkability | Data integrity only | --- # Scaling The Ghost Protocol includes scaling infrastructure for high-throughput scenarios. The standard CommitRevealVault handles moderate throughput, but applications requiring thousands of commitments per minute can use the batch and sharded systems. ## Scaling approaches | Approach | Description | Use case | |---|---|---| | **Batch operations** | Multiple commits/reveals in a single transaction | Reduce gas costs for bulk operations | | **Session vaults** | Temporary session-based batching | Interactive sessions with multiple operations | | **Sharded trees** | 16 parallel Merkle trees | Increase total throughput | ## When to use scaling - **Standard vault** — fewer than 100 commits/minute. Most applications. - **Batch vault** — 100–1000 commits/minute. Payment processors, batch transfers. - **Sharded trees** — 1000+ commits/minute. High-frequency trading, mass onboarding. ## Architecture --- # Batch Operations The BatchCommitRevealVault enables multiple commit and reveal operations in a single transaction, reducing per-operation gas costs. ## How it works Instead of submitting each commit individually, batch operations: 1. Collect multiple commitments 2. Submit them in a single transaction 3. Distribute across 16 shards in the ShardedTreeRegistry 4. Each shard has its own Merkle tree and root ## Shard assignment Commitments are assigned to shards based on their hash: ``` shard = commitment % 16 ``` This distributes load evenly across 16 parallel Merkle trees. ## Root updates The batch root updater service monitors all 16 shards and updates their roots independently. Each shard's root is updated as new commitments are added. ## Trade-offs | Advantage | Disadvantage | |---|---| | Lower gas per commitment | Smaller anonymity set per shard | | Higher throughput (16x) | More complex proof generation | | Parallel processing | 16 separate root updates | ## When to use Use batch operations when: - You're processing more than 100 commitments per minute - Gas cost per commitment is a concern - You can accept a smaller per-shard anonymity set For most applications, the standard CommitRevealVault is sufficient and provides a larger anonymity set (all commitments in one tree). --- # Session Vaults Session vaults provide temporary, session-based batching for interactive applications. A session collects multiple operations and submits them as a batch when the session ends. ## How it works 1. **Open a session** — create a temporary vault for a user session 2. **Collect operations** — queue commits and reveals during the session 3. **Close session** — submit all queued operations as a batch to the BatchCommitRevealVault 4. **Settle** — the batch is processed and roots are updated ## Use cases - **Interactive privacy** — users make multiple commit/reveal operations in a browsing session - **Payment batching** — collect multiple payments and settle them in batch - **Gas optimization** — amortize transaction costs across multiple operations ## Architecture The SessionVault acts as a buffer, collecting operations before submitting them to the batch system. --- # Sharded Trees The ShardedTreeRegistry manages 16 parallel Merkle trees, enabling 16x throughput compared to a single tree. ## Architecture ``` ShardedTreeRegistry ├── Shard 0 → Merkle Tree (depth 20) ├── Shard 1 → Merkle Tree (depth 20) ├── ... └── Shard 15 → Merkle Tree (depth 20) ``` Each shard is an independent CommitmentTree with its own root, capacity (~1M leaves), and root history. ## Shard assignment ``` shard = uint256(commitment) % 16 ``` Deterministic: the same commitment always maps to the same shard. ## Proof generation When generating a proof for a sharded commitment: 1. Identify the shard: `shard = commitment % 16` 2. Get the shard's current root 3. Build the Merkle proof within that shard's tree 4. Generate the Groth16 proof as normal The proof format is identical — the only difference is which root is used. ## Trade-off: anonymity set Each shard has approximately 1/16th of the total commitments. This means the anonymity set per shard is smaller than the unified tree. For privacy-sensitive applications where anonymity set size is critical, use the standard single-tree CommitRevealVault. ## Capacity | Configuration | Total capacity | |---|---| | Single tree | ~1,048,576 commitments | | 16 shards | ~16,777,216 commitments | --- # Testnet The Specter testnet is a fully operational network for development and testing. It runs the same software as mainnet with identical contract deployments. ## Network status | Parameter | Value | |---|---| | Chain ID (EVM) | `5445` | | Chain ID (Cosmos) | `specter-testnet-1` | | RPC | `https://testnet.specterchain.com` | | WebSocket | `wss://testnet.specterchain.com/ws` | | Relayer API | `https://relayer.specterchain.com` | | Block Explorer | `https://explorer.specterchain.com` | | Block time | 1–2 seconds | | Finality | Instant (BFT) | ## What's deployed The testnet includes the full Ghost Protocol stack: - **Core contracts** — CommitRevealVault, CommitmentTree, NullifierRegistry - **Token contracts** — NativeAssetHandler, GhostERC20Factory, AssetGuard - **Proof verification** — Groth16ProofVerifier, GhostRedemptionVerifier - **Policy contracts** — PolicyRegistry, TimelockExpiry, DestinationRestriction, ThresholdWitness - **Open Protocol** — OpenGhostVault, OpenGhostReveal, OpenGhostKeyVault, PersistentKeyVault - **Supporting libraries** — PoseidonT3 See [Deployed Addresses](/contracts/deployed-addresses) for the complete address table. ## Relayer services Seven relayer services run on testnet to support Ghost Protocol operations: | Service | Purpose | |---|---| | Root Updater | Updates Merkle tree roots on-chain after new commitments | | Batch Root Updater | Updates roots for sharded batch operations | | Open Ghost Root Updater | Updates roots for the Open Protocol | | Commitment Relayer | Computes Poseidon hashes for frontend clients | | Proof Relayer | Generates Groth16 ZK proofs server-side | | Faucet | Distributes testnet GHOST tokens | | Stale Monitor | Alerts if Merkle roots go stale | ## Getting started on testnet 1. [Add the network to MetaMask](/evm/connecting-metamask) 2. [Get testnet GHOST from the faucet](/testnet/faucet) 3. [Deploy a contract](/evm/deploying-contracts) 4. [View it on the block explorer](/testnet/block-explorer) --- # Faucet The Specter testnet faucet distributes free GHOST tokens for development and testing. ## Claim tokens ```bash curl -X POST https://relayer.specterchain.com/api/faucet/claim \ -H "Content-Type: application/json" \ -d '{"address": "0xYOUR_EVM_ADDRESS"}' ``` ### Response (success) ```json { "success": true, "txHash": "0xabc123...", "amount": "100000000000000000000" } ``` ### Response (already claimed) ```json { "error": "Address has already claimed", "claimedAt": "2025-01-15T10:30:00.000Z" } ``` ## Check claim status ```bash curl "https://relayer.specterchain.com/api/faucet/status?address=0xYOUR_EVM_ADDRESS" ``` ### Response ```json { "address": "0xYOUR_EVM_ADDRESS", "claimed": true, "claimedAt": "2025-01-15T10:30:00.000Z", "txHash": "0xabc123..." } ``` ## Limits | Parameter | Value | |---|---| | Amount per claim | 100 GHOST | | Claims per address | 1 | | Rate limit | 5 requests/minute/IP | | Status check rate limit | 15 requests/minute/IP | ## Programmatic usage ```javascript async function claimFromFaucet(address) { const response = await fetch('https://relayer.specterchain.com/api/faucet/claim', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address }), }); return response.json(); } ``` If you need more testnet GHOST beyond the faucet limit, reach out on the [Specter Discord](https://discord.gg/specter). --- # Block Explorer Specter uses [Blockscout](https://explorer.specterchain.com) as its block explorer. Blockscout provides a full-featured interface for viewing transactions, contracts, tokens, and chain statistics. ## Features - **Transaction viewer** — search by hash, view decoded input data, event logs, internal transactions - **Contract verification** — verify and publish contract source code - **Token tracker** — view ERC20 token transfers and balances - **Address pages** — transaction history, token holdings, internal transactions - **API** — programmatic access to chain data ## Verifying contracts After deploying a contract, verify its source code on Blockscout for public auditability: ```bash forge verify-contract $CONTRACT_ADDRESS src/MyContract.sol:MyContract \ --verifier blockscout \ --verifier-url https://explorer.specterchain.com/api \ --chain-id 5445 ``` ## API access Blockscout exposes an Etherscan-compatible API: ```bash # Get contract ABI curl "https://explorer.specterchain.com/api?module=contract&action=getabi&address=$CONTRACT_ADDRESS" # Get transaction status curl "https://explorer.specterchain.com/api?module=transaction&action=gettxinfo&txhash=$TX_HASH" # Get token balance curl "https://explorer.specterchain.com/api?module=account&action=tokenbalance&contractaddress=$TOKEN&address=$WALLET" ``` --- # Troubleshooting Common issues when developing on Specter testnet and how to resolve them. ## Gas price returns 0 **Symptom**: `eth_gasPrice` returns `0x0`, causing transactions to fail. **Cause**: The Cosmos EVM module may report 0 gas price, but the chain enforces a minimum of 1 gwei. **Fix**: Always set gas price explicitly: ```javascript // viem const hash = await walletClient.sendTransaction({ to: recipient, value: amount, gasPrice: 1_000_000_000n, // 1 gwei }); // ethers.js const tx = await contract.myFunction({ gasPrice: 1_000_000_000n, }); ``` ```bash # Foundry cast send $CONTRACT "myFunction()" \ --gas-price 1000000000 \ --rpc-url https://testnet.specterchain.com ``` ## Transaction nonce errors **Symptom**: `nonce too low` or `nonce already used` errors. **Fix**: Reset your MetaMask account (Settings > Advanced > Clear activity tab data), or manually specify the nonce: ```bash NONCE=$(cast nonce $YOUR_ADDRESS --rpc-url https://testnet.specterchain.com) cast send $CONTRACT "myFunction()" --nonce $NONCE ... ``` ## PoseidonT3 library not linked **Symptom**: Deployment fails with `unresolved library` or similar errors when deploying Ghost Protocol contracts. **Fix**: Add the `libraries` directive to your `foundry.toml`: ```toml libraries = [ "lib/poseidon-solidity/contracts/PoseidonT3.sol:PoseidonT3:0xa786eDD407eb9EbaCA5E624B7Ee7C31E3b7f9521", ] ``` See [Foundry Setup](/evm/foundry-setup) for the complete configuration. ## Commitment not found after commit **Symptom**: You committed tokens but the reveal fails with `InvalidRoot`. **Cause**: The Merkle root hasn't been updated on-chain yet. The root updater relayer watches for `CommitmentAdded` events and updates the on-chain root. This typically takes 5–15 seconds. **Fix**: Wait for the root update. You can poll the commitment tree contract: ```bash # Check if root has been updated cast call 0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87 \ "isKnownRoot(bytes32)(bool)" $MERKLE_ROOT \ --rpc-url https://testnet.specterchain.com ``` ## Reveal proof rejected **Symptom**: `InvalidProof` error when calling `reveal()`. **Common causes**: 1. **Wrong inputs** — public inputs must exactly match the commitment preimage values 2. **Stale root** — the proof was generated against a root that's no longer in the 100-root history window 3. **Nullifier already spent** — this commitment has already been revealed Check the nullifier registry: ```bash cast call 0x0987cc3dE6f76c4c8834Dc6205De24968091C58b \ "isSpent(bytes32)(bool)" $NULLIFIER \ --rpc-url https://testnet.specterchain.com ``` ## Relayer API errors **Symptom**: 401 or 403 from relayer endpoints. **Cause**: The commitment relayer and proof relayer require HMAC authentication. **Fix**: Ensure you're including the correct HMAC signature in request headers. See [Commitment Relayer](/relayer/commitment-relayer) and [Proof Relayer](/relayer/proof-relayer) for authentication details. ## RPC connection issues **Symptom**: Cannot connect to `https://testnet.specterchain.com`. **Checklist**: 1. Verify URL is correct (no trailing slash for JSON-RPC) 2. Check chain ID is `5445` 3. Try the direct IP: `http://143.198.23.194:8545` (for debugging only) 4. WebSocket endpoint is `wss://testnet.specterchain.com/ws` --- # Security Best Practices Guidelines for building secure applications on Specter. ## Commitment secrets - **Never reuse secrets** — each commitment must have unique `secret`, `nullifierSecret`, and `blinding` values - **Use cryptographically random values** — generate secrets from `crypto.getRandomValues()` or `crypto.randomBytes()`, never from predictable sources - **Store securely** — commitment secrets are the only way to reveal committed tokens. If lost, tokens are permanently unrecoverable - **Never transmit in plaintext** — if using the commitment relayer or proof relayer, understand that the operator can see your secrets. For maximum privacy, compute hashes and proofs client-side ## Smart contract security - **Use ReentrancyGuard** — all contracts interacting with the vault should be non-reentrant - **Check return values** — verify that precompile calls return `true` - **Set gas limits** — policy validation is capped at 100K gas, but your own contracts should use appropriate gas limits - **Follow CEI pattern** — checks-effects-interactions to prevent reentrancy ## Gas price - **Always set explicitly** — `eth_gasPrice` may return 0. Use 1 gwei minimum. - **Don't rely on gas estimation** — Cosmos EVM gas estimation may differ from Ethereum ## Key management - **Separate testnet and mainnet keys** — never use testnet keys for real funds - **Use hardware wallets** for production deployments - **Rotate deployer keys** after initial deployment ## Privacy considerations - **Use fresh addresses for reveals** — revealing to your commit address defeats the privacy purpose - **Wait before revealing** — immediate reveals narrow the anonymity set - **Batch with other users** — privacy improves with more commitments in the tree - **Don't link on-chain behavior** — avoid patterns that correlate your commit and reveal addresses ## Policy security - **Verify policy logic** — policies are called via `staticcall` and cannot modify state, but they can read any on-chain data - **Test policy edge cases** — expired timelocks, boundary conditions, zero values - **Keep gas usage low** — policies exceeding 100K gas will fail silently --- # Threat Model Known attack surfaces and mitigations for the Ghost Protocol. ## Commitment grinding **Threat**: An attacker precomputes commitments to find one that matches a target. **Mitigation**: The blinding factor adds entropy. With a random 31-byte blinding factor, the search space is ~$2^{248}$, making grinding infeasible. ## Double-reveal **Threat**: An attacker reveals the same commitment twice to mint tokens. **Mitigation**: The NullifierRegistry tracks spent nullifiers. Each commitment produces a unique nullifier via `Poseidon2(nullifierSecret, leafIndex)`. A second reveal with the same nullifier is rejected. ## Supply inflation **Threat**: A malicious contract mints more tokens than were burned. **Mitigation**: 1. Only governance-authorized contracts can call the Ghostmint precompile 2. The `PreCommit` ABCI hook verifies `totalMinted - totalBurned <= 1B GHOST` on every block 3. The NativeAssetHandler restricts callers to the CommitRevealVault ## Merkle root manipulation **Threat**: A malicious root updater submits an incorrect root. **Mitigation**: The root updater operator address is restricted. Only authorized operators can submit roots. The on-chain contract verifies that the new root is consistent with inserted leaves. ## Proof forgery **Threat**: An attacker creates a valid-looking proof without knowing the preimage. **Mitigation**: Groth16 proofs are computationally sound — forging a proof requires breaking the discrete log problem on BN254 or the knowledge-of-exponent assumption. The verification key is hardcoded from the trusted setup. ## Front-running **Threat**: A miner/validator reorders transactions to front-run reveals. **Mitigation**: Reveals are non-competitive — the nullifier and recipient are bound in the proof. Front-running a reveal only moves the gas cost to the attacker without gaining them any tokens. ## Relayer trust **Threat**: The relayer operator sees secret values submitted to the commitment and proof relayer APIs. **Mitigation**: Use client-side proof generation (snarkjs in browser) for maximum privacy. The relayer APIs are convenience services, not required. The chain itself never receives plaintext secrets. ## Quantum computing **Threat**: Future quantum computers could break BN254/Groth16. **Mitigation**: The commitment structure includes a `quantumCommitment` field for post-quantum resistance. This is a forward-compatible design — quantum-resistant proofs can be required via a protocol upgrade without re-committing. --- # Security Audits Specter has undergone comprehensive security audits covering smart contracts, ZK circuits, and infrastructure. ## Audit reports | Audit | Scope | Status | |---|---|---| | Smart Contract Audit | All Solidity contracts (core, token, policy, scaling) | Completed, all findings remediated | | ZK Circuit Audit | Circom circuits (redemption, access proof) | Completed, all findings remediated | | Infrastructure Audit | Validator, relayer, nginx, systemd, PM2 | Completed, 25 findings all remediated | | Scaling Audit | Batch/sharded architecture (BatchCommitRevealVault, SessionVault, ShardedTreeRegistry) | Completed | ## Audit coverage ### Smart contracts - CommitRevealVault — reentrancy, access control, overflow - CommitmentTree — Merkle tree integrity, root history - NullifierRegistry — uniqueness, ordering - NativeAssetHandler — precompile interaction, authorization - GhostERC20 / Factory — mint/burn access control, CREATE2 - Policy contracts — validation logic, gas limits - AssetGuard — authorization model ### ZK circuits - Redemption circuit — constraint soundness, completeness - Access proof circuit — non-destructive proof properties - Poseidon hash — implementation correctness - Merkle proof verification — path validation - Nullifier derivation — uniqueness guarantees ### Infrastructure - Validator node security — firewall, TLS, key management - Relayer services — rate limiting, HMAC auth, CORS - nginx configuration — header security, TLS settings - PM2 process management — restart policies, memory limits ## Responsible disclosure If you discover a security vulnerability, please report it responsibly. Contact the team via the [Specter Discord](https://discord.gg/specter) security channel or email security@specterchain.com. --- # Glossary | Term | Definition | |---|---| | **aghost** | The base unit of the GHOST token. 1 GHOST = 10^18 aghost. Equivalent to wei in Ethereum. | | **Access proof** | A non-destructive ZK proof that proves knowledge of a commitment without consuming it. Used for persistent key access. | | **AssetGuard** | Smart contract that maintains the whitelist of tokens authorized for Ghost Protocol operations. | | **Blinding factor** | A random value included in the commitment preimage to prevent grinding attacks. | | **BN254** | The elliptic curve (also called alt_bn128) used for Groth16 proof verification. Supported natively by Ethereum precompiles. | | **CometBFT** | The Byzantine Fault Tolerant consensus engine used by Specter. Provides instant finality. | | **Commitment** | A Poseidon hash of secret inputs (secret, nullifierSecret, tokenIdHash, amount, blinding). Stored in the Merkle tree. | | **CommitRevealVault** | The central smart contract that orchestrates all Ghost Protocol commit and reveal operations. | | **CommitmentTree** | An append-only Merkle tree (depth 20, ~1M capacity) that stores all commitments. | | **Ghost Protocol** | Specter's core privacy primitive — a commit-reveal system using ZK proofs for unlinkable token transfers. | | **GhostERC20** | A privacy-enabled ERC20 token with vault-controlled mint/burn functions. | | **Ghostmint** | The precompile at address 0x0808 that enables smart contracts to mint and burn native GHOST tokens. | | **GHOST token** | The native gas and staking token of the Specter network. | | **Groth16** | A zero-knowledge proof system with constant-size proofs (256 bytes) and fast verification. | | **Merkle tree** | A hash tree structure used to prove set membership. Specter uses depth-20 Poseidon Merkle trees. | | **NativeAssetHandler** | The smart contract authorized to call the Ghostmint precompile on behalf of the CommitRevealVault. | | **Nullifier** | A value derived from the user's nullifierSecret and leaf index. Registered on-chain to prevent double-reveals. | | **NullifierRegistry** | Smart contract that tracks spent nullifiers. | | **Open Protocol** | A variant of Ghost Protocol for public data commitments (no privacy, but integrity and provenance). | | **Phantom Keys** | A key management system using access proofs for persistent, reusable key storage. | | **Policy** | A smart contract implementing IRevealPolicy that enforces conditions on reveals (timelocks, restrictions, etc.). | | **PolicyRegistry** | Smart contract that maps commitments to their associated reveal policies. | | **Poseidon** | A ZK-friendly hash function used for commitments and Merkle tree nodes. | | **PoseidonT3** | The on-chain Poseidon hash library (arity 2, ~55KB bytecode). | | **Reveal** | The process of proving knowledge of a commitment's preimage and minting fresh tokens. | | **Root updater** | A relayer service that maintains the Merkle tree off-chain and submits updated roots on-chain. | | **ShardedTreeRegistry** | A registry managing 16 parallel Merkle trees for scaling. | | **snarkjs** | A JavaScript library for generating and verifying Groth16 ZK proofs. | | **specterd** | The Specter node binary (CLI + daemon). | | **Trusted setup** | The ceremony that generates the Groth16 proving and verification keys. | --- # Reference ## Quick reference | Resource | URL | |---|---| | RPC (HTTPS) | `https://testnet.specterchain.com` | | WebSocket | `wss://testnet.specterchain.com/ws` | | CometBFT RPC | `https://testnet.specterchain.com/rpc/` | | Relayer API | `https://relayer.specterchain.com` | | Block Explorer | `https://explorer.specterchain.com` | | Whitepaper | `https://whitepaper.specterchain.com` | | Main Site | `https://specterchain.com` | | GitHub | `https://github.com/Specter-Foundation` | | Discord | `https://discord.gg/specter` | | X / Twitter | `https://x.com/specterchain` | ## Chain IDs | Network | EVM Chain ID | Cosmos Chain ID | |---|---|---| | Testnet | 5445 | specter-testnet-1 | | Mainnet | 5447 | — | ## Key contract addresses (testnet) | Contract | Address | |---|---| | CommitRevealVault | `0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70` | | CommitmentTree | `0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87` | | NullifierRegistry | `0x0987cc3dE6f76c4c8834Dc6205De24968091C58b` | | NativeAssetHandler | `0x35cdaE691037fcBb3ff9D0518725F1ae98d502b7` | | GhostERC20Factory | `0x925B548F059C0B8B6CF7168Efb84881252F88C8E` | | Groth16ProofVerifier | `0x2C8Fb67874E5f380efB995a9Ab59b2Ef327E5bd2` | | Ghostmint Precompile | `0x0000000000000000000000000000000000000808` | | PoseidonT3 | `0xa786eDD407eb9EbaCA5E624B7Ee7C31E3b7f9521` | See [Deployed Addresses](/contracts/deployed-addresses) for the complete table. ## Relayer API endpoints | Endpoint | Method | Auth | Description | |---|---|---|---| | `/api/commitment/compute` | POST | HMAC | Compute Poseidon commitment | | `/api/proof/generate` | POST | HMAC | Generate Groth16 proof | | `/api/faucet/claim` | POST | None | Claim testnet GHOST | | `/api/faucet/status` | GET | None | Check claim status | | `/health` | GET | None | Service health check | ## Foundry quick start ```bash forge create src/MyContract.sol:MyContract \ --rpc-url https://testnet.specterchain.com \ --chain-id 5445 \ --private-key $PRIVATE_KEY ``` ## MetaMask network config | Field | Value | |---|---| | Network Name | Specter Testnet | | RPC URL | `https://testnet.specterchain.com` | | Chain ID | 5445 | | Currency Symbol | GHOST | | Explorer | `https://explorer.specterchain.com` | ## External documentation - [Cosmos SDK](https://docs.cosmos.network/) — framework documentation - [CometBFT](https://docs.cometbft.com/) — consensus engine - [Foundry Book](https://book.getfoundry.sh/) — Solidity development toolkit - [snarkjs](https://github.com/iden3/snarkjs) — ZK proof generation - [circomlibjs](https://github.com/iden3/circomlibjs) — Poseidon hashing - [viem](https://viem.sh/) — TypeScript Ethereum client - [ethers.js](https://docs.ethers.org/v6/) — Ethereum library