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
- "I know the secret behind a commitment that sits in the registry tree" — without saying which leaf.
- "My ID for this app is
app_id = Poseidon(s, domain_hash, 1)" — an ID derived for one domain. Knowing the secret allows derivation for every domain. - Optionally: "that wallet held at least
thresholdofmintin a census snapshot no older thanmin_slot." - It binds a session commitment and home hash. The pocket re-derives those values and refuses a session expiry at or below its previous maximum. Off-chain authentication separately needs a signed, single-use server challenge; the proof is not a bearer token.
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.
Verification paths
| Path | Cost | Use |
|---|---|---|
Off-chain — @ghostlink/verify | No transaction | Login, gating, allowlists; trusted roots plus a verified single-use challenge |
On-chain — pocket.open_session | ~400k CU budget | Opening a spending pocket |