Security model
GhostLink limits what a spending session can authorize. It does not make an untrusted page safe to receive your master identity, make deposits private, or replace backend authentication. These are the source-level controls and the assumptions they rely on.
Invariants (program-enforced; test-covered except where noted)
- No instruction moves tokens to a non-home address except an atomic
fill_begin/fill_endpair in a single transaction, andfill_endmeasures delivery on the very token accountfill_beginsnapshotted. - The pocket uses its PDA signer only for its own SPL token transfers and account closures. Every
invoke_signedcall site lives intoken.rs; signing authority is not forwarded to solver-selected programs. open_sessioncan never change the home or policy of an existing pocket, and every new session must expire later than the one before it, so a capturedopen_sessioncannot reinstate a revoked session.- Proofs bind a domain, a session commitment or action hash, and roots are checked against a 128-entry history ring — never a single current root.
- Signature precompiles are verified byte-for-byte (program id, key, message, offsets), never by mere presence; passkey assertions must carry both user-presence and user-verification.
- State-changing instructions refuse to run under CPI (
get_stack_height() == 1) — tested through a caller program that forwards register, fills, revoke and sweep. - Intent nonces are a sliding window: values the window has moved past read as used forever, so an old signed intent can never resurface.
- Owner/session administrative authorizations bind the current
owner_action_nonce. Successful sweep, revoke, propose, cancel and execute operations advance it, invalidating already-consumed authorizations.
What an attacker gets
| Attacker | Consequence | Control and assumptions |
|---|---|---|
| App holding only a software session | Can spend within policy limits and cause losses through trades until expiry or revocation | Daily cap, priced-output floor, passkey threshold and home-only direct withdrawals; market risk remains |
| Stolen software session key | Same as above, until you revoke | Caps, expiry and passkey threshold; owner recovery can authorize revoke or sweep if the secret and submission access remain available |
| Compromised relayer or indexer | Downtime and request linkage; a trusted off-chain root source can also lie about accepted roots | On-chain programs enforce their root rings. Backends must trust their configured indexer or read rings from a trusted RPC |
| Rogue solver | Can refuse service or execute a signed trade that satisfies its bounds | Under-delivery reverts the transaction; the reference solver separately refuses uneconomic intents to protect its own inventory |
| Secret thief proposing a new home | Can take the pocket's funds if an unauthorized home change completes | A timelock allows cancellation or sweep before execution. Detection requires an available, configured watcher or active monitoring |
| Phishing page that gets you to sign the sign-in message | Without a factor: the identity secret. With a factor: the signature alone cannot reconstruct that secret | The message names the identity domain, but wallet warnings vary. The passkey factor helps only while the factor and derived secret remain protected |
| App or injected script given the raw identity | Can derive identities for every domain and generate owner proofs | The SDK is not an origin sandbox. Keep secrets and wallet witnesses in a separately trusted host; no isolated broker ships in this SDK |
| Captured off-chain login payload | Can be replayed if the backend treats a proof as a bearer credential | Verify the proof-bound session, verify its signature over loginChallengeDigest, and atomically consume the server's nonce once |
| Compromised Actions endpoint | Can return a malicious deposit transaction after serving correct metadata | verifyFunding checks metadata only; validate the actual transaction or trust its provider before signing |
Assumptions
- The identity host protects the secret and keeps proving local. The SDK does not automatically persist or encrypt it; a host that stores it must provide that protection.
- Artifact SHA-256 checking is enabled only when the host supplies trusted hashes. The prover runs in its caller's context; a worker is an integration choice.
- The pinned Pyth feeds are honest within their confidence bounds.
- The Groth16/BN254 assumptions and trusted setup hold. Existing development keys are not a production ceremony; an independently reviewed mainnet setup remains a release requirement.
What we refuse to claim
GhostLink is not a mixer: pocket activity, deposits and sweeps are public on-chain. Per-domain IDs avoid directly disclosing the wallet in a proof, but transaction trails, identifying requests, a small registration set or a leaked secret can link activity. The census proves a balance existed at a moment, not exclusivity of funds.
Process
Public source and passing tests are not an independent audit or proof of a deployment's configuration. The mainnet release plan calls for two independent audits, a public proving-key ceremony, verifiable builds and removal of upgrade authority. These are planned gates, not completed guarantees. A future incompatible program requires an explicit migration; existing deployments must not be assumed to include every source change.