Skip to main content

System Components

This page details how each component in the Specter architecture connects to the others and walks through the complete data flow for both commit and reveal operations.

Component Interaction Map

Commit Flow — Step by Step

The commit operation destroys tokens and records an on-chain commitment that can later be claimed by anyone who knows the corresponding secret.

Step 1: Client-Side Computation

The user's client (web browser or mobile app) performs all sensitive computation locally:

  1. Generate random secret — a 256-bit random value known only to the user.
  2. Derive nullifier — a deterministic value derived from the secret. This will be used at reveal time to prevent double-spending.
  3. Compute Poseidon7 commitment:
commitment = Poseidon7(secret, nullifier, assetId, amount, chainId, phantom, aux)

The seven inputs encode everything about what is being committed: the secret (for ownership), the nullifier (for spend tracking), the asset type, the amount, the chain ID (for cross-chain safety), a phantom key component, and auxiliary data.

  1. Compute keccak256 quantum commitment:
quantumCommitment = keccak256(abi.encodePacked(secret, nullifier, amount, ...))

This redundant commitment uses a hash function resistant to quantum attacks, providing a second authentication factor at reveal time.

  1. Generate phantom key — a compact representation of all the data needed to reconstruct the secret and perform a reveal. This key is what gets stored (on device, exported as QR code, or written to an NFC card).

Step 2: On-Chain Commit Transaction

The client (or the Commitment Relayer on behalf of the client) calls CommitRevealVault.commit():

function commit(
bytes32 commitment,
uint256 amount,
bytes32 quantumCommitment,
address asset,
bytes calldata auxData
) external payable;

The vault executes the following sequence:

  1. Token burn. For native GHOST commits, the vault calls NativeAssetHandler.burn(), which invokes the ghostmint precompile at 0x0808 to destroy amount of aghost via the Cosmos x/bank module. For ERC-20 commits, the tokens are transferred to and locked in the vault.

  2. Tree insertion. The vault calls CommitmentTree.insert(commitment), which appends the commitment as a new leaf in the on-chain Merkle tree and returns the leafIndex.

  3. Quantum commitment storage. The quantumCommitment is stored in a mapping indexed by the Poseidon commitment. It will be checked at reveal time.

  4. Event emission. A Commit event is emitted containing the commitment hash, leaf index, asset, amount, and timestamp. Clients and the Indexer listen for this event.

Step 3: Key Delivery

After a successful commit, the user's phantom key is the sole credential needed to reveal. It can be:

  • Stored on device — encrypted in the webapp's local storage or the mobile app's secure enclave.
  • Exported as QR code — scanned by another device to transfer reveal capability.
  • Written to NFC card — the Card Relayer writes the key to an NTAG 424 DNA card, creating a physical bearer instrument.

Reveal Flow — Step by Step

The reveal operation proves knowledge of a valid commitment and mints fresh tokens to a designated recipient.

Step 1: Key Loading

The user loads their phantom key by one of:

  • Opening the webapp/mobile app where the key is stored.
  • Scanning a QR code.
  • Tapping an NFC card (the Card Relayer validates the NTAG 424 DNA SUN signature before extracting the key).

From the phantom key, the client reconstructs the secret, nullifier, amount, assetId, and all other commitment parameters.

Step 2: Proof Generation

The client must generate a Groth16 zero-knowledge proof demonstrating:

  1. Knowledge of a valid commitment. The prover knows values (secret, nullifier, assetId, amount, chainId, phantom, aux) such that Poseidon7(...) equals a leaf in the Merkle tree.
  2. Merkle membership. The commitment exists in the tree under the current root. The prover supplies the Merkle path (sibling hashes) as private inputs.
  3. Correct nullifier derivation. The nullifier in the public signals was correctly derived from the secret.

Public signals (visible on-chain): root, nullifier, recipient, amount, assetId, chainId.

Private inputs (never leave the prover): secret, phantom, aux, merklePath, merklePathIndices.

Client-Side vs. Relayer-Side Proving

EnvironmentProving approachTrade-off
Web browserWASM Groth16 prover runs in-browser. All private inputs stay local.Maximum privacy; slower (~15-30 seconds on modern hardware).
Mobile devicePrivate inputs sent to the Proof Relayer over TLS. Relayer generates proof and returns it.Faster; requires trusting the relayer with secret inputs during proof generation.
Server/CLINative Groth16 prover. All private inputs stay local.Fastest; full privacy.

Step 3: On-Chain Reveal Transaction

The Reveal Relayer (or the client directly) calls CommitRevealVault.reveal():

function reveal(
bytes calldata proof,
bytes32 root,
bytes32 nullifier,
address recipient,
uint256 amount,
address asset,
bytes32 quantumPreimage,
bytes calldata auxData
) external;

