signer/README.md
v0.1.0 · 6.4 KB
# Optional Phoenix signing runtime
This experimental Node process signs exact ticks/lots for Phoenix perpetuals. It makes no trading decisions. One intent produces at most one signature; retransmission uses the identical signed bytes. Unknown signed intents are retained for resolution and are never re-signed.
## Configuration
The `PHX_SIGNER_` prefix belongs to this package. No configuration file or account identity is bundled. `parseConfig(env)` does not read key material. `takeBotSecret(env)` reads the delegate key from a systemd credential named `markpaper-phx-delegate` when present, otherwise from `PHX_SIGNER_BOT_SECRET_KEY`; it deletes the environment variable after reading. Never place a real key in a command-line argument.
Windows PowerShell starts the retained script after settings have been provisioned in the environment:
```powershell
node .\signer\sidecar.mjs
```
Linux uses `node signer/sidecar.mjs` with the same configuration. The script listens on loopback only, rejects browser Origin headers, requires a bearer token of at least 32 characters, and compares it in constant time.
Required identity and transport settings:
| Setting suffix after `PHX_SIGNER_` | Meaning |
|---|---|
| `BOT_PUBKEY` | Canonical public key of the delegated signer |
| `AUTHORITY` | Canonical public key of the owner; must differ from the delegate |
| `TOKEN` | Bearer token, no whitespace |
| `RPC_URL` | Caller-selected Solana RPC; HTTPS unless loopback |
| `PORT` | Explicit loopback listener port |
| `CLIENT_ORDER_NAMESPACE` | Caller-defined hash namespace for public client order IDs |
`READONLY` defaults to true, `BIND` to `127.0.0.1`, and `API_URL` to the public Phoenix REST host. `TRADER_PDA_INDEX` and `SUBACCOUNT_INDEX` default to the supported cross account 0/0. `JOURNAL` is required when readonly is false; journal records are fsynced before broadcast. `SKIP_PREFLIGHT` defaults to false. `MAX_ORDER_NOTIONAL_USD` and `MAX_ORDER_COLLATERAL_MULT` are disabled unless provided. `COLLATERAL_LAZY` enables lazy on-chain collateral reads.
The `createSidecar` factory also requires an open journal and a signer matching the configured delegate when writes are enabled. An unreadable, corrupt or structurally invalid journal refuses startup. An injected journal must provide durable, atomic persistence equivalent to `openJournal`; the factory cannot prove the durability of a caller's implementation.
Every numeric policy below is required; there are no workload presets. Values must satisfy the decoder's documented bounds and cross-field checks.
| Policy suffixes | Purpose |
|---|---|
| `CU_ORDER`, `CU_CANCEL`, `CU_CANCEL_PER_ID` | Caller-measured compute budgets; total cannot exceed Solana's compute ceiling |
| `PRIORITY_FEE_MIN`, `PRIORITY_FEE_MAX`, `PRIORITY_FEE_PERCENTILE` | Priority fee estimation and explicit bounds |
| `IOC_SLOT_OFFSET`, `IOC_SLOT_OFFSET_MAX` | IoC lifetime in slots |
| `HTTP_WAIT_MS`, `RPC_TIMEOUT_MS` | Request/response windows |
| `MAX_INFLIGHT` | Maximum unresolved concurrent intents |
| `INTENT_TTL_MS`, `INTENT_TOMBSTONE_MS` | Final-result and spent-intent retention; stale signatures do not expire |
| `CONFIRM_POLL_MS`, `REBROADCAST_MS`, `CONFIRM_MAX_MS` | Confirmation polling and identical-byte rebroadcast |
| `EXPIRY_MARGIN_BLOCKS`, `EXPIRY_RECHECK_MS`, `RESOLVE_MS` | Expiry proof and background stale resolution |
| `META_REFRESH_MS`, `GLOBAL_CONFIG_TTL_MS` | Exchange metadata and chain-key cache lifetime |
| `HEADER_SHARE_MS`, `HEADER_HEARTBEAT_MS` | On-chain header sharing and refresh |
| `COLLATERAL_MAX_AGE_MS`, `COLLATERAL_REFRESH_MS`, `COLLATERAL_LAZY_MAX_AGE_MS` | Collateral freshness, polling and lazy reads |
| `SOL_REFRESH_MS`, `SOL_MAX_AGE_MS`, `MIN_SOL_LAMPORTS` | Fee-payer balance freshness and caller-selected minimum |
Numeric test settings in the offline suite are synthetic and are not recommended operating values. Measure compute use on the account you will operate. The per-ID compute allowance and PostOnly reduce-only acceptance remain unverified.
## Routes and outcomes
- `GET /health`: signer identity, binding, capabilities, fee-payer SOL, metadata and unresolved intent counts.
- `POST /order`, `POST /cancel`, `POST /cancelAll`: exact string u64 fields and a unique `intentId` required. Same ID and different request fingerprint is refused.
- `POST /simulate/order`, `/simulate/cancel`, `/simulate/cancelAll`: simulations only; no key signature or broadcast.
- `GET /tx/:signature`: reconcile an existing signature.
- `GET /account/:pda`: raw account bytes for independent header validation.
Writes return `confirmed`, `failed` or `unknown`. A timeout never proves failure. HTTP refusals before signing are distinct from a missing response. A known intent cannot be used for another operation. After retained results become tombstones, reuse is refused rather than signed again.
IoC packets set minimum fill to zero, use `CancelProvide`, and do not cancel existing orders. PostOnly packets use `slide:false`. Cancel batches are limited to 30 IDs and transaction size is checked before and after signing against 1232 bytes.
The signer cross-checks REST exchange keys with GlobalConfig on chain and pins market book/spline/tick/lot identity. Fixed caps and optional collateral-multiplier caps are caller supplied; reducing orders are exempt from the collateral multiplier only. When required collateral is unavailable, ordinary capped orders are refused.
## Delegation and key semantics
`buildDelegateTraderTx` is in `experimental/delegation`. It accepts the caller's owner and delegate keys and an injected Solana-kit RPC. It checks the on-chain header, builds unsigned owner-paid v0 bytes, and simulates with signature verification disabled. It never submits or signs.
Delegation restricts the operational key to position authority, but the public Rust SwapNative path may still permit collateral loss unless trader preferences or exchange flags close it. `keyRiskOf` is an experimental source-based verdict, not a verified attack demonstration. Key material loaded into this process is also available to code executing in its dependency graph; deleting an environment field is not memory erasure.
## Dependencies and license
The package's optional dependencies and the workspace lock provide the pinned SDKs. There is no second signer manifest or lockfile. The root core import does not load this runtime. Upstream MIT license notices are in `RISE-LICENSE.txt` and `SOLANA-KIT-LICENSE.txt`; the package itself is Apache-2.0.