unrip/IMPLEMENTATION.md
philipp 8d2ca61fa1 Plan verifier salt hot-path turn
Proof: Archived the quote lifecycle retention turn and opened detailed PROOF.md/IMPLEMENTATION.md for removing verifier salt refresh from quote-response execution hot path.

Assumptions: The current retention turn should be paused so the live salt-induced executor queue latency defect can take priority; no backlog item was selected because the turn was opened directly from live operator evidence.

Still fake: Planning only; salt cache refactor, executor rejection behavior, health/dashboard surfaces, deployment, and live latency evidence are not implemented yet.
2026-06-12 17:13:03 +02:00

235 lines
11 KiB
Markdown

# Implementation Turn: Verifier salt hot-path removal and executor queue guardrails
Status: open
Opened: 2026-06-12
## Goal
Remove verifier `current_salt` RPC latency from quote-response execution, fail closed quickly when no fresh signing salt is available, and make salt freshness plus executor queue delay visible to operators.
## Selected backlog items
- none selected; this turn was opened directly from the live latency defect observed on 2026-06-12.
## Design rules
- This turn is execution latency and visibility only.
- Do not change strategy selection, pricing, edge thresholds, notional limits, inventory checks, arming, signer identity, or pair enablement.
- Do not skip quotes because relay errors are likely.
- Do not submit a quote response if signing prerequisites are unavailable.
- Do not add active pair, edge, notional, latency, response-policy, or retention env vars.
- Keep old executor result rows readable.
- Deploy only through repo workflow.
## Problem statement
The quote-response executor currently calls `verifierSaltCache.getFreshSalt()` from inside `handleCommand`. If the cache decides it needs to refresh, the command waits for the NEAR verifier `current_salt` RPC before signing.
Live evidence showed this can block for seconds:
- `ae824f64-ddfd-4310-a44f-69c401fc8ad7`
- `current_salt_source=refresh`
- `current_salt_ms=7481.458`
- executor received the command quickly, then blocked inside salt refresh
- following commands waited in the single executor consumer queue:
- `command_to_executor_ms` around `7.5s`
- some later rows around `10-12s`
- most failed with `quote_not_found_or_finished`
External verifier latency must not sit on the per-quote execution critical path.
## Backend changes
### 1. Inspect current salt and executor paths
- Inspect:
- `src/core/verifier-salt-cache.mjs`
- `src/apps/trade-executor.mjs`
- executor command expiry helpers
- executor timing payload construction
- runtime health and service snapshot summaries
- ops sentinel alert generation
- operator dashboard service/system views
- existing verifier salt cache and executor tests
- Identify every call path that can refresh verifier salt.
- Confirm which call paths are quote-response hot path versus background or operator-triggered work.
### 2. Define salt cache contract
- Add or refine a cache API with separate responsibilities:
- background refresh may perform network I/O
- hot-path read must never perform network I/O
- Candidate API:
- `start()` starts periodic refresh and immediate refresh attempt
- `refreshNow()` performs explicit refresh with in-flight de-duplication
- `getCachedFreshSalt({ now, maxAgeMs })` returns fresh salt or a typed unavailable result without I/O
- `getState()` returns freshness, age, last refresh times, duration, in-flight state, and error
- Preserve existing public behavior where needed by non-hot-path tests, but quote-response execution must use the non-blocking API.
- Validate malformed salt before it can enter the cache.
### 3. Background refresh behavior
- Ensure startup kicks off refresh without making the executor command handler wait on it.
- Refresh periodically before the salt becomes stale.
- If refresh fails:
- keep a still-fresh cached salt usable until its age limit expires
- expose last error and stale/unavailable state
- do not block command handling
- Record:
- `last_refresh_started_at`
- `last_refresh_completed_at`
- `last_refresh_duration_ms`
- `last_refresh_error`
- `refresh_in_flight`
- `salt_age_ms`
- `salt_fresh`
### 4. Trade-executor hot path
- Replace hot-path `await verifierSaltCache.getFreshSalt()` with non-blocking cached salt read.
- If no fresh cached salt is available:
- publish a durable trade execution result
- use status `rejected`
- use explicit result code such as `verifier_salt_unavailable`
- include salt cache state summary in the result payload
- do not call `buildQuoteResponseSubmission`
- do not call the relay
- Keep command expiry before signing so stale queued commands remain rejected as `stale_execute_command`.
- Keep idempotency and executor state-store semantics intact.
- Remove misleading timing for salt refresh from hot-path results; after this turn hot-path timing should show local cache read or unavailable.
### 5. Queue delay guardrails
- Keep recording `command_to_executor_ms` and `quote_age_at_executor_receipt_ms`.
- Add summary state for recent command queue delay, using existing durable result payloads or in-memory rolling metrics.
- Add thresholded runtime health fields:
- recent max command-to-executor
- p90 or bucketed queue delay if practical
- latest command queue delay warning reason
- Do not use queue delay to change strategy selection or pricing in this turn.
### 6. Runtime health and alerts
- Extend trade-executor `/state` and `/health` output with salt cache freshness and queue-delay status.
- Add ops-sentinel warning alerts for:
- verifier salt stale or unavailable
- verifier salt refresh failing
- executor command queue delay above a conservative threshold
- Keep alert scopes executor/service-level unless pair-specific evidence is available.
- Do not disarm automatically unless an existing safety invariant already does so.
### 7. Operator dashboard
- Surface salt cache state in the system/service view:
- fresh/stale/unavailable
- salt age
- last refresh time
- last refresh duration
- last refresh error
- Surface executor queue delay alongside existing trade-executor health.
- Preserve existing lifecycle table timing fields and old-row compatibility.
- Avoid presenting salt failures as trade PnL or settled outcomes.
### 8. Tests
- Add verifier salt cache tests:
- cached fresh read returns immediately and does not call the loader
- missing/stale salt returns unavailable without loader call
- background refresh records success duration and freshness
- background refresh records error without destroying still-fresh salt
- malformed salts fail closed
- Add trade-executor tests or refactorable helper tests:
- slow salt loader cannot block quote command handling
- no cached fresh salt publishes `verifier_salt_unavailable`
- no relay call happens when salt is unavailable
- stale command expiry still happens before salt lookup
- successful command uses cached salt and records `current_salt_source=cache`
- Add runtime health and alert tests for salt and queue warnings.
- Add dashboard/static tests for salt and queue visibility.
### 9. Validation and deploy
- Run targeted tests:
- verifier salt cache
- trade executor
- runtime health / ops sentinel
- dashboard static/operator tests
- Run full `npm test`.
- Build operator dashboard bundle.
- Commit with required workflow metadata.
- Push to `main` so repo workflow deploys.
- Do not manually patch or roll Kubernetes deployments.
### 10. Live evidence after deploy
- Confirm all repo-owned deployments run the new image.
- Query recent trade execution results:
- `current_salt_source`
- `current_salt_ms`
- `command_to_executor_ms`
- `quote_age_at_executor_receipt_ms`
- `result_code`
- `failure_category`
- Prove no post-deploy quote-response result used `current_salt_source=refresh`.
- Report command-to-executor p50/p90/p99 after rollout.
- Report salt cache health from trade-executor state or health.
- Confirm recent quote flow still writes decisions, commands, and results.
## Data and persistence
- Use existing executor result payloads for per-command salt and queue timing.
- Add fields additively so old rows remain readable.
- Prefer in-memory runtime state for recent queue summaries unless durable aggregation is already available.
- Do not add new long-term storage unless required for dashboard or alert correctness.
## Edge cases
- Salt cache empty at startup: commands reject quickly until background refresh succeeds.
- Salt cache stale: commands reject quickly, no relay call.
- Salt refresh in flight: commands use still-fresh cached salt or reject; they do not wait.
- Salt refresh fails while cached salt is still fresh: commands may continue using cached salt.
- Salt refresh fails and cache is stale: commands reject with explicit salt-unavailable result.
- Malformed salt from verifier: fail closed and expose error.
- Existing stale command: reject as stale command before salt lookup.
- Executor paused or disarmed: existing behavior remains.
- Old result rows without salt fields remain visible.
## Concrete implementation order
### Phase 1. Audit and contracts
- Inspect current salt cache and executor usage.
- Add salt-cache contract tests for non-blocking cached reads and refresh state.
- Decide exact result code and payload shape for salt-unavailable rejection.
### Phase 2. Salt cache refactor
- Implement non-blocking cached salt read.
- Implement refresh state fields and in-flight de-duplication.
- Ensure background refresh starts and continues.
- Keep existing cache tests passing with updated contract.
### Phase 3. Executor integration
- Change quote-response hot path to use cached salt only.
- Add salt-unavailable rejection result.
- Add timing/state fields for cache read and unavailable rejection.
- Add regression tests for slow salt loader and no relay call.
### Phase 4. Health, alerts, dashboard
- Expose salt freshness and queue delay in trade-executor state/health.
- Add ops-sentinel/runtime alert tests.
- Add dashboard visibility tests and UI fields.
### Phase 5. Validation and deployment
- Run targeted tests.
- Run full `npm test`.
- Build dashboard bundle.
- Commit and push.
- Collect live post-deploy evidence.
## Test plan
- `test/verifier-salt-cache.test.mjs`
- `test/trade-executor*.test.mjs` or new extracted executor helper tests
- `test/runtime-health.test.mjs`
- `test/ops-sentinel*.test.mjs`
- `test/operator-dashboard*.test.mjs`
- Static config tests proving no new active env knobs are added
- Full `npm test`
- Dashboard build
## Validation checklist against the proof
- Quote-response hot path contains no verifier salt network refresh.
- Slow `current_salt` loader does not cause multi-second command queue delay.
- Missing/stale salt creates durable explicit rejection without relay call.
- Salt freshness and refresh errors are operator-visible.
- Executor queue delay is operator-visible and alertable.
- Strategy behavior, edge, notional, inventory, arming, signer, and pair enablement are unchanged.
- Repo workflow deploys the result.
- Live results show `current_salt_source=cache` or salt-unavailable rejection, not hot-path `refresh`.
## Known fakes allowed at start of this turn
- Venue-native terminal fill ids remain unavailable.
- Fee-complete realized PnL remains unavailable.
- Executor concurrency remains single-consumer unless proven necessary after salt hot-path removal.