The vault executes the following sequence:

  1. Proof verification. The vault calls ProofVerifier.verifyProof(proof, publicSignals). The verifier checks the Groth16 proof against the verification key hardcoded in the contract. If invalid, the transaction reverts.

  2. Quantum preimage verification. The vault computes keccak256(quantumPreimage) and checks it against the stored quantum commitment. If it does not match, the transaction reverts. This is the quantum defense: even if an attacker forges a Groth16 proof (hypothetically, via a quantum computer breaking BN254), they cannot supply the correct keccak256 preimage without knowing the original secret.

  3. Nullifier check. The vault calls NullifierRegistry.checkAndRecord(nullifier). If this nullifier has been seen before, the transaction reverts (preventing double-spend). Otherwise, the nullifier is recorded permanently.

  4. Policy enforcement. The vault calls AssetGuard.checkPolicy(recipient, amount, asset, auxData). AssetGuard iterates over registered policy contracts and calls each one. If any policy rejects the operation, the transaction reverts. Policies can enforce sanctions screening, time locks, amount limits, or custom business logic.

  5. Token minting. For native GHOST reveals, the vault calls NativeAssetHandler.mint(recipient, amount), which invokes the ghostmint precompile to mint fresh aghost via x/bank. For ERC-20 reveals, locked tokens are transferred from the vault to the recipient.

  6. Event emission. A Reveal event is emitted containing the nullifier, recipient, amount, and timestamp. The link to the original commitment is not recorded — this is the privacy guarantee.

Supporting Services

Root Updater

The Merkle root changes every time a new commitment is inserted into the CommitmentTree. The Root Updater service:

  1. Listens for LeafInserted events from the CommitmentTree contract.
  2. Recomputes the Merkle root from the full set of leaves.
  3. Caches the latest root and recent historical roots (proofs may reference a slightly stale root if a new commitment was inserted between proof generation and reveal submission).
  4. Serves the current root and Merkle path data to clients via a REST API.

Without the Root Updater, clients would need to reconstruct the entire Merkle tree client-side to generate proofs — which is impractical for mobile devices.

Commitment Relayer

Mobile clients face two constraints that make direct commitment difficult:

  1. Poseidon computation. The Poseidon hash function requires large prime field arithmetic that is expensive on mobile CPUs. The Commitment Relayer accepts the raw commitment inputs and computes Poseidon7(...) server-side.

  2. Gas payment. New users may not have GHOST for gas. The Commitment Relayer can submit the commit transaction on behalf of the user and include the gas cost in the committed amount.

The workflow:

Mobile → HTTPS POST to Commitment Relayer (secret, nullifier, amount, ...)
Commitment Relayer → Compute Poseidon7 → Submit commit tx → Return commitment hash + leaf index

Security note: When using the Commitment Relayer, the user's secret inputs are transmitted to the relayer. The relayer sees the raw secret. This is a trust trade-off accepted for mobile usability. Users who require maximum privacy should use the web application with in-browser Poseidon computation.

Proof Relayer

The Proof Relayer generates Groth16 proofs for clients that cannot run the prover locally:

Client → HTTPS POST to Proof Relayer (secret, nullifier, amount, merklePath, ...)
Proof Relayer → Run Groth16 prover → Return proof + publicSignals

The prover requires the full proving key (~50-100 MB) and significant memory. Mobile devices cannot efficiently run this. The Proof Relayer maintains the proving key in memory and generates proofs in 2-5 seconds.

Reveal Relayer

The Reveal Relayer submits reveal transactions so that users do not need GHOST in the recipient address for gas:

  1. Client sends the proof, public signals, and recipient address to the Reveal Relayer.
  2. The Reveal Relayer submits the reveal() transaction, paying gas from its own funded account.
  3. The relayer is reimbursed by deducting a small fee from the revealed amount (the recipient receives amount - relayerFee).

This is critical for the privacy model: if the user had to fund the recipient address with gas before revealing, the funding transaction would link the recipient to an existing address, potentially deanonymizing the reveal.

Card Relayer

The Card Relayer manages the NFC card lifecycle:

OperationDescription
Card provisioningWrites a phantom key to a blank NTAG 424 DNA card. Configures the card's SUN authentication parameters.
Card readingWhen a user taps a card, the relayer receives the SUN message (a cryptographic MAC over a rolling counter and card UID). It validates the MAC against the card's known key, confirming the tap is authentic and the card has not been cloned.
Card-bound revealAfter validating the card tap, the relayer extracts the phantom key, generates a proof (or delegates to the Proof Relayer), and initiates the reveal flow.

Data Flow Summary

The fundamental privacy property is that the path from G to H is off-chain and unlinkable. There is no on-chain transaction connecting a specific commit to a specific reveal. The only on-chain evidence is that some commitment was made and some reveal was executed, with the ZK proof guaranteeing that the reveal corresponds to a valid (but unidentified) commitment.