Skip to main content

How Phantom Identity Works

The Phantom Identity lifecycle has six phases: Generate, Seal, Connect, Use, Disconnect, and Revoke. The first two happen once. Connect/Use/Disconnect repeat indefinitely. Revoke is optional and irreversible.

Phase 1: Generate

Identity generation happens entirely client-side. No network requests, no server interaction, no on-chain transactions.

Step 1: Create the Keypair

A fresh secp256k1 keypair is generated using a cryptographic random number generator:

privateKey = random(256 bits)  // secp256k1 scalar
publicKey = privateKey * G // secp256k1 point multiplication

This keypair is the identity's signing key. The public key becomes the pseudonymous address; the private key is what gets split and protected.

Step 2: Derive Commitment Secrets

From the keypair (or associated entropy), the following BN254 field elements are derived:

SecretDerivationPurpose
secretHKDF-SHA256 from keypair entropyPrimary secret in commitment preimage
nullifierSecretHKDF-SHA256 from keypair entropyaccessTag derivation (connect) and nullifier derivation (revoke)
blindingHKDF-SHA256 from keypair entropyRandomizes the commitment (hiding property)
dataHashHash of identity-associated dataBinds the commitment to specific data content
quantumSecretRandom 256 bitsPost-quantum protection (keccak256 preimage)

Step 3: Compute Commitments

The primary commitment is a 4-input Poseidon hash:

commitment=Poseidon4(secret,  nullifierSecret,  dataHash,  blinding)\text{commitment} = \text{Poseidon}_4(\text{secret},\; \text{nullifierSecret},\; \text{dataHash},\; \text{blinding})

The quantum commitment is a keccak256 hash:

quantumCommitment=keccak256(quantumSecret)\text{quantumCommitment} = \text{keccak256}(\text{quantumSecret})

Step 4: Split the Private Key

The secp256k1 private key is split into two parts via XOR:

encKeyPartA = random(256 bits)
encKeyPartB = privateKey XOR encKeyPartA

Part A stays in the PNG. Part B goes on-chain. Neither part alone reveals the private key. See Split-Key Architecture for the full security analysis.

Step 5: Render the PNG

All metadata is embedded in the PNG tEXt chunk using standard PNG metadata fields. If the user chose a passphrase, sensitive fields are encrypted with AES-256-GCM (key derived via PBKDF2-SHA256, 100,000 iterations) before embedding.

The resulting PNG file is the Phantom Identity. The user downloads it and stores it securely.

Phase 2: Seal

Sealing publishes the commitment on-chain and stores the split key material in the PersistentKeyVault.

Step 1: Commit to the Merkle Tree

The commitment is submitted to the OpenGhostVault:

uint256 leafIndex = openGhostVault.commit{value: fee}(commitment);

The vault inserts the commitment into the OpenCommitmentTree (a depth-20 Merkle tree, shared with the Open Ghost data system) and returns the leaf index. A commitment fee is paid in native GHOST to prevent spam.

Step 2: Store Key Part B

The encrypted key part B, along with the quantum commitment, is stored in the PersistentKeyVault:

persistentKeyVault.storeKeyPartB(keyId, commitment, encKeyPartB, quantumCommitment);

The keyId is deterministically computed as:

keyId=keccak256(commitment,  issuerAddress)\text{keyId} = \text{keccak256}(\text{commitment},\; \text{issuerAddress})

Including issuerAddress in the key ID prevents front-running: an attacker who observes a commitment in the mempool cannot race to store their own key part B at the same key ID, because they would need to submit from the same address.

The PersistentKeyVault stores:

FieldTypeDescription
commitmentbytes32The associated commitment hash
encKeyPartBbytesEncrypted second half of the private key
quantumCommitmentbytes32Current quantum commitment (rotated on each access)
existsboolWhether the key entry is active

Unlike OpenGhostKeyVault, the PersistentKeyVault never deletes key material on access. The entry persists until explicitly revoked.

Phase 3: Connect (Repeatable)

Connect is the core operation of Phantom Identity. It recovers the secp256k1 private key from the PNG and the on-chain vault, using a zero-knowledge proof to authorize access. This phase can be repeated an unlimited number of times.

Step 1: Upload and Extract

The user uploads the PNG file. The client extracts all metadata from the tEXt chunk. If the PNG is passphrase-protected, the user enters the passphrase and the client decrypts the metadata using AES-256-GCM.

Step 2: Generate Access Proof

The client generates a Groth16 proof using the Access Proof Circuit:

Public inputs (4):

