Identity & proofs

A deterministic wallet signature derives an identity secret. Login proofs hide the registered wallet while exposing an app ID and the requested public claims. This page explains what the proof establishes and what its host application must protect.

Registration and restoring an identity

On a trusted GhostLink identity page, the user's wallet signs the GhostLink sign-in message twice. It is a Sign-In-With-Solana message whose first line names GhostLink's domain and whose statement says only the GhostLink sign-in ever asks for it. Native sign-in lets a wallet display and check the requested domain; its warnings are wallet-dependent and are not a hard origin boundary. Wallets without native support sign the fixed bytes through signMessage. The SDK verifies both signatures against the requested wallet and message. If they match, the secret is SHA-256(signature) — which makes recovery "sign again on a new device". Restoring the identity requires the same wallet key, cluster, identity domain and fixed message. Non-deterministic signers are refused by default; the SDK's explicit random-secret fallback requires an independently stored backup and is not enabled by the dashboard.

Optionally, a passkey factor is mixed in: the passkey evaluates a pseudo-random function (WebAuthn PRF) over a fixed salt and the result joins the secret, so that wallet signature alone is insufficient to reproduce the identity. Choose it at registration — the registry is append-only, so the choice cannot be changed later — and use the same passkey on every device. Sync and PRF availability depend on the platform and authenticator; losing that factor can prevent recovery.

The wallet then signs one registration message committing to C = Poseidon(s, 0), a relayer pays the fee, and the registry program — after byte-for-byte verifying the signature through the Ed25519 precompile — appends one leaf to an append-only Merkle tree. One binding per wallet, no update, no delete operation. The usual setup signs the identity text twice and registration once; wallet fallback and passkey flows may add prompts. The relayer submits the registration transaction. Later recovery signs again, and wallet-funded deposits are transactions.

What a login proof states

Eleven public inputs, ~30k constraints, proved on the user's device in 1–3 seconds on a laptop in the recorded benchmark; phone timings are not yet measured (docs/benchmarks.md). The supplied prover runs locally, in its caller's context. It does not automatically create a worker. Its input contains the identity secret and wallet witness: never send that input to a remote prover. Artifact hashes are optional API fields; supplying trusted SHA-256 pins is what enables artifact integrity checking.

The host application's trust boundary

GhostLink.prove uses the current browser origin, but this is a convenience, not an isolation mechanism. An app receiving DerivedIdentity or the witness can read them and invoke the pure helpers for another origin. The SDK does not ship a cross-origin identity broker; an integration that promises to hide the wallet from an untrusted app must keep both secret and witness in a separately trusted execution context.

The census — balances without exposure

Anyone may call census.snapshot(wallet, token_account); the program reads the account itself (it refuses anything not owned by the token programs) and appends "wallet W held amount A of mint M at slot S" to a second tree. Your claim's maxAgeSlots decides how fresh a snapshot you demand.

Known limitation, stated plainly: the census proves a wallet held funds at a moment, not that those funds are exclusive to one identity. Keep claim windows short; don't promise more than this.

Verification paths

PathCostUse
Off-chain — @ghostlink/verifyNo transactionLogin, gating, allowlists; trusted roots plus a verified single-use challenge
On-chain — pocket.open_session~400k CU budgetOpening a spending pocket