#InputValue
0rootCurrent Merkle tree root (fetched from chain)
1dataHashFrom PNG metadata
2sessionNonceFresh random value generated for this session
3accessTagPoseidon2(nullifierSecret,  sessionNonce)\text{Poseidon}_2(\text{nullifierSecret},\; \text{sessionNonce})

Private inputs (5):

InputSource
secretFrom PNG metadata
nullifierSecretFrom PNG metadata
blindingFrom PNG metadata
pathElements[20]Merkle proof fetched from chain
pathIndices[20]Merkle proof fetched from chain

The proof demonstrates:

  1. The prover knows the preimage of a commitment in the Merkle tree.
  2. The accessTag is correctly derived from the prover's nullifierSecret and the session nonce.

The proof does not compute or reveal a nullifier. The commitment is not consumed.

Step 3: Access the Vault

The client calls accessKeyPartB on the PersistentKeyVault:

bytes memory encKeyPartB = persistentKeyVault.accessKeyPartB(
keyId,
proof,
publicInputs,
quantumSecret,
newQuantumCommitment
);

The vault performs the following checks:

  1. Proof verification: forwards the proof and public inputs to the on-chain Groth16 verifier. Reverts if invalid.
  2. Anti-replay: checks that the accessTag has not been previously recorded. Reverts if it has. Records the new accessTag.
  3. Quantum verification: checks that keccak256(quantumSecret) == stored quantumCommitment. Reverts if it does not match.
  4. Quantum rotation: updates the stored quantum commitment to newQuantumCommitment (provided by the client).
  5. Key return: returns encKeyPartB without deleting it.

Step 4: Recover the Private Key

The client recombines the split key:

privateKey = encKeyPartA XOR encKeyPartB

The full secp256k1 private key is now available in the client's memory. The identity is "connected."

Phase 4: Use

With the recovered private key, the user can perform any on-chain operation:

OperationDescription
Sign transactionsStandard secp256k1 ECDSA signatures. No MetaMask, no external wallet.
Vanish/SummonCommit and reveal data or tokens through the Ghost Protocol.
Fund transfersSend native GHOST or ERC-20 tokens from the pseudonymous address.
Contract interactionsCall any smart contract function. Deploy contracts.
Multi-step workflowsPerform complex sequences of operations across multiple blocks, all from the same persistent pseudonym.

The pseudonymous address (derived from the secp256k1 public key) can hold balances, own NFTs, maintain state in contracts, and accumulate reputation — all without any link to the holder's real identity.

Phase 5: Disconnect

When the user is done, the client zeroes all sensitive key material from memory:

  • privateKey is overwritten with zeros.
  • encKeyPartB is overwritten with zeros.
  • Any derived signing keys or session material are cleared.

The PNG file is unchanged. The on-chain vault entry is unchanged. The identity persists in both locations, ready for the next connect.

Phase 6: Revoke (Optional)

Revocation permanently destroys the on-chain key material, making the identity unrecoverable. This is an irreversible operation — it is the Phantom Identity equivalent of "burning the key."

Step 1: Spend the Nullifier

The user generates a Redemption Proof (not an Access Proof) using the standard redemption circuit. This proof computes the commitment's nullifier:

nullifier=Poseidon2 ⁣(Poseidon2(nullifierSecret,  commitment),  leafIndex)\text{nullifier} = \text{Poseidon}_2\!\big(\text{Poseidon}_2(\text{nullifierSecret},\; \text{commitment}),\; \text{leafIndex}\big)

The proof is submitted to OpenGhostReveal, which verifies it and records the nullifier in the OpenNullifierRegistry. The nullifier is now permanently spent.

Step 2: Delete Key Material

The user calls revokeKey on the PersistentKeyVault:

persistentKeyVault.revokeKey(keyId, nullifier);

The vault verifies that the nullifier is spent in the OpenNullifierRegistry, then permanently deletes the key entry (commitment, encKeyPartB, quantumCommitment). The exists flag is set to false.

After revocation:

  • The on-chain key part B is gone. No one can recover the private key, even with the PNG.
  • The nullifier is spent. No new access proofs can authorize vault access (the vault checks exists before returning key material).
  • The identity is dead. The PNG is now a useless file.

Why Revocation Uses a Different Circuit

Revocation deliberately uses the Redemption Circuit (which spends a nullifier) rather than the Access Proof Circuit (which does not). This ensures that revocation is a one-time, irreversible operation — the nullifier can only be spent once, so the identity can only be revoked once. There is no way to "accidentally" revoke by using the wrong circuit during a normal connect, because the connect flow uses the Access Proof Circuit exclusively.