From b82a369deb428f472994b9a5288dcb974a0595e6 Mon Sep 17 00:00:00 2001 From: christopher harris Date: Mon, 27 Jul 2026 01:29:36 -0500 Subject: [PATCH] Integrate DumbMoney Kalshi execution cell --- autonomy/brain.py | 56 +- autonomy/executor.py | 40 + autonomy/kill_reconciliation.py | 76 +- autonomy/reconciler.py | 118 +- .../dummy-kalshi-runner.v1.template.json | 41 + .../dummy-kalshi-runner.v1.schema.json | 169 ++ core/interprocess_lock.py | 76 + core/logger.py | 3 +- core/ontology.py | 13 +- core/state.py | 256 +- docs/DUMBMONEY_INTEGRATION.md | 236 ++ kalshi/client.py | 259 +- kalshi/signer.py | 8 +- kalshi/strict_json.py | 77 + live_firewall/dumbmoney_broker_witness.py | 101 + live_firewall/dumbmoney_capital.py | 2623 +++++++++++++++++ live_firewall/dumbmoney_command_feed.py | 1722 +++++++++++ live_firewall/dumbmoney_execution_cycle.py | 73 + live_firewall/dumbmoney_journal_anchor.py | 638 ++++ live_firewall/dumbmoney_lineage.py | 1323 +++++++++ live_firewall/dumbmoney_reconciliation.py | 128 + live_firewall/dumbmoney_windows_service.py | 1857 ++++++++++++ live_firewall/exposure_tracker.py | 1151 ++++++-- live_firewall/firewall.py | 750 ++++- live_firewall/kalshi_broker_truth.py | 725 +++++ live_firewall/kalshi_reconciliation.py | 891 ++++++ live_firewall/operational_journal.py | 414 +++ live_firewall/sqlite_operational_journal.py | 1215 ++++++++ .../kalshi_livebrokerfirewall_adapter.py | 15 +- .../dumbmoney/signed-capital-envelope.v1.json | 50 + tests/test_autonomy_pipeline.py | 7 + tests/test_dumbmoney_capital_envelope.py | 1969 +++++++++++++ tests/test_dumbmoney_command_feed.py | 1163 ++++++++ tests/test_dumbmoney_execution_cycle.py | 68 + tests/test_dumbmoney_journal_anchor.py | 302 ++ tests/test_dumbmoney_kalshi_broker_truth.py | 831 ++++++ tests/test_dumbmoney_lineage.py | 573 ++++ tests/test_dumbmoney_windows_service.py | 692 +++++ tests/test_kalshi_client.py | 178 +- .../test_kalshi_livebrokerfirewall_adapter.py | 2 - tests/test_kalshi_order_payload_schema.py | 13 +- tests/test_kalshi_strict_json.py | 94 + tests/test_live_cap_firewall_rehearsal.py | 5 +- tests/test_live_firewall.py | 2 +- tests/test_live_firewall_exposure_truth.py | 120 +- tests/test_live_kalshi.py | 11 +- tests/test_reconciler_transport_safety.py | 35 + tests/test_risk_state_persistence.py | 215 +- tests/test_sqlite_operational_journal.py | 528 ++++ tests/test_taker_execution_truth.py | 47 + 50 files changed, 21554 insertions(+), 405 deletions(-) create mode 100644 configs/dumbmoney/dummy-kalshi-runner.v1.template.json create mode 100644 configs/dumbmoney/schemas/dummy-kalshi-runner.v1.schema.json create mode 100644 core/interprocess_lock.py create mode 100644 docs/DUMBMONEY_INTEGRATION.md create mode 100644 kalshi/strict_json.py create mode 100644 live_firewall/dumbmoney_broker_witness.py create mode 100644 live_firewall/dumbmoney_capital.py create mode 100644 live_firewall/dumbmoney_command_feed.py create mode 100644 live_firewall/dumbmoney_execution_cycle.py create mode 100644 live_firewall/dumbmoney_journal_anchor.py create mode 100644 live_firewall/dumbmoney_lineage.py create mode 100644 live_firewall/dumbmoney_reconciliation.py create mode 100644 live_firewall/dumbmoney_windows_service.py create mode 100644 live_firewall/kalshi_broker_truth.py create mode 100644 live_firewall/kalshi_reconciliation.py create mode 100644 live_firewall/operational_journal.py create mode 100644 live_firewall/sqlite_operational_journal.py create mode 100644 tests/fixtures/dumbmoney/signed-capital-envelope.v1.json create mode 100644 tests/test_dumbmoney_capital_envelope.py create mode 100644 tests/test_dumbmoney_command_feed.py create mode 100644 tests/test_dumbmoney_execution_cycle.py create mode 100644 tests/test_dumbmoney_journal_anchor.py create mode 100644 tests/test_dumbmoney_kalshi_broker_truth.py create mode 100644 tests/test_dumbmoney_lineage.py create mode 100644 tests/test_dumbmoney_windows_service.py create mode 100644 tests/test_kalshi_strict_json.py create mode 100644 tests/test_reconciler_transport_safety.py create mode 100644 tests/test_sqlite_operational_journal.py diff --git a/autonomy/brain.py b/autonomy/brain.py index 21db199d..83a8a921 100644 --- a/autonomy/brain.py +++ b/autonomy/brain.py @@ -429,6 +429,7 @@ def _sync_live_exposure(self, outcomes: list[TradeOutcome]) -> None: OutcomeKind.FILLED: "filled", OutcomeKind.CANCELED: "canceled", OutcomeKind.EXPIRED: "expired", + OutcomeKind.REJECTED: "rejected", } for outcome in outcomes: order_id = str(outcome.order_id or "").strip() @@ -436,7 +437,8 @@ def _sync_live_exposure(self, outcomes: list[TradeOutcome]) -> None: continue terminal_state = terminal_states.get(outcome.kind) if outcome.kind is OutcomeKind.PARTIALLY_FILLED or terminal_state: - tracker.record_cumulative_fill( + proposal_id = tracker.client_order_id_for(order_id) + persisted = tracker.record_cumulative_fill( order_id, int(outcome.fill_count), ( @@ -445,6 +447,58 @@ def _sync_live_exposure(self, outcomes: list[TradeOutcome]) -> None: ), terminal_state=terminal_state, ) + capital_adapter = getattr( + self.executor, "capital_envelope_adapter", None + ) + witnessed_state = str( + outcome.detail.get("state") or "" + ).strip().lower() + terminal_witnesses = { + "filled": {"filled", "executed"}, + "canceled": {"canceled", "cancelled"}, + "expired": {"expired"}, + "rejected": {"rejected"}, + } + if ( + persisted + and terminal_state is not None + and witnessed_state + in terminal_witnesses.get(terminal_state, set()) + and proposal_id + and capital_adapter is not None + and outcome.broker_contacted is True + ): + receipt = { + "schema": "dummy.terminal-order-reconciliation.v1", + "terminal_status": terminal_state, + "proposal_id": proposal_id, + "decision_id": outcome.decision_id, + "market_ticker": outcome.market_ticker, + "order_id": order_id, + "fill_count": int(outcome.fill_count), + "fill_price_cents": outcome.fill_price_cents, + "broker_contacted": True, + "outcome_created_at": outcome.created_at, + "detail": dict(outcome.detail), + "local_exposure_projection_persisted": True, + } + try: + capital_adapter.release_from_terminal_reconciliation( + proposal_id=proposal_id, + terminal_status=terminal_state, + reconciliation_receipt=receipt, + ) + except Exception: + # Retaining the capital reservation is the fail-closed + # response to a missing/malformed terminal receipt. + logger.exception( + "DumbMoney terminal reservation release failed", + extra={ + "component": "reconciliation", + "proposal_id": proposal_id, + "order_id": order_id, + }, + ) def _close_position(self, state: RiskState, open_decision: dict[str, Any], result_yes: bool) -> None: diff --git a/autonomy/executor.py b/autonomy/executor.py index e3457b6b..7c2d6c38 100644 --- a/autonomy/executor.py +++ b/autonomy/executor.py @@ -95,6 +95,8 @@ def __init__( exchange_status_fn: Callable[[], dict[str, Any]] | None = None, now_fn: Callable[[], float] | None = None, execution_policy: ExecutionPolicy | None = None, + capital_envelope_adapter: Any | None = None, + core_submission_authority: Callable[[], bool] | None = None, ) -> None: self.mode = mode # Typed execution policy (Wave-2 A2/F2; Wave-5 taker path). The default @@ -131,6 +133,11 @@ def __init__( # failed, or malformed status blocks the submit # (exchange_status_unavailable_at_submit), as does maintenance/halt. self.exchange_status_fn = exchange_status_fn + # Optional DumbMoney deny-only capital ceiling. This adapter cannot + # replace the session acknowledgement, local risk state, kill switch, + # central firewall, or live-submit authority. + self.capital_envelope_adapter = capital_envelope_adapter + self.core_submission_authority = core_submission_authority self._now_fn = now_fn or (lambda: datetime.now(timezone.utc).timestamp()) # Counter so a refusal is never a silent skip: surfaced on the executor # for the daemon/dashboard, and each refusal is also recorded as a @@ -331,11 +338,17 @@ def _make_firewall(self): from live_firewall.exposure_tracker import get_persistent_exposure_tracker from live_firewall.firewall import LiveBrokerFirewall + if self.capital_envelope_adapter is None: + raise RuntimeError( + "DumbMoney capital adapter is mandatory for LIVE execution" + ) return LiveBrokerFirewall( KalshiClient(), get_persistent_exposure_tracker(), autonomy_risk_state_path=self.risk_state_path, require_autonomy_risk_state=True, + capital_envelope_adapter=self.capital_envelope_adapter, + core_submission_authority=self.core_submission_authority, ) def _risk_state_digest(self) -> str | None: @@ -545,6 +558,11 @@ async def _execute_live_via_firewall( ) -> TradeOutcome: if market is None or market.ticker != decision.market_ticker: return self._blocked(decision, "live_market_context_missing_or_mismatched") + if self.capital_envelope_adapter is None: + return self._blocked( + decision, + "dumbmoney_capital_adapter_required_for_live_execution", + ) try: firewall = self._make_firewall() except Exception as exc: @@ -560,6 +578,27 @@ async def _execute_live_via_firewall( risk_digest = self._risk_state_digest() if risk_digest is None: return self._blocked(decision, "persisted_autonomy_risk_state_unavailable") + capital_binding: dict[str, Any] = {} + if self.capital_envelope_adapter is not None: + from live_firewall.dumbmoney_capital import strategy_binding_hash + + try: + capital_binding = self.capital_envelope_adapter.binding_for( + strategy_hash=strategy_binding_hash( + f"autonomy_strategy_v2:{decision.decision_id}" + ), + passport_hash=strategy_binding_hash( + f"autonomy_forecast:{decision.decision_id}" + ), + authorized_instrument=( + f"event_contract:{decision.market_ticker}" + ), + ) + except Exception as exc: + return self._blocked( + decision, + f"signed_capital_envelope_unavailable:{type(exc).__name__}", + ) client = getattr(firewall, "client", None) get_orderbook = getattr(client, "get_orderbook", None) if not callable(get_orderbook): @@ -597,6 +636,7 @@ async def _execute_live_via_firewall( "risk_state_sha256": risk_digest, "risk_snapshot": dict(decision.risk_snapshot), "expiration_ts": expiration_ts, + **capital_binding, } request = LiveOrderRequest( **request_fields, diff --git a/autonomy/kill_reconciliation.py b/autonomy/kill_reconciliation.py index 4f1115a0..13f6503b 100644 --- a/autonomy/kill_reconciliation.py +++ b/autonomy/kill_reconciliation.py @@ -63,28 +63,62 @@ def queue_kill_reconciliation( *, kill_asserted_at: str, receipt_path: Path | None = DEFAULT_KILL_RECONCILIATION_PATH, + capital_envelope_adapter: Any | None = None, ) -> dict[str, Any]: - """Record that cancel/reconcile remains required; perform no broker I/O.""" - receipt = _stamp( - { - "kill_reconciliation_version": KILL_RECONCILIATION_VERSION, - "generated_at": datetime.now(timezone.utc).isoformat(), - "kill_asserted_at": kill_asserted_at, - "status": "PENDING_CANCEL_AND_RECONCILE", - "submission_authority": False, - "cancel_authority": False, - "broker_contacted": False, - "canceled_order_ids": [], - "open_orders_after": None, - "residual_positions": None, - "flat_book_observed": False, - "liquidation_attempted": False, - "operator_action": ( - "run the separately authorized central cancellation and " - "reconciliation procedure" - ), - } - ) + """Record that cancel/reconcile remains required; perform no broker I/O. + + A DumbMoney adapter may be supplied to mirror the state into the compact + operational journal. This queue path always records ``cancel_authority`` + false; a capital grant never supplies cancellation authority. + """ + payload: dict[str, Any] = { + "kill_reconciliation_version": KILL_RECONCILIATION_VERSION, + "generated_at": datetime.now(timezone.utc).isoformat(), + "kill_asserted_at": kill_asserted_at, + "status": "PENDING_CANCEL_AND_RECONCILE", + "submission_authority": False, + "cancel_authority": False, + "broker_contacted": False, + "canceled_order_ids": [], + "open_orders_after": None, + "residual_positions": None, + "flat_book_observed": False, + "liquidation_attempted": False, + "operator_action": ( + "run the separately authorized central cancellation and " + "reconciliation procedure" + ), + } + if capital_envelope_adapter is not None: + try: + parsed_kill_at = datetime.fromisoformat( + str(kill_asserted_at).replace("Z", "+00:00") + ) + if parsed_kill_at.tzinfo is None: + raise ValueError("kill_asserted_at must be timezone-aware") + canonical_kill_at = ( + parsed_kill_at.astimezone(timezone.utc) + .isoformat() + .replace("+00:00", "Z") + ) + operational = capital_envelope_adapter.enter_cancel_only( + reason="Dummy kill reconciliation queued", + kill_asserted_at=canonical_kill_at, + cancel_authorized=False, + ) + payload["dumbmoney_operational_event_sha256"] = operational[ + "event_sha256" + ] + payload["dumbmoney_operational_status"] = ( + "CANCEL_AUTHORITY_REQUIRED" + ) + except Exception as exc: + # The KILL queue still persists locally; journal failure can only + # reduce authority and is exposed for operator reconciliation. + payload["dumbmoney_operational_status"] = ( + f"JOURNAL_FAILED:{type(exc).__name__}" + ) + receipt = _stamp(payload) _write_atomic(receipt_path, receipt) return receipt diff --git a/autonomy/reconciler.py b/autonomy/reconciler.py index ac20e213..e7136295 100644 --- a/autonomy/reconciler.py +++ b/autonomy/reconciler.py @@ -9,12 +9,12 @@ from datetime import datetime, timezone from decimal import Decimal, InvalidOperation, ROUND_HALF_UP -import os from typing import Any, Callable from autonomy.fees import kalshi_maker_fee_cents, kalshi_taker_fee_cents from autonomy.ledger import AutonomyLedger from autonomy.ontology import MarketView, OutcomeKind, TradeOutcome +from kalshi.strict_json import load_strict_json_response STALE_ORDER_MINUTES = 45 @@ -22,6 +22,7 @@ # "phantom grading coverage % is not emitted anywhere"). Bump only on a # breaking shape change; dashboards read this block off the cycle report. PHANTOM_COVERAGE_VERSION = "phantom-coverage-v1" +_KALSHI_PUBLIC_BASE = "https://external-api.kalshi.com/trade-api/v2" def _series_of(ticker: str) -> str: @@ -64,9 +65,25 @@ def _whole_cents(amount_dollars: Decimal | None) -> int | None: def _public_base() -> str: - base = os.environ.get("KALSHI_API_BASE", "https://api.elections.kalshi.com").rstrip("/") - version = os.environ.get("KALSHI_API_VERSION", "trade-api/v2").strip("/") - return f"{base}/{version}" + """Return the reviewed production endpoint, never an ambient override.""" + return _KALSHI_PUBLIC_BASE + + +def _public_get( + path: str, + *, + params: dict[str, Any] | None = None, + timeout_seconds: float, +) -> Any: + """GET broker truth without inheriting proxy or netrc configuration.""" + import httpx + + with httpx.Client( + base_url=_KALSHI_PUBLIC_BASE, + timeout=max(0.1, float(timeout_seconds)), + trust_env=False, + ) as client: + return client.get(path, params=params) def default_fetch_market_result( @@ -74,38 +91,36 @@ def default_fetch_market_result( *, timeout_seconds: float = 15.0, ) -> dict[str, Any]: - import httpx - - response = httpx.get( - f"{_public_base()}/markets/{ticker}", - timeout=max(0.1, float(timeout_seconds)), + response = _public_get( + f"/markets/{ticker}", + timeout_seconds=timeout_seconds, ) response.raise_for_status() - market = response.json().get("market", {}) + market = load_strict_json_response(response).get("market", {}) return market if isinstance(market, dict) else {} def default_fetch_settled_page(series_ticker: str, min_close_ts: int, cursor: str | None = None) -> dict[str, Any]: """One page of recently settled markets for a series (public GET).""" - import httpx - params: dict[str, Any] = { "series_ticker": series_ticker, "status": "settled", "min_close_ts": min_close_ts, "limit": 200, } if cursor: params["cursor"] = cursor - response = httpx.get(f"{_public_base()}/markets", params=params, timeout=20) + response = _public_get( + "/markets", + params=params, + timeout_seconds=20, + ) response.raise_for_status() - data = response.json() + data = load_strict_json_response(response) return data if isinstance(data, dict) else {} def default_fetch_trades(ticker: str, min_ts: int, max_ts: int) -> list[dict[str, Any]]: """Fetch standard-book public prints for an exact market/time window.""" - import httpx - collected: list[dict[str, Any]] = [] cursor: str | None = None for _page in range(5): @@ -115,9 +130,13 @@ def default_fetch_trades(ticker: str, min_ts: int, max_ts: int) -> list[dict[str } if cursor: params["cursor"] = cursor - response = httpx.get(f"{_public_base()}/markets/trades", params=params, timeout=20) + response = _public_get( + "/markets/trades", + params=params, + timeout_seconds=20, + ) response.raise_for_status() - payload = response.json() + payload = load_strict_json_response(response) rows = payload.get("trades") or [] if isinstance(rows, list): collected.extend(row for row in rows if isinstance(row, dict)) @@ -394,11 +413,25 @@ def reconcile_open_orders(self) -> list[TradeOutcome]: wrapped = status_payload.get("order") status = wrapped if isinstance(wrapped, dict) else status_payload state = str(status.get("status", "")).lower() - try: - filled = int(float(status.get("fill_count_fp") or status.get("fill_count") - or status.get("filled_count") or 0)) - except (TypeError, ValueError): - filled = 0 + raw_filled = status.get("fill_count_fp") + if raw_filled is None or raw_filled == "": + raw_filled = status.get("fill_count") + if raw_filled is None or raw_filled == "": + raw_filled = status.get("filled_count", 0) + filled_decimal = _decimal_or_none(raw_filled) + # Dummy's position, ledger, and capital units are currently whole + # contracts. Never truncate a broker fixed-point fill: 0.50 is + # real exposure, not zero. Until every downstream unit supports + # fixed point, retain the open order/capital reservation and emit + # no terminal fact for any fractional, negative, or malformed + # count. + if ( + filled_decimal is None + or filled_decimal < 0 + or filled_decimal != filled_decimal.to_integral_value() + ): + continue + filled = int(filled_decimal) prior_filled = int(open_decision.get("filled_count") or 0) created = str(status.get("created_time", "")) fill_price, fill_detail = self._broker_fill_evidence( @@ -414,6 +447,16 @@ def reconcile_open_orders(self) -> list[TradeOutcome]: open_decision, OutcomeKind.CANCELED, order_id, filled, {"state": state, **fill_detail}, fill_price, )) + elif state == "expired": + outcomes.append(self._outcome( + open_decision, OutcomeKind.EXPIRED, order_id, filled, + {"state": state, **fill_detail}, fill_price, + )) + elif state == "rejected": + outcomes.append(self._outcome( + open_decision, OutcomeKind.REJECTED, order_id, filled, + {"state": state, **fill_detail}, fill_price, + )) elif filled > prior_filled: outcomes.append(self._outcome( open_decision, OutcomeKind.PARTIALLY_FILLED, order_id, filled, @@ -426,7 +469,27 @@ def reconcile_open_orders(self) -> list[TradeOutcome]: )) elif state == "resting" and self._is_stale(created) and self.cancel_fn is not None: try: - self.cancel_fn(order_id) + cancel_payload = self.cancel_fn(order_id) + wrapped_cancel = ( + cancel_payload.get("order") + if isinstance(cancel_payload, dict) + else None + ) + cancel_status = ( + wrapped_cancel + if isinstance(wrapped_cancel, dict) + else cancel_payload + ) + confirmed_state = ( + str(cancel_status.get("status") or "").lower() + if isinstance(cancel_status, dict) + else "" + ) + # A cancel request/204 is not a terminal order witness. + # Retain the order until this response or a later status + # read explicitly reports canceled/cancelled. + if confirmed_state not in {"canceled", "cancelled"}: + continue role = str(open_decision.get("liquidity_role") or "maker") reason = ( "stale_taker_remainder_auto_cancel" @@ -434,7 +497,12 @@ def reconcile_open_orders(self) -> list[TradeOutcome]: ) outcomes.append(self._outcome( open_decision, OutcomeKind.CANCELED, order_id, filled, - {"reason": reason, **fill_detail}, fill_price, + { + "state": confirmed_state, + "reason": reason, + **fill_detail, + }, + fill_price, )) except Exception: pass diff --git a/configs/dumbmoney/dummy-kalshi-runner.v1.template.json b/configs/dumbmoney/dummy-kalshi-runner.v1.template.json new file mode 100644 index 00000000..112705e0 --- /dev/null +++ b/configs/dumbmoney/dummy-kalshi-runner.v1.template.json @@ -0,0 +1,41 @@ +{ + "schema": "dummy.dumbmoney-kalshi-runner-config.v1", + "service_name": "DumbMoneyDummyKalshi", + "release_id": "REPLACE_WITH_RELEASE_ID", + "core_endpoint_ref": "endpoint-ref:DumbMoneyCore", + "core_readiness_path": "C:\\ProgramData\\DumbMoney\\readiness\\DumbMoneyCore.json", + "core_cell_token_target": "credential-target:DumbMoney/DummyCellToken", + "kalshi_key_id_target": "credential-target:DumbMoney/KalshiApiKeyId", + "kalshi_private_key_target": "credential-target:DumbMoney/KalshiPrivateKeyPem", + "readiness_signing_key_target": "credential-target:DumbMoney/DummyReadinessEd25519", + "start_mode": "RECONCILIATION_ONLY", + "data_root": "C:\\ProgramData\\DumbMoney\\dummy-kalshi", + "readiness_ref": "readiness-ref:DumbMoneyDummyKalshi", + "readiness_path": "C:\\ProgramData\\DumbMoney\\readiness\\DumbMoneyDummyKalshi.json", + "core_public_keys_base64url": { + "34750f98bd59fcfc946da45aaabe933be154a4b5094e1c4abf42866505f3c97e": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w" + }, + "operator_public_keys_base64url": { + "6a3803d5f059902a1c6dafbc9ba4729212f7caac08634cc3ae76b27529f03827": "gTl3Dqh9F19Wo1Rmw0x-zMuNipG07jeiXfYPW4_Js5Q" + }, + "promoter_public_keys_base64url": { + "b62e867fa2f33afe62d5d6b1642e1621d543307846b2a57b897e710919b76709": "7UkoxijRwsbq6QM4kFmVYSlZJzpcY_k2NsFGFKyHN9E" + }, + "research_public_keys_base64url": { + "c5b940ed3f65c391965de8295fc5d25f474fa57b48d36eb10ad363b8539c1b79": "ypOsFwUYcHHWe4PH_w7-gQjo7EUwV113JoeTM9vavnw" + }, + "evaluator_public_keys_base64url": { + "72456720412037a6b339f884ce6d91bb4cc163a7dc3e4c58c658e6c2097b92a2": "iodf_x6zhFFXes1a_uQFRWVo3XyJ4JCGOgVXvHr0nxc" + }, + "expected_account_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "kalshi_subaccount_number": 0, + "fund_lock_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "service_manifest_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "role_public_keys_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "core_runner_config_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "risk_policy_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "readiness_signer_public_key_sha256": "7599776c3085e3f9da0d13071eb0b4ab50fd2bf64c06dd92c2365af3a328eca3", + "poll_interval_seconds": 5, + "readiness_ttl_seconds": 30, + "broker_truth_max_age_seconds": 30 +} diff --git a/configs/dumbmoney/schemas/dummy-kalshi-runner.v1.schema.json b/configs/dumbmoney/schemas/dummy-kalshi-runner.v1.schema.json new file mode 100644 index 00000000..b9a6d56c --- /dev/null +++ b/configs/dumbmoney/schemas/dummy-kalshi-runner.v1.schema.json @@ -0,0 +1,169 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:dumbmoney:schema:dummy-kalshi-runner:v1", + "title": "DumbMoney Dummy Kalshi Windows runner public config", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "service_name", + "release_id", + "core_endpoint_ref", + "core_readiness_path", + "core_cell_token_target", + "kalshi_key_id_target", + "kalshi_private_key_target", + "readiness_signing_key_target", + "start_mode", + "data_root", + "readiness_ref", + "readiness_path", + "core_public_keys_base64url", + "operator_public_keys_base64url", + "promoter_public_keys_base64url", + "research_public_keys_base64url", + "evaluator_public_keys_base64url", + "expected_account_hash", + "kalshi_subaccount_number", + "fund_lock_sha256", + "service_manifest_sha256", + "role_public_keys_sha256", + "core_runner_config_sha256", + "risk_policy_sha256", + "readiness_signer_public_key_sha256", + "poll_interval_seconds", + "readiness_ttl_seconds", + "broker_truth_max_age_seconds" + ], + "properties": { + "schema": { + "const": "dummy.dumbmoney-kalshi-runner-config.v1" + }, + "service_name": { + "const": "DumbMoneyDummyKalshi" + }, + "release_id": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "core_endpoint_ref": { + "const": "endpoint-ref:DumbMoneyCore" + }, + "core_readiness_path": { + "const": "C:\\ProgramData\\DumbMoney\\readiness\\DumbMoneyCore.json" + }, + "core_cell_token_target": { + "const": "credential-target:DumbMoney/DummyCellToken" + }, + "kalshi_key_id_target": { + "const": "credential-target:DumbMoney/KalshiApiKeyId" + }, + "kalshi_private_key_target": { + "const": "credential-target:DumbMoney/KalshiPrivateKeyPem" + }, + "readiness_signing_key_target": { + "const": "credential-target:DumbMoney/DummyReadinessEd25519" + }, + "start_mode": { + "const": "RECONCILIATION_ONLY" + }, + "data_root": { + "const": "C:\\ProgramData\\DumbMoney\\dummy-kalshi" + }, + "readiness_ref": { + "const": "readiness-ref:DumbMoneyDummyKalshi" + }, + "readiness_path": { + "const": "C:\\ProgramData\\DumbMoney\\readiness\\DumbMoneyDummyKalshi.json" + }, + "core_public_keys_base64url": { + "$ref": "#/$defs/key_map" + }, + "operator_public_keys_base64url": { + "$ref": "#/$defs/key_map" + }, + "promoter_public_keys_base64url": { + "$ref": "#/$defs/key_map" + }, + "research_public_keys_base64url": { + "$ref": "#/$defs/key_map" + }, + "evaluator_public_keys_base64url": { + "$ref": "#/$defs/evaluator_key_map" + }, + "expected_account_hash": { + "$ref": "#/$defs/sha256" + }, + "kalshi_subaccount_number": { + "type": "integer", + "minimum": 0, + "maximum": 1000000 + }, + "fund_lock_sha256": { + "$ref": "#/$defs/sha256" + }, + "service_manifest_sha256": { + "$ref": "#/$defs/sha256" + }, + "role_public_keys_sha256": { + "$ref": "#/$defs/sha256" + }, + "core_runner_config_sha256": { + "$ref": "#/$defs/sha256" + }, + "risk_policy_sha256": { + "$ref": "#/$defs/sha256" + }, + "readiness_signer_public_key_sha256": { + "$ref": "#/$defs/sha256" + }, + "poll_interval_seconds": { + "type": "integer", + "minimum": 5, + "maximum": 300 + }, + "readiness_ttl_seconds": { + "type": "integer", + "minimum": 10, + "maximum": 120 + }, + "broker_truth_max_age_seconds": { + "type": "integer", + "minimum": 5, + "maximum": 120 + } + }, + "$defs": { + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "public_key": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{43}$" + }, + "key_map": { + "type": "object", + "minProperties": 1, + "maxProperties": 16, + "additionalProperties": false, + "patternProperties": { + "^[0-9a-f]{64}$": { + "$ref": "#/$defs/public_key" + } + } + }, + "evaluator_key_map": { + "type": "object", + "minProperties": 1, + "maxProperties": 32, + "additionalProperties": false, + "patternProperties": { + "^[0-9a-f]{64}$": { + "$ref": "#/$defs/public_key" + } + } + } + } +} diff --git a/core/interprocess_lock.py b/core/interprocess_lock.py new file mode 100644 index 00000000..e90857fc --- /dev/null +++ b/core/interprocess_lock.py @@ -0,0 +1,76 @@ +"""Small cross-platform file lock for durable state transactions.""" + +from __future__ import annotations + +import os +import threading +from contextlib import contextmanager +from pathlib import Path +from typing import BinaryIO, Iterator + + +_REGISTRY_GUARD = threading.Lock() +_THREAD_LOCKS: dict[str, threading.RLock] = {} + + +def _shared_thread_lock(path: Path) -> threading.RLock: + key = os.path.normcase(str(path.resolve())) + with _REGISTRY_GUARD: + lock = _THREAD_LOCKS.get(key) + if lock is None: + lock = threading.RLock() + _THREAD_LOCKS[key] = lock + return lock + + +class InterprocessFileLock: + """Serialize one-byte sidecar locks across threads and processes.""" + + def __init__(self, path: Path) -> None: + self.path = Path(path) + self._thread_lock = _shared_thread_lock(self.path) + + @staticmethod + @contextmanager + def _lock_handle(handle: BinaryIO) -> Iterator[None]: + handle.seek(0) + if os.name == "nt": + import msvcrt + + msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1) + try: + yield + finally: + handle.seek(0) + msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1) + return + + import fcntl + + fcntl.flock( # type: ignore[attr-defined] + handle.fileno(), + fcntl.LOCK_EX, # type: ignore[attr-defined] + ) + try: + yield + finally: + fcntl.flock( # type: ignore[attr-defined] + handle.fileno(), + fcntl.LOCK_UN, # type: ignore[attr-defined] + ) + + @contextmanager + def hold(self) -> Iterator[None]: + with self._thread_lock: + self.path.parent.mkdir(parents=True, exist_ok=True) + with self.path.open("a+b") as handle: + handle.seek(0, os.SEEK_END) + if handle.tell() == 0: + handle.write(b"\0") + handle.flush() + os.fsync(handle.fileno()) + with self._lock_handle(handle): + yield + + +__all__ = ["InterprocessFileLock"] diff --git a/core/logger.py b/core/logger.py index a27bb731..075faf74 100644 --- a/core/logger.py +++ b/core/logger.py @@ -38,7 +38,8 @@ def emit(self, record: logging.LogRecord): value = repr(value) payload[key] = value if record.exc_info: - payload["exception"] = self.formatException(record.exc_info) + formatter = self.formatter or logging.Formatter() + payload["exception"] = formatter.formatException(record.exc_info) safe = redact(payload) with LOG_FILE.open("a") as f: f.write(json.dumps(safe, default=str) + "\n") diff --git a/core/ontology.py b/core/ontology.py index ebae5ae3..960605da 100644 --- a/core/ontology.py +++ b/core/ontology.py @@ -60,8 +60,8 @@ class TradeProposalDraft(BaseModel): class HybridReviewResult(BaseModel): task: str - primary: dict - secondary: dict + primary: dict[str, Any] + secondary: dict[str, Any] agreement_score: Decimal confidence_adjustment: Decimal verdict: str @@ -280,6 +280,15 @@ class LiveOrderRequest(BaseModel): # LiveBrokerFirewall rejects omission, while every production constructor # emits an explicit QUANT_ONLY or MODEL_WEIGHTED attestation. model_influence_attestation: Optional[ModelInfluenceAttestation] = None + # Optional only for compatibility with the pre-DumbMoney standalone + # runtime. When a CapitalEnvelopeAdapter is installed at the central + # firewall, all five fields become mandatory and are revalidated at the + # trusted sink immediately before a durable capital reservation. + capital_envelope_id: Optional[str] = None + capital_strategy_hash: Optional[str] = None + capital_passport_hash: Optional[str] = None + capital_promotion_hash: Optional[str] = None + capital_fencing_generation: Optional[int] = None class LiveOrderResult(BaseModel): success: bool diff --git a/core/state.py b/core/state.py index 19645e87..73d3430f 100644 --- a/core/state.py +++ b/core/state.py @@ -1,9 +1,11 @@ import json import os +from collections.abc import Callable from datetime import datetime, timezone from pathlib import Path from uuid import uuid4 +from core.interprocess_lock import InterprocessFileLock from core.ontology import AccountMode, KillSwitchState, EmergencyStopState @@ -26,13 +28,21 @@ def __init__(self, *, persist: bool = False, state_path: Path | None = None): self.balance_cents = 0 configured_path = os.environ.get("DUMMY_RISK_STATE_PATH") self._state_path = state_path or (Path(configured_path) if configured_path else DEFAULT_RISK_STATE_PATH) + self._state_lock = InterprocessFileLock( + self._state_path.with_name(f"{self._state_path.name}.lock") + ) self._persist_enabled = persist self._daily_loss_cents = 0 self._daily_loss_date = datetime.now(timezone.utc).date().isoformat() self._processed_settlements: set[str] = set() self.daily_loss_window_start = datetime.now(timezone.utc) self.persistence_error: str | None = None - if self._persist_enabled: + # Importing the module must remain read-only. In particular, status + # commands and watchdog inspections must not create a runtime + # directory merely because the singleton is constructed. A missing + # persisted state is handled by the first explicit persistence or + # fail-closed refresh operation. + if self._persist_enabled and self._state_path.exists(): self._load_risk_state() @property @@ -44,17 +54,33 @@ def daily_loss_cents(self) -> int: def daily_loss_cents(self, value: int) -> None: self._daily_loss_cents = max(0, int(value)) - def _rollover_if_needed(self) -> None: + def _rollover_memory_if_needed(self) -> bool: if self.persistence_error is not None: - return + return False today = datetime.now(timezone.utc).date().isoformat() if self._daily_loss_date == today: - return + return False self._daily_loss_date = today self._daily_loss_cents = 0 self._processed_settlements.clear() self.daily_loss_window_start = datetime.now(timezone.utc) - self._persist_risk_state() + return True + + def _rollover_if_needed(self) -> None: + if self.persistence_error is not None: + return + if not self._persist_enabled: + self._rollover_memory_if_needed() + return + try: + with self._state_lock.hold(): + if not self._synchronize_locked(require_existing=False): + return + except Exception as exc: + self._activate_safety_controls_fail_closed( + "persisted risk state lock unavailable" + ) + self.persistence_error = f"{type(exc).__name__}: {exc}" def _activate_safety_controls_fail_closed(self, reason: str) -> None: now = datetime.now(timezone.utc) @@ -81,14 +107,18 @@ def _parse_emergency_stop(value: object) -> EmergencyStopState: raise ValueError("active emergency_stop requires triggered_at") return parsed - def _load_risk_state(self, *, require_existing: bool = False) -> bool: + def _load_risk_state_locked( + self, + *, + require_existing: bool = False, + ) -> tuple[bool, bool]: if not self._state_path.exists(): if require_existing: reason = "persisted risk state missing during safety refresh" self._activate_safety_controls_fail_closed(reason) self.persistence_error = f"FileNotFoundError: {reason}" - return False - return True + return False, False + return True, False try: data = json.loads(self._state_path.read_text(encoding="utf-8")) if not isinstance(data, dict): @@ -114,43 +144,62 @@ def _load_risk_state(self, *, require_existing: bool = False) -> bool: settlements = data.get("processed_settlement_ids", []) if not isinstance(settlements, list) or not all(isinstance(item, str) for item in settlements): raise ValueError("processed_settlement_ids must be a string list") - self._processed_settlements = set(settlements[-10000:]) + # This set is one UTC day's replay boundary. Truncating it lets an + # old settlement be counted again after enough later settlements. + self._processed_settlements = set(settlements) self.persistence_error = None - self._rollover_if_needed() - if migrated_legacy_controls and not self._persist_risk_state(): - return False - return True + return True, migrated_legacy_controls except Exception as exc: # An unreadable state is not equivalent to zero loss or inactive # safety controls. Both controls latch on in memory and the file is # quarantined until an operator repairs it. self._activate_safety_controls_fail_closed("persisted risk state unreadable") self.persistence_error = f"{type(exc).__name__}: {exc}" - return False + return False, False - def _persist_risk_state(self) -> bool: - if not self._persist_enabled: - return True - payload = { + def _risk_state_payload(self) -> dict[str, object]: + return { "utc_date": self._daily_loss_date, "daily_loss_cents": self._daily_loss_cents, - "processed_settlement_ids": sorted(self._processed_settlements)[-10000:], + "processed_settlement_ids": sorted( + self._processed_settlements + ), "kill_switch": self.kill_switch.model_dump(mode="json"), "emergency_stop": self.emergency_stop.model_dump(mode="json"), "schema_version": RISK_STATE_SCHEMA_VERSION, "updated_at": datetime.now(timezone.utc).isoformat(), } + + def _persist_risk_state_locked(self) -> bool: + if not self._persist_enabled: + return True tmp = self._state_path.with_name( f".{self._state_path.name}.{os.getpid()}.{uuid4().hex}.tmp" ) try: self._state_path.parent.mkdir(parents=True, exist_ok=True) with tmp.open("x", encoding="utf-8") as handle: - json.dump(payload, handle, indent=2, sort_keys=True) + json.dump( + self._risk_state_payload(), + handle, + indent=2, + sort_keys=True, + ) handle.write("\n") handle.flush() os.fsync(handle.fileno()) os.replace(tmp, self._state_path) + with self._state_path.open("r+b") as handle: + os.fsync(handle.fileno()) + if os.name != "nt": + directory_fd = os.open( + self._state_path.parent, + os.O_RDONLY, + ) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) self.persistence_error = None return True except Exception as exc: @@ -162,16 +211,79 @@ def _persist_risk_state(self) -> bool: except OSError: pass + def _synchronize_locked(self, *, require_existing: bool) -> bool: + loaded, migrated = self._load_risk_state_locked( + require_existing=require_existing + ) + if not loaded: + return False + rolled_over = self._rollover_memory_if_needed() + if (migrated or rolled_over) and not self._persist_risk_state_locked(): + return False + return True + + def _load_risk_state(self, *, require_existing: bool = False) -> bool: + if not self._persist_enabled: + return True + try: + with self._state_lock.hold(): + return self._synchronize_locked( + require_existing=require_existing + ) + except Exception as exc: + self._activate_safety_controls_fail_closed( + "persisted risk state lock unavailable" + ) + self.persistence_error = f"{type(exc).__name__}: {exc}" + return False + + def _mutate_persisted_state( + self, + mutation: Callable[[], bool], + ) -> bool: + if self.persistence_error is not None: + return False + if not self._persist_enabled: + self._rollover_memory_if_needed() + return mutation() + try: + with self._state_lock.hold(): + if not self._synchronize_locked(require_existing=False): + return False + if not mutation(): + return False + return self._persist_risk_state_locked() + except Exception as exc: + self._activate_safety_controls_fail_closed( + "persisted risk state transaction failed" + ) + self.persistence_error = f"{type(exc).__name__}: {exc}" + return False + def verify_persistence(self) -> bool: - """Prove the risk/safety sink is writable without erasing unknown state. + """Reload and rewrite only the exact latest durable snapshot. - An earlier load/write failure is a hard stop. In particular, a - corrupt state file must never be overwritten with an optimistic zero - merely because a pre-submit probe ran. + The prior implementation persisted this instance's in-memory fields. + A stale executor could therefore erase a kill switch or newer loss + record written by another process during a pre-submit check. The + locked reload preserves those facts while still proving the actual + target can be atomically replaced. """ if self.persistence_error is not None: return False - return self._persist_risk_state() + if not self._persist_enabled: + return True + try: + with self._state_lock.hold(): + if not self._synchronize_locked(require_existing=False): + return False + return self._persist_risk_state_locked() + except Exception as exc: + self._activate_safety_controls_fail_closed( + "persisted risk state writability probe failed" + ) + self.persistence_error = f"{type(exc).__name__}: {exc}" + return False def refresh_persisted_state(self) -> bool: """Reload persisted controls before a live submit. @@ -190,41 +302,53 @@ def record_realized_pnl(self, pnl_cents: int, *, settlement_id: str | None = Non Returns False for a duplicate settlement id. Gains do not erase the daily loss cap, matching the conservative loss-limit semantics. """ - if self.persistence_error is not None: - return False - self._rollover_if_needed() identity = str(settlement_id) if settlement_id else None - if identity and identity in self._processed_settlements: - return False - if identity: - self._processed_settlements.add(identity) - if int(pnl_cents) < 0: - self._daily_loss_cents += -int(pnl_cents) - return self._persist_risk_state() - def set_mode(self, mode: AccountMode): + def mutation() -> bool: + if identity and identity in self._processed_settlements: + return False + if identity: + self._processed_settlements.add(identity) + if int(pnl_cents) < 0: + self._daily_loss_cents += -int(pnl_cents) + return True + + return self._mutate_persisted_state(mutation) + + def set_mode(self, mode: AccountMode) -> None: self.mode = mode - def enable_kill_switch(self, reason: str): - self.kill_switch = KillSwitchState( - active=True, - triggered_at=datetime.now(timezone.utc), - reason=reason, - ) - if self.persistence_error is not None: - # Preserve the quarantined file; never overwrite unknown loss data. - return False - return self._persist_risk_state() + def enable_kill_switch(self, reason: str) -> bool: + def mutation() -> bool: + self.kill_switch = KillSwitchState( + active=True, + triggered_at=datetime.now(timezone.utc), + reason=reason, + ) + return True - def disable_kill_switch(self): + return self._mutate_persisted_state(mutation) + + def disable_kill_switch(self) -> bool: if self.persistence_error is not None: self._activate_safety_controls_fail_closed( "cannot disable kill switch while persisted state is unhealthy" ) return False + expected = self.kill_switch.model_dump(mode="json") + if expected.get("active") is not True: + return False previous = self.kill_switch - self.kill_switch = KillSwitchState(active=False) - if self._persist_risk_state(): + + def mutation() -> bool: + nonlocal previous + previous = self.kill_switch + if self.kill_switch.model_dump(mode="json") != expected: + return False + self.kill_switch = KillSwitchState(active=False) + return True + + if self._mutate_persisted_state(mutation): return True # The durable state still says active (or is unknowable); retain the # conservative in-memory latch if the deactivation write fails. @@ -235,25 +359,37 @@ def disable_kill_switch(self): ) return False - def trigger_emergency_stop(self): - self.emergency_stop = EmergencyStopState( - active=True, - triggered_at=datetime.now(timezone.utc), - ) - if self.persistence_error is not None: - return False - return self._persist_risk_state() + def trigger_emergency_stop(self) -> bool: + def mutation() -> bool: + self.emergency_stop = EmergencyStopState( + active=True, + triggered_at=datetime.now(timezone.utc), + ) + return True + + return self._mutate_persisted_state(mutation) - def clear_emergency_stop(self): + def clear_emergency_stop(self) -> bool: """Persist an explicit operator reset; never auto-clear on restart.""" if self.persistence_error is not None: self._activate_safety_controls_fail_closed( "cannot clear emergency stop while persisted state is unhealthy" ) return False + expected = self.emergency_stop.model_dump(mode="json") + if expected.get("active") is not True: + return False previous = self.emergency_stop - self.emergency_stop = EmergencyStopState(active=False) - if self._persist_risk_state(): + + def mutation() -> bool: + nonlocal previous + previous = self.emergency_stop + if self.emergency_stop.model_dump(mode="json") != expected: + return False + self.emergency_stop = EmergencyStopState(active=False) + return True + + if self._mutate_persisted_state(mutation): return True self.emergency_stop = previous if previous.active else EmergencyStopState( active=True, diff --git a/docs/DUMBMONEY_INTEGRATION.md b/docs/DUMBMONEY_INTEGRATION.md new file mode 100644 index 00000000..e6c401e1 --- /dev/null +++ b/docs/DUMBMONEY_INTEGRATION.md @@ -0,0 +1,236 @@ +# DumbMoney Core integration + +Dummy remains the sovereign Kalshi venue cell. DumbMoney supplies an +additional signed ceiling; it does not supply a live session, enable +`configs/live_submit.json`, change local caps, resolve credentials, or bypass +`LiveBrokerFirewall`. + +## Wire contract + +`CapitalEnvelopeAdapter` verifies: + +- `dumbmoney.signed-envelope.v1` using Ed25519 over canonical JSON of every + wrapper field except `signature`; +- unpadded URL-safe base64 signatures and a signer key ID equal to the + lowercase SHA-256 of the raw 32-byte public key; +- an event ID equal to the lowercase SHA-256 of canonical wrapper fields + excluding `event_id` and `signature`; +- a `dumbmoney.capital-envelope.v1` body whose digest, venue, account hash, + equal-count strategy/passport/promotion hashes, exact broker instrument IDs, + UTC window, policy epoch, and integer-cent limits all match exactly; +- strictly increasing source sequence and global fencing generation, with a + maximum-ever fence high-water mark so an older lease cannot revive after a + newer lease expires; +- `max_order_risk_cents <= max_correlated_risk_cents <= + max_open_risk_cents`, plus enough remaining daily-loss capacity for the + proposed order rather than only checking whether the prior loss reached the + ceiling. + +The cross-repository fixture is mirrored at +`tests/fixtures/dumbmoney/signed-capital-envelope.v1.json`. It contains a +public key and signed envelope but no private key. + +## Local authority intersection + +When an adapter is installed on `LiveBrokerFirewall`, a request must carry the +exact envelope ID, strategy hash, passport hash, promotion hash, and fencing +generation. Dummy derives `event_contract:` at the +trusted sink and requires that exact identifier in `authorized_instruments`; +prefixes, classes, and globs do not authorize an order. +Dummy evaluates all existing local risk, authority, allowlist, exposure, +frequency, and order caps first. The signed grant is then evaluated as another +deny-only ceiling. Both the external reservation and Dummy's existing +exposure reservation must persist before the broker transport can be called. + +The adapter is opt-in at construction so the standalone Dummy runtime remains +backward compatible. Supplying capital-binding fields without an installed +adapter fails closed. + +## Broker bootstrap and journal + +An adapter cannot reserve risk until it has a fresh, account-bound broker +bootstrap receipt: + +- `dummy.broker-bootstrap.flat.v1` proves a broker-observed empty order and + position book. +- `dummy.broker-bootstrap.inherited-exposure.v1` records inherited open risk + and consumes the signed grant before any new request. + +No receipt means unknown exposure, never a flat account. Observations must be +strictly increasing and are bound to a broker-snapshot digest. + +Production uses indexed SQLite WAL journals with `synchronous=FULL`, +`BEGIN IMMEDIATE` cross-process conditional appends, immutable-event +triggers, an event hash chain, bounded indexed kind/outbox reads, and a full +rescan whenever SQLite reports an external data-version change. The +broker-bootstrap and command-feed polling hot paths validate only their +indexed latest kinds, so five-second polling does not repeatedly materialize +the full journal. JSONL remains a compatibility implementation for isolated +tests, not the Windows service hot path. + +An internal database hash chain cannot detect replacement with an older, +byte-for-byte valid database image. The runner therefore anchors three exact +local heads in Core's independently persisted signed ledger: +`capital-operational`, `command-feed`, and the revisioned semantic digest of +`live-exposure`. Each stream is account-, schema-, and name-bound. Core rejects +a lower sequence or a different digest at the same sequence. Dummy verifies +the Core envelope, independently recomputes the ledger-event proof, verifies a +fresh Core-signed request-bound checkpoint, and anchors before broker reads and +again after cycle mutations. A rollback cannot regain execution readiness. + +Caller-authored local-failure receipts cannot release a capital reservation. +Terminal order and settlement recovery instead require typed, service-signed +broker witnesses exactly bound to the dispatch claim, account, subaccount, +client order ID, broker order ID, order terms, terminal state, observation +window, and local exposure projection. Ambiguous outcomes remain reserved; +filled capital is released only after venue exposure is projected, and that +position exposure remains until a stable position-absence read plus a matching +settlement witness. + +## Core contract authority + +Each passport and promotion resolution must include the frozen +`dumbmoney.cell-authority-state.v1` projection inside the Core-signed +checkpoint. Dummy verifies current kill-clear and LIVE desired mode, policy +and mandate identity, capital/fence/strategy/passport/promotion bindings, +capital and contract ledger-event continuity, at least two unique current +PASS facts from at least two courts, and evaluator signer IDs from a sealed +role-disjoint keyring. Promotion and passport must resolve from the same +authority state. The binding expires at the earliest contract, capital, or +`authority_valid_until` boundary and is resolved again at final use. + +## Core command feed + +`CoreCommandFeedConsumer` is an injected, local-only `poll_once` API. It has +no HTTP client, retry loop, scheduler, background thread, broker writer, or +import-time side effect. A supervised runner must inject: + +- a GET-only loopback transport for + `/v1/cells/dummy_kalshi/commands?after=N&cursor=&limit=L`; +- an OS-secret-backed `dummy_kalshi` cell bearer provider distinct from the + operator token; +- a sealed release-role bundle containing disjoint pinned keyrings: Core keys + for page checkpoints and capital grants, and operator keys for kill and + desired-mode envelopes; +- the account-pinned capital adapter and a separate operational journal for + feed state; +- idempotent authority-reducing handlers that assert local KILL or PAUSE. + +The consumer first verifies the Core Ed25519 signature over the exact +`dumbmoney.cell-command-checkpoint.v1` canonical JSON. Every top-level cursor, +head digest, observation time, required action, and ordered command projection +must byte-canonically mirror that checkpoint. `has_more` is derived from the +signed next/head sequences rather than trusted as an unsigned assertion. +Each poll generates 32 cryptographically random bytes and sends the lowercase +hex value as `request_nonce`; the same value must appear in the page and its +signed checkpoint. A captured response therefore cannot satisfy a later +request even while its observation timestamp remains fresh. A repeated or +noncanonical locally generated nonce fails before any GET. +Every projected `dumbmoney.ledger-event-proof.v1` is also checked +independently: its payload digest must bind the envelope body, all duplicated +identity and provenance fields must agree, its event digest is recomputed, and +observable adjacent global/source chain links must extend the known cursor. + +Each kill or desired-mode envelope must then verify under the operator +keyring; each capital envelope must verify under the Core keyring. The +keyrings are required to be disjoint and every key ID must equal the SHA-256 +of its raw Ed25519 public key. A valid signature under the wrong role is still +rejected. + +The consumer persists `(next_sequence, next_digest)` and the resulting +controls in one fsynced journal event. Its append also compares the expected +starting cursor under the interprocess writer lock, so two runners cannot both +commit from one cursor. A backlog (`has_more=true`), +authentication/transport failure, stale or future signed checkpoint, unknown +schema or signer, cursor mismatch, projection/proof tamper, or dispatch failure +remains fail-closed. One call performs at most one GET and never retries a +write. + +Historical signed events still advance the durable cursor. Expired +kill-active and non-LIVE mode events remain authority-reducing and persist. +Expired or future kill-clear, LIVE-mode, and capital grants update only their +monotonic high-water marks and never activate authority. Unsigned +page fields cannot change the checkpoint-signed `required_action`, and even a +valid signed positive action can never clear a local kill latch. +`has_more` requires another explicit `poll_once`; there is no autonomous drain +loop. + +## Kalshi write boundary + +The central firewall constructs the current event-order V2 wire shape and +uses `POST /portfolio/events/orders`: one YES-denominated `bid`/`ask` book, +fixed-point `count` and `price` strings, explicit time-in-force, +self-trade prevention, subaccount zero, pause cancellation, and `post_only` +for makers. Buying NO maps to an ask at the complementary YES price. The +transport capability gate recognizes both the V2 collection and the legacy +collection so the new route cannot bypass the chokepoint. + +A write waits for its private mutation lane before the final Core resolution, +then consumes a single-use permit before exactly one socket attempt. +Timeouts, request errors, HTTP 429, malformed acknowledgements, and immediate +fill/remainder projections are ambiguous outcomes: they are never resent and +retain the deterministic `client_order_id` reservation for reconciliation. +The default production REST origin is +`https://external-api.kalshi.com/trade-api/v2`. Conflicting ambient +host/version overrides are rejected, HTTP clients do not inherit proxy or +netrc settings, and broker JSON is decoded as bounded strict UTF-8 with +duplicate keys and non-finite numbers rejected. + +## Kill and cancellation behavior + +Queueing kill reconciliation can mirror `authority.cancel_only` into the +operational journal. This always removes submission authority. It does not +create cancellation authority or contact Kalshi. A cancel-reconciliation +command is written to the outbox only when a distinct existing +cancellation-authority receipt is supplied. + +## Activation status + +The sealed Windows runner, public config schema/template, Credential Manager +targets, signed readiness, Core command/contract transports, and SQLite +journals exist. The production runner now constructs a credential-owned, +read-only Kalshi truth provider without exporting its RSA key or key ID into +the process environment. The provider pins +`https://external-api.kalshi.com/trade-api/v2`, disables ambient proxy and +redirect behavior, signs only the documented balance, positions, and resting +orders GET paths, bounds every response, and performs no retry. Reconciliation +also reads the documented current and historical order/fill tiers plus +settlements so archival movement cannot manufacture an absent order or fill. +Positions, orders, fills, and settlements are fully paginated, constrained to +subaccount `0`, then read twice; any cursor truncation, duplicate identity, +cross-subaccount row, schema drift, aggregate disagreement, or change between +the two projections fails closed. The stable fixed-point projection is +recorded in both the local and Core-capital exposure domains. + +Every reservation now includes the submitted notional plus the general +worst-case Kalshi taker fee, including post-only maker intents. A terminal +broker observation is accepted only as a typed, domain-separated witness +signed by the sealed Dummy service identity and tied to the exact reservation, +account, subaccount, order, fills, observation window, and projection digest. +The restart-safe sweeper first idempotently projects fills into venue-local +exposure, then releases the order reservation. Filled exposure remains +reserved independently until a second stable position-absence read and +matching settlement witness have been persisted. These service signatures +attest which local service observed the authenticated broker response; they do +not imply that Kalshi signed the response. + +The service still intentionally starts and remains +`RECONCILIATION_ONLY`. Production installs +`SealedDisabledExecutionCycle`, which accepts the exact broker snapshot and +returns its digest but owns no broker client, credential, callback, or submit +method. It always returns `BLOCKED`, `broker_contacted=false`, and +`orders_submitted=0`. This proves the production cycle boundary and snapshot +binding without creating order authority. + +Live activation is blocked until all of the following are sealed and tested +end to end: + +- a real authenticated target-account validation of the implemented, + fully-paginated double-read broker and reconciliation projections; +- a separately reviewed, sealed order-capable execution adapter replacing the + disabled harness while preserving the exact verified broker-snapshot digest; +- witnessed place/reconcile/settle/kill/restart drills against the intended + account, beginning with an attended mechanical canary. + +Do not interpret signed readiness, passing unit tests, or a fresh flat-book +receipt as live authorization or profitability evidence. diff --git a/kalshi/client.py b/kalshi/client.py index 3315811d..f73f5129 100644 --- a/kalshi/client.py +++ b/kalshi/client.py @@ -11,10 +11,13 @@ from kalshi.signer import sign_request from kalshi.error_classifier import classify from kalshi.rate_limiter import KalshiRateLimiter +from kalshi.strict_json import load_strict_json_response from core.logger import logger _REQUEST_TIMEOUT_SECONDS = 10 _REQUEST_OUTER_TIMEOUT_SECONDS = 10 +_KALSHI_PRODUCTION_ORIGIN = "https://external-api.kalshi.com" +_KALSHI_PRODUCTION_VERSION = "trade-api/v2" # Raw write methods are transport primitives, not public execution surfaces. # The identity token is deliberately private and is presented only by the @@ -24,6 +27,16 @@ _READ_ONLY_HTTP_METHODS = frozenset({"GET", "HEAD", "OPTIONS"}) +class _OneShotMutationPermit: + """Opaque, client-owned permit consumed by exactly one mutation attempt.""" + + __slots__ = ("method", "path") + + def __init__(self, method: str, path: str) -> None: + self.method = method + self.path = path + + def _canonical_path_segments(path: str) -> tuple[str, ...]: """Return decoded, dot-normalized URL path segments for safety checks.""" raw_path = urlsplit(str(path)).path.replace("\\", "/") @@ -55,18 +68,37 @@ def _is_order_mutation(method: str, path: str) -> bool: segments = _canonical_path_segments(path) return any( segments[index : index + 2] == ("portfolio", "orders") + or segments[index : index + 3] + == ("portfolio", "events", "orders") for index in range(max(0, len(segments) - 1)) ) -# Defaults must stay aligned with kalshi.signer so the signed path prefix and -# the request URL agree. The legacy v1 host (trading-api.kalshi.com) is dead. +# The production transport is deliberately not configurable through ambient +# environment variables. A service environment inherited from another process +# must never be able to redirect an authenticated request to a different host +# or change the path prefix after deployment review. def _kalshi_base() -> str: - return os.environ.get("KALSHI_API_BASE", "https://api.elections.kalshi.com").rstrip("/") + return _KALSHI_PRODUCTION_ORIGIN def _kalshi_version() -> str: - return os.environ.get("KALSHI_API_VERSION", "trade-api/v2").strip("/") + return _KALSHI_PRODUCTION_VERSION + + +def _reject_ambient_endpoint_redirect() -> None: + configured_base = os.environ.get("KALSHI_API_BASE") + if ( + configured_base is not None + and configured_base.rstrip("/") != _KALSHI_PRODUCTION_ORIGIN + ): + raise RuntimeError("KALSHI_ENDPOINT_OVERRIDE_REJECTED") + configured_version = os.environ.get("KALSHI_API_VERSION") + if ( + configured_version is not None + and configured_version.strip("/") != _KALSHI_PRODUCTION_VERSION + ): + raise RuntimeError("KALSHI_ENDPOINT_OVERRIDE_REJECTED") # Backward-compatible module-level aliases for code that reads them at import time. @@ -75,10 +107,84 @@ def _kalshi_version() -> str: class KalshiClient: - def __init__(self): - self.client = httpx.AsyncClient(base_url=f"{_kalshi_base()}/{_kalshi_version()}", timeout=_REQUEST_TIMEOUT_SECONDS) + def __init__(self) -> None: + _reject_ambient_endpoint_redirect() + self.client = httpx.AsyncClient( + base_url=f"{_kalshi_base()}/{_kalshi_version()}", + timeout=_REQUEST_TIMEOUT_SECONDS, + trust_env=False, + ) self.limiter = KalshiRateLimiter() self.request_audit_log: deque[dict[str, Any]] = deque(maxlen=1000) + self._mutation_lock = asyncio.Lock() + self._active_mutation_permit: _OneShotMutationPermit | None = None + + async def prepare_order_mutation( + self, + method: str, + path: str, + *, + _capability: object | None = None, + ) -> object: + """Wait for the mutation lane before the caller's final authority check. + + The permit is deliberately opaque and single-use. It serializes writes + without placing a limiter wait between the final Core resolution and + the socket send. + """ + if ( + _capability is not _CENTRAL_FIREWALL_SUBMIT_CAPABILITY + or not _is_order_mutation(method, path) + ): + raise PermissionError( + "DIRECT_ORDER_SUBMIT_RETIRED_USE_CENTRAL_LIVE_FIREWALL" + ) + await self._mutation_lock.acquire() + permit = _OneShotMutationPermit( + str(method).strip().upper(), + "/" + "/".join(_canonical_path_segments(path)), + ) + self._active_mutation_permit = permit + return permit + + def cancel_prepared_order_mutation( + self, + permit: object, + *, + _capability: object | None = None, + ) -> None: + """Release an unused permit when final authority fails closed.""" + if ( + _capability is not _CENTRAL_FIREWALL_SUBMIT_CAPABILITY + or permit is not self._active_mutation_permit + ): + raise PermissionError("INVALID_ORDER_MUTATION_PERMIT") + self._active_mutation_permit = None + self._mutation_lock.release() + + def _consume_mutation_permit( + self, + permit: object, + *, + method: str, + path: str, + ) -> None: + normalized_method = str(method).strip().upper() + normalized_path = "/" + "/".join(_canonical_path_segments(path)) + if ( + permit is not self._active_mutation_permit + or not isinstance(permit, _OneShotMutationPermit) + or permit.method != normalized_method + or permit.path != normalized_path + ): + raise PermissionError("INVALID_ORDER_MUTATION_PERMIT") + # Invalidate before transport so the same object can never authorize a + # retry, including after a timeout or an HTTP 429. + self._active_mutation_permit = None + + def _finish_mutation_attempt(self) -> None: + if self._mutation_lock.locked(): + self._mutation_lock.release() def _family_path(self, path: str) -> str: """Collapse ticker-specific segments so `/markets/FOO/orderbook` becomes `/markets/{ticker}/orderbook`.""" @@ -88,32 +194,56 @@ def _family_path(self, path: str) -> str: if len(parts) >= 3 and parts[0] == "portfolio" and parts[1] == "orders": if not parts[2].startswith("{"): parts[2] = "{order_id}" + if ( + len(parts) >= 4 + and parts[0] == "portfolio" + and parts[1] == "events" + and parts[2] == "orders" + and not parts[3].startswith("{") + and parts[3] != "batched" + ): + parts[3] = "{order_id}" return "/" + "/".join(parts) def _redacted_summary(self, response: httpx.Response) -> dict[str, Any]: summary: dict[str, Any] = {"status_code": response.status_code} if response.status_code < 400: try: - data = response.json() + data = load_strict_json_response(response) if isinstance(data, list): summary["count"] = len(data) elif isinstance(data, dict): summary["keys"] = sorted(data.keys())[:10] - summary["count"] = len(data) if isinstance(list(data.values())[0], list) else None + values = list(data.values()) + summary["count"] = ( + len(data) + if values and isinstance(values[0], list) + else None + ) except Exception: summary["body_preview"] = response.text[:120] else: summary["error_preview"] = response.text[:120] return summary - async def _request(self, method: str, path: str, **kwargs): + async def _request( + self, + method: str, + path: str, + **kwargs: Any, + ) -> Any: capability = kwargs.pop("_capability", None) - if ( - _is_order_mutation(method, path) - and capability is not _CENTRAL_FIREWALL_SUBMIT_CAPABILITY - ): - raise PermissionError( - "DIRECT_ORDER_SUBMIT_RETIRED_USE_CENTRAL_LIVE_FIREWALL" + mutation = _is_order_mutation(method, path) + mutation_permit = kwargs.pop("_mutation_permit", None) + if mutation: + if capability is not _CENTRAL_FIREWALL_SUBMIT_CAPABILITY: + raise PermissionError( + "DIRECT_ORDER_SUBMIT_RETIRED_USE_CENTRAL_LIVE_FIREWALL" + ) + self._consume_mutation_permit( + mutation_permit, + method=method, + path=path, ) json_body = kwargs.pop("json", "") if isinstance(json_body, dict): @@ -125,35 +255,49 @@ async def _request(self, method: str, path: str, **kwargs): else: body_str = "" body_bytes = b"" - headers = sign_request(method, path, body_str) - headers["Content-Type"] = "application/json" - async def call(): - response = await self.client.request(method, path, headers=headers, content=body_bytes, **kwargs) - self.request_audit_log.append({ - "method": method.upper(), - "path": path, - "path_family": self._family_path(path), - "status_code": response.status_code, - "status_class": f"{response.status_code // 100}xx", - "redacted_summary": self._redacted_summary(response), - }) - if response.status_code >= 400: - cat = classify(response.status_code, response.text) - logger.error("Kalshi API error", extra={"component": "kalshi_client", "status": response.status_code, "category": cat.value}) - response.raise_for_status() - return response.json() - # Hard outer bound so no Kalshi request can block the caller indefinitely. - loop = asyncio.get_running_loop() - deadline = loop.time() + _REQUEST_OUTER_TIMEOUT_SECONDS - 0.05 - return await asyncio.wait_for( - self.limiter.execute(call, deadline=deadline), - timeout=_REQUEST_OUTER_TIMEOUT_SECONDS, - ) + try: + headers = sign_request(method, path, body_str) + headers["Content-Type"] = "application/json" + + async def call() -> Any: + response = await self.client.request(method, path, headers=headers, content=body_bytes, **kwargs) + self.request_audit_log.append({ + "method": method.upper(), + "path": path, + "path_family": self._family_path(path), + "status_code": response.status_code, + "status_class": f"{response.status_code // 100}xx", + "redacted_summary": self._redacted_summary(response), + }) + if response.status_code >= 400: + cat = classify(response.status_code, response.text) + logger.error("Kalshi API error", extra={"component": "kalshi_client", "status": response.status_code, "category": cat.value}) + response.raise_for_status() + return load_strict_json_response(response) + + # A mutation is exactly one socket attempt. Any timeout, + # RequestError, or HTTP error is an ambiguous broker outcome and + # must be reconciled by deterministic client_order_id, never sent + # again. Read-only requests retain bounded retry behavior. + if mutation: + return await asyncio.wait_for( + call(), + timeout=_REQUEST_OUTER_TIMEOUT_SECONDS, + ) + loop = asyncio.get_running_loop() + deadline = loop.time() + _REQUEST_OUTER_TIMEOUT_SECONDS - 0.05 + return await asyncio.wait_for( + self.limiter.execute(call, deadline=deadline), + timeout=_REQUEST_OUTER_TIMEOUT_SECONDS, + ) + finally: + if mutation: + self._finish_mutation_attempt() - async def get_events(self): + async def get_events(self) -> Any: return await self._request("GET", "/events") - async def get_markets(self, max_pages: int = 10): + async def get_markets(self, max_pages: int = 10) -> Any: combined: list[Any] = [] cursor: str | None = None first_page: dict[str, Any] | None = None @@ -183,13 +327,13 @@ async def get_markets(self, max_pages: int = 10): result["pagination_truncated"] = cursor is not None return result - async def get_event(self, ticker: str): + async def get_event(self, ticker: str) -> Any: return await self._request("GET", f"/events/{ticker}") - async def get_series(self, ticker: str): + async def get_series(self, ticker: str) -> Any: return await self._request("GET", f"/series/{ticker}") - async def get_market(self, ticker: str): + async def get_market(self, ticker: str) -> Any: return await self._request("GET", f"/markets/{ticker}") async def get_orderbook(self, ticker: str, depth: int = 10) -> OrderBook: @@ -258,28 +402,35 @@ async def get_orderbook(self, ticker: str, depth: int = 10) -> OrderBook: }, ) - async def get_account(self): + async def get_account(self) -> Any: return await self._request("GET", "/portfolio/balance") - async def get_positions(self): + async def get_positions(self) -> Any: return await self._request("GET", "/portfolio/positions") - async def get_fills(self): + async def get_fills(self) -> Any: return await self._request("GET", "/portfolio/fills") - async def get_orders(self): + async def get_orders(self) -> Any: return await self._request("GET", "/portfolio/orders") - async def create_order(self, order: dict, *, _capability: object | None = None): + async def create_order( + self, + order: dict[str, Any], + *, + _capability: object | None = None, + _mutation_permit: object | None = None, + ) -> Any: if _capability is not _CENTRAL_FIREWALL_SUBMIT_CAPABILITY: raise PermissionError( "DIRECT_ORDER_SUBMIT_RETIRED_USE_CENTRAL_LIVE_FIREWALL" ) return await self._request( "POST", - "/portfolio/orders", + "/portfolio/events/orders", json=order, _capability=_capability, + _mutation_permit=_mutation_permit, ) async def cancel_order( @@ -287,12 +438,14 @@ async def cancel_order( order_id: str, *, _capability: object | None = None, - ): + _mutation_permit: object | None = None, + ) -> Any: return await self._request( "DELETE", - f"/portfolio/orders/{order_id}", + f"/portfolio/events/orders/{order_id}", _capability=_capability, + _mutation_permit=_mutation_permit, ) - async def close(self): + async def close(self) -> None: await self.client.aclose() diff --git a/kalshi/signer.py b/kalshi/signer.py index 2438ad18..67e97667 100644 --- a/kalshi/signer.py +++ b/kalshi/signer.py @@ -5,12 +5,16 @@ from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import padding +_KALSHI_PRODUCTION_ORIGIN = "https://external-api.kalshi.com" +_KALSHI_PRODUCTION_VERSION = "trade-api/v2" + + def _kalshi_base() -> str: - return os.environ.get("KALSHI_API_BASE", "https://api.elections.kalshi.com").rstrip("/") + return _KALSHI_PRODUCTION_ORIGIN def _kalshi_version() -> str: - return os.environ.get("KALSHI_API_VERSION", "trade-api/v2").strip("/") + return _KALSHI_PRODUCTION_VERSION # Backward-compatible module-level aliases for code that reads them at import time. diff --git a/kalshi/strict_json.py b/kalshi/strict_json.py new file mode 100644 index 00000000..93c49715 --- /dev/null +++ b/kalshi/strict_json.py @@ -0,0 +1,77 @@ +"""Bounded strict JSON decoding for untrusted Kalshi HTTP responses.""" + +from __future__ import annotations + +import json +import math +from typing import Any, Protocol + +MAX_KALSHI_JSON_RESPONSE_BYTES = 8 * 1024 * 1024 + + +class StrictJSONResponseError(ValueError): + """Raised when a broker response is not bounded strict UTF-8 JSON.""" + + +class _ByteResponse(Protocol): + @property + def content(self) -> bytes: ... + + +def _reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + value: dict[str, Any] = {} + for key, item in pairs: + if key in value: + raise StrictJSONResponseError( + "Kalshi response contains duplicate JSON keys" + ) + value[key] = item + return value + + +def _reject_non_finite(token: str) -> None: + raise StrictJSONResponseError( + f"Kalshi response contains non-finite JSON value {token}" + ) + + +def _parse_finite_float(token: str) -> float: + value = float(token) + if not math.isfinite(value): + _reject_non_finite(token) + return value + + +def load_strict_json_response( + response: _ByteResponse, + *, + maximum_bytes: int = MAX_KALSHI_JSON_RESPONSE_BYTES, +) -> Any: + """Decode one fully buffered response without permissive JSON extensions.""" + if not isinstance(maximum_bytes, int) or isinstance(maximum_bytes, bool): + raise TypeError("maximum_bytes must be an integer") + if maximum_bytes <= 0: + raise ValueError("maximum_bytes must be positive") + + body = response.content + if not isinstance(body, bytes): + raise StrictJSONResponseError("Kalshi response body must be bytes") + if len(body) > maximum_bytes: + raise StrictJSONResponseError( + f"Kalshi response exceeds {maximum_bytes} byte limit" + ) + + try: + text = body.decode("utf-8", errors="strict") + return json.loads( + text, + object_pairs_hook=_reject_duplicate_keys, + parse_constant=_reject_non_finite, + parse_float=_parse_finite_float, + ) + except StrictJSONResponseError: + raise + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise StrictJSONResponseError( + "Kalshi response is not valid strict UTF-8 JSON" + ) from exc diff --git a/live_firewall/dumbmoney_broker_witness.py b/live_firewall/dumbmoney_broker_witness.py new file mode 100644 index 00000000..4011385f --- /dev/null +++ b/live_firewall/dumbmoney_broker_witness.py @@ -0,0 +1,101 @@ +"""Domain-separated signatures for authenticated DumbMoney broker evidence. + +Kalshi authenticates the caller and TLS protects the response in transit, but +portfolio responses are not themselves exchange-signed artifacts. The venue +service therefore signs the exact normalized response projection immediately +after its stable authenticated reads. The signature proves which sealed +service identity observed the broker response; it does not turn local +assertions into exchange facts. +""" + +from __future__ import annotations + +import base64 +import hashlib +from datetime import datetime, timedelta, timezone +from typing import Any, Mapping + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from live_firewall.operational_journal import canonical_json, sha256_json + + +SIGNED_ENVELOPE_SCHEMA = "dumbmoney.signed-envelope.v1" +BROKER_WITNESS_SOURCE_ID = "dummy-kalshi-broker-witness" +TERMINAL_WITNESS_SCHEMA = "dummy.kalshi-order-terminal-witness.v1" +SETTLEMENT_WITNESS_SCHEMA = "dummy.kalshi-settlement-witness.v1" +WITNESS_TTL = timedelta(minutes=5) +_ALLOWED_SCHEMAS = frozenset( + {TERMINAL_WITNESS_SCHEMA, SETTLEMENT_WITNESS_SCHEMA} +) + + +def _format_utc(value: datetime) -> str: + rendered = value.astimezone(timezone.utc).isoformat( + timespec="microseconds" if value.microsecond else "seconds" + ) + return rendered.replace("+00:00", "Z") + + +def sign_broker_witness( + body: Mapping[str, Any], + *, + private_key: Ed25519PrivateKey, + observed_at: datetime, + correlation_id: str, +) -> dict[str, Any]: + """Sign one canonical typed witness with the venue-service identity.""" + if ( + observed_at.tzinfo is None + or observed_at.utcoffset() is None + ): + raise ValueError("broker witness clock must be timezone-aware") + value = dict(body) + schema = value.get("schema") + if schema not in _ALLOWED_SCHEMAS: + raise ValueError("broker witness body schema is unsupported") + if not isinstance(correlation_id, str) or not correlation_id: + raise ValueError("broker witness correlation id is required") + observed = observed_at.astimezone(timezone.utc) + not_before = _format_utc(observed) + expires_at = _format_utc(observed + WITNESS_TTL) + public_key = private_key.public_key().public_bytes_raw() + signer_key_id = hashlib.sha256(public_key).hexdigest() + body_digest = sha256_json(value) + event_material = { + "schema": SIGNED_ENVELOPE_SCHEMA, + "source_id": BROKER_WITNESS_SOURCE_ID, + "source_sequence": max(1, int(observed.timestamp() * 1_000_000)), + "correlation_id": correlation_id, + "causation_id": None, + "nonce": body_digest, + "not_before": not_before, + "expires_at": expires_at, + "body_schema": schema, + "body_digest": body_digest, + "body": value, + "signature_algorithm": "Ed25519", + "signer_key_id": signer_key_id, + } + wrapper = { + **event_material, + "event_id": sha256_json(event_material), + } + wrapper["signature"] = ( + base64.urlsafe_b64encode( + private_key.sign(canonical_json(wrapper).encode("utf-8")) + ) + .decode("ascii") + .rstrip("=") + ) + return wrapper + + +__all__ = [ + "BROKER_WITNESS_SOURCE_ID", + "SETTLEMENT_WITNESS_SCHEMA", + "SIGNED_ENVELOPE_SCHEMA", + "TERMINAL_WITNESS_SCHEMA", + "WITNESS_TTL", + "sign_broker_witness", +] diff --git a/live_firewall/dumbmoney_capital.py b/live_firewall/dumbmoney_capital.py new file mode 100644 index 00000000..741052eb --- /dev/null +++ b/live_firewall/dumbmoney_capital.py @@ -0,0 +1,2623 @@ +"""DumbMoney signed capital-envelope adapter for Dummy's local firewall. + +The adapter is venue-neutral at the wire level and configured here for a +specific expected venue/account. A valid grant is only an additional upper +bound. It cannot enable live submission, alter Dummy's local caps, provide +broker credentials, or establish cancellation authority. +""" +from __future__ import annotations + +import base64 +import hashlib +import json +import re +from collections.abc import Callable, Mapping +from dataclasses import dataclass, replace +from datetime import datetime, timedelta, timezone +from typing import Any, Protocol + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + +from autonomy.fees import kalshi_taker_fee_cents +from live_firewall.dumbmoney_broker_witness import ( + BROKER_WITNESS_SOURCE_ID, + SETTLEMENT_WITNESS_SCHEMA, + TERMINAL_WITNESS_SCHEMA, + WITNESS_TTL, +) +from live_firewall.operational_journal import ( + AppendOnlyOperationalJournal, + OperationalJournalError, + canonical_json, + sha256_json, +) + + +SIGNED_ENVELOPE_SCHEMA = "dumbmoney.signed-envelope.v1" +CAPITAL_ENVELOPE_SCHEMA = "dumbmoney.capital-envelope.v1" +FLAT_BOOTSTRAP_SCHEMA = "dummy.broker-bootstrap.flat.v1" +INHERITED_BOOTSTRAP_SCHEMA = "dummy.broker-bootstrap.inherited-exposure.v1" +CAPITAL_RESERVATION_SCHEMA = "dummy.capital-reservation.v1" +CAPITAL_DISPATCH_CLAIM_SCHEMA = "dummy.capital-dispatch-claim.v1" +CAPITAL_POSITION_EXPOSURE_SCHEMA = "dummy.capital-position-exposure.v1" +CAPITAL_TERMINAL_RELEASE_SCHEMA = "dummy.capital-terminal-release.v1" +CAPITAL_POSITION_RELEASE_SCHEMA = "dummy.capital-position-release.v1" +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$") +_RFC3339_UTC_RE = re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$" +) + + +def _utc_now() -> datetime: + return datetime.now(timezone.utc) + + +def _parse_utc(value: Any, *, field: str) -> datetime: + if not isinstance(value, str) or not _RFC3339_UTC_RE.fullmatch(value): + raise ValueError(f"{field} must be canonical RFC3339 UTC") + parsed = datetime.fromisoformat(value[:-1] + "+00:00") + if parsed.tzinfo is None: + raise ValueError(f"{field} must be timezone-aware") + return parsed.astimezone(timezone.utc) + + +def _require_text(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not value.strip() or value != value.strip(): + raise ValueError(f"{field} must be a nonblank canonical string") + return value + + +def _require_identifier(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not _IDENTIFIER_RE.fullmatch(value): + raise ValueError(f"{field} must be a canonical identifier") + return value + + +def _require_sha256(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not _SHA256_RE.fullmatch(value): + raise ValueError(f"{field} must be a lowercase sha256") + return value + + +def _require_int(value: Any, *, field: str, minimum: int = 0) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise ValueError(f"{field} must be an integer >= {minimum}") + return value + + +def _require_hashes(value: Any, *, field: str) -> tuple[str, ...]: + if not isinstance(value, list) or not value: + raise ValueError(f"{field} must be a nonempty sorted list") + normalized = tuple(_require_sha256(item, field=field) for item in value) + if list(normalized) != sorted(set(normalized)): + raise ValueError(f"{field} must be sorted and unique") + return normalized + + +def _require_identifiers(value: Any, *, field: str) -> tuple[str, ...]: + if not isinstance(value, list) or not value: + raise ValueError(f"{field} must be a nonempty sorted list") + normalized = tuple( + _require_identifier(item, field=f"{field}[]") for item in value + ) + if list(normalized) != sorted(set(normalized)): + raise ValueError(f"{field} must be sorted and unique") + return normalized + + +def _decode_base64url_no_padding(value: Any, *, field: str) -> bytes: + if not isinstance(value, str) or not value or "=" in value: + raise ValueError(f"{field} must be unpadded base64url") + if re.search(r"[^A-Za-z0-9_-]", value): + raise ValueError(f"{field} must be unpadded base64url") + try: + decoded = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) + except Exception as exc: + raise ValueError(f"{field} is invalid base64url") from exc + encoded = base64.urlsafe_b64encode(decoded).decode("ascii").rstrip("=") + if encoded != value: + raise ValueError(f"{field} is not canonical base64url") + return decoded + + +def _public_key_bytes(value: bytes | Ed25519PublicKey) -> bytes: + if isinstance(value, Ed25519PublicKey): + return bytes(value.public_bytes_raw()) + raw = bytes(value) + if len(raw) != 32: + raise ValueError("Ed25519 public key must be 32 bytes") + return raw + + +def strategy_binding_hash(reference: str) -> str: + """Bind an existing proof/passport reference without changing its schema.""" + return hashlib.sha256(reference.encode("utf-8")).hexdigest() + + +@dataclass(frozen=True) +class VerifiedCapitalEnvelope: + wrapper: dict[str, Any] + body: dict[str, Any] + event_id: str + envelope_id: str + source_id: str + source_sequence: int + nonce: str + signer_key_id: str + venue: str + account_hash: str + strategy_hashes: tuple[str, ...] + passport_hashes: tuple[str, ...] + promotion_hashes: tuple[str, ...] + authorized_instruments: tuple[str, ...] + authorized_mode: str + max_order_risk_cents: int + max_open_risk_cents: int + max_correlated_risk_cents: int + max_daily_loss_cents: int + max_open_orders: int + fencing_generation: int + policy_epoch: int + not_before: datetime + expires_at: datetime + ledger_event_digest: str | None = None + ledger_global_sequence: int | None = None + + +@dataclass(frozen=True) +class VerifiedSignedEnvelope: + """Cryptographically verified DumbMoney wrapper with an active window.""" + + wrapper: dict[str, Any] + body: dict[str, Any] + event_id: str + source_id: str + source_sequence: int + nonce: str + signer_key_id: str + body_schema: str + not_before: datetime + expires_at: datetime + + +@dataclass(frozen=True) +class CapitalVerdict: + allow: bool + reason: str + rejected_by: str | None = None + reservation_id: str | None = None + effective_limits: dict[str, int] | None = None + + +class CapitalLineageResolver(Protocol): + """Authenticate one exact singleton lineage through DumbMoney Core.""" + + def resolve_lineage( + self, + *, + capital: VerifiedCapitalEnvelope, + strategy_hash: str, + passport_hash: str, + promotion_hash: str, + authorized_instrument: str, + expected_binding: Mapping[str, Any] | None = None, + ) -> Any: ... + + +def verify_signed_envelope( + wrapper: Mapping[str, Any], + *, + trusted_public_keys: Mapping[str, bytes | Ed25519PublicKey], + now: datetime | None = None, + expected_body_schema: str | None = None, + max_ttl: timedelta | None = None, + require_active: bool = True, +) -> VerifiedSignedEnvelope: + """Validate a complete DumbMoney signed-envelope wrapper. + + This is the shared authority verifier for capital, kill, and desired-mode + commands. It verifies exact wire shape, canonical hashes, signer binding, + signature, and the active authority window. Body-specific semantics remain + the responsibility of the receiving authority adapter. + """ + value = json.loads(canonical_json(dict(wrapper))) + if value.get("schema") != SIGNED_ENVELOPE_SCHEMA: + raise ValueError("signed envelope schema mismatch") + required_wrapper = { + "schema", + "source_id", + "source_sequence", + "event_id", + "correlation_id", + "causation_id", + "nonce", + "not_before", + "expires_at", + "body_schema", + "body_digest", + "body", + "signature_algorithm", + "signer_key_id", + "signature", + } + if set(value) != required_wrapper: + raise ValueError("signed envelope fields mismatch") + source_id = _require_identifier(value["source_id"], field="source_id") + source_sequence = _require_int( + value["source_sequence"], field="source_sequence", minimum=1 + ) + event_id = _require_sha256(value["event_id"], field="event_id") + _require_identifier(value["correlation_id"], field="correlation_id") + if value["causation_id"] is not None: + _require_sha256(value["causation_id"], field="causation_id") + nonce = _require_identifier(value["nonce"], field="nonce") + wrapper_not_before = _parse_utc(value["not_before"], field="not_before") + wrapper_expires_at = _parse_utc(value["expires_at"], field="expires_at") + body_schema = _require_identifier(value["body_schema"], field="body_schema") + if expected_body_schema is not None and body_schema != expected_body_schema: + raise ValueError("body schema mismatch") + if value["signature_algorithm"] != "Ed25519": + raise ValueError("signature algorithm mismatch") + signer_key_id = _require_sha256(value["signer_key_id"], field="signer_key_id") + body_digest = _require_sha256(value["body_digest"], field="body_digest") + body = value["body"] + if not isinstance(body, dict): + raise ValueError("signed envelope body must be an object") + if body.get("schema") != body_schema: + raise ValueError("signed envelope body schema mismatch") + if sha256_json(body) != body_digest: + raise ValueError("signed envelope body digest mismatch") + + event_material = { + key: item + for key, item in value.items() + if key not in {"event_id", "signature"} + } + if sha256_json(event_material) != event_id: + raise ValueError("signed envelope event id mismatch") + + key_material = trusted_public_keys.get(signer_key_id) + if key_material is None: + raise ValueError("signer key is not trusted") + raw_public_key = _public_key_bytes(key_material) + if hashlib.sha256(raw_public_key).hexdigest() != signer_key_id: + raise ValueError("signer key id does not match public key") + signature = _decode_base64url_no_padding( + value["signature"], field="signature" + ) + if len(signature) != 64: + raise ValueError("signed envelope signature must be 64 bytes") + signed_material = {key: item for key, item in value.items() if key != "signature"} + try: + Ed25519PublicKey.from_public_bytes(raw_public_key).verify( + signature, + canonical_json(signed_material).encode("utf-8"), + ) + except (InvalidSignature, ValueError) as exc: + raise ValueError("signed envelope signature invalid") from exc + + if wrapper_expires_at <= wrapper_not_before: + raise ValueError("signed envelope authority window is invalid") + if max_ttl is not None and wrapper_expires_at - wrapper_not_before > max_ttl: + raise ValueError("signed envelope authority window exceeds maximum") + if require_active: + current = now or _utc_now() + if current.tzinfo is None or current.utcoffset() is None: + raise ValueError("verification clock must be timezone-aware") + current = current.astimezone(timezone.utc) + if current < wrapper_not_before: + raise ValueError("signed envelope is not active yet") + if current >= wrapper_expires_at: + raise ValueError("signed envelope expired") + + return VerifiedSignedEnvelope( + wrapper=value, + body=body, + event_id=event_id, + source_id=source_id, + source_sequence=source_sequence, + nonce=nonce, + signer_key_id=signer_key_id, + body_schema=body_schema, + not_before=wrapper_not_before, + expires_at=wrapper_expires_at, + ) + + +def verify_signed_capital_envelope( + wrapper: Mapping[str, Any], + *, + trusted_public_keys: Mapping[str, bytes | Ed25519PublicKey], + expected_venue: str, + expected_account_hash: str, + now: datetime | None = None, + max_ttl: timedelta = timedelta(minutes=5), + require_active: bool = True, +) -> VerifiedCapitalEnvelope: + """Validate the complete DumbMoney wrapper and capital body.""" + signed = verify_signed_envelope( + wrapper, + trusted_public_keys=trusted_public_keys, + now=now, + expected_body_schema=CAPITAL_ENVELOPE_SCHEMA, + max_ttl=max_ttl, + require_active=require_active, + ) + value = signed.wrapper + body = signed.body + required_body = { + "schema", + "envelope_id", + "mandate_id", + "venue", + "account_hash", + "strategy_hashes", + "passport_hashes", + "promotion_hashes", + "authorized_instruments", + "authorized_mode", + "max_order_risk_cents", + "max_open_risk_cents", + "max_correlated_risk_cents", + "max_daily_loss_cents", + "max_open_orders", + "fencing_generation", + "policy_epoch", + "not_before", + "expires_at", + } + if set(body) != required_body: + raise ValueError("capital envelope fields mismatch") + + envelope_id = _require_sha256(body["envelope_id"], field="envelope_id") + _require_sha256(body["mandate_id"], field="mandate_id") + venue = _require_text(body["venue"], field="venue") + if venue not in {"dummy_kalshi", "dopey_robinhood"}: + raise ValueError("capital envelope venue is unsupported") + if venue != expected_venue: + raise ValueError("capital envelope venue mismatch") + account_hash = _require_sha256(body["account_hash"], field="account_hash") + if account_hash != _require_sha256( + expected_account_hash, field="expected_account_hash" + ): + raise ValueError("capital envelope account mismatch") + strategy_hashes = _require_hashes(body["strategy_hashes"], field="strategy_hashes") + passport_hashes = _require_hashes(body["passport_hashes"], field="passport_hashes") + promotion_hashes = _require_hashes( + body["promotion_hashes"], field="promotion_hashes" + ) + if not ( + len(strategy_hashes) == len(passport_hashes) == len(promotion_hashes) + ): + raise ValueError( + "strategy, passport, and promotion hash counts must match" + ) + if len(strategy_hashes) != 1: + raise ValueError( + "capital envelopes authorize exactly one " + "strategy/passport/promotion tuple" + ) + authorized_instruments = _require_identifiers( + body["authorized_instruments"], + field="authorized_instruments", + ) + expected_prefixes = { + "dummy_kalshi": ("event_contract:",), + "dopey_robinhood": ("equity:", "option:"), + }[venue] + if any( + not item.startswith(expected_prefixes) + or item.endswith(":") + or "*" in item + for item in authorized_instruments + ): + raise ValueError("authorized instrument is invalid for venue") + authorized_mode = _require_text(body["authorized_mode"], field="authorized_mode") + if authorized_mode != "LIVE": + raise ValueError("authorized_mode must be LIVE") + max_order = _require_int( + body["max_order_risk_cents"], field="max_order_risk_cents", minimum=1 + ) + max_open = _require_int( + body["max_open_risk_cents"], field="max_open_risk_cents", minimum=1 + ) + max_correlated = _require_int( + body["max_correlated_risk_cents"], + field="max_correlated_risk_cents", + minimum=1, + ) + max_daily_loss = _require_int( + body["max_daily_loss_cents"], field="max_daily_loss_cents", minimum=1 + ) + max_open_orders = _require_int( + body["max_open_orders"], field="max_open_orders", minimum=1 + ) + if not max_order <= max_correlated <= max_open: + raise ValueError("capital envelope limits are internally inconsistent") + fencing_generation = _require_int( + body["fencing_generation"], field="fencing_generation", minimum=1 + ) + policy_epoch = _require_int( + body["policy_epoch"], field="policy_epoch", minimum=1 + ) + body_not_before = _parse_utc(body["not_before"], field="body.not_before") + body_expires_at = _parse_utc(body["expires_at"], field="body.expires_at") + if ( + body_not_before != signed.not_before + or body_expires_at != signed.expires_at + ): + raise ValueError("wrapper and body authority windows differ") + + return VerifiedCapitalEnvelope( + wrapper=value, + body=body, + event_id=signed.event_id, + envelope_id=envelope_id, + source_id=signed.source_id, + source_sequence=signed.source_sequence, + nonce=signed.nonce, + signer_key_id=signed.signer_key_id, + venue=venue, + account_hash=account_hash, + strategy_hashes=strategy_hashes, + passport_hashes=passport_hashes, + promotion_hashes=promotion_hashes, + authorized_instruments=authorized_instruments, + authorized_mode=authorized_mode, + max_order_risk_cents=max_order, + max_open_risk_cents=max_open, + max_correlated_risk_cents=max_correlated, + max_daily_loss_cents=max_daily_loss, + max_open_orders=max_open_orders, + fencing_generation=fencing_generation, + policy_epoch=policy_epoch, + not_before=body_not_before, + expires_at=body_expires_at, + ) + + +def flat_book_receipt( + *, + receipt_id: str, + venue: str, + account_hash: str, + observed_at: str, + broker_snapshot_sha256: str, +) -> dict[str, Any]: + return { + "schema": FLAT_BOOTSTRAP_SCHEMA, + "receipt_id": _require_text(receipt_id, field="receipt_id"), + "venue": _require_text(venue, field="venue"), + "account_hash": _require_sha256(account_hash, field="account_hash"), + "observed_at": observed_at, + "broker_snapshot_sha256": _require_sha256( + broker_snapshot_sha256, field="broker_snapshot_sha256" + ), + "flat_book_observed": True, + "total_exposure_cents": 0, + "open_order_count": 0, + "market_exposure_cents": {}, + "correlated_exposure_cents": {}, + } + + +def inherited_exposure_receipt( + *, + receipt_id: str, + venue: str, + account_hash: str, + observed_at: str, + broker_snapshot_sha256: str, + total_exposure_cents: int, + open_order_count: int, + market_exposure_cents: Mapping[str, int], + correlated_exposure_cents: Mapping[str, int], +) -> dict[str, Any]: + total = _require_int( + total_exposure_cents, field="total_exposure_cents", minimum=0 + ) + orders = _require_int(open_order_count, field="open_order_count", minimum=0) + markets = { + _require_text(str(key), field="market_exposure_key"): _require_int( + item, field="market_exposure_cents", minimum=0 + ) + for key, item in market_exposure_cents.items() + } + correlated = { + _require_text(str(key), field="correlated_exposure_key"): _require_int( + item, field="correlated_exposure_cents", minimum=0 + ) + for key, item in correlated_exposure_cents.items() + } + if total == 0 and orders == 0: + raise ValueError("inherited exposure receipt cannot represent a flat book") + if sum(markets.values()) > total or max(correlated.values(), default=0) > total: + raise ValueError("inherited exposure breakdown exceeds total exposure") + return { + "schema": INHERITED_BOOTSTRAP_SCHEMA, + "receipt_id": _require_text(receipt_id, field="receipt_id"), + "venue": _require_text(venue, field="venue"), + "account_hash": _require_sha256(account_hash, field="account_hash"), + "observed_at": observed_at, + "broker_snapshot_sha256": _require_sha256( + broker_snapshot_sha256, field="broker_snapshot_sha256" + ), + "flat_book_observed": False, + "total_exposure_cents": total, + "open_order_count": orders, + "market_exposure_cents": markets, + "correlated_exposure_cents": correlated, + } + + +class CapitalEnvelopeAdapter: + """Validate grants and durably reserve only bounded additional risk.""" + + def __init__( + self, + *, + journal: AppendOnlyOperationalJournal, + trusted_public_keys: Mapping[str, bytes | Ed25519PublicKey], + expected_venue: str, + expected_account_hash: str, + lineage_resolver: CapitalLineageResolver, + trusted_broker_witness_public_keys: ( + Mapping[str, bytes | Ed25519PublicKey] | None + ) = None, + expected_subaccount_number: int = 0, + now_fn: Callable[[], datetime] | None = None, + max_envelope_ttl: timedelta = timedelta(minutes=5), + max_bootstrap_age: timedelta = timedelta(minutes=5), + ) -> None: + self.journal = journal + self.trusted_public_keys = dict(trusted_public_keys) + self.trusted_broker_witness_public_keys = dict( + trusted_broker_witness_public_keys or {} + ) + self.expected_venue = expected_venue + self.expected_account_hash = _require_sha256( + expected_account_hash, field="expected_account_hash" + ) + if ( + isinstance(expected_subaccount_number, bool) + or not isinstance(expected_subaccount_number, int) + or expected_subaccount_number != 0 + ): + raise ValueError( + "expected_subaccount_number must be 0 until end-to-end " + "subaccount routing is sealed" + ) + self.expected_subaccount_number = expected_subaccount_number + if not hasattr(lineage_resolver, "resolve_lineage") or not callable( + lineage_resolver.resolve_lineage + ): + raise TypeError("a Core authority lineage resolver is required") + self.lineage_resolver = lineage_resolver + self._now_fn = now_fn or _utc_now + self.max_envelope_ttl = max_envelope_ttl + self.max_bootstrap_age = max_bootstrap_age + + def _events(self, kind: str | None = None) -> tuple[dict[str, Any], ...]: + if not self.journal.healthy: + raise OperationalJournalError("capital journal is unhealthy") + return tuple( + dict(row) for row in self.journal.events(kind=kind) + ) + + def accept_signed_envelope( + self, + wrapper: Mapping[str, Any], + *, + ledger_event_digest: str | None = None, + ledger_global_sequence: int | None = None, + ) -> VerifiedCapitalEnvelope: + verified = verify_signed_capital_envelope( + wrapper, + trusted_public_keys=self.trusted_public_keys, + expected_venue=self.expected_venue, + expected_account_hash=self.expected_account_hash, + now=self._now_fn(), + max_ttl=self.max_envelope_ttl, + ) + if (ledger_event_digest is None) != (ledger_global_sequence is None): + raise ValueError( + "capital ledger digest and global sequence must be supplied together" + ) + if ledger_event_digest is not None: + verified = replace( + verified, + ledger_event_digest=_require_sha256( + ledger_event_digest, + field="capital ledger_event_digest", + ), + ledger_global_sequence=_require_int( + ledger_global_sequence, + field="capital ledger_global_sequence", + minimum=1, + ), + ) + payload = { + "event_id": verified.event_id, + "envelope_id": verified.envelope_id, + "source_id": verified.source_id, + "source_sequence": verified.source_sequence, + "nonce": verified.nonce, + "fencing_generation": verified.fencing_generation, + "ledger_event_digest": verified.ledger_event_digest, + "ledger_global_sequence": verified.ledger_global_sequence, + "wrapper": verified.wrapper, + } + + def validate_existing(rows: tuple[dict[str, Any], ...]) -> None: + accepted = [ + row + for row in rows + if row.get("kind") == "capital.envelope.accepted" + ] + seen_nonce = { + str(row["payload"].get("nonce")) + for row in accepted + } + if verified.nonce in seen_nonce: + raise ValueError("capital envelope nonce replay") + source_rows = [ + row + for row in accepted + if row["payload"].get("source_id") == verified.source_id + ] + if source_rows: + highest_sequence = max( + int(row["payload"]["source_sequence"]) + for row in source_rows + ) + if verified.source_sequence <= highest_sequence: + raise ValueError("capital envelope source sequence replay") + highest_fence = max( + ( + int(row["payload"]["fencing_generation"]) + for row in accepted + ), + default=0, + ) + if ( + highest_fence + and verified.fencing_generation <= highest_fence + ): + raise ValueError( + "capital envelope fencing generation is not strictly increasing" + ) + highest_policy_epoch = max( + ( + int(row["payload"]["wrapper"]["body"]["policy_epoch"]) + for row in accepted + ), + default=0, + ) + if verified.policy_epoch < highest_policy_epoch: + raise ValueError("capital envelope policy epoch regressed") + + self.journal.append( + "capital.envelope.accepted", + payload, + outbox_id=f"capital-envelope:{verified.event_id}", + validate_existing=validate_existing, + ) + return verified + + def record_broker_bootstrap( + self, receipt: Mapping[str, Any] + ) -> dict[str, Any]: + value = json.loads(canonical_json(dict(receipt))) + schema = value.get("schema") + if schema not in {FLAT_BOOTSTRAP_SCHEMA, INHERITED_BOOTSTRAP_SCHEMA}: + raise ValueError("broker bootstrap receipt schema mismatch") + if value.get("venue") != self.expected_venue: + raise ValueError("broker bootstrap venue mismatch") + if value.get("account_hash") != self.expected_account_hash: + raise ValueError("broker bootstrap account mismatch") + _require_text(value.get("receipt_id"), field="receipt_id") + observed = _parse_utc(value.get("observed_at"), field="observed_at") + now = self._now_fn().astimezone(timezone.utc) + if observed > now + timedelta(seconds=5): + raise ValueError("broker bootstrap receipt is from the future") + if now - observed > self.max_bootstrap_age: + raise ValueError("broker bootstrap receipt is stale") + _require_sha256( + value.get("broker_snapshot_sha256"), field="broker_snapshot_sha256" + ) + total = _require_int( + value.get("total_exposure_cents"), + field="total_exposure_cents", + minimum=0, + ) + orders = _require_int( + value.get("open_order_count"), field="open_order_count", minimum=0 + ) + markets = value.get("market_exposure_cents") + correlated = value.get("correlated_exposure_cents") + if not isinstance(markets, dict) or not isinstance(correlated, dict): + raise ValueError("broker bootstrap exposure maps are required") + for item in (*markets.values(), *correlated.values()): + _require_int(item, field="bootstrap_exposure_cents", minimum=0) + if schema == FLAT_BOOTSTRAP_SCHEMA: + if ( + value.get("flat_book_observed") is not True + or total != 0 + or orders != 0 + or markets + or correlated + ): + raise ValueError("flat bootstrap receipt contains exposure") + elif ( + value.get("flat_book_observed") is not False + or (total == 0 and orders == 0) + ): + raise ValueError("inherited bootstrap receipt lacks exposure") + def validate_existing(rows: tuple[dict[str, Any], ...]) -> None: + prior_rows = [ + row + for row in rows + if row.get("kind") == "broker.bootstrap.recorded" + ] + if prior_rows: + previous_observed = _parse_utc( + prior_rows[-1]["payload"]["observed_at"], + field="previous.observed_at", + ) + if observed <= previous_observed: + raise ValueError( + "broker bootstrap observations must be strictly increasing" + ) + + event = self.journal.append( + "broker.bootstrap.recorded", + value, + outbox_id=f"broker-bootstrap:{value['receipt_id']}", + validate_existing=validate_existing, + validate_existing_latest_kinds=("broker.bootstrap.recorded",), + ) + return dict(event) + + def _latest_bootstrap(self) -> dict[str, Any] | None: + rows = self._events("broker.bootstrap.recorded") + return dict(rows[-1]["payload"]) if rows else None + + def _accepted_envelopes(self) -> list[VerifiedCapitalEnvelope]: + verified: list[VerifiedCapitalEnvelope] = [] + for row in self._events("capital.envelope.accepted"): + try: + accepted = verify_signed_capital_envelope( + row["payload"]["wrapper"], + trusted_public_keys=self.trusted_public_keys, + expected_venue=self.expected_venue, + expected_account_hash=self.expected_account_hash, + now=self._now_fn(), + max_ttl=self.max_envelope_ttl, + ) + ledger_digest = row["payload"].get("ledger_event_digest") + ledger_sequence = row["payload"].get("ledger_global_sequence") + if ledger_digest is not None or ledger_sequence is not None: + accepted = replace( + accepted, + ledger_event_digest=_require_sha256( + ledger_digest, + field="capital ledger_event_digest", + ), + ledger_global_sequence=_require_int( + ledger_sequence, + field="capital ledger_global_sequence", + minimum=1, + ), + ) + verified.append( + accepted + ) + except ValueError: + # Expired grants are normal history and are simply inactive. + continue + return verified + + def _maximum_accepted_fence(self) -> int: + """Return the monotonic high-water fence, including expired grants.""" + return max( + ( + int(row["payload"]["fencing_generation"]) + for row in self._events("capital.envelope.accepted") + ), + default=0, + ) + + def _active_reservations(self) -> list[dict[str, Any]]: + reservations = { + str(row["payload"]["reservation_id"]): dict(row["payload"]) + for row in self._events("capital.reservation.created") + } + released = { + str(row["payload"]["reservation_id"]) + for row in self._events("capital.reservation.released") + } + return [ + value for key, value in reservations.items() if key not in released + ] + + def _active_position_exposures(self) -> list[dict[str, Any]]: + positions = { + str(row["payload"]["position_exposure_id"]): dict( + row["payload"] + ) + for row in self._events("capital.position.exposure.recorded") + } + released = { + str(row["payload"]["position_exposure_id"]) + for row in self._events("capital.position.exposure.released") + } + return [ + value for key, value in positions.items() if key not in released + ] + + @staticmethod + def _fee_inclusive_order_risk( + request: Any, + ) -> tuple[int, int, int]: + size = _require_int( + int(request.size), + field="order_size", + minimum=1, + ) + price = _require_int( + int(request.price_cents), + field="order_price_cents", + minimum=1, + ) + if price > 100: + raise ValueError("order_price_cents must be <= 100") + notional = size * price + # Reserve at the general taker schedule even for post-only makers. + # This remains conservative if maker classification or the embedded + # maker-series schedule changes before a fill is reconciled. + fee = kalshi_taker_fee_cents( + price, + size, + str(request.market_ticker), + ) + return notional, fee, notional + fee + + def _cancel_only(self) -> bool: + rows = self._events("authority.cancel_only") + return bool(rows) + + def binding_for( + self, + *, + strategy_hash: str, + passport_hash: str, + authorized_instrument: str, + promotion_hash: str | None = None, + ) -> dict[str, Any]: + strategy = _require_sha256(strategy_hash, field="strategy_hash") + passport = _require_sha256(passport_hash, field="passport_hash") + instrument = _require_identifier( + authorized_instrument, + field="authorized_instrument", + ) + promotion = ( + _require_sha256(promotion_hash, field="promotion_hash") + if promotion_hash is not None + else None + ) + eligible = [ + envelope + for envelope in self._accepted_envelopes() + if envelope.authorized_mode == "LIVE" + and envelope.fencing_generation == self._maximum_accepted_fence() + and strategy in envelope.strategy_hashes + and passport in envelope.passport_hashes + and instrument in envelope.authorized_instruments + and ( + promotion in envelope.promotion_hashes + if promotion is not None + else len(envelope.promotion_hashes) == 1 + ) + ] + if not eligible: + raise ValueError("no active capital envelope covers request lineage") + selected = max( + eligible, + key=lambda item: ( + item.fencing_generation, + item.source_sequence, + item.expires_at, + ), + ) + selected_promotion = ( + promotion + if promotion is not None + else selected.promotion_hashes[0] + ) + binding = self.lineage_resolver.resolve_lineage( + capital=selected, + strategy_hash=strategy, + passport_hash=passport, + promotion_hash=selected_promotion, + authorized_instrument=instrument, + ) + evidence = json.loads(canonical_json(binding.evidence())) + # Resolution pages contain a fresh request nonce, so retain only the + # stable, cryptographically authenticated identity for the sink's + # independent revalidation. + evidence.pop("passport_resolution_sha256", None) + evidence.pop("promotion_resolution_sha256", None) + lineage_id = sha256_json(evidence) + self.journal.append( + "capital.lineage.verified", + { + "lineage_id": lineage_id, + "binding": evidence, + }, + outbox_id=f"capital-lineage:{lineage_id}", + ) + return { + "capital_envelope_id": selected.envelope_id, + "capital_strategy_hash": strategy, + "capital_passport_hash": passport, + "capital_promotion_hash": selected_promotion, + "capital_fencing_generation": selected.fencing_generation, + } + + @staticmethod + def _correlation_key(market_ticker: str) -> str: + return market_ticker.split("-", 1)[0].upper() + + @staticmethod + def _request_order_terms(request: Any) -> dict[str, Any]: + return { + "proposal_id": str(request.proposal_id), + "market_ticker": str(request.market_ticker), + "contract_ticker": str(request.contract_ticker), + "side": str(request.side), + "size": int(request.size), + "price_cents": int(request.price_cents), + "expiration_ts": ( + int(request.expiration_ts) + if getattr(request, "expiration_ts", None) is not None + else None + ), + "liquidity_role": str( + getattr(request, "liquidity_role", "maker") or "maker" + ), + "capital_envelope_id": str( + getattr(request, "capital_envelope_id", "") or "" + ), + "capital_strategy_hash": str( + getattr(request, "capital_strategy_hash", "") or "" + ), + "capital_passport_hash": str( + getattr(request, "capital_passport_hash", "") or "" + ), + "capital_promotion_hash": str( + getattr(request, "capital_promotion_hash", "") or "" + ), + "capital_fencing_generation": int( + getattr(request, "capital_fencing_generation") + ), + } + + @staticmethod + def _expected_broker_order(request: Any) -> dict[str, Any]: + """Reconstruct the exact order payload authorized by the request. + + A dispatch claim is a broker-send capability, so hashing an arbitrary + caller-supplied mapping is insufficient. Keep this fail-closed mirror + of ``LiveBrokerFirewall._build_order`` until the broker wire contract + is migrated as one atomic change. + """ + side = str(request.side) + if side not in {"yes", "no"}: + raise ValueError("dispatch request side is invalid") + count = _require_int(request.size, field="dispatch count", minimum=1) + price = _require_int( + request.price_cents, + field="dispatch price_cents", + minimum=1, + ) + if price > 99: + raise ValueError("dispatch price_cents must be <= 99") + yes_price_cents = price if side == "yes" else 100 - price + maker = ( + str(getattr(request, "liquidity_role", "maker") or "maker") + == "maker" + ) + expected: dict[str, Any] = { + "ticker": _require_identifier( + request.contract_ticker, + field="dispatch ticker", + ), + "client_order_id": _require_identifier( + request.proposal_id, + field="dispatch client_order_id", + ), + "side": "bid" if side == "yes" else "ask", + "count": f"{count}.00", + "price": f"0.{yes_price_cents:02d}00", + "time_in_force": ( + "good_till_canceled" if maker else "fill_or_kill" + ), + "self_trade_prevention_type": "taker_at_cross", + "post_only": maker, + "cancel_order_on_pause": True, + "reduce_only": False, + "subaccount": 0, + "exchange_index": 0, + } + expiration = getattr(request, "expiration_ts", None) + if maker: + if expiration is None: + raise ValueError( + "dispatch maker order lacks exchange-enforced expiration" + ) + expected["expiration_time"] = _require_int( + expiration, + field="dispatch expiration_time", + minimum=1, + ) + return expected + + def _resolve_envelope_for_request(self, request: Any) -> VerifiedCapitalEnvelope: + envelope_id = str(getattr(request, "capital_envelope_id", "") or "") + strategy_hash = str(getattr(request, "capital_strategy_hash", "") or "") + passport_hash = str(getattr(request, "capital_passport_hash", "") or "") + promotion_hash = str(getattr(request, "capital_promotion_hash", "") or "") + contract_ticker = _require_identifier( + str(getattr(request, "contract_ticker", "") or ""), + field="contract_ticker", + ) + authorized_instrument = f"event_contract:{contract_ticker}" + try: + fence = int(getattr(request, "capital_fencing_generation")) + except (TypeError, ValueError): + raise ValueError("capital fencing generation missing") from None + candidates = [ + envelope + for envelope in self._accepted_envelopes() + if envelope.envelope_id == envelope_id + and envelope.fencing_generation == fence + ] + if not candidates: + raise ValueError("capital envelope is missing, expired, or fenced") + envelope = max(candidates, key=lambda item: item.source_sequence) + if envelope.authorized_mode != "LIVE": + raise ValueError("capital envelope does not authorize LIVE mode") + if strategy_hash not in envelope.strategy_hashes: + raise ValueError("capital strategy hash mismatch") + if passport_hash not in envelope.passport_hashes: + raise ValueError("capital passport hash mismatch") + if promotion_hash not in envelope.promotion_hashes: + raise ValueError("capital promotion hash mismatch") + if authorized_instrument not in envelope.authorized_instruments: + raise ValueError("capital instrument is not authorized") + highest_fence = self._maximum_accepted_fence() + if fence != highest_fence: + raise ValueError("capital envelope fencing generation is stale") + prior_bindings = [ + row["payload"]["binding"] + for row in self._events("capital.lineage.verified") + if isinstance(row.get("payload", {}).get("binding"), dict) + ] + expected_binding = next( + ( + binding + for binding in reversed(prior_bindings) + if binding.get("capital_envelope_id") == envelope.envelope_id + and binding.get("capital_event_id") == envelope.event_id + and binding.get("capital_body_digest") + == sha256_json(envelope.body) + and binding.get("capital_fencing_generation") == fence + and binding.get("policy_epoch") == envelope.policy_epoch + and binding.get("strategy_hash") == strategy_hash + and binding.get("passport_hash") == passport_hash + and binding.get("promotion_hash") == promotion_hash + and binding.get("authorized_instrument") + == authorized_instrument + ), + None, + ) + binding = self.lineage_resolver.resolve_lineage( + capital=envelope, + strategy_hash=strategy_hash, + passport_hash=passport_hash, + promotion_hash=promotion_hash, + authorized_instrument=authorized_instrument, + expected_binding=expected_binding, + ) + if expected_binding is None: + # A manually assembled request cannot self-assert lineage. The + # sink may establish the missing durable receipt only by resolving + # both original signed contracts through Core immediately now. + evidence = json.loads(canonical_json(binding.evidence())) + evidence.pop("passport_resolution_sha256", None) + evidence.pop("promotion_resolution_sha256", None) + lineage_id = sha256_json(evidence) + self.journal.append( + "capital.lineage.verified", + { + "lineage_id": lineage_id, + "binding": evidence, + }, + outbox_id=f"capital-lineage:{lineage_id}", + ) + return envelope + + def _verdict( + self, + request: Any, + *, + current_daily_loss_cents: int, + current_local_total_exposure_cents: int = 0, + current_local_correlated_exposure_cents: int = 0, + current_local_open_orders: int = 0, + current_request_locally_reserved: bool = False, + ) -> tuple[CapitalVerdict, VerifiedCapitalEnvelope | None, dict[str, Any] | None]: + if self._cancel_only(): + return ( + CapitalVerdict( + False, + "DumbMoney adapter is in cancel-only degraded mode", + "capital_cancel_only", + ), + None, + None, + ) + bootstrap = self._latest_bootstrap() + if bootstrap is None: + return ( + CapitalVerdict( + False, + "Fresh broker bootstrap receipt required; local absence is not flat", + "capital_broker_bootstrap", + ), + None, + None, + ) + try: + observed = _parse_utc(bootstrap["observed_at"], field="observed_at") + if self._now_fn().astimezone(timezone.utc) - observed > self.max_bootstrap_age: + raise ValueError("broker bootstrap receipt is stale") + envelope = self._resolve_envelope_for_request(request) + _, _, order_risk = self._fee_inclusive_order_risk(request) + daily_loss = _require_int( + current_daily_loss_cents, + field="current_daily_loss_cents", + minimum=0, + ) + local_total = _require_int( + current_local_total_exposure_cents, + field="current_local_total_exposure_cents", + minimum=0, + ) + local_correlated = _require_int( + current_local_correlated_exposure_cents, + field="current_local_correlated_exposure_cents", + minimum=0, + ) + local_open_orders = _require_int( + current_local_open_orders, + field="current_local_open_orders", + minimum=0, + ) + except Exception as exc: + return ( + CapitalVerdict(False, str(exc), "capital_envelope"), + None, + bootstrap, + ) + active = self._active_reservations() + active_positions = self._active_position_exposures() + proposal_id = str(request.proposal_id) + terminal_witness = next( + ( + row["payload"] + for row in self._events( + "capital.terminal_reconciliation.witnessed" + ) + if row["payload"].get("proposal_id") == proposal_id + ), + None, + ) + if terminal_witness is not None: + return ( + CapitalVerdict( + False, + "proposal id has a broker terminal witness and cannot be reused", + "capital_idempotency", + ), + envelope, + bootstrap, + ) + existing = next( + ( + item + for item in active + if item.get("proposal_id") == proposal_id + ), + None, + ) + if existing is not None: + request_terms_sha256 = sha256_json( + self._request_order_terms(request) + ) + matches = ( + existing.get("envelope_id") == envelope.envelope_id + and existing.get("market_ticker") == request.market_ticker + and existing.get("contract_ticker") + == request.contract_ticker + and existing.get("side") == request.side + and int(existing.get("size", -1)) == int(request.size) + and int(existing.get("price_cents", -1)) == int(request.price_cents) + and existing.get("account_hash") == envelope.account_hash + and existing.get("venue") == envelope.venue + and existing.get("strategy_hash") + == str(request.capital_strategy_hash) + and existing.get("passport_hash") + == str(request.capital_passport_hash) + and existing.get("promotion_hash") + == str(request.capital_promotion_hash) + and existing.get("order_terms_sha256") + == request_terms_sha256 + ) + if not matches: + return ( + CapitalVerdict( + False, + "proposal id already reserved for different order terms", + "capital_idempotency", + ), + envelope, + bootstrap, + ) + else: + prior_terminal = next( + ( + row["payload"] + for row in self._events("capital.reservation.created") + if row["payload"].get("proposal_id") == proposal_id + ), + None, + ) + if prior_terminal is not None: + return ( + CapitalVerdict( + False, + "proposal id has a terminal capital reservation and cannot be reused", + "capital_idempotency", + ), + envelope, + bootstrap, + ) + inherited_total = int(bootstrap["total_exposure_cents"]) + inherited_orders = int(bootstrap["open_order_count"]) + reserved_total = sum(int(item["risk_cents"]) for item in active) + position_total = sum( + int(item["risk_cents"]) for item in active_positions + ) + correlation_key = self._correlation_key(str(request.market_ticker)) + inherited_correlated = int( + bootstrap["correlated_exposure_cents"].get(correlation_key, 0) + ) + reserved_correlated = sum( + int(item["risk_cents"]) + for item in active + if item.get("correlation_key") == correlation_key + ) + position_correlated = sum( + int(item["risk_cents"]) + for item in active_positions + if item.get("correlation_key") == correlation_key + ) + adjusted_local_total = local_total + adjusted_local_correlated = local_correlated + adjusted_local_orders = local_open_orders + if existing is not None and current_request_locally_reserved: + existing_risk = int(existing["risk_cents"]) + if ( + adjusted_local_total < existing_risk + or adjusted_local_correlated < existing_risk + or adjusted_local_orders < 1 + ): + return ( + CapitalVerdict( + False, + "local reservation witness is inconsistent", + "capital_local_reservation", + ), + envelope, + bootstrap, + ) + # This is the only overlap we can prove: the exact proposal is + # present in both Dummy's local submission ledger and this + # adapter's reservation journal. Preserve conservative addition + # for every other exposure. + adjusted_local_total -= existing_risk + adjusted_local_correlated -= existing_risk + adjusted_local_orders -= 1 + limits = { + "max_order_risk_cents": envelope.max_order_risk_cents, + "max_open_risk_cents": envelope.max_open_risk_cents, + "max_correlated_risk_cents": envelope.max_correlated_risk_cents, + "max_daily_loss_cents": envelope.max_daily_loss_cents, + "max_open_orders": envelope.max_open_orders, + } + # These three books have no signed overlap proof. Treat them as + # disjoint until reconciliation supplies one: using max() can mask a + # post-bootstrap fill behind a larger inherited baseline after its + # reservation is released. Conservative addition may double count an + # order represented in both local and adapter state, but never grants + # capital against exposure whose overlap is merely assumed. + effective_open_risk = ( + inherited_total + + reserved_total + + position_total + + adjusted_local_total + ) + effective_correlated_risk = ( + inherited_correlated + + reserved_correlated + + position_correlated + + adjusted_local_correlated + ) + effective_open_orders = ( + inherited_orders + len(active) + adjusted_local_orders + ) + if order_risk > envelope.max_order_risk_cents: + reason = "DumbMoney maximum order risk exceeded" + elif ( + effective_open_risk + + (0 if existing is not None else order_risk) + > envelope.max_open_risk_cents + ): + reason = "DumbMoney maximum open risk exceeded" + elif ( + effective_correlated_risk + + (0 if existing is not None else order_risk) + > envelope.max_correlated_risk_cents + ): + reason = "DumbMoney maximum correlated risk exceeded" + elif ( + order_risk + > envelope.max_daily_loss_cents - daily_loss + ): + reason = "DumbMoney daily loss capacity exceeded" + elif ( + effective_open_orders + + (0 if existing is not None else 1) + > envelope.max_open_orders + ): + reason = "DumbMoney maximum open orders reached" + else: + return ( + CapitalVerdict( + True, + ( + "Existing durable DumbMoney reservation verified" + if existing is not None + else "Signed DumbMoney capital grant bounds verified" + ), + reservation_id=( + str(existing["reservation_id"]) + if existing is not None + else None + ), + effective_limits=limits, + ), + envelope, + bootstrap, + ) + return ( + CapitalVerdict( + False, + reason, + "capital_limits", + effective_limits=limits, + ), + envelope, + bootstrap, + ) + + def evaluate_request( + self, + request: Any, + *, + current_daily_loss_cents: int, + current_local_total_exposure_cents: int = 0, + current_local_correlated_exposure_cents: int = 0, + current_local_open_orders: int = 0, + current_request_locally_reserved: bool = False, + ) -> CapitalVerdict: + verdict, _, _ = self._verdict( + request, + current_daily_loss_cents=current_daily_loss_cents, + current_local_total_exposure_cents=( + current_local_total_exposure_cents + ), + current_local_correlated_exposure_cents=( + current_local_correlated_exposure_cents + ), + current_local_open_orders=current_local_open_orders, + current_request_locally_reserved=( + current_request_locally_reserved + ), + ) + return verdict + + def reserve_request( + self, + request: Any, + *, + current_daily_loss_cents: int, + current_local_total_exposure_cents: int = 0, + current_local_correlated_exposure_cents: int = 0, + current_local_open_orders: int = 0, + ) -> CapitalVerdict: + verdict, envelope, bootstrap = self._verdict( + request, + current_daily_loss_cents=current_daily_loss_cents, + current_local_total_exposure_cents=( + current_local_total_exposure_cents + ), + current_local_correlated_exposure_cents=( + current_local_correlated_exposure_cents + ), + current_local_open_orders=current_local_open_orders, + ) + if ( + not verdict.allow + or envelope is None + or bootstrap is None + or verdict.reservation_id is not None + ): + return verdict + reservation_id = hashlib.sha256( + ( + f"{envelope.envelope_id}|{envelope.fencing_generation}|" + f"{request.proposal_id}" + ).encode("utf-8") + ).hexdigest() + notional_cents, fee_reserve_cents, risk_cents = ( + self._fee_inclusive_order_risk(request) + ) + payload = { + "schema": CAPITAL_RESERVATION_SCHEMA, + "reservation_id": reservation_id, + "proposal_id": str(request.proposal_id), + "envelope_id": envelope.envelope_id, + "fencing_generation": envelope.fencing_generation, + "market_ticker": str(request.market_ticker), + "contract_ticker": str(request.contract_ticker), + "correlation_key": self._correlation_key(str(request.market_ticker)), + "side": str(request.side), + "size": int(request.size), + "price_cents": int(request.price_cents), + "notional_cents": notional_cents, + "fee_reserve_cents": fee_reserve_cents, + "risk_cents": risk_cents, + "venue": envelope.venue, + "account_hash": envelope.account_hash, + "subaccount_number": self.expected_subaccount_number, + "strategy_hash": str(request.capital_strategy_hash), + "passport_hash": str(request.capital_passport_hash), + "promotion_hash": str(request.capital_promotion_hash), + "expiration_ts": ( + int(request.expiration_ts) + if getattr(request, "expiration_ts", None) is not None + else None + ), + "liquidity_role": str( + getattr(request, "liquidity_role", "maker") or "maker" + ), + "order_terms_sha256": sha256_json( + self._request_order_terms(request) + ), + } + daily_loss = _require_int( + current_daily_loss_cents, + field="current_daily_loss_cents", + minimum=0, + ) + local_total = _require_int( + current_local_total_exposure_cents, + field="current_local_total_exposure_cents", + minimum=0, + ) + local_correlated = _require_int( + current_local_correlated_exposure_cents, + field="current_local_correlated_exposure_cents", + minimum=0, + ) + local_open_orders = _require_int( + current_local_open_orders, + field="current_local_open_orders", + minimum=0, + ) + + def validate_existing(rows: tuple[dict[str, Any], ...]) -> None: + if any( + row.get("kind") == "authority.cancel_only" + for row in rows + ): + raise ValueError( + "DumbMoney adapter is in cancel-only degraded mode" + ) + accepted = [ + row + for row in rows + if row.get("kind") == "capital.envelope.accepted" + ] + highest_fence = max( + ( + int(row["payload"]["fencing_generation"]) + for row in accepted + ), + default=0, + ) + if not any( + row["payload"].get("envelope_id") + == envelope.envelope_id + and int(row["payload"]["fencing_generation"]) + == envelope.fencing_generation + for row in accepted + ): + raise ValueError("capital envelope is no longer accepted") + if highest_fence != envelope.fencing_generation: + raise ValueError("capital envelope fencing generation is stale") + now = self._now_fn() + if now.tzinfo is None or now.utcoffset() is None: + raise ValueError("reservation clock must be timezone-aware") + now = now.astimezone(timezone.utc) + if now >= envelope.expires_at: + raise ValueError("capital envelope expired before reservation") + + bootstrap_rows = [ + row + for row in rows + if row.get("kind") == "broker.bootstrap.recorded" + ] + if not bootstrap_rows: + raise ValueError("broker bootstrap receipt disappeared") + latest_bootstrap = bootstrap_rows[-1]["payload"] + observed = _parse_utc( + latest_bootstrap["observed_at"], + field="observed_at", + ) + if now - observed > self.max_bootstrap_age: + raise ValueError("broker bootstrap receipt is stale") + + created = { + str(row["payload"]["reservation_id"]): row["payload"] + for row in rows + if row.get("kind") == "capital.reservation.created" + } + released = { + str(row["payload"]["reservation_id"]) + for row in rows + if row.get("kind") == "capital.reservation.released" + } + if any( + item.get("proposal_id") == payload["proposal_id"] + for item in created.values() + ): + raise ValueError( + "proposal id already has a capital reservation" + ) + active = [ + item + for key, item in created.items() + if key not in released + ] + position_created = { + str(row["payload"]["position_exposure_id"]): row[ + "payload" + ] + for row in rows + if row.get("kind") + == "capital.position.exposure.recorded" + } + position_released = { + str(row["payload"]["position_exposure_id"]) + for row in rows + if row.get("kind") + == "capital.position.exposure.released" + } + active_positions = [ + item + for key, item in position_created.items() + if key not in position_released + ] + risk_value = payload["risk_cents"] + if isinstance(risk_value, bool) or not isinstance(risk_value, int): + raise ValueError("capital reservation risk is invalid") + order_risk = risk_value + inherited_total = int( + latest_bootstrap["total_exposure_cents"] + ) + inherited_orders = int(latest_bootstrap["open_order_count"]) + inherited_correlated = int( + latest_bootstrap["correlated_exposure_cents"].get( + payload["correlation_key"], + 0, + ) + ) + reserved_total = sum( + int(item["risk_cents"]) for item in active + ) + position_total = sum( + int(item["risk_cents"]) for item in active_positions + ) + reserved_correlated = sum( + int(item["risk_cents"]) + for item in active + if item.get("correlation_key") + == payload["correlation_key"] + ) + position_correlated = sum( + int(item["risk_cents"]) + for item in active_positions + if item.get("correlation_key") + == payload["correlation_key"] + ) + effective_open_risk = ( + inherited_total + + reserved_total + + position_total + + local_total + ) + effective_correlated_risk = ( + inherited_correlated + + reserved_correlated + + position_correlated + + local_correlated + ) + effective_open_orders = ( + inherited_orders + len(active) + local_open_orders + ) + if order_risk > envelope.max_order_risk_cents: + raise ValueError("DumbMoney maximum order risk exceeded") + if ( + effective_open_risk + order_risk + > envelope.max_open_risk_cents + ): + raise ValueError("DumbMoney maximum open risk exceeded") + if ( + effective_correlated_risk + order_risk + > envelope.max_correlated_risk_cents + ): + raise ValueError( + "DumbMoney maximum correlated risk exceeded" + ) + if ( + order_risk + > envelope.max_daily_loss_cents - daily_loss + ): + raise ValueError("DumbMoney daily loss capacity exceeded") + if effective_open_orders >= envelope.max_open_orders: + raise ValueError("DumbMoney maximum open orders reached") + + try: + self.journal.append( + "capital.reservation.created", + payload, + outbox_id=f"capital-reservation:{reservation_id}", + validate_existing=validate_existing, + ) + except ValueError as exc: + return CapitalVerdict( + False, + str(exc), + "capital_atomic_reservation", + effective_limits=verdict.effective_limits, + ) + return CapitalVerdict( + True, + "DumbMoney capital reserved durably", + reservation_id=reservation_id, + effective_limits=verdict.effective_limits, + ) + + def _reservation_for_proposal( + self, + proposal_id: str, + ) -> dict[str, Any] | None: + normalized = str(proposal_id).strip() + return next( + ( + dict(row["payload"]) + for row in self._events("capital.reservation.created") + if row["payload"].get("proposal_id") == normalized + ), + None, + ) + + def claim_broker_dispatch( + self, + request: Any, + *, + reservation_id: str, + order: Mapping[str, Any], + claimant_nonce: str, + ) -> dict[str, Any]: + """Acquire the one-shot cross-process broker-dispatch right. + + The deterministic outbox id is intentionally non-idempotent for this + operation: a prior identical claim is still a consumed authority, not + permission to retry a possibly successful broker request. + """ + nonce = _require_sha256( + claimant_nonce, + field="dispatch claimant_nonce", + ) + reservation = self._reservation_for_proposal( + str(request.proposal_id) + ) + if ( + reservation is None + or reservation.get("reservation_id") != reservation_id + ): + raise ValueError( + "dispatch claim lacks the exact capital reservation" + ) + order_value = json.loads(canonical_json(dict(order))) + expected_order = self._expected_broker_order(request) + if canonical_json(order_value) != canonical_json(expected_order): + raise ValueError( + "dispatch order differs from the exact authorized wire payload" + ) + order_digest = sha256_json(order_value) + request_terms = self._request_order_terms(request) + request_terms_digest = sha256_json(request_terms) + if reservation.get("order_terms_sha256") != request_terms_digest: + raise ValueError( + "dispatch request differs from reserved order terms" + ) + payload = { + "schema": CAPITAL_DISPATCH_CLAIM_SCHEMA, + "reservation_id": reservation_id, + "proposal_id": str(request.proposal_id), + "client_order_id": str(order_value.get("client_order_id") or ""), + "venue": self.expected_venue, + "account_hash": self.expected_account_hash, + "subaccount_number": self.expected_subaccount_number, + "envelope_id": reservation["envelope_id"], + "fencing_generation": reservation["fencing_generation"], + "strategy_hash": reservation["strategy_hash"], + "passport_hash": reservation["passport_hash"], + "promotion_hash": reservation["promotion_hash"], + "authorized_instrument": ( + f"event_contract:{request.contract_ticker}" + ), + "request_terms_sha256": request_terms_digest, + "order_sha256": order_digest, + "order": order_value, + "claimant_nonce": nonce, + } + if payload["client_order_id"] != str(request.proposal_id): + raise ValueError( + "dispatch order client_order_id differs from proposal" + ) + + def validate_existing(rows: tuple[dict[str, Any], ...]) -> None: + if any( + row.get("kind") == "authority.cancel_only" + for row in rows + ): + raise ValueError( + "DumbMoney adapter is in cancel-only degraded mode" + ) + created = { + str(row["payload"]["reservation_id"]): row["payload"] + for row in rows + if row.get("kind") == "capital.reservation.created" + } + released = { + str(row["payload"]["reservation_id"]) + for row in rows + if row.get("kind") == "capital.reservation.released" + } + if reservation_id not in created or reservation_id in released: + raise ValueError( + "dispatch capital reservation is not active" + ) + if created[reservation_id] != reservation: + raise ValueError( + "dispatch capital reservation changed" + ) + if any( + row.get("kind") == "capital.dispatch.claimed" + and ( + row["payload"].get("reservation_id") == reservation_id + or row["payload"].get("proposal_id") + == str(request.proposal_id) + ) + for row in rows + ): + raise ValueError( + "broker dispatch was already claimed" + ) + if any( + row.get("kind") + == "capital.terminal_reconciliation.witnessed" + and row["payload"].get("proposal_id") + == str(request.proposal_id) + for row in rows + ): + raise ValueError( + "terminal proposal cannot acquire dispatch" + ) + accepted = [ + row + for row in rows + if row.get("kind") == "capital.envelope.accepted" + ] + accepted_row = next( + ( + row + for row in reversed(accepted) + if row["payload"].get("envelope_id") + == reservation["envelope_id"] + and int(row["payload"]["fencing_generation"]) + == int(reservation["fencing_generation"]) + ), + None, + ) + if accepted_row is None: + raise ValueError( + "dispatch capital envelope is no longer accepted" + ) + highest_fence = max( + ( + int(row["payload"]["fencing_generation"]) + for row in accepted + ), + default=0, + ) + if highest_fence != int(reservation["fencing_generation"]): + raise ValueError( + "dispatch capital fence is stale" + ) + now = self._now_fn() + if now.tzinfo is None or now.utcoffset() is None: + raise ValueError("dispatch clock must be timezone-aware") + now = now.astimezone(timezone.utc) + verified = verify_signed_capital_envelope( + accepted_row["payload"]["wrapper"], + trusted_public_keys=self.trusted_public_keys, + expected_venue=self.expected_venue, + expected_account_hash=self.expected_account_hash, + now=now, + max_ttl=self.max_envelope_ttl, + ) + if ( + verified.envelope_id != reservation["envelope_id"] + or verified.fencing_generation + != int(reservation["fencing_generation"]) + or verified.authorized_mode != "LIVE" + or reservation["strategy_hash"] + not in verified.strategy_hashes + or reservation["passport_hash"] + not in verified.passport_hashes + or reservation["promotion_hash"] + not in verified.promotion_hashes + or payload["authorized_instrument"] + not in verified.authorized_instruments + ): + raise ValueError( + "dispatch capital envelope binding is not active" + ) + bootstrap_rows = [ + row + for row in rows + if row.get("kind") == "broker.bootstrap.recorded" + ] + if not bootstrap_rows: + raise ValueError( + "dispatch requires a broker bootstrap receipt" + ) + observed = _parse_utc( + bootstrap_rows[-1]["payload"]["observed_at"], + field="observed_at", + ) + if now - observed > self.max_bootstrap_age: + raise ValueError( + "broker bootstrap receipt expired before dispatch" + ) + + event = self.journal.append( + "capital.dispatch.claimed", + payload, + outbox_id=f"capital-dispatch:{reservation_id}", + allow_existing_outbox=False, + validate_existing=validate_existing, + ) + return dict(event) + + def pending_reconciliation_reservations( + self, + ) -> tuple[dict[str, Any], ...]: + """Expose conservative active reservations to the read-only sweeper.""" + claims = { + str(row["payload"]["reservation_id"]) + for row in self._events("capital.dispatch.claimed") + } + return tuple( + { + **item, + "dispatch_claimed": ( + str(item["reservation_id"]) in claims + ), + } + for item in self._active_reservations() + ) + + def active_position_exposures( + self, + ) -> tuple[dict[str, Any], ...]: + """Expose unsettled, independently charged capital positions.""" + return tuple(self._active_position_exposures()) + + def _verify_broker_witness( + self, + wrapper: Mapping[str, Any], + *, + expected_schema: str, + ) -> VerifiedSignedEnvelope: + if not self.trusted_broker_witness_public_keys: + raise ValueError( + "trusted broker witness identity is unavailable" + ) + signed = verify_signed_envelope( + wrapper, + trusted_public_keys=( + self.trusted_broker_witness_public_keys + ), + expected_body_schema=expected_schema, + max_ttl=WITNESS_TTL, + require_active=False, + ) + if signed.source_id != BROKER_WITNESS_SOURCE_ID: + raise ValueError("broker witness source identity mismatch") + body = signed.body + if ( + body.get("venue") != self.expected_venue + or body.get("account_hash") != self.expected_account_hash + or body.get("subaccount_number") + != self.expected_subaccount_number + ): + raise ValueError("broker witness account identity mismatch") + observed = _parse_utc( + body.get("observed_at"), + field="broker witness observed_at", + ) + if observed != signed.not_before: + raise ValueError( + "broker witness observation and signature window differ" + ) + now = self._now_fn() + if now.tzinfo is None or now.utcoffset() is None: + raise ValueError("broker witness clock must be timezone-aware") + if observed > now.astimezone(timezone.utc) + timedelta(seconds=5): + raise ValueError("broker witness is from the future") + return signed + + def verify_terminal_reconciliation_witness( + self, + wrapper: Mapping[str, Any], + ) -> dict[str, Any]: + """Verify a service-signed terminal order/fill projection.""" + signed = self._verify_broker_witness( + wrapper, + expected_schema=TERMINAL_WITNESS_SCHEMA, + ) + body = signed.body + required = { + "schema", + "witness_id", + "venue", + "account_hash", + "subaccount_number", + "reservation_id", + "proposal_id", + "order_id", + "market_ticker", + "contract_ticker", + "side", + "terminal_status", + "initial_count", + "fill_count", + "remaining_count", + "fill_cost_cents", + "fee_cents", + "average_fill_price_cents", + "fill_ids", + "observed_at", + "broker_projection_sha256", + } + if set(body) != required: + raise ValueError("terminal broker witness fields mismatch") + witness_id = _require_sha256( + body["witness_id"], + field="terminal witness_id", + ) + reservation_id = _require_sha256( + body["reservation_id"], + field="terminal reservation_id", + ) + proposal_id = _require_identifier( + body["proposal_id"], + field="terminal proposal_id", + ) + order_id = _require_identifier( + body["order_id"], + field="terminal order_id", + ) + market_ticker = _require_identifier( + body["market_ticker"], + field="terminal market_ticker", + ) + contract_ticker = _require_identifier( + body["contract_ticker"], + field="terminal contract_ticker", + ) + side = body["side"] + if side not in {"yes", "no"}: + raise ValueError("terminal witness side is invalid") + status = body["terminal_status"] + if status not in {"canceled", "executed"}: + raise ValueError("terminal witness status is invalid") + initial_count = _require_int( + body["initial_count"], + field="terminal initial_count", + minimum=1, + ) + fill_count = _require_int( + body["fill_count"], + field="terminal fill_count", + minimum=0, + ) + remaining_count = _require_int( + body["remaining_count"], + field="terminal remaining_count", + minimum=0, + ) + fill_cost = _require_int( + body["fill_cost_cents"], + field="terminal fill_cost_cents", + minimum=0, + ) + fee = _require_int( + body["fee_cents"], + field="terminal fee_cents", + minimum=0, + ) + average = body["average_fill_price_cents"] + fill_ids = body["fill_ids"] + if ( + not isinstance(fill_ids, list) + or fill_ids != sorted(set(fill_ids)) + or any( + not isinstance(item, str) or not item + for item in fill_ids + ) + ): + raise ValueError("terminal fill_ids are invalid") + if ( + fill_count > initial_count + or remaining_count != 0 + or ( + status == "executed" + and fill_count != initial_count + ) + or ( + fill_count == 0 + and ( + fill_cost != 0 + or fee != 0 + or average is not None + or fill_ids + ) + ) + or ( + fill_count > 0 + and ( + isinstance(average, bool) + or not isinstance(average, int) + or not 1 <= average <= 99 + or not fill_ids + or fill_cost < fill_count + or fill_cost > fill_count * 100 + ) + ) + ): + raise ValueError("terminal fill projection is inconsistent") + _require_sha256( + body["broker_projection_sha256"], + field="terminal broker_projection_sha256", + ) + reservation = self._reservation_for_proposal(proposal_id) + if ( + reservation is None + or reservation.get("reservation_id") != reservation_id + or reservation.get("market_ticker") != market_ticker + or reservation.get("contract_ticker") != contract_ticker + or reservation.get("side") != side + or int(reservation.get("size", -1)) != initial_count + ): + raise ValueError( + "terminal witness differs from the capital reservation" + ) + claims = [ + row + for row in self._events("capital.dispatch.claimed") + if row["payload"].get("reservation_id") == reservation_id + ] + if len(claims) != 1: + raise ValueError( + "terminal witness lacks one exact dispatch claim" + ) + if ( + claims[0]["payload"].get("proposal_id") != proposal_id + or claims[0]["payload"].get("client_order_id") + != proposal_id + ): + raise ValueError("terminal dispatch identity mismatch") + observed = _parse_utc( + body["observed_at"], + field="terminal observed_at", + ) + dispatched = _parse_utc( + claims[0]["recorded_at"], + field="dispatch recorded_at", + ) + if observed < dispatched: + raise ValueError( + "terminal witness predates broker dispatch" + ) + if fill_cost + fee > int(reservation["risk_cents"]): + raise ValueError( + "terminal filled risk exceeds the fee-inclusive reservation" + ) + return { + **json.loads(canonical_json(body)), + "signed_event_id": signed.event_id, + "signed_wrapper": signed.wrapper, + "witness_id": witness_id, + "order_id": order_id, + } + + def record_signed_terminal_reconciliation( + self, + wrapper: Mapping[str, Any], + ) -> dict[str, Any]: + """Persist terminal evidence, then move reservation risk to position.""" + witness = self.verify_terminal_reconciliation_witness(wrapper) + reservation_id = str(witness["reservation_id"]) + proposal_id = str(witness["proposal_id"]) + witness_id = str(witness["witness_id"]) + + def validate_terminal(rows: tuple[dict[str, Any], ...]) -> None: + matching = [ + row["payload"] + for row in rows + if row.get("kind") + == "capital.terminal_reconciliation.witnessed" + and row["payload"].get("proposal_id") + == proposal_id + ] + if matching and matching[-1].get("witness_id") != witness_id: + raise ValueError( + "proposal already has a conflicting terminal witness" + ) + if not any( + row.get("kind") == "capital.dispatch.claimed" + and row["payload"].get("reservation_id") + == reservation_id + for row in rows + ): + raise ValueError( + "terminal witness dispatch claim disappeared" + ) + + self.journal.append( + "capital.terminal_reconciliation.witnessed", + { + "witness_id": witness_id, + "proposal_id": proposal_id, + "reservation_id": reservation_id, + "order_id": witness["order_id"], + "terminal_status": witness["terminal_status"], + "fill_count": witness["fill_count"], + "fill_cost_cents": witness["fill_cost_cents"], + "fee_cents": witness["fee_cents"], + "observed_at": witness["observed_at"], + "signed_event_id": witness["signed_event_id"], + "signed_wrapper": witness["signed_wrapper"], + }, + outbox_id=f"capital-terminal-witness:{witness_id}", + validate_existing=validate_terminal, + ) + position: dict[str, Any] | None = None + if int(witness["fill_count"]) > 0: + reservation = self._reservation_for_proposal(proposal_id) + assert reservation is not None + position_id = sha256_json( + { + "schema": CAPITAL_POSITION_EXPOSURE_SCHEMA, + "reservation_id": reservation_id, + "order_id": witness["order_id"], + "terminal_witness_id": witness_id, + } + ) + position = { + "schema": CAPITAL_POSITION_EXPOSURE_SCHEMA, + "position_exposure_id": position_id, + "reservation_id": reservation_id, + "proposal_id": proposal_id, + "order_id": witness["order_id"], + "venue": self.expected_venue, + "account_hash": self.expected_account_hash, + "subaccount_number": self.expected_subaccount_number, + "market_ticker": reservation["market_ticker"], + "contract_ticker": reservation["contract_ticker"], + "correlation_key": reservation["correlation_key"], + "side": reservation["side"], + "fill_count": witness["fill_count"], + "fill_cost_cents": witness["fill_cost_cents"], + "fee_cents": witness["fee_cents"], + "risk_cents": ( + int(witness["fill_cost_cents"]) + + int(witness["fee_cents"]) + ), + "average_fill_price_cents": witness[ + "average_fill_price_cents" + ], + "terminal_witness_id": witness_id, + "observed_at": witness["observed_at"], + } + self.journal.append( + "capital.position.exposure.recorded", + position, + outbox_id=f"capital-position:{position_id}", + ) + + terminal_reservation = self._reservation_for_proposal(proposal_id) + if terminal_reservation is None: + raise ValueError( + "terminal capital reservation disappeared" + ) + release = { + "schema": CAPITAL_TERMINAL_RELEASE_SCHEMA, + "reservation_id": reservation_id, + "proposal_id": proposal_id, + "terminal_witness_id": witness_id, + "terminal_status": witness["terminal_status"], + "released_risk_cents": int( + terminal_reservation["risk_cents"] + ), + "position_exposure_id": ( + position["position_exposure_id"] + if position is not None + else None + ), + } + + def validate_release(rows: tuple[dict[str, Any], ...]) -> None: + if not any( + row.get("kind") + == "capital.terminal_reconciliation.witnessed" + and row["payload"].get("witness_id") == witness_id + for row in rows + ): + raise ValueError("terminal witness is not durable") + if position is not None and not any( + row.get("kind") + == "capital.position.exposure.recorded" + and row["payload"].get("position_exposure_id") + == position["position_exposure_id"] + for row in rows + ): + raise ValueError( + "filled position exposure is not durable" + ) + + self.journal.append( + "capital.reservation.released", + release, + outbox_id=f"capital-terminal-release:{reservation_id}", + validate_existing=validate_release, + ) + return { + "status": "TERMINAL_RECONCILED", + "witness_id": witness_id, + "reservation_id": reservation_id, + "position_exposure_id": ( + position["position_exposure_id"] + if position is not None + else None + ), + } + + def verify_settlement_reconciliation_witness( + self, + wrapper: Mapping[str, Any], + ) -> dict[str, Any]: + """Verify stable position absence plus a service-signed settlement.""" + signed = self._verify_broker_witness( + wrapper, + expected_schema=SETTLEMENT_WITNESS_SCHEMA, + ) + body = signed.body + required = { + "schema", + "witness_id", + "venue", + "account_hash", + "subaccount_number", + "position_exposure_id", + "reservation_id", + "proposal_id", + "contract_ticker", + "side", + "fill_count", + "market_result", + "settled_at", + "revenue_cents", + "settlement_fee_cents", + "position_absent", + "observed_at", + "broker_projection_sha256", + } + if set(body) != required: + raise ValueError("settlement broker witness fields mismatch") + witness_id = _require_sha256( + body["witness_id"], + field="settlement witness_id", + ) + position_id = _require_sha256( + body["position_exposure_id"], + field="position_exposure_id", + ) + _require_sha256( + body["reservation_id"], + field="settlement reservation_id", + ) + _require_identifier( + body["proposal_id"], + field="settlement proposal_id", + ) + _require_identifier( + body["contract_ticker"], + field="settlement contract_ticker", + ) + if body["side"] not in {"yes", "no"}: + raise ValueError("settlement side is invalid") + fill_count = _require_int( + body["fill_count"], + field="settlement fill_count", + minimum=1, + ) + if body["market_result"] not in {"yes", "no"}: + raise ValueError("settlement result is invalid") + settled = _parse_utc( + body["settled_at"], + field="settled_at", + ) + observed = _parse_utc( + body["observed_at"], + field="settlement observed_at", + ) + if settled > observed: + raise ValueError( + "settlement observation predates settlement" + ) + _require_int( + body["revenue_cents"], + field="settlement revenue_cents", + minimum=0, + ) + _require_int( + body["settlement_fee_cents"], + field="settlement fee_cents", + minimum=0, + ) + if body["position_absent"] is not True: + raise ValueError( + "settlement witness lacks stable position absence" + ) + _require_sha256( + body["broker_projection_sha256"], + field="settlement broker_projection_sha256", + ) + positions = [ + row["payload"] + for row in self._events( + "capital.position.exposure.recorded" + ) + if row["payload"].get("position_exposure_id") + == position_id + ] + if len(positions) != 1: + raise ValueError( + "settlement witness lacks one exact capital position" + ) + position = positions[0] + position_observed = _parse_utc( + position.get("observed_at"), + field="capital position observed_at", + ) + if ( + position.get("reservation_id") != body["reservation_id"] + or position.get("proposal_id") != body["proposal_id"] + or position.get("contract_ticker") + != body["contract_ticker"] + or position.get("side") != body["side"] + or int(position.get("fill_count", -1)) != fill_count + ): + raise ValueError( + "settlement witness differs from capital position" + ) + if settled < position_observed: + raise ValueError( + "settlement predates the capital position exposure" + ) + return { + **json.loads(canonical_json(body)), + "signed_event_id": signed.event_id, + "signed_wrapper": signed.wrapper, + "witness_id": witness_id, + } + + def record_signed_settlement_reconciliation( + self, + wrapper: Mapping[str, Any], + ) -> dict[str, Any]: + """Persist settlement evidence before releasing position exposure.""" + witness = self.verify_settlement_reconciliation_witness(wrapper) + witness_id = str(witness["witness_id"]) + position_id = str(witness["position_exposure_id"]) + self.journal.append( + "capital.settlement_reconciliation.witnessed", + { + "witness_id": witness_id, + "position_exposure_id": position_id, + "proposal_id": witness["proposal_id"], + "market_result": witness["market_result"], + "settled_at": witness["settled_at"], + "position_absent": True, + "signed_event_id": witness["signed_event_id"], + "signed_wrapper": witness["signed_wrapper"], + }, + outbox_id=f"capital-settlement-witness:{witness_id}", + ) + position = next( + item + for item in self._events( + "capital.position.exposure.recorded" + ) + if item["payload"].get("position_exposure_id") + == position_id + )["payload"] + release = { + "schema": CAPITAL_POSITION_RELEASE_SCHEMA, + "position_exposure_id": position_id, + "reservation_id": position["reservation_id"], + "proposal_id": position["proposal_id"], + "settlement_witness_id": witness_id, + "released_risk_cents": position["risk_cents"], + "market_result": witness["market_result"], + "settled_at": witness["settled_at"], + } + + def validate_release(rows: tuple[dict[str, Any], ...]) -> None: + if not any( + row.get("kind") + == "capital.settlement_reconciliation.witnessed" + and row["payload"].get("witness_id") == witness_id + for row in rows + ): + raise ValueError("settlement witness is not durable") + + self.journal.append( + "capital.position.exposure.released", + release, + outbox_id=f"capital-position-release:{position_id}", + validate_existing=validate_release, + ) + return { + "status": "POSITION_SETTLED", + "witness_id": witness_id, + "position_exposure_id": position_id, + } + + def release_after_local_reservation_failure( + self, + request: Any, + *, + reservation_id: str, + reason: str, + ) -> dict[str, Any]: + """Retain capital until a sealed broker/local CAS witness exists. + + A caller assertion that transport did not occur is not proof across + process crashes. This recovery surface is intentionally disabled; the + central firewall already retains the reservation on local failure. + """ + del request, reservation_id, reason + raise ValueError( + "local-failure capital release is disabled pending sealed CAS proof" + ) + + def release_from_terminal_reconciliation( + self, + *, + proposal_id: str, + terminal_status: str, + reconciliation_receipt: Mapping[str, Any], + ) -> dict[str, Any] | None: + """Reject caller-authored terminal facts until broker truth is sealed.""" + del proposal_id, terminal_status, reconciliation_receipt + raise ValueError( + "terminal capital release requires a sealed broker witness" + ) + + def enter_cancel_only( + self, + *, + reason: str, + kill_asserted_at: str, + cancel_authorized: bool = False, + cancel_authority_receipt: str | None = None, + ) -> dict[str, Any]: + """Disable submissions without inventing cancellation authority.""" + _parse_utc(kill_asserted_at, field="kill_asserted_at") + authority_receipt = ( + str(cancel_authority_receipt).strip() + if cancel_authority_receipt is not None + else "" + ) + payload = { + "reason": _require_text(reason, field="reason"), + "kill_asserted_at": kill_asserted_at, + "submission_authority": False, + "cancel_authority": False, + "cancel_authority_receipt": None, + "broker_contacted": False, + "status": "CANCEL_AUTHORITY_REQUIRED", + } + event = self.journal.append("authority.cancel_only", payload) + if cancel_authorized or authority_receipt: + raise ValueError( + "cancellation authority requires a verified broker " + "capability; opaque receipt strings are not authority" + ) + return dict(event) + + +__all__ = [ + "CAPITAL_ENVELOPE_SCHEMA", + "CAPITAL_DISPATCH_CLAIM_SCHEMA", + "CAPITAL_RESERVATION_SCHEMA", + "CAPITAL_POSITION_EXPOSURE_SCHEMA", + "CAPITAL_TERMINAL_RELEASE_SCHEMA", + "CAPITAL_POSITION_RELEASE_SCHEMA", + "CapitalEnvelopeAdapter", + "CapitalLineageResolver", + "CapitalVerdict", + "FLAT_BOOTSTRAP_SCHEMA", + "INHERITED_BOOTSTRAP_SCHEMA", + "SIGNED_ENVELOPE_SCHEMA", + "VerifiedCapitalEnvelope", + "VerifiedSignedEnvelope", + "flat_book_receipt", + "inherited_exposure_receipt", + "strategy_binding_hash", + "verify_signed_capital_envelope", + "verify_signed_envelope", +] diff --git a/live_firewall/dumbmoney_command_feed.py b/live_firewall/dumbmoney_command_feed.py new file mode 100644 index 00000000..5fd6e1bb --- /dev/null +++ b/live_firewall/dumbmoney_command_feed.py @@ -0,0 +1,1722 @@ +"""Local-only DumbMoney Core command-feed consumer. + +The consumer has no scheduler, background thread, HTTP client, or broker +interface. A supervised local runner injects one loopback GET transport and +calls :meth:`CoreCommandFeedConsumer.poll_once` explicitly. Every cursor and +its resulting control state are persisted together in the hash-chained, +fsynced operational journal. + +Positive authority is never dispatched through callbacks. Signed LIVE and +kill-clear state becomes visible only through the durable snapshot; injected +handlers are invoked solely for authority-reducing kill/paused controls. +""" +from __future__ import annotations + +import base64 +import hashlib +import json +import re +import secrets +from collections.abc import Callable, Mapping +from copy import deepcopy +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from typing import Any, Protocol, cast + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + +from live_firewall.dumbmoney_capital import ( + CAPITAL_ENVELOPE_SCHEMA, + CapitalEnvelopeAdapter, + VerifiedCapitalEnvelope, + VerifiedSignedEnvelope, + verify_signed_capital_envelope, + verify_signed_envelope, +) +from live_firewall.operational_journal import ( + AppendOnlyOperationalJournal, + canonical_json, + sha256_json, +) + + +CELL_ID = "dummy_kalshi" +PAGE_SCHEMA = "dumbmoney.cell-command-page.v1" +CHECKPOINT_SCHEMA = "dumbmoney.cell-command-checkpoint.v1" +STATE_SCHEMA = "dummy.dumbmoney-command-feed-state.v1" +STATE_EVENT_KIND = "dumbmoney.command-feed.state" +KILL_STATE_SCHEMA = "dumbmoney.kill-state.v1" +DESIRED_MODE_SCHEMA = "dumbmoney.desired-mode.v1" +ZERO_DIGEST = "0" * 64 + +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$") +_RFC3339_UTC_RE = re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$" +) +_PAGE_FIELDS = { + "schema", + "cell_id", + "request_nonce", + "observed_at", + "after_sequence", + "after_digest", + "next_sequence", + "next_digest", + "ledger_head_sequence", + "ledger_head_digest", + "has_more", + "required_action", + "commands", + "checkpoint", + "checkpoint_signature", +} +_COMMAND_FIELDS = { + "global_sequence", + "event_id", + "event_digest", + "body_schema", + "valid_now", + "transport_window_current", + "authority_effect", + "ledger_proof", + "envelope", +} +_CHECKPOINT_FIELDS = { + "schema", + "cell_id", + "request_nonce", + "after_sequence", + "after_digest", + "ordered_commands", + "next_sequence", + "next_digest", + "ledger_head_sequence", + "ledger_head_digest", + "observed_at", + "required_action", +} +_CHECKPOINT_COMMAND_FIELDS = _COMMAND_FIELDS - {"envelope"} +_CHECKPOINT_SIGNATURE_FIELDS = {"algorithm", "signer_key_id", "signature"} +_LEDGER_PROOF_FIELDS = { + "schema", + "global_sequence", + "event_id", + "source_id", + "source_sequence", + "signer_key_id", + "nonce", + "event_schema", + "observed_at", + "received_at", + "correlation_id", + "causation_id", + "payload_digest", + "previous_source_digest", + "previous_global_digest", + "event_digest", +} +_LEDGER_PROOF_SCHEMA = "dumbmoney.ledger-event-proof.v1" +_STATE_FIELDS = { + "schema", + "cell_id", + "cursor", + "status", + "reason", + "policy_epoch", + "kill_generation_high_water", + "mode_revision_high_water", + "capital_fencing_high_water", + "kill_envelope", + "desired_mode_envelope", + "required_action", + "updated_at", +} +_ACTIONS = { + "APPLY_SIGNED_CONTROLS", + "PAUSE_NEW_RISK", + "CANCEL_AND_RECONCILE", +} +_MODES = {"READ_ONLY", "PAPER", "LIVE", "PAUSED"} +_AUTHORITY_EFFECTS = { + "APPLY_FAIL_CLOSED", + "APPLY_POSITIVE", + "HISTORICAL_ONLY", +} +_CORE_COMMAND_FUTURE_SKEW = timedelta(seconds=5) +_SUPPORTED_BODY_SCHEMAS = { + KILL_STATE_SCHEMA, + DESIRED_MODE_SCHEMA, + CAPITAL_ENVELOPE_SCHEMA, +} + + +class CommandFeedError(RuntimeError): + """A feed, cursor, signature, or dispatch failure that removes authority.""" + + +@dataclass(frozen=True) +class CommandFeedResponse: + """One injected loopback GET response.""" + + status_code: int + body: bytes + + +class CommandFeedTransport(Protocol): + def __call__( + self, + path: str, + headers: Mapping[str, str], + ) -> CommandFeedResponse: ... + + +@dataclass(frozen=True) +class VerifiedFeedCommand: + global_sequence: int + event_digest: str + envelope: VerifiedSignedEnvelope + ledger_proof: dict[str, Any] + capital: VerifiedCapitalEnvelope | None = None + authority_current: bool = False + authority_current_at_core: bool = False + narrows_authority: bool = False + + @property + def body(self) -> dict[str, Any]: + return deepcopy(self.envelope.body) + + +@dataclass(frozen=True) +class CommandFeedPollResult: + page_accepted: bool + cursor_sequence: int + cursor_digest: str + commands_applied: int + required_action: str | None + submission_allowed: bool + reason: str + + +def _utc_now() -> datetime: + return datetime.now(timezone.utc) + + +def _format_utc(value: datetime) -> str: + if value.tzinfo is None or value.utcoffset() is None: + raise ValueError("timestamp must be timezone-aware") + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _parse_utc(value: Any, *, field: str) -> datetime: + if not isinstance(value, str) or not _RFC3339_UTC_RE.fullmatch(value): + raise CommandFeedError(f"{field} must be canonical RFC3339 UTC") + parsed = datetime.fromisoformat(value[:-1] + "+00:00") + return parsed.astimezone(timezone.utc) + + +def _require_int(value: Any, *, field: str, minimum: int = 0) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise CommandFeedError(f"{field} must be an integer >= {minimum}") + return value + + +def _require_digest(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not _SHA256_RE.fullmatch(value): + raise CommandFeedError(f"{field} must be a lowercase sha256") + return value + + +def _require_identifier(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not _IDENTIFIER_RE.fullmatch(value): + raise CommandFeedError(f"{field} must be a canonical identifier") + return value + + +def _decode_base64url_signature(value: Any) -> bytes: + if ( + not isinstance(value, str) + or not value + or "=" in value + or re.search(r"[^A-Za-z0-9_-]", value) + ): + raise CommandFeedError( + "checkpoint signature must be unpadded base64url" + ) + try: + decoded = base64.urlsafe_b64decode( + value + "=" * (-len(value) % 4) + ) + except Exception as exc: + raise CommandFeedError("checkpoint signature is invalid base64url") from exc + canonical = ( + base64.urlsafe_b64encode(decoded).decode("ascii").rstrip("=") + ) + if canonical != value or len(decoded) != 64: + raise CommandFeedError("checkpoint signature encoding is invalid") + return decoded + + +def _public_key_bytes(value: bytes | Ed25519PublicKey) -> bytes: + if isinstance(value, Ed25519PublicKey): + return bytes(value.public_bytes_raw()) + raw = bytes(value) + if len(raw) != 32: + raise CommandFeedError("checkpoint Ed25519 public key must be 32 bytes") + return raw + + +def _strict_json(raw: bytes, *, maximum_bytes: int) -> dict[str, Any]: + if not isinstance(raw, bytes): + raise CommandFeedError("command feed response body must be bytes") + if len(raw) > maximum_bytes: + raise CommandFeedError("command feed response exceeds maximum size") + + def pairs_hook(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + value: dict[str, Any] = {} + for key, item in pairs: + if key in value: + raise CommandFeedError("command feed JSON contains duplicate keys") + value[key] = item + return value + + def reject_constant(value: str) -> None: + raise CommandFeedError( + f"command feed JSON contains non-finite constant {value}" + ) + + try: + parsed = json.loads( + raw.decode("utf-8"), + object_pairs_hook=pairs_hook, + parse_constant=reject_constant, + ) + except CommandFeedError: + raise + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise CommandFeedError("command feed response is not strict UTF-8 JSON") from exc + if not isinstance(parsed, dict): + raise CommandFeedError("command feed response must be an object") + return parsed + + +def _validate_kill_body( + body: Mapping[str, Any], + *, + now: datetime, + require_current: bool, + future_skew: timedelta, +) -> dict[str, Any]: + required = { + "schema", + "active", + "generation", + "reason", + "changed_by", + "policy_epoch", + "changed_at", + } + if set(body) != required or body.get("schema") != KILL_STATE_SCHEMA: + raise CommandFeedError("kill-state body fields mismatch") + if not isinstance(body["active"], bool): + raise CommandFeedError("kill-state active must be boolean") + _require_int(body["generation"], field="kill-state generation", minimum=1) + if not isinstance(body["reason"], str) or not body["reason"].strip(): + raise CommandFeedError("kill-state reason must be nonempty") + _require_identifier(body["changed_by"], field="kill-state changed_by") + _require_int(body["policy_epoch"], field="kill-state policy_epoch", minimum=1) + changed_at = _parse_utc(body["changed_at"], field="kill-state changed_at") + if require_current and changed_at > now + future_skew: + raise CommandFeedError("kill-state changed_at is from the future") + return cast( + dict[str, Any], + json.loads(canonical_json(dict(body))), + ) + + +def _validate_mode_body( + body: Mapping[str, Any], + *, + now: datetime, + require_current: bool, +) -> dict[str, Any]: + required = { + "schema", + "venue", + "mode", + "revision", + "reason", + "policy_epoch", + "not_before", + "expires_at", + } + if set(body) != required or body.get("schema") != DESIRED_MODE_SCHEMA: + raise CommandFeedError("desired-mode body fields mismatch") + if body["venue"] != CELL_ID: + raise CommandFeedError("desired-mode venue mismatch") + if body["mode"] not in _MODES: + raise CommandFeedError("desired-mode value is unsupported") + _require_int(body["revision"], field="desired-mode revision", minimum=1) + if not isinstance(body["reason"], str) or not body["reason"].strip(): + raise CommandFeedError("desired-mode reason must be nonempty") + _require_int(body["policy_epoch"], field="desired-mode policy_epoch", minimum=1) + not_before = _parse_utc( + body["not_before"], + field="desired-mode not_before", + ) + expires_at = _parse_utc( + body["expires_at"], + field="desired-mode expires_at", + ) + if expires_at <= not_before: + raise CommandFeedError("desired-mode authority window is invalid") + if require_current: + if now < not_before: + raise CommandFeedError("desired-mode is not active yet") + if now >= expires_at: + raise CommandFeedError("desired-mode expired") + return cast( + dict[str, Any], + json.loads(canonical_json(dict(body))), + ) + + +class CoreCommandFeedConsumer: + """Consume one authenticated Core page without any automatic polling.""" + + def __init__( + self, + *, + transport: CommandFeedTransport, + cell_token_provider: Callable[[], str], + state_journal: AppendOnlyOperationalJournal, + trusted_operator_public_keys: Mapping[ + str, + bytes | Ed25519PublicKey, + ], + trusted_checkpoint_public_keys: Mapping[ + str, + bytes | Ed25519PublicKey, + ], + capital_adapter: CapitalEnvelopeAdapter, + kill_handler: Callable[[VerifiedFeedCommand], None], + mode_handler: Callable[[VerifiedFeedCommand], None], + fail_closed_handler: Callable[[str], None], + now_fn: Callable[[], datetime] | None = None, + request_nonce_fn: Callable[[], str] | None = None, + page_limit: int = 250, + maximum_response_bytes: int = 1_048_576, + maximum_page_age: timedelta = timedelta(seconds=60), + maximum_future_skew: timedelta = timedelta(seconds=5), + ) -> None: + if not callable(transport): + raise TypeError("transport must be callable") + if not callable(cell_token_provider): + raise TypeError("cell_token_provider must be callable") + if not callable(kill_handler) or not callable(mode_handler): + raise TypeError("authority-reducing handlers must be callable") + if not callable(fail_closed_handler): + raise TypeError("fail_closed_handler must be callable") + if request_nonce_fn is not None and not callable(request_nonce_fn): + raise TypeError("request_nonce_fn must be callable") + if isinstance(page_limit, bool) or not 1 <= page_limit <= 1000: + raise ValueError("page_limit must be between 1 and 1000") + if maximum_response_bytes < 1: + raise ValueError("maximum_response_bytes must be positive") + if maximum_page_age <= timedelta(0): + raise ValueError("maximum_page_age must be positive") + if maximum_future_skew < timedelta(0): + raise ValueError("maximum_future_skew cannot be negative") + if capital_adapter.expected_venue != CELL_ID: + raise ValueError("capital adapter must be pinned to dummy_kalshi") + if not trusted_checkpoint_public_keys: + raise ValueError("at least one pinned Core checkpoint key is required") + if not trusted_operator_public_keys: + raise ValueError( + "at least one pinned operator command key is required" + ) + checkpoint_keys: dict[str, bytes] = {} + for key_id, key_material in trusted_checkpoint_public_keys.items(): + normalized_key_id = _require_digest( + key_id, + field="checkpoint signer key id", + ) + raw_key = _public_key_bytes(key_material) + if hashlib.sha256(raw_key).hexdigest() != normalized_key_id: + raise ValueError( + "checkpoint signer key id does not match public key" + ) + checkpoint_keys[normalized_key_id] = raw_key + operator_keys: dict[str, bytes] = {} + for key_id, key_material in trusted_operator_public_keys.items(): + normalized_key_id = _require_digest( + key_id, + field="operator signer key id", + ) + raw_key = _public_key_bytes(key_material) + if hashlib.sha256(raw_key).hexdigest() != normalized_key_id: + raise ValueError( + "operator signer key id does not match public key" + ) + operator_keys[normalized_key_id] = raw_key + if set(checkpoint_keys) & set(operator_keys): + raise ValueError( + "Core checkpoint and operator command keys must be disjoint" + ) + if not set(checkpoint_keys) <= set( + capital_adapter.trusted_public_keys + ): + raise ValueError( + "capital adapter must trust every pinned Core checkpoint key" + ) + + self._transport = transport + self._cell_token_provider = cell_token_provider + self._journal = state_journal + self._trusted_operator_public_keys = operator_keys + self._trusted_checkpoint_public_keys = checkpoint_keys + self._capital_adapter = capital_adapter + self._kill_handler = kill_handler + self._mode_handler = mode_handler + self._fail_closed_handler = fail_closed_handler + self._now_fn = now_fn or _utc_now + self._request_nonce_fn = request_nonce_fn or ( + lambda: secrets.token_hex(32) + ) + self._last_request_nonce: str | None = None + self.page_limit = page_limit + self.maximum_response_bytes = maximum_response_bytes + self.maximum_page_age = maximum_page_age + self.maximum_future_skew = maximum_future_skew + + def _now(self) -> datetime: + now = self._now_fn() + if now.tzinfo is None or now.utcoffset() is None: + raise CommandFeedError("command-feed clock must be timezone-aware") + return now.astimezone(timezone.utc) + + def _bootstrap_state(self, now: datetime) -> dict[str, Any]: + return { + "schema": STATE_SCHEMA, + "cell_id": CELL_ID, + "cursor": {"sequence": 0, "digest": ZERO_DIGEST}, + "status": "FAIL_CLOSED", + "reason": "explicit bootstrap cursor has not been polled", + "policy_epoch": 0, + "kill_generation_high_water": 0, + "mode_revision_high_water": 0, + "capital_fencing_high_water": 0, + "kill_envelope": None, + "desired_mode_envelope": None, + "required_action": None, + "updated_at": _format_utc(now), + } + + def _validate_stored_state( + self, + raw: Mapping[str, Any], + *, + now: datetime, + ) -> dict[str, Any]: + state = json.loads(canonical_json(dict(raw))) + if set(state) != _STATE_FIELDS: + raise CommandFeedError("persisted command-feed state fields mismatch") + if state["schema"] != STATE_SCHEMA or state["cell_id"] != CELL_ID: + raise CommandFeedError("persisted command-feed state identity mismatch") + cursor = state["cursor"] + if not isinstance(cursor, dict) or set(cursor) != {"sequence", "digest"}: + raise CommandFeedError("persisted command-feed cursor fields mismatch") + _require_int(cursor["sequence"], field="cursor sequence") + _require_digest(cursor["digest"], field="cursor digest") + if cursor["sequence"] == 0 and cursor["digest"] != ZERO_DIGEST: + raise CommandFeedError("bootstrap cursor digest mismatch") + if cursor["sequence"] > 0 and cursor["digest"] == ZERO_DIGEST: + raise CommandFeedError("non-bootstrap cursor cannot use zero digest") + if state["status"] not in {"READY", "CATCHING_UP", "FAIL_CLOSED"}: + raise CommandFeedError("persisted command-feed status is unsupported") + if not isinstance(state["reason"], str) or not state["reason"].strip(): + raise CommandFeedError("persisted command-feed reason is missing") + _require_int(state["policy_epoch"], field="persisted policy_epoch") + _require_int( + state["kill_generation_high_water"], + field="persisted kill generation high-water", + ) + _require_int( + state["mode_revision_high_water"], + field="persisted mode revision high-water", + ) + _require_int( + state["capital_fencing_high_water"], + field="persisted capital fence high-water", + ) + if state["required_action"] is not None: + if state["required_action"] not in _ACTIONS: + raise CommandFeedError("persisted required_action is unsupported") + _parse_utc(state["updated_at"], field="persisted updated_at") + + kill = state["kill_envelope"] + if kill is not None: + signed = verify_signed_envelope( + kill, + trusted_public_keys=self._trusted_operator_public_keys, + expected_body_schema=KILL_STATE_SCHEMA, + require_active=False, + ) + _validate_kill_body( + signed.body, + now=now, + require_current=False, + future_skew=self.maximum_future_skew, + ) + if ( + int(signed.body["generation"]) + > int(state["kill_generation_high_water"]) + ): + raise CommandFeedError( + "persisted kill generation exceeds high-water" + ) + mode = state["desired_mode_envelope"] + if mode is not None: + signed = verify_signed_envelope( + mode, + trusted_public_keys=self._trusted_operator_public_keys, + expected_body_schema=DESIRED_MODE_SCHEMA, + require_active=False, + ) + _validate_mode_body( + signed.body, + now=now, + require_current=False, + ) + if ( + int(signed.body["revision"]) + > int(state["mode_revision_high_water"]) + ): + raise CommandFeedError( + "persisted mode revision exceeds high-water" + ) + return cast(dict[str, Any], state) + + def _load_state(self, *, now: datetime) -> dict[str, Any]: + rows = self._journal.events(kind=STATE_EVENT_KIND) + if not rows: + return self._bootstrap_state(now) + return self._validate_stored_state(rows[-1]["payload"], now=now) + + @staticmethod + def _cursor(state: Mapping[str, Any]) -> tuple[int, str]: + cursor = state["cursor"] + return int(cursor["sequence"]), str(cursor["digest"]) + + def snapshot(self) -> dict[str, Any]: + """Return the latest durable state; an invalid journal raises closed.""" + return deepcopy(self._load_state(now=self._now())) + + def submission_allowed(self) -> bool: + """Return only durable, current mode/kill authority. + + Capital, Dummy-local risk, and broker/session gates remain independent + mandatory checks at the execution sink. + """ + try: + now = self._now() + state = self._load_state(now=now) + return ( + state["status"] == "READY" + and self._state_action(state, now=now) + == "APPLY_SIGNED_CONTROLS" + ) + except Exception: + return False + + def _request_path( + self, + sequence: int, + digest: str, + request_nonce: str, + ) -> str: + return ( + f"/v1/cells/{CELL_ID}/commands" + f"?after={sequence}&cursor={digest}" + f"&request_nonce={request_nonce}&limit={self.page_limit}" + ) + + def _verify_checkpoint( + self, + checkpoint_raw: Any, + signature_raw: Any, + ) -> dict[str, Any]: + if not isinstance(checkpoint_raw, dict): + raise CommandFeedError("command checkpoint must be an object") + if not isinstance(signature_raw, dict): + raise CommandFeedError( + "command checkpoint signature must be an object" + ) + checkpoint = json.loads(canonical_json(checkpoint_raw)) + signature = json.loads(canonical_json(signature_raw)) + if ( + set(checkpoint) != _CHECKPOINT_FIELDS + or checkpoint.get("schema") != CHECKPOINT_SCHEMA + ): + raise CommandFeedError("command checkpoint fields mismatch") + if set(signature) != _CHECKPOINT_SIGNATURE_FIELDS: + raise CommandFeedError( + "command checkpoint signature fields mismatch" + ) + if signature["algorithm"] != "Ed25519": + raise CommandFeedError( + "command checkpoint signature algorithm mismatch" + ) + signer_key_id = _require_digest( + signature["signer_key_id"], + field="checkpoint signer_key_id", + ) + public_key = self._trusted_checkpoint_public_keys.get(signer_key_id) + if public_key is None: + raise CommandFeedError("command checkpoint signer is not pinned") + encoded_signature = _decode_base64url_signature( + signature["signature"] + ) + try: + Ed25519PublicKey.from_public_bytes(public_key).verify( + encoded_signature, + canonical_json(checkpoint).encode("utf-8"), + ) + except (InvalidSignature, ValueError) as exc: + raise CommandFeedError( + "command checkpoint signature is invalid" + ) from exc + return cast(dict[str, Any], checkpoint) + + @staticmethod + def _validate_ledger_proof( + proof_raw: Any, + *, + sequence: int, + event_id: str, + event_digest: str, + body_schema: str, + envelope: VerifiedSignedEnvelope, + page_observed_at: datetime, + ) -> dict[str, Any]: + if not isinstance(proof_raw, dict): + raise CommandFeedError("command ledger_proof must be an object") + proof = json.loads(canonical_json(proof_raw)) + if ( + set(proof) != _LEDGER_PROOF_FIELDS + or proof.get("schema") != _LEDGER_PROOF_SCHEMA + ): + raise CommandFeedError("command ledger_proof fields mismatch") + proof_sequence = _require_int( + proof["global_sequence"], + field="ledger proof global_sequence", + minimum=1, + ) + proof_event_id = _require_digest( + proof["event_id"], + field="ledger proof event_id", + ) + proof_event_digest = _require_digest( + proof["event_digest"], + field="ledger proof event_digest", + ) + proof_source_id = _require_identifier( + proof["source_id"], + field="ledger proof source_id", + ) + proof_source_sequence = _require_int( + proof["source_sequence"], + field="ledger proof source_sequence", + minimum=1, + ) + proof_signer = _require_digest( + proof["signer_key_id"], + field="ledger proof signer_key_id", + ) + proof_nonce = _require_identifier( + proof["nonce"], + field="ledger proof nonce", + ) + proof_schema = _require_identifier( + proof["event_schema"], + field="ledger proof event_schema", + ) + proof_correlation = _require_identifier( + proof["correlation_id"], + field="ledger proof correlation_id", + ) + proof_causation = proof["causation_id"] + if proof_causation is not None: + _require_digest( + proof_causation, + field="ledger proof causation_id", + ) + payload_digest = _require_digest( + proof["payload_digest"], + field="ledger proof payload_digest", + ) + previous_source_digest = _require_digest( + proof["previous_source_digest"], + field="ledger proof previous_source_digest", + ) + previous_global_digest = _require_digest( + proof["previous_global_digest"], + field="ledger proof previous_global_digest", + ) + record_observed_at = _parse_utc( + proof["observed_at"], + field="ledger proof observed_at", + ) + received_at = _parse_utc( + proof["received_at"], + field="ledger proof received_at", + ) + if received_at > page_observed_at: + raise CommandFeedError( + "ledger proof was received after page observation" + ) + if record_observed_at != max(received_at, envelope.not_before): + raise CommandFeedError( + "ledger proof observed_at differs from ingestion semantics" + ) + if proof_sequence == 1 and previous_global_digest != ZERO_DIGEST: + raise CommandFeedError( + "first ledger event previous_global_digest mismatch" + ) + if proof_sequence > 1 and previous_global_digest == ZERO_DIGEST: + raise CommandFeedError( + "noninitial ledger event has zero previous_global_digest" + ) + if proof_source_sequence == 1 and previous_source_digest != ZERO_DIGEST: + raise CommandFeedError( + "first source event previous_source_digest mismatch" + ) + if proof_source_sequence > 1 and previous_source_digest == ZERO_DIGEST: + raise CommandFeedError( + "noninitial source event has zero previous_source_digest" + ) + + wrapper = envelope.wrapper + duplicated = { + "global_sequence": (proof_sequence, sequence), + "event_id": (proof_event_id, event_id), + "event_digest": (proof_event_digest, event_digest), + "source_id": (proof_source_id, envelope.source_id), + "source_sequence": ( + proof_source_sequence, + envelope.source_sequence, + ), + "signer_key_id": (proof_signer, envelope.signer_key_id), + "nonce": (proof_nonce, envelope.nonce), + "event_schema": (proof_schema, body_schema), + "correlation_id": ( + proof_correlation, + wrapper["correlation_id"], + ), + "causation_id": ( + proof_causation, + wrapper["causation_id"], + ), + "payload_digest": ( + payload_digest, + wrapper["body_digest"], + ), + } + for field, (proof_value, expected_value) in duplicated.items(): + if proof_value != expected_value: + raise CommandFeedError( + f"ledger proof {field} differs from command envelope" + ) + + event_material = { + key: value + for key, value in proof.items() + if key not in {"schema", "event_digest"} + } + if sha256_json(event_material) != proof_event_digest: + raise CommandFeedError( + "ledger proof event_digest does not match event material" + ) + return cast(dict[str, Any], proof) + + def _decode_command( + self, + raw: Any, + *, + now: datetime, + observed_at: datetime, + after_sequence: int, + next_sequence: int, + ) -> VerifiedFeedCommand: + if not isinstance(raw, dict) or set(raw) != _COMMAND_FIELDS: + raise CommandFeedError("command record fields mismatch") + sequence = _require_int( + raw["global_sequence"], + field="command global_sequence", + minimum=1, + ) + if not after_sequence < sequence <= next_sequence: + raise CommandFeedError("command sequence falls outside page cursor range") + event_id = _require_digest(raw["event_id"], field="command event_id") + event_digest = _require_digest( + raw["event_digest"], + field="command event_digest", + ) + body_schema = _require_identifier( + raw["body_schema"], + field="command body_schema", + ) + if body_schema not in _SUPPORTED_BODY_SCHEMAS: + raise CommandFeedError("command body schema is unsupported") + if not isinstance(raw["valid_now"], bool): + raise CommandFeedError("command valid_now must be boolean") + if not isinstance(raw["transport_window_current"], bool): + raise CommandFeedError( + "command transport_window_current must be boolean" + ) + if raw["authority_effect"] not in _AUTHORITY_EFFECTS: + raise CommandFeedError("command authority_effect is unsupported") + envelope_raw = raw["envelope"] + if not isinstance(envelope_raw, dict): + raise CommandFeedError("command envelope must be an object") + try: + command_keys = ( + self._trusted_checkpoint_public_keys + if body_schema == CAPITAL_ENVELOPE_SCHEMA + else self._trusted_operator_public_keys + ) + signed = verify_signed_envelope( + envelope_raw, + trusted_public_keys=command_keys, + expected_body_schema=body_schema, + require_active=False, + ) + except ValueError as exc: + raise CommandFeedError(f"signed command invalid: {exc}") from exc + if signed.event_id != event_id: + raise CommandFeedError("command event_id differs from signed envelope") + ledger_proof = self._validate_ledger_proof( + raw["ledger_proof"], + sequence=sequence, + event_id=event_id, + event_digest=event_digest, + body_schema=body_schema, + envelope=signed, + page_observed_at=observed_at, + ) + core_window_active = ( + signed.not_before <= observed_at < signed.expires_at + ) + if raw["transport_window_current"] is not core_window_active: + raise CommandFeedError( + "transport_window_current differs from signed envelope" + ) + local_window_active = signed.not_before <= now < signed.expires_at + + capital: VerifiedCapitalEnvelope | None = None + narrows_authority = False + body_current_at_core = True + body_current_locally = True + if body_schema == KILL_STATE_SCHEMA: + _validate_kill_body( + signed.body, + now=now, + require_current=False, + future_skew=self.maximum_future_skew, + ) + narrows_authority = signed.body["active"] is True + changed_at = _parse_utc( + signed.body["changed_at"], + field="kill-state changed_at", + ) + body_current_at_core = ( + changed_at <= observed_at + _CORE_COMMAND_FUTURE_SKEW + ) + body_current_locally = ( + changed_at <= now + _CORE_COMMAND_FUTURE_SKEW + ) + elif body_schema == DESIRED_MODE_SCHEMA: + _validate_mode_body( + signed.body, + now=now, + require_current=False, + ) + narrows_authority = signed.body["mode"] != "LIVE" + body_not_before = _parse_utc( + signed.body["not_before"], + field="desired-mode not_before", + ) + body_expires_at = _parse_utc( + signed.body["expires_at"], + field="desired-mode expires_at", + ) + body_current_at_core = ( + body_not_before <= observed_at < body_expires_at + ) + body_current_locally = ( + body_not_before <= now < body_expires_at + ) + else: + try: + capital = verify_signed_capital_envelope( + envelope_raw, + trusted_public_keys=self._trusted_checkpoint_public_keys, + expected_venue=CELL_ID, + expected_account_hash=( + self._capital_adapter.expected_account_hash + ), + now=now, + max_ttl=self._capital_adapter.max_envelope_ttl, + require_active=False, + ) + except ValueError as exc: + raise CommandFeedError( + f"signed capital command invalid: {exc}" + ) from exc + body_current_at_core = ( + capital.not_before <= observed_at < capital.expires_at + ) + body_current_locally = ( + capital.not_before <= now < capital.expires_at + ) + valid_at_core = core_window_active and body_current_at_core + if raw["valid_now"] is not valid_at_core: + raise CommandFeedError( + "command valid_now differs from signed authority state" + ) + positive_grant = not narrows_authority + expected_effect = ( + "APPLY_FAIL_CLOSED" + if not positive_grant + else "APPLY_POSITIVE" + if valid_at_core + else "HISTORICAL_ONLY" + ) + if raw["authority_effect"] != expected_effect: + raise CommandFeedError( + "command authority_effect differs from signed authority state" + ) + authority_current = ( + local_window_active and body_current_locally + ) + return VerifiedFeedCommand( + global_sequence=sequence, + event_digest=event_digest, + envelope=signed, + ledger_proof=ledger_proof, + capital=capital, + authority_current=authority_current, + authority_current_at_core=valid_at_core, + narrows_authority=narrows_authority, + ) + + def _decode_page( + self, + response: CommandFeedResponse, + *, + state: Mapping[str, Any], + now: datetime, + expected_request_nonce: str, + ) -> tuple[dict[str, Any], list[VerifiedFeedCommand]]: + if isinstance(response.status_code, bool) or not isinstance( + response.status_code, + int, + ): + raise CommandFeedError("command feed response status is invalid") + if response.status_code in {401, 403}: + raise CommandFeedError("cell bearer authentication failed") + if response.status_code == 409: + raise CommandFeedError("Core rejected the persisted cursor") + if response.status_code != 200: + raise CommandFeedError( + f"command feed returned HTTP {response.status_code}" + ) + page = _strict_json( + response.body, + maximum_bytes=self.maximum_response_bytes, + ) + if set(page) != _PAGE_FIELDS: + raise CommandFeedError("command page fields mismatch") + if page["schema"] != PAGE_SCHEMA: + raise CommandFeedError("command page schema mismatch") + + # The checkpoint authenticates every authority-bearing projection + # except the independently signed command envelopes. Verify it before + # trusting cursors, action, timing, or command metadata. + checkpoint = self._verify_checkpoint( + page["checkpoint"], + page["checkpoint_signature"], + ) + mirrored_fields = { + "cell_id", + "request_nonce", + "observed_at", + "after_sequence", + "after_digest", + "next_sequence", + "next_digest", + "ledger_head_sequence", + "ledger_head_digest", + "required_action", + } + for field in mirrored_fields: + if canonical_json(page[field]) != canonical_json(checkpoint[field]): + raise CommandFeedError( + f"command page {field} differs from signed checkpoint" + ) + if checkpoint["cell_id"] != CELL_ID: + raise CommandFeedError("command page identity mismatch") + request_nonce = _require_digest( + checkpoint["request_nonce"], + field="checkpoint request_nonce", + ) + if request_nonce != expected_request_nonce: + raise CommandFeedError( + "checkpoint request_nonce differs from current request" + ) + + if not isinstance(page["commands"], list): + raise CommandFeedError("command page commands must be an array") + ordered_commands = checkpoint["ordered_commands"] + if not isinstance(ordered_commands, list): + raise CommandFeedError( + "checkpoint ordered_commands must be an array" + ) + if len(ordered_commands) != len(page["commands"]): + raise CommandFeedError( + "checkpoint command count differs from page" + ) + for raw_command, projected in zip( + page["commands"], + ordered_commands, + strict=True, + ): + if not isinstance(raw_command, dict): + raise CommandFeedError("command record must be an object") + if ( + not isinstance(projected, dict) + or set(projected) != _CHECKPOINT_COMMAND_FIELDS + ): + raise CommandFeedError( + "checkpoint command projection fields mismatch" + ) + projection = { + key: value + for key, value in raw_command.items() + if key != "envelope" + } + if canonical_json(projection) != canonical_json(projected): + raise CommandFeedError( + "page command differs from signed checkpoint projection" + ) + + observed_at = _parse_utc(page["observed_at"], field="page observed_at") + if observed_at > now + self.maximum_future_skew: + raise CommandFeedError("command page observation is from the future") + if now - observed_at > self.maximum_page_age: + raise CommandFeedError("command page observation is stale") + + current_sequence, current_digest = self._cursor(state) + after_sequence = _require_int( + page["after_sequence"], + field="page after_sequence", + ) + after_digest = _require_digest( + page["after_digest"], + field="page after_digest", + ) + if (after_sequence, after_digest) != ( + current_sequence, + current_digest, + ): + raise CommandFeedError("command page does not continue persisted cursor") + next_sequence = _require_int( + page["next_sequence"], + field="page next_sequence", + ) + next_digest = _require_digest( + page["next_digest"], + field="page next_digest", + ) + ledger_head = _require_int( + page["ledger_head_sequence"], + field="page ledger_head_sequence", + ) + ledger_head_digest = _require_digest( + page["ledger_head_digest"], + field="page ledger_head_digest", + ) + if not after_sequence <= next_sequence <= ledger_head: + raise CommandFeedError("command page cursor range is invalid") + if after_sequence == 0 and after_digest != ZERO_DIGEST: + raise CommandFeedError("bootstrap cursor digest mismatch") + if after_sequence > 0 and after_digest == ZERO_DIGEST: + raise CommandFeedError("non-bootstrap cursor has zero digest") + if ledger_head == 0 and ledger_head_digest != ZERO_DIGEST: + raise CommandFeedError("empty ledger head digest mismatch") + if ledger_head > 0 and ledger_head_digest == ZERO_DIGEST: + raise CommandFeedError("nonempty ledger head has zero digest") + if ledger_head == next_sequence and ledger_head_digest != next_digest: + raise CommandFeedError( + "page cursor digest differs from ledger head" + ) + if next_sequence - after_sequence > self.page_limit: + raise CommandFeedError("command page contains a ledger sequence gap") + if next_sequence == after_sequence and next_digest != after_digest: + raise CommandFeedError("stationary cursor digest mismatch") + if next_sequence > after_sequence and next_digest == after_digest: + raise CommandFeedError("advancing cursor digest did not change") + if next_sequence > 0 and next_digest == ZERO_DIGEST: + raise CommandFeedError("advancing cursor cannot use zero digest") + if page["has_more"] is not (next_sequence < ledger_head): + raise CommandFeedError("command page has_more is inconsistent") + if page["required_action"] not in _ACTIONS: + raise CommandFeedError("command page required_action is unsupported") + if len(page["commands"]) > self.page_limit: + raise CommandFeedError("command page exceeds requested limit") + + commands = [ + self._decode_command( + item, + now=now, + observed_at=observed_at, + after_sequence=after_sequence, + next_sequence=next_sequence, + ) + for item in page["commands"] + ] + sequences = [command.global_sequence for command in commands] + if sequences != sorted(set(sequences)): + raise CommandFeedError("command sequences are not strictly increasing") + previous_command: VerifiedFeedCommand | None = None + latest_source_command: dict[ + str, + VerifiedFeedCommand, + ] = {} + for command in commands: + proof = command.ledger_proof + if ( + previous_command is None + and command.global_sequence == after_sequence + 1 + and proof["previous_global_digest"] != after_digest + ): + raise CommandFeedError( + "first command does not extend persisted cursor" + ) + if ( + previous_command is not None + and command.global_sequence + == previous_command.global_sequence + 1 + and proof["previous_global_digest"] + != previous_command.event_digest + ): + raise CommandFeedError( + "adjacent command ledger chain is discontinuous" + ) + prior_source = latest_source_command.get(command.envelope.source_id) + if prior_source is not None: + current_source_sequence = command.envelope.source_sequence + prior_source_sequence = prior_source.envelope.source_sequence + if current_source_sequence <= prior_source_sequence: + raise CommandFeedError( + "command source sequence is not increasing" + ) + if ( + current_source_sequence == prior_source_sequence + 1 + and proof["previous_source_digest"] + != prior_source.event_digest + ): + raise CommandFeedError( + "adjacent source command chain is discontinuous" + ) + latest_source_command[command.envelope.source_id] = command + previous_command = command + if next_sequence == after_sequence and commands: + raise CommandFeedError("stationary command page cannot contain commands") + last_at_cursor = next( + ( + command + for command in reversed(commands) + if command.global_sequence == next_sequence + ), + None, + ) + if last_at_cursor is not None and last_at_cursor.event_digest != next_digest: + raise CommandFeedError("command event digest differs from page cursor") + return page, commands + + def _stage_commands( + self, + state: Mapping[str, Any], + commands: list[VerifiedFeedCommand], + *, + now: datetime, + use_core_authority: bool = False, + ) -> tuple[dict[str, Any], int, bool]: + staged = deepcopy(dict(state)) + policy_epoch = int(staged["policy_epoch"]) + applied_count = 0 + ignored_positive = False + for command in commands: + authority_current = ( + command.authority_current_at_core + if use_core_authority + else command.authority_current + ) + body = command.envelope.body + command_epoch = _require_int( + body["policy_epoch"], + field="command policy_epoch", + minimum=1, + ) + if command_epoch < policy_epoch: + raise CommandFeedError("command policy epoch regressed") + policy_epoch = max(policy_epoch, command_epoch) + if command.envelope.body_schema == KILL_STATE_SCHEMA: + generation = int(body["generation"]) + high_water = int(staged["kill_generation_high_water"]) + if generation <= high_water: + raise CommandFeedError( + "kill-state generation did not advance" + ) + staged["kill_generation_high_water"] = generation + if command.narrows_authority or authority_current: + staged["kill_envelope"] = command.envelope.wrapper + applied_count += 1 + else: + ignored_positive = True + elif command.envelope.body_schema == DESIRED_MODE_SCHEMA: + revision = int(body["revision"]) + high_water = int(staged["mode_revision_high_water"]) + if revision <= high_water: + raise CommandFeedError( + "desired-mode revision did not advance" + ) + staged["mode_revision_high_water"] = revision + if command.narrows_authority or authority_current: + staged["desired_mode_envelope"] = command.envelope.wrapper + applied_count += 1 + else: + ignored_positive = True + else: + if command.capital is None: + raise CommandFeedError( + "capital command lacks verified body" + ) + fence = command.capital.fencing_generation + high_water = int(staged["capital_fencing_high_water"]) + if fence <= high_water: + raise CommandFeedError( + "capital fencing generation did not advance" + ) + staged["capital_fencing_high_water"] = fence + if authority_current: + applied_count += 1 + else: + ignored_positive = True + staged["policy_epoch"] = policy_epoch + staged["updated_at"] = _format_utc(now) + return staged, applied_count, ignored_positive + + def _state_action( + self, + state: Mapping[str, Any], + *, + now: datetime, + ) -> str: + kill = state["kill_envelope"] + mode = state["desired_mode_envelope"] + if kill is not None and kill["body"]["active"] is True: + return "CANCEL_AND_RECONCILE" + if mode is None: + return "PAUSE_NEW_RISK" + if mode["body"]["mode"] != "LIVE": + return "PAUSE_NEW_RISK" + mode_not_before = _parse_utc( + mode["not_before"], + field="desired-mode wrapper not_before", + ) + mode_expires_at = _parse_utc( + mode["expires_at"], + field="desired-mode wrapper expires_at", + ) + if not mode_not_before <= now < mode_expires_at: + return "PAUSE_NEW_RISK" + try: + _validate_mode_body( + mode["body"], + now=now, + require_current=True, + ) + except CommandFeedError: + return "PAUSE_NEW_RISK" + if kill is None or kill["body"]["active"] is not False: + return "PAUSE_NEW_RISK" + kill_not_before = _parse_utc( + kill["not_before"], + field="kill-state wrapper not_before", + ) + kill_expires_at = _parse_utc( + kill["expires_at"], + field="kill-state wrapper expires_at", + ) + if not kill_not_before <= now < kill_expires_at: + return "PAUSE_NEW_RISK" + _validate_kill_body( + kill["body"], + now=now, + require_current=True, + future_skew=self.maximum_future_skew, + ) + return "APPLY_SIGNED_CONTROLS" + + def _persist_state( + self, + state: Mapping[str, Any], + *, + expected_cursor: tuple[int, str], + ) -> None: + next_cursor = self._cursor(state) + + def validate_existing(rows: tuple[dict[str, Any], ...]) -> None: + prior_states = [ + row + for row in rows + if row.get("kind") == STATE_EVENT_KIND + ] + actual_cursor = ( + (0, ZERO_DIGEST) + if not prior_states + else self._cursor(prior_states[-1]["payload"]) + ) + if actual_cursor != expected_cursor: + raise CommandFeedError( + "command-feed cursor changed during poll" + ) + if next_cursor[0] < actual_cursor[0]: + raise CommandFeedError("command-feed cursor cannot regress") + if ( + next_cursor[0] == actual_cursor[0] + and next_cursor[1] != actual_cursor[1] + ): + raise CommandFeedError( + "stationary command-feed cursor digest changed" + ) + + self._journal.append( + STATE_EVENT_KIND, + dict(state), + validate_existing=validate_existing, + validate_existing_latest_kinds=(STATE_EVENT_KIND,), + ) + + def _fail_closed( + self, + *, + state: Mapping[str, Any], + reason: str, + now: datetime, + ) -> CommandFeedPollResult: + safe_reason = str(reason).strip()[:512] or "command feed failed closed" + try: + self._fail_closed_handler(safe_reason) + except Exception as exc: + safe_reason = ( + f"{safe_reason}; fail-closed handler failed:{type(exc).__name__}" + )[:512] + failed = deepcopy(dict(state)) + failed["status"] = "FAIL_CLOSED" + failed["reason"] = safe_reason + failed["updated_at"] = _format_utc(now) + try: + self._persist_state( + failed, + expected_cursor=self._cursor(state), + ) + except Exception as exc: + safe_reason = ( + f"{safe_reason}; state persistence failed:{type(exc).__name__}" + )[:512] + sequence, digest = self._cursor(state) + return CommandFeedPollResult( + page_accepted=False, + cursor_sequence=sequence, + cursor_digest=digest, + commands_applied=0, + required_action=state.get("required_action"), + submission_allowed=False, + reason=safe_reason, + ) + + def poll_once(self) -> CommandFeedPollResult: + """Serialize and apply at most one page across local runner processes.""" + try: + with self._journal.serialized_operation(): + return self._poll_once_serialized() + except Exception as exc: + try: + self._fail_closed_handler( + f"command operation lock failed:{type(exc).__name__}" + ) + except Exception: + pass + return CommandFeedPollResult( + page_accepted=False, + cursor_sequence=0, + cursor_digest=ZERO_DIGEST, + commands_applied=0, + required_action=None, + submission_allowed=False, + reason=( + f"command operation lock failed:{type(exc).__name__}" + ), + ) + + def _poll_once_serialized(self) -> CommandFeedPollResult: + """Fetch, verify, dispatch, and durably advance at most one page. + + There are no retries. The injected transport is called exactly once, + and no method in this module can submit or cancel a broker order. + """ + now = self._now() + try: + state = self._load_state(now=now) + except Exception as exc: + bootstrap = self._bootstrap_state(now) + return self._fail_closed( + state=bootstrap, + reason=f"persisted cursor unavailable:{type(exc).__name__}", + now=now, + ) + + try: + token = self._cell_token_provider() + except Exception as exc: + return self._fail_closed( + state=state, + reason=f"cell token unavailable:{type(exc).__name__}", + now=now, + ) + if ( + not isinstance(token, str) + or not token + or token != token.strip() + or "\r" in token + or "\n" in token + ): + return self._fail_closed( + state=state, + reason="cell token is not a canonical secret", + now=now, + ) + + sequence, digest = self._cursor(state) + try: + request_nonce = self._request_nonce_fn() + except Exception as exc: + return self._fail_closed( + state=state, + reason=f"request nonce unavailable:{type(exc).__name__}", + now=now, + ) + try: + request_nonce = _require_digest( + request_nonce, + field="request nonce", + ) + except CommandFeedError as exc: + return self._fail_closed( + state=state, + reason=str(exc), + now=now, + ) + if request_nonce == self._last_request_nonce: + return self._fail_closed( + state=state, + reason="request nonce generator repeated a nonce", + now=now, + ) + self._last_request_nonce = request_nonce + path = self._request_path(sequence, digest, request_nonce) + try: + response = self._transport( + path, + {"Authorization": f"Bearer {token}"}, + ) + except Exception as exc: + return self._fail_closed( + state=state, + reason=f"command feed transport failed:{type(exc).__name__}", + now=now, + ) + finally: + token = "" + if not isinstance(response, CommandFeedResponse): + return self._fail_closed( + state=state, + reason="command feed transport returned an unsupported response", + now=now, + ) + + try: + received_now = self._now() + page, commands = self._decode_page( + response, + state=state, + now=received_now, + expected_request_nonce=request_nonce, + ) + page_observed_at = _parse_utc( + page["observed_at"], + field="page observed_at", + ) + core_staged, _, _ = self._stage_commands( + state, + commands, + now=page_observed_at, + use_core_authority=True, + ) + expected_core_action = self._state_action( + core_staged, + now=page_observed_at, + ) + if ( + page["has_more"] is False + and page["required_action"] != expected_core_action + ): + raise CommandFeedError( + "Core required_action differs from verified local state" + ) + staged, applied_count, ignored_positive = self._stage_commands( + state, + commands, + now=received_now, + ) + + # Only narrowing transitions leave the durable feed snapshot. + # Inactive kill and LIVE mode never clear a local latch here. + # Apply every reduction before exposing any positive capital from + # the same page. + for command in commands: + if ( + command.envelope.body_schema == KILL_STATE_SCHEMA + and command.envelope.body["active"] is True + ): + self._kill_handler(command) + elif ( + command.envelope.body_schema == DESIRED_MODE_SCHEMA + and command.envelope.body["mode"] != "LIVE" + ): + self._mode_handler(command) + + # Capital acceptance is a local ceiling only. Replays caused by a + # crash before cursor persistence are exact-event idempotent. + for command in commands: + if ( + command.envelope.body_schema == CAPITAL_ENVELOPE_SCHEMA + and command.authority_current + ): + self._capital_adapter.accept_signed_envelope( + command.envelope.wrapper, + ledger_event_digest=command.event_digest, + ledger_global_sequence=command.global_sequence, + ) + + staged["cursor"] = { + "sequence": page["next_sequence"], + "digest": page["next_digest"], + } + staged["required_action"] = page["required_action"] + commit_now = self._now() + if ignored_positive: + staged["status"] = "FAIL_CLOSED" + staged["reason"] = ( + "expired or future positive authority was ignored" + ) + self._fail_closed_handler(staged["reason"]) + submission_allowed = False + elif page["has_more"] is True: + staged["status"] = "CATCHING_UP" + staged["reason"] = "verified command backlog remains" + self._fail_closed_handler(staged["reason"]) + submission_allowed = False + else: + expected_action = self._state_action( + staged, + now=commit_now, + ) + submission_allowed = ( + expected_action == "APPLY_SIGNED_CONTROLS" + ) + staged["status"] = ( + "READY" if submission_allowed else "FAIL_CLOSED" + ) + staged["reason"] = ( + "signed controls verified and cursor synchronized" + if submission_allowed + else f"local controls require {expected_action}" + ) + if not submission_allowed: + self._fail_closed_handler(staged["reason"]) + staged["updated_at"] = _format_utc(commit_now) + self._persist_state( + staged, + expected_cursor=self._cursor(state), + ) + post_commit_now = self._now() + if ( + submission_allowed + and self._state_action(staged, now=post_commit_now) + != "APPLY_SIGNED_CONTROLS" + ): + downgraded = deepcopy(staged) + downgraded["status"] = "FAIL_CLOSED" + downgraded["reason"] = ( + "positive authority expired during state commit" + ) + downgraded["updated_at"] = _format_utc(post_commit_now) + self._fail_closed_handler(downgraded["reason"]) + self._persist_state( + downgraded, + expected_cursor=self._cursor(staged), + ) + staged = downgraded + submission_allowed = False + except Exception as exc: + try: + failure_now = self._now() + except Exception: + failure_now = now + return self._fail_closed( + state=state, + reason=f"command page rejected:{type(exc).__name__}:{exc}", + now=failure_now, + ) + + next_sequence, next_digest = self._cursor(staged) + return CommandFeedPollResult( + page_accepted=True, + cursor_sequence=next_sequence, + cursor_digest=next_digest, + commands_applied=applied_count, + required_action=page["required_action"], + submission_allowed=submission_allowed, + reason=str(staged["reason"]), + ) + + +__all__ = [ + "CELL_ID", + "CommandFeedError", + "CommandFeedPollResult", + "CommandFeedResponse", + "CoreCommandFeedConsumer", + "DESIRED_MODE_SCHEMA", + "KILL_STATE_SCHEMA", + "PAGE_SCHEMA", + "STATE_EVENT_KIND", + "STATE_SCHEMA", + "VerifiedFeedCommand", + "ZERO_DIGEST", +] diff --git a/live_firewall/dumbmoney_execution_cycle.py b/live_firewall/dumbmoney_execution_cycle.py new file mode 100644 index 00000000..58847efd --- /dev/null +++ b/live_firewall/dumbmoney_execution_cycle.py @@ -0,0 +1,73 @@ +"""Sealed execution-cycle boundary with no order-submission capability.""" + +from __future__ import annotations + +import re +from collections.abc import Mapping +from typing import Any + +from live_firewall.dumbmoney_capital import CapitalEnvelopeAdapter +from live_firewall.dumbmoney_command_feed import CoreCommandFeedConsumer + + +EXECUTION_CYCLE_SCHEMA = "dummy.dumbmoney-execution-cycle.v1" +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_BROKER_TRUTH_FIELDS = { + "schema", + "venue", + "account_hash", + "subaccount_number", + "observed_at", + "broker_snapshot_sha256", + "flat_book_observed", + "total_exposure_cents", + "open_order_count", + "market_exposure_cents", + "correlated_exposure_cents", + "unresolved_open_orders", + "unresolved_positions", +} + + +class SealedDisabledExecutionCycle: + """Exercise the execution boundary without owning a broker write sink. + + This object deliberately accepts no broker client, signing key, or callback. + It proves the production service is wired through the exact execution-cycle + interface while remaining structurally unable to submit an order. + """ + + submission_capable = False + deployment_gate = "ATTENDED_LIVE_CANARY_REQUIRED" + + def __call__( + self, + *, + capital_adapter: CapitalEnvelopeAdapter, + command_feed: CoreCommandFeedConsumer, + broker_snapshot: Mapping[str, Any], + ) -> Mapping[str, Any]: + del capital_adapter, command_feed + if ( + not isinstance(broker_snapshot, Mapping) + or set(broker_snapshot) != _BROKER_TRUTH_FIELDS + or broker_snapshot.get("schema") + != "dummy.kalshi-broker-truth.v1" + ): + raise ValueError("broker truth snapshot fields mismatch") + digest = broker_snapshot.get("broker_snapshot_sha256") + if not isinstance(digest, str) or not _SHA256_RE.fullmatch(digest): + raise ValueError("broker truth snapshot digest is invalid") + return { + "schema": EXECUTION_CYCLE_SCHEMA, + "status": "BLOCKED", + "broker_contacted": False, + "orders_submitted": 0, + "broker_snapshot_sha256": digest, + } + + +__all__ = [ + "EXECUTION_CYCLE_SCHEMA", + "SealedDisabledExecutionCycle", +] diff --git a/live_firewall/dumbmoney_journal_anchor.py b/live_firewall/dumbmoney_journal_anchor.py new file mode 100644 index 00000000..227e9b88 --- /dev/null +++ b/live_firewall/dumbmoney_journal_anchor.py @@ -0,0 +1,638 @@ +"""Core-backed rollback detection for Dummy's venue-local state stores.""" + +from __future__ import annotations + +import base64 +import hashlib +import json +import math +import re +import secrets +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from typing import Any, Protocol, cast + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PublicKey, +) + +from live_firewall.dumbmoney_capital import verify_signed_envelope +from live_firewall.operational_journal import canonical_json, sha256_json + + +CELL_ID = "dummy_kalshi" +ANCHOR_REQUEST_SCHEMA = "dumbmoney.cell-journal-anchor-request.v1" +ANCHOR_RESPONSE_SCHEMA = "dumbmoney.cell-journal-anchor-response.v1" +ANCHOR_CHECKPOINT_SCHEMA = "dumbmoney.cell-journal-anchor-checkpoint.v1" +ANCHOR_BODY_SCHEMA = "dumbmoney.cell-journal-head-anchor.v1" +LEDGER_PROOF_SCHEMA = "dumbmoney.ledger-event-proof.v1" +CORE_SOURCE_ID = "dumbmoney-core" +MAXIMUM_RESPONSE_BYTES = 524_288 +ZERO_DIGEST = "0" * 64 + +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$") +_RFC3339_UTC_RE = re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$" +) +_RESPONSE_FIELDS = { + "schema", + "cell_id", + "request_nonce", + "observed_at", + "reused", + "anchor_envelope", + "ledger_proof", + "checkpoint", + "checkpoint_signature", +} +_CHECKPOINT_FIELDS = { + "schema", + "cell_id", + "request_nonce", + "account_hash", + "journal_name", + "journal_schema", + "journal_stream_id", + "journal_sequence", + "journal_head_sha256", + "anchor_body_digest", + "anchor_event_digest", + "reused", + "observed_at", +} +_SIGNATURE_FIELDS = {"algorithm", "signer_key_id", "signature"} +_ANCHOR_BODY_FIELDS = { + "schema", + "anchor_id", + "venue", + "account_hash", + "journal_name", + "journal_schema", + "journal_stream_id", + "journal_sequence", + "journal_head_sha256", + "previous_anchor_body_digest", + "anchored_at", +} +_LEDGER_PROOF_FIELDS = { + "schema", + "global_sequence", + "event_id", + "source_id", + "source_sequence", + "signer_key_id", + "nonce", + "event_schema", + "observed_at", + "received_at", + "correlation_id", + "causation_id", + "payload_digest", + "previous_source_digest", + "previous_global_digest", + "event_digest", +} + + +class JournalAnchorError(RuntimeError): + """A Core anchor, signature, or monotonicity check failed closed.""" + + +@dataclass(frozen=True) +class JournalAnchorResponse: + status_code: int + body: bytes + + +class JournalAnchorTransport(Protocol): + def __call__( + self, + path: str, + headers: Mapping[str, str], + body: bytes, + ) -> JournalAnchorResponse: ... + + +@dataclass(frozen=True) +class VerifiedJournalAnchor: + journal_name: str + journal_sequence: int + journal_head_sha256: str + anchor_body_digest: str + anchor_event_digest: str + reused: bool + observed_at: str + + +def _utc_now() -> datetime: + return datetime.now(timezone.utc) + + +def _parse_utc(value: Any, *, field: str) -> datetime: + if not isinstance(value, str) or not _RFC3339_UTC_RE.fullmatch(value): + raise JournalAnchorError( + f"{field} must be canonical RFC3339 UTC" + ) + return datetime.fromisoformat(value[:-1] + "+00:00").astimezone( + timezone.utc + ) + + +def _digest(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not _SHA256_RE.fullmatch(value): + raise JournalAnchorError(f"{field} must be a lowercase sha256") + return value + + +def _identifier(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not _IDENTIFIER_RE.fullmatch(value): + raise JournalAnchorError(f"{field} must be a canonical identifier") + return value + + +def _integer(value: Any, *, field: str, minimum: int = 0) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise JournalAnchorError( + f"{field} must be an integer >= {minimum}" + ) + return value + + +def _strict_json(body: bytes) -> dict[str, Any]: + if not isinstance(body, bytes) or len(body) > MAXIMUM_RESPONSE_BYTES: + raise JournalAnchorError("Core anchor response size is invalid") + + def pairs(values: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in values: + if key in result: + raise JournalAnchorError( + "Core anchor response contains duplicate keys" + ) + result[key] = value + return result + + def constant(token: str) -> None: + raise JournalAnchorError( + f"Core anchor response contains non-finite value {token}" + ) + + def finite_float(token: str) -> float: + value = float(token) + if not math.isfinite(value): + constant(token) + return value + + try: + decoded = body.decode("utf-8", errors="strict") + value = json.loads( + decoded, + object_pairs_hook=pairs, + parse_constant=constant, + parse_float=finite_float, + ) + except JournalAnchorError: + raise + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise JournalAnchorError( + "Core anchor response is not strict UTF-8 JSON" + ) from exc + if not isinstance(value, dict): + raise JournalAnchorError("Core anchor response must be an object") + return cast(dict[str, Any], value) + + +def _decode_signature(value: Any) -> bytes: + if ( + not isinstance(value, str) + or not value + or "=" in value + or re.search(r"[^A-Za-z0-9_-]", value) + ): + raise JournalAnchorError("anchor checkpoint signature is invalid") + try: + decoded = base64.urlsafe_b64decode( + value + "=" * (-len(value) % 4) + ) + except Exception as exc: + raise JournalAnchorError( + "anchor checkpoint signature is invalid" + ) from exc + if ( + len(decoded) != 64 + or base64.urlsafe_b64encode(decoded) + .decode("ascii") + .rstrip("=") + != value + ): + raise JournalAnchorError( + "anchor checkpoint signature encoding is invalid" + ) + return decoded + + +class CoreJournalAnchorClient: + """Publish exact local heads and verify Core's durable signed receipt.""" + + def __init__( + self, + *, + transport: JournalAnchorTransport, + cell_token_provider: Callable[[], str], + trusted_core_public_keys: Mapping[str, bytes | Ed25519PublicKey], + expected_account_hash: str, + request_nonce_fn: Callable[[], str] = lambda: secrets.token_hex(32), + now_fn: Callable[[], datetime] = _utc_now, + ) -> None: + if not trusted_core_public_keys: + raise ValueError("trusted Core public keys are required") + self._transport = transport + self._cell_token_provider = cell_token_provider + self._trusted_core_public_keys = dict(trusted_core_public_keys) + self._expected_account_hash = _digest( + expected_account_hash, + field="expected_account_hash", + ) + self._request_nonce_fn = request_nonce_fn + self._now_fn = now_fn + self._last_nonce: str | None = None + + def stream_id(self, journal_name: str) -> str: + normalized = _identifier(journal_name, field="journal_name") + return sha256_json( + { + "schema": "dummy.journal-anchor-stream.v1", + "cell_id": CELL_ID, + "account_hash": self._expected_account_hash, + "journal_name": normalized, + } + ) + + def _verify_checkpoint( + self, + checkpoint: Any, + signature: Any, + ) -> dict[str, Any]: + if not isinstance(checkpoint, dict) or set(checkpoint) != ( + _CHECKPOINT_FIELDS + ): + raise JournalAnchorError( + "Core anchor checkpoint fields mismatch" + ) + if not isinstance(signature, dict) or set(signature) != ( + _SIGNATURE_FIELDS + ): + raise JournalAnchorError( + "Core anchor checkpoint signature fields mismatch" + ) + if signature["algorithm"] != "Ed25519": + raise JournalAnchorError( + "Core anchor checkpoint algorithm mismatch" + ) + signer = _digest( + signature["signer_key_id"], + field="checkpoint signer_key_id", + ) + key = self._trusted_core_public_keys.get(signer) + if key is None: + raise JournalAnchorError( + "Core anchor checkpoint signer is not pinned" + ) + raw_key = ( + key.public_bytes_raw() + if isinstance(key, Ed25519PublicKey) + else bytes(key) + ) + if len(raw_key) != 32 or hashlib.sha256(raw_key).hexdigest() != signer: + raise JournalAnchorError( + "Core anchor checkpoint key identity mismatch" + ) + try: + Ed25519PublicKey.from_public_bytes(raw_key).verify( + _decode_signature(signature["signature"]), + canonical_json(checkpoint).encode("utf-8"), + ) + except (InvalidSignature, ValueError) as exc: + raise JournalAnchorError( + "Core anchor checkpoint signature is invalid" + ) from exc + return cast(dict[str, Any], checkpoint) + + @staticmethod + def _verify_ledger_proof( + proof_raw: Any, + *, + signed: Any, + observed_at: datetime, + ) -> dict[str, Any]: + if not isinstance(proof_raw, dict): + raise JournalAnchorError("anchor ledger proof must be an object") + proof = json.loads(canonical_json(proof_raw)) + if ( + set(proof) != _LEDGER_PROOF_FIELDS + or proof.get("schema") != LEDGER_PROOF_SCHEMA + ): + raise JournalAnchorError("anchor ledger proof fields mismatch") + sequence = _integer( + proof["global_sequence"], + field="ledger proof global_sequence", + minimum=1, + ) + source_sequence = _integer( + proof["source_sequence"], + field="ledger proof source_sequence", + minimum=1, + ) + received_at = _parse_utc( + proof["received_at"], + field="ledger proof received_at", + ) + record_observed = _parse_utc( + proof["observed_at"], + field="ledger proof observed_at", + ) + if received_at > observed_at: + raise JournalAnchorError( + "anchor ledger proof postdates the checkpoint" + ) + if record_observed != max(received_at, signed.not_before): + raise JournalAnchorError( + "anchor ledger proof observation semantics differ" + ) + previous_global = _digest( + proof["previous_global_digest"], + field="ledger proof previous_global_digest", + ) + previous_source = _digest( + proof["previous_source_digest"], + field="ledger proof previous_source_digest", + ) + if (sequence == 1) != (previous_global == ZERO_DIGEST): + raise JournalAnchorError( + "anchor ledger proof global predecessor is invalid" + ) + if (source_sequence == 1) != (previous_source == ZERO_DIGEST): + raise JournalAnchorError( + "anchor ledger proof source predecessor is invalid" + ) + wrapper = signed.wrapper + expected = { + "event_id": signed.event_id, + "source_id": signed.source_id, + "source_sequence": signed.source_sequence, + "signer_key_id": signed.signer_key_id, + "nonce": signed.nonce, + "event_schema": signed.body_schema, + "correlation_id": wrapper["correlation_id"], + "causation_id": wrapper["causation_id"], + "payload_digest": wrapper["body_digest"], + } + for field, expected_value in expected.items(): + if proof.get(field) != expected_value: + raise JournalAnchorError( + f"anchor ledger proof {field} differs from envelope" + ) + event_digest = _digest( + proof["event_digest"], + field="ledger proof event_digest", + ) + event_material = { + key: value + for key, value in proof.items() + if key not in {"schema", "event_digest"} + } + if sha256_json(event_material) != event_digest: + raise JournalAnchorError( + "anchor ledger proof digest is invalid" + ) + return cast(dict[str, Any], proof) + + def anchor( + self, + *, + journal_name: str, + journal_schema: str, + journal_sequence: int, + journal_head_sha256: str, + ) -> VerifiedJournalAnchor: + name = _identifier(journal_name, field="journal_name") + schema = _identifier(journal_schema, field="journal_schema") + sequence = _integer( + journal_sequence, + field="journal_sequence", + ) + head = _digest( + journal_head_sha256, + field="journal_head_sha256", + ) + if (sequence == 0) != (head == ZERO_DIGEST): + raise JournalAnchorError( + "journal sequence zero must exactly bind the zero head" + ) + stream_id = self.stream_id(name) + nonce = _digest( + self._request_nonce_fn(), + field="request_nonce", + ) + if nonce == self._last_nonce: + raise JournalAnchorError( + "journal anchor nonce generator repeated a nonce" + ) + self._last_nonce = nonce + request = { + "schema": ANCHOR_REQUEST_SCHEMA, + "cell_id": CELL_ID, + "account_hash": self._expected_account_hash, + "journal_name": name, + "journal_schema": schema, + "journal_stream_id": stream_id, + "journal_sequence": sequence, + "journal_head_sha256": head, + "request_nonce": nonce, + } + token = self._cell_token_provider() + if ( + not isinstance(token, str) + or not token + or token != token.strip() + or any(character.isspace() for character in token) + ): + raise JournalAnchorError( + "cell token is not a canonical secret" + ) + try: + response = self._transport( + f"/v1/cells/{CELL_ID}/journal-heads:anchor", + { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + }, + canonical_json(request).encode("utf-8"), + ) + except Exception as exc: + raise JournalAnchorError( + f"journal anchor transport failed:{type(exc).__name__}" + ) from exc + finally: + token = "" + if ( + not isinstance(response, JournalAnchorResponse) + or response.status_code != 200 + ): + raise JournalAnchorError( + "Core rejected the journal head anchor" + ) + payload = _strict_json(response.body) + if ( + set(payload) != _RESPONSE_FIELDS + or payload.get("schema") != ANCHOR_RESPONSE_SCHEMA + or payload.get("cell_id") != CELL_ID + or payload.get("request_nonce") != nonce + or not isinstance(payload.get("reused"), bool) + ): + raise JournalAnchorError( + "Core anchor response fields or identity mismatch" + ) + checkpoint = self._verify_checkpoint( + payload["checkpoint"], + payload["checkpoint_signature"], + ) + observed_at = _parse_utc( + payload["observed_at"], + field="anchor response observed_at", + ) + now = self._now_fn() + if now.tzinfo is None or now.utcoffset() is None: + raise JournalAnchorError( + "journal anchor clock must be timezone-aware" + ) + now = now.astimezone(timezone.utc) + if ( + observed_at > now + timedelta(seconds=5) + or now - observed_at > timedelta(seconds=30) + ): + raise JournalAnchorError( + "Core anchor response is not current" + ) + expected_checkpoint = { + "schema": ANCHOR_CHECKPOINT_SCHEMA, + "cell_id": CELL_ID, + "request_nonce": nonce, + "account_hash": self._expected_account_hash, + "journal_name": name, + "journal_schema": schema, + "journal_stream_id": stream_id, + "journal_sequence": sequence, + "journal_head_sha256": head, + "anchor_body_digest": checkpoint["anchor_body_digest"], + "anchor_event_digest": checkpoint["anchor_event_digest"], + "reused": payload["reused"], + "observed_at": payload["observed_at"], + } + if checkpoint != expected_checkpoint: + raise JournalAnchorError( + "Core anchor checkpoint differs from the request" + ) + try: + signed = verify_signed_envelope( + payload["anchor_envelope"], + trusted_public_keys=self._trusted_core_public_keys, + expected_body_schema=ANCHOR_BODY_SCHEMA, + max_ttl=timedelta(seconds=120), + require_active=False, + ) + except (TypeError, ValueError) as exc: + raise JournalAnchorError( + "Core journal anchor envelope is invalid" + ) from exc + if signed.source_id != CORE_SOURCE_ID: + raise JournalAnchorError( + "journal anchor source is not DumbMoney Core" + ) + body = signed.body + if set(body) != _ANCHOR_BODY_FIELDS: + raise JournalAnchorError( + "journal anchor body fields mismatch" + ) + anchored_at = _parse_utc( + body["anchored_at"], + field="anchor body anchored_at", + ) + if ( + body.get("venue") != CELL_ID + or body.get("account_hash") != self._expected_account_hash + or body.get("journal_name") != name + or body.get("journal_schema") != schema + or body.get("journal_stream_id") != stream_id + or body.get("journal_sequence") != sequence + or body.get("journal_head_sha256") != head + or anchored_at > observed_at + ): + raise JournalAnchorError( + "journal anchor body differs from the local head" + ) + previous = _digest( + body["previous_anchor_body_digest"], + field="previous_anchor_body_digest", + ) + expected_anchor_id = sha256_json( + { + "schema": ANCHOR_BODY_SCHEMA, + "venue": CELL_ID, + "account_hash": self._expected_account_hash, + "journal_name": name, + "journal_schema": schema, + "journal_stream_id": stream_id, + "journal_sequence": sequence, + "journal_head_sha256": head, + "previous_anchor_body_digest": previous, + } + ) + if body.get("anchor_id") != expected_anchor_id: + raise JournalAnchorError("journal anchor identity is invalid") + proof = self._verify_ledger_proof( + payload["ledger_proof"], + signed=signed, + observed_at=observed_at, + ) + body_digest = _digest( + checkpoint["anchor_body_digest"], + field="checkpoint anchor_body_digest", + ) + event_digest = _digest( + checkpoint["anchor_event_digest"], + field="checkpoint anchor_event_digest", + ) + if ( + body_digest != signed.wrapper["body_digest"] + or body_digest != proof["payload_digest"] + or event_digest != proof["event_digest"] + ): + raise JournalAnchorError( + "journal anchor checkpoint proof binding is invalid" + ) + return VerifiedJournalAnchor( + journal_name=name, + journal_sequence=sequence, + journal_head_sha256=head, + anchor_body_digest=body_digest, + anchor_event_digest=event_digest, + reused=bool(payload["reused"]), + observed_at=cast(str, payload["observed_at"]), + ) + + +__all__ = [ + "ANCHOR_BODY_SCHEMA", + "ANCHOR_CHECKPOINT_SCHEMA", + "ANCHOR_REQUEST_SCHEMA", + "ANCHOR_RESPONSE_SCHEMA", + "CoreJournalAnchorClient", + "JournalAnchorError", + "JournalAnchorResponse", + "JournalAnchorTransport", + "VerifiedJournalAnchor", +] diff --git a/live_firewall/dumbmoney_lineage.py b/live_firewall/dumbmoney_lineage.py new file mode 100644 index 00000000..f16ba4fd --- /dev/null +++ b/live_firewall/dumbmoney_lineage.py @@ -0,0 +1,1323 @@ +"""Authenticated Core resolver for Dummy's singleton live lineage. + +Capital is an upper bound, not evidence that the named strategy, passport, or +promotion exists. This module resolves the exact passport and promotion +digests through DumbMoney Core's loopback-only cell endpoint and verifies: + +* a fresh, nonce-bound Core checkpoint; +* the original ledger proof and signed envelope; +* disjoint Core, research, and promoter signing roles; and +* the complete strategy/passport/promotion/instrument/capital relationship. + +There is deliberately no HTTP client or retry loop here. A sealed local +runner injects one loopback GET transport and a Windows-credential-backed cell +token provider. +""" +from __future__ import annotations + +import base64 +import hashlib +import json +import re +import secrets +import threading +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from typing import Any, Protocol, cast +from urllib.parse import urlencode + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + +from live_firewall.dumbmoney_capital import ( + VerifiedCapitalEnvelope, + VerifiedSignedEnvelope, + verify_signed_envelope, +) +from live_firewall.operational_journal import canonical_json, sha256_json + + +CELL_ID = "dummy_kalshi" +ALPHA_PASSPORT_SCHEMA = "dumbmoney.alpha_passport.v1" +PROMOTION_CERTIFICATE_SCHEMA = "dumbmoney.promotion-certificate.v1" +CONTRACT_RESOLUTION_SCHEMA = "dumbmoney.cell-contract-resolution.v1" +CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA = ( + "dumbmoney.cell-contract-resolution-checkpoint.v1" +) +LEDGER_PROOF_SCHEMA = "dumbmoney.ledger-event-proof.v1" +LIVE_PROMOTION_STAGES = frozenset( + {"EXPLORATORY_LIVE", "AGGRESSIVE_BOUNDED"} +) +EVIDENCE_CLASSES = frozenset( + {"SYNTHETIC", "REPLAY", "BACKTEST", "PAPER", "FORWARD", "REALIZED"} +) +ZERO_DIGEST = "0" * 64 + +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$") +_RFC3339_UTC_RE = re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$" +) +_RESPONSE_FIELDS = { + "schema", + "cell_id", + "request_nonce", + "requested_body_digest", + "capital_envelope_digest", + "fencing_generation", + "observed_at", + "contract_schema", + "transport_window_current", + "body_window_current", + "eligible_live_input", + "authority_state", + "ledger_proof", + "envelope", + "checkpoint", + "checkpoint_signature", +} +_CHECKPOINT_FIELDS = _RESPONSE_FIELDS - { + "checkpoint", + "checkpoint_signature", +} +_SIGNATURE_FIELDS = {"algorithm", "signer_key_id", "signature"} +_LEDGER_PROOF_FIELDS = { + "schema", + "global_sequence", + "event_id", + "source_id", + "source_sequence", + "signer_key_id", + "nonce", + "event_schema", + "observed_at", + "received_at", + "correlation_id", + "causation_id", + "payload_digest", + "previous_source_digest", + "previous_global_digest", + "event_digest", +} +_PASSPORT_FIELDS = { + "schema", + "passport_id", + "strategy_lineage_id", + "venue", + "strategy_hash", + "artifact_hashes", + "evidence_verdict_hashes", + "intended_instruments", + "maximum_loss_cents", + "evidence_class", + "created_at", + "expires_at", +} +_PROMOTION_FIELDS = { + "schema", + "certificate_id", + "passport_digest", + "verdict_digests", + "stage", + "venue", + "instruments", + "maximum_loss_cents", + "rollback_triggers", + "policy_epoch", + "not_before", + "expires_at", +} +AUTHORITY_STATE_SCHEMA = "dumbmoney.cell-authority-state.v1" +_AUTHORITY_STATE_FIELDS = { + "schema", + "evaluated_at", + "authority_valid_until", + "policy_epoch", + "mandate_id", + "mandate_event_digest", + "kill_clear", + "kill_generation", + "kill_event_digest", + "desired_mode", + "desired_mode_revision", + "desired_mode_event_digest", + "capital_envelope_digest", + "capital_event_digest", + "fencing_generation", + "strategy_hash", + "passport_digest", + "passport_event_digest", + "promotion_digest", + "promotion_event_digest", + "verdicts", + "ledger_head_sequence", + "ledger_head_digest", +} +_AUTHORITY_VERDICT_FIELDS = { + "verdict_digest", + "verdict_event_digest", + "verdict_id", + "court", + "decision", + "signer_key_id", + "evaluated_at", + "expires_at", + "transport_expires_at", +} +_VERDICT_COURTS = frozenset( + {"integrity", "statistics", "economics", "adversarial_operations"} +) + + +class LineageResolutionError(RuntimeError): + """A resolver, provenance, or relationship failure that removes authority.""" + + +@dataclass(frozen=True) +class ContractResolutionResponse: + """One injected loopback GET response.""" + + status_code: int + body: bytes + + +class ContractResolutionTransport(Protocol): + def __call__( + self, + path: str, + headers: Mapping[str, str], + ) -> ContractResolutionResponse: ... + + +@dataclass(frozen=True) +class VerifiedLineageBinding: + """Exact authenticated lineage pinned to one current capital fence.""" + + strategy_hash: str + passport_hash: str + promotion_hash: str + authorized_instrument: str + maximum_loss_cents: int + policy_epoch: int + capital_envelope_id: str + capital_event_id: str + capital_body_digest: str + capital_fencing_generation: int + passport_event_id: str + promotion_event_id: str + passport_signer_key_id: str + promotion_signer_key_id: str + expires_at: str + passport_resolution_sha256: str + promotion_resolution_sha256: str + authority_state_sha256: str + authority_kill_generation: int + authority_desired_mode_revision: int + authority_ledger_head_sequence: int + authority_ledger_head_digest: str + authority_valid_until: str + + def evidence(self) -> dict[str, Any]: + return { + "strategy_hash": self.strategy_hash, + "passport_hash": self.passport_hash, + "promotion_hash": self.promotion_hash, + "authorized_instrument": self.authorized_instrument, + "maximum_loss_cents": self.maximum_loss_cents, + "policy_epoch": self.policy_epoch, + "capital_envelope_id": self.capital_envelope_id, + "capital_event_id": self.capital_event_id, + "capital_body_digest": self.capital_body_digest, + "capital_fencing_generation": self.capital_fencing_generation, + "passport_event_id": self.passport_event_id, + "promotion_event_id": self.promotion_event_id, + "passport_signer_key_id": self.passport_signer_key_id, + "promotion_signer_key_id": self.promotion_signer_key_id, + "expires_at": self.expires_at, + "passport_resolution_sha256": self.passport_resolution_sha256, + "promotion_resolution_sha256": self.promotion_resolution_sha256, + "authority_state_sha256": self.authority_state_sha256, + "authority_kill_generation": self.authority_kill_generation, + "authority_desired_mode_revision": ( + self.authority_desired_mode_revision + ), + "authority_ledger_head_sequence": ( + self.authority_ledger_head_sequence + ), + "authority_ledger_head_digest": self.authority_ledger_head_digest, + "authority_valid_until": self.authority_valid_until, + } + + +@dataclass(frozen=True) +class _ResolvedContract: + response: dict[str, Any] + signed: VerifiedSignedEnvelope + body_not_before: datetime + body_expires_at: datetime + authority_state: dict[str, Any] + + +def _utc_now() -> datetime: + return datetime.now(timezone.utc) + + +def _parse_utc(value: Any, *, field: str) -> datetime: + if not isinstance(value, str) or not _RFC3339_UTC_RE.fullmatch(value): + raise LineageResolutionError(f"{field} must be canonical RFC3339 UTC") + parsed = datetime.fromisoformat(value[:-1] + "+00:00") + return parsed.astimezone(timezone.utc) + + +def _format_utc(value: datetime) -> str: + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _digest(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not _SHA256_RE.fullmatch(value): + raise LineageResolutionError(f"{field} must be a lowercase sha256") + return value + + +def _identifier(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not _IDENTIFIER_RE.fullmatch(value): + raise LineageResolutionError(f"{field} must be a canonical identifier") + return value + + +def _positive_int(value: Any, *, field: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < 1: + raise LineageResolutionError(f"{field} must be a positive integer") + return value + + +def _sorted_hashes( + value: Any, + *, + field: str, + allow_empty: bool = False, +) -> tuple[str, ...]: + if not isinstance(value, list) or (not value and not allow_empty): + qualifier = "an array" if allow_empty else "a nonempty array" + raise LineageResolutionError(f"{field} must be {qualifier}") + result = tuple(_digest(item, field=f"{field}[]") for item in value) + if result != tuple(sorted(set(result))): + raise LineageResolutionError(f"{field} must be sorted and unique") + return result + + +def _sorted_identifiers(value: Any, *, field: str) -> tuple[str, ...]: + if not isinstance(value, list) or not value: + raise LineageResolutionError(f"{field} must be a nonempty array") + result = tuple(_identifier(item, field=f"{field}[]") for item in value) + if result != tuple(sorted(set(result))): + raise LineageResolutionError(f"{field} must be sorted and unique") + return result + + +def _decode_signature(value: Any) -> bytes: + if ( + not isinstance(value, str) + or not value + or "=" in value + or re.search(r"[^A-Za-z0-9_-]", value) + ): + raise LineageResolutionError("signature must be unpadded base64url") + try: + decoded = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) + except Exception as exc: + raise LineageResolutionError("signature is invalid base64url") from exc + if base64.urlsafe_b64encode(decoded).decode("ascii").rstrip("=") != value: + raise LineageResolutionError("signature is not canonical base64url") + return decoded + + +def _keyring( + raw: Mapping[str, bytes | Ed25519PublicKey], + *, + role: str, + maximum_keys: int = 16, +) -> dict[str, bytes]: + if not isinstance(raw, Mapping) or not 1 <= len(raw) <= maximum_keys: + raise ValueError( + f"{role} keyring must contain 1 to {maximum_keys} keys" + ) + result: dict[str, bytes] = {} + for raw_key_id, material in raw.items(): + key_id = _digest(raw_key_id, field=f"{role} key id") + key = ( + material.public_bytes_raw() + if isinstance(material, Ed25519PublicKey) + else bytes(material) + ) + if len(key) != 32 or hashlib.sha256(key).hexdigest() != key_id: + raise ValueError(f"{role} key id does not bind its Ed25519 key") + result[key_id] = key + return result + + +def _strict_json(body: bytes, *, maximum_bytes: int) -> dict[str, Any]: + if not isinstance(body, bytes): + raise LineageResolutionError("contract response body must be bytes") + if not body or len(body) > maximum_bytes: + raise LineageResolutionError("contract response size is invalid") + + def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + value: dict[str, Any] = {} + for key, item in pairs: + if key in value: + raise LineageResolutionError( + f"contract response contains duplicate key {key!r}" + ) + value[key] = item + return value + + try: + value = json.loads( + body.decode("utf-8"), + object_pairs_hook=reject_duplicates, + parse_constant=lambda token: (_ for _ in ()).throw( + LineageResolutionError( + f"contract response contains non-finite value {token}" + ) + ), + ) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise LineageResolutionError("contract response is not strict JSON") from exc + if not isinstance(value, dict): + raise LineageResolutionError("contract response must be an object") + return value + + +def _validate_checkpoint( + checkpoint_raw: Any, + signature_raw: Any, + *, + core_keys: Mapping[str, bytes], +) -> dict[str, Any]: + if not isinstance(checkpoint_raw, dict) or not isinstance(signature_raw, dict): + raise LineageResolutionError( + "contract checkpoint and signature must be objects" + ) + checkpoint = json.loads(canonical_json(checkpoint_raw)) + signature = json.loads(canonical_json(signature_raw)) + if ( + set(checkpoint) != _CHECKPOINT_FIELDS + or checkpoint.get("schema") + != CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA + ): + raise LineageResolutionError("contract checkpoint fields mismatch") + if set(signature) != _SIGNATURE_FIELDS: + raise LineageResolutionError("contract checkpoint signature fields mismatch") + if signature["algorithm"] != "Ed25519": + raise LineageResolutionError("contract checkpoint algorithm mismatch") + signer = _digest( + signature["signer_key_id"], + field="contract checkpoint signer_key_id", + ) + public_key = core_keys.get(signer) + if public_key is None: + raise LineageResolutionError("contract checkpoint signer is not pinned Core") + raw_signature = _decode_signature(signature["signature"]) + if len(raw_signature) != 64: + raise LineageResolutionError( + "contract checkpoint signature must be 64 bytes" + ) + try: + Ed25519PublicKey.from_public_bytes(public_key).verify( + raw_signature, + canonical_json(checkpoint).encode("utf-8"), + ) + except (InvalidSignature, ValueError) as exc: + raise LineageResolutionError( + "contract checkpoint signature is invalid" + ) from exc + return cast(dict[str, Any], checkpoint) + + +def _validate_ledger_proof( + proof_raw: Any, + *, + signed: VerifiedSignedEnvelope, + expected_schema: str, + observed_at: datetime, +) -> dict[str, Any]: + if not isinstance(proof_raw, dict): + raise LineageResolutionError("contract ledger proof must be an object") + proof = json.loads(canonical_json(proof_raw)) + if ( + set(proof) != _LEDGER_PROOF_FIELDS + or proof.get("schema") != LEDGER_PROOF_SCHEMA + ): + raise LineageResolutionError("contract ledger proof fields mismatch") + global_sequence = _positive_int( + proof["global_sequence"], + field="ledger global_sequence", + ) + source_sequence = _positive_int( + proof["source_sequence"], + field="ledger source_sequence", + ) + event_id = _digest(proof["event_id"], field="ledger event_id") + event_digest = _digest(proof["event_digest"], field="ledger event_digest") + payload_digest = _digest( + proof["payload_digest"], + field="ledger payload_digest", + ) + previous_source = _digest( + proof["previous_source_digest"], + field="ledger previous_source_digest", + ) + previous_global = _digest( + proof["previous_global_digest"], + field="ledger previous_global_digest", + ) + source_id = _identifier(proof["source_id"], field="ledger source_id") + signer = _digest(proof["signer_key_id"], field="ledger signer_key_id") + nonce = _identifier(proof["nonce"], field="ledger nonce") + event_schema = _identifier(proof["event_schema"], field="ledger event_schema") + correlation = _identifier( + proof["correlation_id"], + field="ledger correlation_id", + ) + causation = proof["causation_id"] + if causation is not None: + causation = _digest(causation, field="ledger causation_id") + record_observed = _parse_utc( + proof["observed_at"], + field="ledger observed_at", + ) + received_at = _parse_utc(proof["received_at"], field="ledger received_at") + if received_at > observed_at: + raise LineageResolutionError( + "ledger proof was received after contract observation" + ) + if record_observed != max(received_at, signed.not_before): + raise LineageResolutionError( + "ledger proof observed_at differs from ingestion semantics" + ) + if (global_sequence == 1) != (previous_global == ZERO_DIGEST): + raise LineageResolutionError("ledger previous_global_digest mismatch") + if (source_sequence == 1) != (previous_source == ZERO_DIGEST): + raise LineageResolutionError("ledger previous_source_digest mismatch") + wrapper = signed.wrapper + duplicated = { + "event_id": (event_id, signed.event_id), + "source_id": (source_id, signed.source_id), + "source_sequence": (source_sequence, signed.source_sequence), + "signer_key_id": (signer, signed.signer_key_id), + "nonce": (nonce, signed.nonce), + "event_schema": (event_schema, expected_schema), + "correlation_id": (correlation, wrapper["correlation_id"]), + "causation_id": (causation, wrapper["causation_id"]), + "payload_digest": (payload_digest, wrapper["body_digest"]), + } + for field, (actual, expected) in duplicated.items(): + if actual != expected: + raise LineageResolutionError( + f"ledger proof {field} differs from contract envelope" + ) + event_material = { + key: item + for key, item in proof.items() + if key not in {"schema", "event_digest"} + } + if sha256_json(event_material) != event_digest: + raise LineageResolutionError( + "ledger proof event_digest does not match event material" + ) + return cast(dict[str, Any], proof) + + +def _validate_authority_state( + raw_state: Any, + *, + capital: VerifiedCapitalEnvelope, + now: datetime, + evaluator_keys: Mapping[str, bytes], + maximum_resolution_age: timedelta, + maximum_future_skew: timedelta, +) -> dict[str, Any]: + """Validate the complete Core-signed authority projection. + + The verdict documents themselves remain Core ledger records; this + projection pins their event identities and requires their signer key IDs + to belong to the sealed evaluator role. The surrounding checkpoint signs + every byte of this state. + """ + if not isinstance(raw_state, dict): + raise LineageResolutionError( + "contract authority_state must be an object" + ) + state = json.loads(canonical_json(raw_state)) + if ( + set(state) != _AUTHORITY_STATE_FIELDS + or state.get("schema") != AUTHORITY_STATE_SCHEMA + ): + raise LineageResolutionError("contract authority_state fields mismatch") + + evaluated_at = _parse_utc( + state["evaluated_at"], + field="authority_state evaluated_at", + ) + valid_until = _parse_utc( + state["authority_valid_until"], + field="authority_state authority_valid_until", + ) + if evaluated_at > now + maximum_future_skew: + raise LineageResolutionError("contract authority_state is future-dated") + if now - evaluated_at > maximum_resolution_age: + raise LineageResolutionError("contract authority_state is stale") + if not evaluated_at < valid_until or not now < valid_until: + raise LineageResolutionError( + "contract authority_state validity window is not current" + ) + if ( + capital.ledger_event_digest is None + or capital.ledger_global_sequence is None + ): + raise LineageResolutionError( + "active capital lacks its authenticated Core ledger binding" + ) + expected = { + "policy_epoch": capital.policy_epoch, + "mandate_id": capital.body["mandate_id"], + "kill_clear": True, + "desired_mode": "LIVE", + "capital_envelope_digest": sha256_json(capital.body), + "capital_event_digest": capital.ledger_event_digest, + "fencing_generation": capital.fencing_generation, + "strategy_hash": capital.strategy_hashes[0], + "passport_digest": capital.passport_hashes[0], + "promotion_digest": capital.promotion_hashes[0], + } + for field, value in expected.items(): + if state.get(field) != value: + raise LineageResolutionError( + f"contract authority_state {field} differs from active authority" + ) + for field in ( + "kill_generation", + "desired_mode_revision", + "ledger_head_sequence", + ): + _positive_int(state[field], field=f"authority_state {field}") + for field in ( + "mandate_event_digest", + "kill_event_digest", + "desired_mode_event_digest", + "capital_event_digest", + "passport_event_digest", + "promotion_event_digest", + "ledger_head_digest", + ): + _digest(state[field], field=f"authority_state {field}") + + capital_sequence = capital.ledger_global_sequence + head_sequence = int(state["ledger_head_sequence"]) + if head_sequence < capital_sequence or ( + head_sequence == capital_sequence + and state["ledger_head_digest"] != capital.ledger_event_digest + ): + raise LineageResolutionError( + "active capital is not continuous with the authority ledger head" + ) + + verdicts = state["verdicts"] + if not isinstance(verdicts, list) or len(verdicts) < 2: + raise LineageResolutionError( + "contract authority_state requires at least two PASS verdicts" + ) + facts: set[tuple[str, str, str, str]] = set() + courts: set[str] = set() + for index, raw_verdict in enumerate(verdicts): + if not isinstance(raw_verdict, dict): + raise LineageResolutionError( + f"authority_state verdict {index} must be an object" + ) + verdict = json.loads(canonical_json(raw_verdict)) + if set(verdict) != _AUTHORITY_VERDICT_FIELDS: + raise LineageResolutionError( + f"authority_state verdict {index} fields mismatch" + ) + verdict_digest = _digest( + verdict["verdict_digest"], + field=f"authority_state verdict {index} digest", + ) + event_digest = _digest( + verdict["verdict_event_digest"], + field=f"authority_state verdict {index} event_digest", + ) + verdict_id = _identifier( + verdict["verdict_id"], + field=f"authority_state verdict {index} id", + ) + court = verdict["court"] + if ( + court not in _VERDICT_COURTS + or verdict["decision"] != "PASS" + ): + raise LineageResolutionError( + "contract authority verdict is not a recognized PASS" + ) + signer = _digest( + verdict["signer_key_id"], + field=f"authority_state verdict {index} signer_key_id", + ) + if signer not in evaluator_keys: + raise LineageResolutionError( + "contract authority verdict signer is not sealed" + ) + verdict_evaluated = _parse_utc( + verdict["evaluated_at"], + field=f"authority_state verdict {index} evaluated_at", + ) + verdict_expires = _parse_utc( + verdict["expires_at"], + field=f"authority_state verdict {index} expires_at", + ) + transport_expires = _parse_utc( + verdict["transport_expires_at"], + field=f"authority_state verdict {index} transport_expires_at", + ) + if ( + verdict_evaluated > now + maximum_future_skew + or not verdict_evaluated < verdict_expires + or not now < verdict_expires + or not now < transport_expires + or valid_until > min(verdict_expires, transport_expires) + ): + raise LineageResolutionError( + "contract authority verdict is stale, future-dated, or expired" + ) + facts.add((verdict_digest, event_digest, verdict_id, court)) + courts.add(court) + if len(facts) != len(verdicts) or len(courts) < 2: + raise LineageResolutionError( + "contract authority verdict facts or courts are not unique" + ) + return cast(dict[str, Any], state) + + +def _validate_passport( + signed: VerifiedSignedEnvelope, + *, + now: datetime, +) -> tuple[datetime, datetime]: + body = signed.body + if set(body) != _PASSPORT_FIELDS: + raise LineageResolutionError("AlphaPassport fields mismatch") + if body["schema"] != ALPHA_PASSPORT_SCHEMA or body["venue"] != CELL_ID: + raise LineageResolutionError("AlphaPassport identity mismatch") + _identifier(body["passport_id"], field="passport_id") + _identifier(body["strategy_lineage_id"], field="strategy_lineage_id") + _digest(body["strategy_hash"], field="passport strategy_hash") + _sorted_hashes(body["artifact_hashes"], field="passport artifact_hashes") + _sorted_hashes( + body["evidence_verdict_hashes"], + field="passport evidence_verdict_hashes", + allow_empty=True, + ) + instruments = _sorted_identifiers( + body["intended_instruments"], + field="passport intended_instruments", + ) + if any( + not item.startswith("event_contract:") + or item.endswith(":") + or "*" in item + for item in instruments + ): + raise LineageResolutionError( + "AlphaPassport contains a noncanonical Dummy instrument" + ) + _positive_int(body["maximum_loss_cents"], field="passport maximum_loss_cents") + if body["evidence_class"] not in EVIDENCE_CLASSES: + raise LineageResolutionError("AlphaPassport evidence_class is unsupported") + starts = _parse_utc(body["created_at"], field="passport created_at") + expires = _parse_utc(body["expires_at"], field="passport expires_at") + if expires <= starts or not starts <= now < expires: + raise LineageResolutionError("AlphaPassport body window is not current") + return starts, expires + + +def _validate_promotion( + signed: VerifiedSignedEnvelope, + *, + now: datetime, +) -> tuple[datetime, datetime]: + body = signed.body + if set(body) != _PROMOTION_FIELDS: + raise LineageResolutionError("PromotionCertificate fields mismatch") + if body["schema"] != PROMOTION_CERTIFICATE_SCHEMA or body["venue"] != CELL_ID: + raise LineageResolutionError("PromotionCertificate identity mismatch") + _identifier(body["certificate_id"], field="certificate_id") + _digest(body["passport_digest"], field="promotion passport_digest") + _sorted_hashes(body["verdict_digests"], field="promotion verdict_digests") + if body["stage"] not in LIVE_PROMOTION_STAGES: + raise LineageResolutionError( + "PromotionCertificate stage is not live-authorizing" + ) + instruments = _sorted_identifiers( + body["instruments"], + field="promotion instruments", + ) + if any( + not item.startswith("event_contract:") + or item.endswith(":") + or "*" in item + for item in instruments + ): + raise LineageResolutionError( + "PromotionCertificate contains a noncanonical Dummy instrument" + ) + _positive_int( + body["maximum_loss_cents"], + field="promotion maximum_loss_cents", + ) + _sorted_identifiers( + body["rollback_triggers"], + field="promotion rollback_triggers", + ) + _positive_int(body["policy_epoch"], field="promotion policy_epoch") + starts = _parse_utc(body["not_before"], field="promotion not_before") + expires = _parse_utc(body["expires_at"], field="promotion expires_at") + if expires <= starts or not starts <= now < expires: + raise LineageResolutionError( + "PromotionCertificate body window is not current" + ) + return starts, expires + + +class CoreAuthorityContractResolver: + """Fetch and authenticate the exact singleton lineage named by capital.""" + + def __init__( + self, + *, + transport: ContractResolutionTransport, + cell_token_provider: Callable[[], str], + trusted_core_public_keys: Mapping[ + str, + bytes | Ed25519PublicKey, + ], + trusted_research_public_keys: Mapping[ + str, + bytes | Ed25519PublicKey, + ], + trusted_promoter_public_keys: Mapping[ + str, + bytes | Ed25519PublicKey, + ], + trusted_evaluator_public_keys: Mapping[ + str, + bytes | Ed25519PublicKey, + ], + now_fn: Callable[[], datetime] | None = None, + request_nonce_fn: Callable[[], str] | None = None, + maximum_response_bytes: int = 1_048_576, + maximum_resolution_age: timedelta = timedelta(seconds=60), + maximum_future_skew: timedelta = timedelta(seconds=5), + ) -> None: + if not callable(transport) or not callable(cell_token_provider): + raise TypeError("resolver transport and token provider must be callable") + if request_nonce_fn is not None and not callable(request_nonce_fn): + raise TypeError("resolver nonce provider must be callable") + if maximum_response_bytes < 1: + raise ValueError("maximum response bytes must be positive") + if maximum_resolution_age <= timedelta(0): + raise ValueError("maximum resolution age must be positive") + if maximum_future_skew < timedelta(0): + raise ValueError("maximum future skew cannot be negative") + core = _keyring(trusted_core_public_keys, role="Core") + research = _keyring(trusted_research_public_keys, role="research") + promoter = _keyring(trusted_promoter_public_keys, role="promoter") + evaluator = _keyring( + trusted_evaluator_public_keys, + role="evaluator", + maximum_keys=32, + ) + if set(core) & set(research) or set(core) & set(promoter): + raise ValueError("Core and contract signing roles must be disjoint") + if set(research) & set(promoter): + raise ValueError("research and promoter signing roles must be disjoint") + if ( + set(evaluator) & set(core) + or set(evaluator) & set(research) + or set(evaluator) & set(promoter) + ): + raise ValueError( + "evaluator and control/contract signing roles must be disjoint" + ) + self._transport = transport + self._cell_token_provider = cell_token_provider + self._core_keys = core + self._research_keys = research + self._promoter_keys = promoter + self._evaluator_keys = evaluator + self._now_fn = now_fn or _utc_now + self._request_nonce_fn = request_nonce_fn or ( + lambda: secrets.token_hex(32) + ) + self._nonce_lock = threading.Lock() + self._last_nonce: str | None = None + self.maximum_response_bytes = maximum_response_bytes + self.maximum_resolution_age = maximum_resolution_age + self.maximum_future_skew = maximum_future_skew + + def _now(self) -> datetime: + now = self._now_fn() + if now.tzinfo is None or now.utcoffset() is None: + raise LineageResolutionError( + "contract resolver clock must be timezone-aware" + ) + return now.astimezone(timezone.utc) + + def _nonce(self) -> str: + try: + nonce = _digest( + self._request_nonce_fn(), + field="contract request_nonce", + ) + except Exception as exc: + raise LineageResolutionError( + "cryptographic contract nonce generation failed" + ) from exc + with self._nonce_lock: + if nonce == self._last_nonce: + raise LineageResolutionError("contract request nonce replay") + self._last_nonce = nonce + return nonce + + def _token(self) -> str: + try: + token = self._cell_token_provider() + except Exception as exc: + raise LineageResolutionError( + "cell bearer retrieval failed closed" + ) from exc + if ( + not isinstance(token, str) + or token != token.strip() + or not 32 <= len(token) <= 4096 + or any(ord(char) < 33 or ord(char) > 126 for char in token) + ): + raise LineageResolutionError("cell bearer is invalid") + return token + + def _resolve( + self, + *, + digest: str, + expected_schema: str, + capital: VerifiedCapitalEnvelope, + ) -> _ResolvedContract: + requested_digest = _digest(digest, field="requested contract digest") + capital_digest = _digest( + sha256_json(capital.body), + field="active capital body digest", + ) + if expected_schema not in { + ALPHA_PASSPORT_SCHEMA, + PROMOTION_CERTIFICATE_SCHEMA, + }: + raise LineageResolutionError("requested contract schema is unsupported") + fence = _positive_int( + capital.fencing_generation, + field="active capital fencing generation", + ) + request_nonce = self._nonce() + query = urlencode( + { + "capital_envelope_digest": capital_digest, + "request_nonce": request_nonce, + } + ) + path = ( + f"/v1/cells/{CELL_ID}/contracts/{requested_digest}?{query}" + ) + try: + response = self._transport( + path, + { + "Accept": "application/json", + "Authorization": f"Bearer {self._token()}", + }, + ) + except Exception as exc: + raise LineageResolutionError( + f"contract transport failed closed:{type(exc).__name__}" + ) from exc + if not isinstance(response, ContractResolutionResponse): + raise LineageResolutionError( + "contract transport returned an unsupported response" + ) + if isinstance(response.status_code, bool) or not isinstance( + response.status_code, + int, + ): + raise LineageResolutionError("contract response status is invalid") + if response.status_code in {401, 403}: + raise LineageResolutionError( + "contract resolution authentication or scope was denied" + ) + if response.status_code != 200: + raise LineageResolutionError( + f"contract resolver returned HTTP {response.status_code}" + ) + payload = _strict_json( + response.body, + maximum_bytes=self.maximum_response_bytes, + ) + now = self._now() + if ( + set(payload) != _RESPONSE_FIELDS + or payload.get("schema") != CONTRACT_RESOLUTION_SCHEMA + ): + raise LineageResolutionError("contract resolution fields mismatch") + checkpoint = _validate_checkpoint( + payload["checkpoint"], + payload["checkpoint_signature"], + core_keys=self._core_keys, + ) + expected_checkpoint = { + key: item + for key, item in payload.items() + if key not in {"schema", "checkpoint", "checkpoint_signature"} + } + expected_checkpoint["schema"] = ( + CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA + ) + if checkpoint != expected_checkpoint: + raise LineageResolutionError( + "signed checkpoint does not bind the full contract resolution" + ) + if payload["cell_id"] != CELL_ID: + raise LineageResolutionError( + "contract resolution targets another venue cell" + ) + if ( + _digest(payload["request_nonce"], field="response request_nonce") + != request_nonce + ): + raise LineageResolutionError("contract response nonce mismatch") + if ( + _digest( + payload["requested_body_digest"], + field="response requested_body_digest", + ) + != requested_digest + ): + raise LineageResolutionError("contract response digest mismatch") + if ( + _digest( + payload["capital_envelope_digest"], + field="response capital_envelope_digest", + ) + != capital_digest + ): + raise LineageResolutionError( + "contract response is not bound to active capital" + ) + if payload["fencing_generation"] != fence or isinstance( + payload["fencing_generation"], + bool, + ): + raise LineageResolutionError( + "contract response is not bound to active capital fence" + ) + if payload["contract_schema"] != expected_schema: + raise LineageResolutionError("contract response schema mismatch") + for field in ( + "transport_window_current", + "body_window_current", + "eligible_live_input", + ): + if not isinstance(payload[field], bool): + raise LineageResolutionError( + f"contract response {field} must be boolean" + ) + observed_at = _parse_utc( + payload["observed_at"], + field="contract observed_at", + ) + if observed_at > now + self.maximum_future_skew: + raise LineageResolutionError( + "contract response observation is future-dated" + ) + if now - observed_at > self.maximum_resolution_age: + raise LineageResolutionError("contract response observation is stale") + envelope = payload["envelope"] + if not isinstance(envelope, dict): + raise LineageResolutionError( + "contract response envelope must be an object" + ) + role_keys = ( + self._research_keys + if expected_schema == ALPHA_PASSPORT_SCHEMA + else self._promoter_keys + ) + try: + signed = verify_signed_envelope( + envelope, + trusted_public_keys=role_keys, + now=now, + expected_body_schema=expected_schema, + ) + except ValueError as exc: + raise LineageResolutionError( + f"resolved signed contract invalid: {exc}" + ) from exc + if signed.wrapper["body_digest"] != requested_digest: + raise LineageResolutionError( + "resolved envelope body digest differs from request" + ) + authority_state = _validate_authority_state( + payload["authority_state"], + capital=capital, + now=now, + evaluator_keys=self._evaluator_keys, + maximum_resolution_age=self.maximum_resolution_age, + maximum_future_skew=self.maximum_future_skew, + ) + if ( + _parse_utc( + authority_state["evaluated_at"], + field="authority_state evaluated_at", + ) + != observed_at + ): + raise LineageResolutionError( + "contract authority_state evaluated_at does not mirror observed_at" + ) + proof = _validate_ledger_proof( + payload["ledger_proof"], + signed=signed, + expected_schema=expected_schema, + observed_at=observed_at, + ) + authority_event_digest = authority_state[ + ( + "promotion_event_digest" + if expected_schema == PROMOTION_CERTIFICATE_SCHEMA + else "passport_event_digest" + ) + ] + if proof["event_digest"] != authority_event_digest: + raise LineageResolutionError( + "contract proof event digest differs from authority_state" + ) + proof_sequence = int(proof["global_sequence"]) + head_sequence = int(authority_state["ledger_head_sequence"]) + if proof_sequence > head_sequence or ( + proof_sequence == head_sequence + and proof["event_digest"] != authority_state["ledger_head_digest"] + ): + raise LineageResolutionError( + "contract proof is not continuous with the authority ledger head" + ) + if expected_schema == ALPHA_PASSPORT_SCHEMA: + body_starts, body_expires = _validate_passport(signed, now=now) + else: + body_starts, body_expires = _validate_promotion(signed, now=now) + transport_current = ( + signed.not_before <= observed_at < signed.expires_at + ) + body_current = body_starts <= observed_at < body_expires + eligible = transport_current and body_current + if expected_schema == PROMOTION_CERTIFICATE_SCHEMA: + eligible = ( + eligible + and signed.body["stage"] in LIVE_PROMOTION_STAGES + ) + if payload["transport_window_current"] is not transport_current: + raise LineageResolutionError( + "contract transport-window projection mismatch" + ) + if payload["body_window_current"] is not body_current: + raise LineageResolutionError( + "contract body-window projection mismatch" + ) + if payload["eligible_live_input"] is not eligible: + raise LineageResolutionError( + "contract live-eligibility projection mismatch" + ) + if not eligible: + raise LineageResolutionError( + "resolved contract is not an eligible current live input" + ) + return _ResolvedContract( + response=json.loads(canonical_json(payload)), + signed=signed, + body_not_before=body_starts, + body_expires_at=body_expires, + authority_state=authority_state, + ) + + def resolve_lineage( + self, + *, + capital: VerifiedCapitalEnvelope, + strategy_hash: str, + passport_hash: str, + promotion_hash: str, + authorized_instrument: str, + expected_binding: Mapping[str, Any] | None = None, + ) -> VerifiedLineageBinding: + """Resolve and bind the exact active singleton lineage.""" + now = self._now() + strategy = _digest(strategy_hash, field="strategy_hash") + passport = _digest(passport_hash, field="passport_hash") + promotion = _digest(promotion_hash, field="promotion_hash") + instrument = _identifier( + authorized_instrument, + field="authorized_instrument", + ) + if ( + capital.venue != CELL_ID + or capital.strategy_hashes != (strategy,) + or capital.passport_hashes != (passport,) + or capital.promotion_hashes != (promotion,) + or instrument not in capital.authorized_instruments + ): + raise LineageResolutionError( + "requested lineage differs from singleton capital authority" + ) + capital_body_digest = sha256_json(capital.body) + promotion_resolution = self._resolve( + digest=promotion, + expected_schema=PROMOTION_CERTIFICATE_SCHEMA, + capital=capital, + ) + passport_resolution = self._resolve( + digest=passport, + expected_schema=ALPHA_PASSPORT_SCHEMA, + capital=capital, + ) + promotion_authority = { + key: item + for key, item in promotion_resolution.authority_state.items() + if key != "evaluated_at" + } + passport_authority = { + key: item + for key, item in passport_resolution.authority_state.items() + if key != "evaluated_at" + } + if promotion_authority != passport_authority: + raise LineageResolutionError( + "promotion and passport were not resolved from one authority state" + ) + promotion_body = promotion_resolution.signed.body + passport_body = passport_resolution.signed.body + if promotion_body["passport_digest"] != passport: + raise LineageResolutionError( + "promotion does not bind the singleton capital passport" + ) + if passport_body["strategy_hash"] != strategy: + raise LineageResolutionError( + "passport does not bind the singleton capital strategy" + ) + if ( + instrument not in passport_body["intended_instruments"] + or instrument not in promotion_body["instruments"] + ): + raise LineageResolutionError( + "instrument is not jointly authorized by lineage" + ) + if promotion_body["policy_epoch"] != capital.policy_epoch: + raise LineageResolutionError( + "promotion and capital policy epochs differ" + ) + lineage_loss = min( + int(passport_body["maximum_loss_cents"]), + int(promotion_body["maximum_loss_cents"]), + ) + if capital.max_order_risk_cents > lineage_loss: + raise LineageResolutionError( + "capital order bound exceeds lineage maximum loss" + ) + expires_at = min( + capital.expires_at, + passport_resolution.signed.expires_at, + passport_resolution.body_expires_at, + promotion_resolution.signed.expires_at, + promotion_resolution.body_expires_at, + _parse_utc( + promotion_authority["authority_valid_until"], + field="authority_state authority_valid_until", + ), + ) + if now >= expires_at: + raise LineageResolutionError( + "authenticated lineage binding is already expired" + ) + binding = VerifiedLineageBinding( + strategy_hash=strategy, + passport_hash=passport, + promotion_hash=promotion, + authorized_instrument=instrument, + maximum_loss_cents=lineage_loss, + policy_epoch=capital.policy_epoch, + capital_envelope_id=capital.envelope_id, + capital_event_id=capital.event_id, + capital_body_digest=capital_body_digest, + capital_fencing_generation=capital.fencing_generation, + passport_event_id=passport_resolution.signed.event_id, + promotion_event_id=promotion_resolution.signed.event_id, + passport_signer_key_id=passport_resolution.signed.signer_key_id, + promotion_signer_key_id=promotion_resolution.signed.signer_key_id, + expires_at=_format_utc(expires_at), + passport_resolution_sha256=sha256_json( + passport_resolution.response + ), + promotion_resolution_sha256=sha256_json( + promotion_resolution.response + ), + authority_state_sha256=sha256_json(promotion_authority), + authority_kill_generation=int( + promotion_authority["kill_generation"] + ), + authority_desired_mode_revision=int( + promotion_authority["desired_mode_revision"] + ), + authority_ledger_head_sequence=int( + promotion_authority["ledger_head_sequence"] + ), + authority_ledger_head_digest=str( + promotion_authority["ledger_head_digest"] + ), + authority_valid_until=str( + promotion_authority["authority_valid_until"] + ), + ) + if expected_binding is not None: + expected = json.loads(canonical_json(dict(expected_binding))) + actual = binding.evidence() + # A fresh resolution necessarily has a new request nonce and + # therefore new page digests. Compare the signed contract/event + # identity and complete authority relationship, not transport + # transcript hashes from the earlier binding pass. + expected.pop("passport_resolution_sha256", None) + expected.pop("promotion_resolution_sha256", None) + actual.pop("passport_resolution_sha256", None) + actual.pop("promotion_resolution_sha256", None) + if expected != actual: + raise LineageResolutionError( + "lineage or active capital fence changed during execution" + ) + return binding + + +__all__ = [ + "ALPHA_PASSPORT_SCHEMA", + "CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA", + "CONTRACT_RESOLUTION_SCHEMA", + "ContractResolutionResponse", + "ContractResolutionTransport", + "CoreAuthorityContractResolver", + "LineageResolutionError", + "PROMOTION_CERTIFICATE_SCHEMA", + "VerifiedLineageBinding", +] diff --git a/live_firewall/dumbmoney_reconciliation.py b/live_firewall/dumbmoney_reconciliation.py new file mode 100644 index 00000000..3527cd81 --- /dev/null +++ b/live_firewall/dumbmoney_reconciliation.py @@ -0,0 +1,128 @@ +"""Restart-safe read-only reconciliation orchestration for DumbMoney Dummy.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Protocol + +from live_firewall.dumbmoney_capital import CapitalEnvelopeAdapter +from live_firewall.exposure_tracker import ExposureTracker + + +class SignedReconciliationReader(Protocol): + def terminal_reconciliation_witness( + self, + reservation: Mapping[str, Any], + ) -> Mapping[str, Any] | None: ... + + def settlement_reconciliation_witness( + self, + position_exposure: Mapping[str, Any], + ) -> Mapping[str, Any] | None: ... + + +class DumbMoneyKalshiReconciliationSweeper: + """Resolve prior dispatches before allowing another execution cycle.""" + + def __init__( + self, + *, + capital_adapter: CapitalEnvelopeAdapter, + broker_reader: SignedReconciliationReader, + exposure_tracker: ExposureTracker, + ) -> None: + self._capital_adapter = capital_adapter + self._broker_reader = broker_reader + self._exposure_tracker = exposure_tracker + + def run_once(self) -> dict[str, Any]: + broker_contacted = False + terminal_witnesses = 0 + settlement_witnesses = 0 + unresolved_reservations = 0 + unresolved_positions = 0 + + reservations = ( + self._capital_adapter.pending_reconciliation_reservations() + ) + for reservation in reservations: + if reservation.get("dispatch_claimed") is not True: + unresolved_reservations += 1 + continue + broker_contacted = True + wrapper = self._broker_reader.terminal_reconciliation_witness( + reservation + ) + if wrapper is None: + unresolved_reservations += 1 + continue + witness = ( + self._capital_adapter + .verify_terminal_reconciliation_witness(wrapper) + ) + terminal_state = ( + "filled" + if witness["terminal_status"] == "executed" + else "canceled" + ) + if not self._exposure_tracker.record_cumulative_fill( + str(witness["proposal_id"]), + int(witness["fill_count"]), + witness["average_fill_price_cents"], + terminal_state=terminal_state, + reconciliation_id=str(witness["witness_id"]), + ): + raise RuntimeError( + "local terminal exposure projection failed closed" + ) + self._capital_adapter.record_signed_terminal_reconciliation( + wrapper + ) + terminal_witnesses += 1 + + positions = self._capital_adapter.active_position_exposures() + for position in positions: + broker_contacted = True + wrapper = self._broker_reader.settlement_reconciliation_witness( + position + ) + if wrapper is None: + unresolved_positions += 1 + continue + witness = ( + self._capital_adapter + .verify_settlement_reconciliation_witness(wrapper) + ) + if not self._exposure_tracker.record_position_close( + str(witness["contract_ticker"]), + str(witness["side"]), + reconciliation_id=str(witness["witness_id"]), + ): + raise RuntimeError( + "local settlement projection failed closed" + ) + self._capital_adapter.record_signed_settlement_reconciliation( + wrapper + ) + settlement_witnesses += 1 + + blocked = bool( + unresolved_reservations or unresolved_positions + ) + return { + "schema": "dummy.kalshi-reconciliation-sweep.v1", + "status": "BLOCKED" if blocked else "COMPLETE", + "broker_contacted": broker_contacted, + "reservations_scanned": len(reservations), + "terminal_witnesses_recorded": terminal_witnesses, + "positions_scanned": len(positions), + "settlement_witnesses_recorded": settlement_witnesses, + "unresolved_reservations": unresolved_reservations, + "unresolved_positions": unresolved_positions, + } + + +__all__ = [ + "DumbMoneyKalshiReconciliationSweeper", + "SignedReconciliationReader", +] diff --git a/live_firewall/dumbmoney_windows_service.py b/live_firewall/dumbmoney_windows_service.py new file mode 100644 index 00000000..bff5354e --- /dev/null +++ b/live_firewall/dumbmoney_windows_service.py @@ -0,0 +1,1857 @@ +"""Sealed private-local Windows runner for the Dummy/Kalshi venue cell. + +Importing this module performs no I/O, starts no threads, and makes no network +request. The production entry point accepts only release-manifest arguments, +loads one raw-byte SHA-256-pinned public config from ProgramData, reads secrets +only from Windows Credential Manager, and always publishes +``RECONCILIATION_ONLY`` readiness before the first network operation. +""" +from __future__ import annotations + +import argparse +import base64 +import ctypes +import hashlib +import json +import os +import re +import signal +import sys +import threading +import uuid +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any, Protocol +from urllib.parse import urlsplit + +import httpx +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, +) + +from live_firewall.dumbmoney_capital import CapitalEnvelopeAdapter +from live_firewall.dumbmoney_command_feed import ( + CommandFeedResponse, + CoreCommandFeedConsumer, +) +from live_firewall.dumbmoney_execution_cycle import ( + SealedDisabledExecutionCycle, +) +from live_firewall.dumbmoney_lineage import ( + ContractResolutionResponse, + CoreAuthorityContractResolver, +) +from live_firewall.dumbmoney_journal_anchor import ( + CoreJournalAnchorClient, + JournalAnchorResponse, +) +from live_firewall.dumbmoney_reconciliation import ( + DumbMoneyKalshiReconciliationSweeper, +) +from live_firewall.exposure_tracker import ( + EXPOSURE_STATE_ANCHOR_SCHEMA, + ExposureTracker, +) +from live_firewall.kalshi_broker_truth import ( + KalshiBrokerTruthError, + KalshiBrokerTruthProvider, +) +from live_firewall.kalshi_reconciliation import ( + KalshiReconciliationReader, +) +from live_firewall.operational_journal import ( + canonical_json, + sha256_json, +) +from live_firewall.sqlite_operational_journal import SQLiteOperationalJournal + + +SERVICE_NAME = "DumbMoneyDummyKalshi" +CONFIG_SCHEMA = "dummy.dumbmoney-kalshi-runner-config.v1" +READINESS_SCHEMA = "dumbmoney.readiness-descriptor.v1" +HEALTH_SCHEMA = "dumbmoney.health.v1" +SIGNED_ENVELOPE_SCHEMA = "dumbmoney.signed-envelope.v1" +START_MODE = "RECONCILIATION_ONLY" +CORE_ENDPOINT_REF = "endpoint-ref:DumbMoneyCore" +CELL_TOKEN_TARGET_REF = "credential-target:DumbMoney/DummyCellToken" +KALSHI_KEY_ID_TARGET_REF = "credential-target:DumbMoney/KalshiApiKeyId" +KALSHI_PRIVATE_KEY_TARGET_REF = ( + "credential-target:DumbMoney/KalshiPrivateKeyPem" +) +READINESS_KEY_TARGET_REF = ( + "credential-target:DumbMoney/DummyReadinessEd25519" +) +READINESS_REF = "readiness-ref:DumbMoneyDummyKalshi" +CONFIG_RELATIVE_PATH = ( + Path("DumbMoney") / "config" / "dummy-kalshi-runner.v1.json" +) +CORE_READINESS_RELATIVE_PATH = ( + Path("DumbMoney") / "readiness" / "DumbMoneyCore.json" +) +READINESS_RELATIVE_PATH = ( + Path("DumbMoney") / "readiness" / f"{SERVICE_NAME}.json" +) +DATA_ROOT_RELATIVE_PATH = Path("DumbMoney") / "dummy-kalshi" +MAXIMUM_CONFIG_BYTES = 262_144 +MAXIMUM_READINESS_BYTES = 262_144 +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_TARGET_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$") +_KEY_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,255}$") +_CONFIG_FIELDS = { + "schema", + "service_name", + "release_id", + "core_endpoint_ref", + "core_readiness_path", + "core_cell_token_target", + "kalshi_key_id_target", + "kalshi_private_key_target", + "readiness_signing_key_target", + "start_mode", + "data_root", + "readiness_ref", + "readiness_path", + "core_public_keys_base64url", + "operator_public_keys_base64url", + "promoter_public_keys_base64url", + "research_public_keys_base64url", + "evaluator_public_keys_base64url", + "expected_account_hash", + "kalshi_subaccount_number", + "fund_lock_sha256", + "service_manifest_sha256", + "role_public_keys_sha256", + "core_runner_config_sha256", + "risk_policy_sha256", + "readiness_signer_public_key_sha256", + "poll_interval_seconds", + "readiness_ttl_seconds", + "broker_truth_max_age_seconds", +} +_CORE_READINESS_FIELDS = { + "schema", + "service_name", + "release_id", + "instance_id", + "process_id", + "generation", + "observed_at", + "valid_until", + "endpoint", + "fund_lock_sha256", + "service_manifest_sha256", + "authority", + "health", + "capabilities", +} + + +class DumbMoneyWindowsServiceError(RuntimeError): + """A sealed runner invariant failed closed.""" + + +class CredentialProvider(Protocol): + def read_bytes(self, target: str) -> bytes: ... + + +class BrokerTruthProvider(Protocol): + def snapshot(self) -> Mapping[str, Any]: ... + + +class ExecutionCycle(Protocol): + def __call__( + self, + *, + capital_adapter: CapitalEnvelopeAdapter, + command_feed: CoreCommandFeedConsumer, + broker_snapshot: Mapping[str, Any], + ) -> Mapping[str, Any]: ... + + +class WindowsCredentialManager: + """Read one Windows generic credential without logging its value.""" + + CRED_TYPE_GENERIC = 1 + + def read_bytes(self, target: str) -> bytes: + if not isinstance(target, str) or not _TARGET_RE.fullmatch(target): + raise DumbMoneyWindowsServiceError( + "Windows credential target is invalid" + ) + if os.name != "nt": + raise DumbMoneyWindowsServiceError( + "Windows Credential Manager is required" + ) + from ctypes import wintypes + + class FileTime(ctypes.Structure): + _fields_ = [ + ("low_datetime", wintypes.DWORD), + ("high_datetime", wintypes.DWORD), + ] + + class CredentialAttribute(ctypes.Structure): + _fields_ = [ + ("keyword", wintypes.LPWSTR), + ("flags", wintypes.DWORD), + ("value_size", wintypes.DWORD), + ("value", ctypes.POINTER(ctypes.c_ubyte)), + ] + + class Credential(ctypes.Structure): + _fields_ = [ + ("flags", wintypes.DWORD), + ("credential_type", wintypes.DWORD), + ("target_name", wintypes.LPWSTR), + ("comment", wintypes.LPWSTR), + ("last_written", FileTime), + ("credential_blob_size", wintypes.DWORD), + ("credential_blob", ctypes.POINTER(ctypes.c_ubyte)), + ("persist", wintypes.DWORD), + ("attribute_count", wintypes.DWORD), + ("attributes", ctypes.POINTER(CredentialAttribute)), + ("target_alias", wintypes.LPWSTR), + ("user_name", wintypes.LPWSTR), + ] + + pointer = ctypes.POINTER(Credential)() + advapi32 = ctypes.WinDLL("Advapi32.dll", use_last_error=True) + cred_read = advapi32.CredReadW + cred_read.argtypes = [ + wintypes.LPCWSTR, + wintypes.DWORD, + wintypes.DWORD, + ctypes.POINTER(ctypes.POINTER(Credential)), + ] + cred_read.restype = wintypes.BOOL + cred_free = advapi32.CredFree + cred_free.argtypes = [ctypes.c_void_p] + cred_free.restype = None + if not cred_read( + target, + self.CRED_TYPE_GENERIC, + 0, + ctypes.byref(pointer), + ): + code = ctypes.get_last_error() + raise DumbMoneyWindowsServiceError( + f"required Windows credential is unavailable; winerror={code}" + ) + try: + credential = pointer.contents + size = int(credential.credential_blob_size) + if not 1 <= size <= 65_536: + raise DumbMoneyWindowsServiceError( + "Windows credential blob size is invalid" + ) + return ctypes.string_at(credential.credential_blob, size) + finally: + cred_free(pointer) + + +@dataclass(frozen=True) +class DummyKalshiRunnerConfig: + release_id: str + core_readiness_path: Path + data_root: Path + readiness_path: Path + core_public_keys: Mapping[str, bytes] + operator_public_keys: Mapping[str, bytes] + promoter_public_keys: Mapping[str, bytes] + research_public_keys: Mapping[str, bytes] + evaluator_public_keys: Mapping[str, bytes] + expected_account_hash: str + kalshi_subaccount_number: int + fund_lock_sha256: str + service_manifest_sha256: str + role_public_keys_sha256: str + core_runner_config_sha256: str + risk_policy_sha256: str + readiness_signer_public_key_sha256: str + poll_interval_seconds: int + readiness_ttl_seconds: int + broker_truth_max_age_seconds: int + config_sha256: str + + +@dataclass(frozen=True) +class RunnerSecrets: + cell_token: str + kalshi_key_id: str + kalshi_private_key_pem: bytes + readiness_private_key: Ed25519PrivateKey + + +@dataclass(frozen=True) +class CoreEndpoint: + base_url: str + instance_id: str + observed_at: str + valid_until: str + + +@dataclass +class RunnerState: + mode: str = START_MODE + health_status: str = "BLOCKED" + reason: str = "RECONCILIATION_REQUIRED" + core_ready: bool = False + broker_truth_fresh: bool = False + rollback_anchor_current: bool = False + reconciled_once: bool = False + execution_enabled: bool = False + unresolved_open_orders: int = 0 + unresolved_positions: int = 0 + last_cycle_status: str = "not_started" + + +def _strict_json(raw: bytes, *, label: str) -> dict[str, Any]: + def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise DumbMoneyWindowsServiceError( + f"{label} contains a duplicate JSON key" + ) + result[key] = value + return result + + try: + value = json.loads( + raw.decode("utf-8"), + object_pairs_hook=reject_duplicates, + parse_constant=lambda token: (_ for _ in ()).throw( + DumbMoneyWindowsServiceError( + f"{label} contains non-finite value {token}" + ) + ), + ) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise DumbMoneyWindowsServiceError( + f"{label} is not valid UTF-8 JSON" + ) from exc + if not isinstance(value, dict): + raise DumbMoneyWindowsServiceError(f"{label} must be a JSON object") + return value + + +def _digest(value: Any, *, field: str) -> str: + if not isinstance(value, str) or not _SHA256_RE.fullmatch(value): + raise DumbMoneyWindowsServiceError( + f"{field} must be a lowercase sha256" + ) + return value + + +def _public_key_map( + value: Any, + *, + role: str, + maximum_keys: int = 16, +) -> dict[str, bytes]: + if not isinstance(value, dict) or not 1 <= len(value) <= maximum_keys: + raise DumbMoneyWindowsServiceError( + f"{role} public-key map must contain 1 to {maximum_keys} keys" + ) + result: dict[str, bytes] = {} + for raw_key_id, raw_encoded in value.items(): + key_id = _digest(raw_key_id, field=f"{role} key id") + if ( + not isinstance(raw_encoded, str) + or "=" in raw_encoded + or re.search(r"[^A-Za-z0-9_-]", raw_encoded) + ): + raise DumbMoneyWindowsServiceError( + f"{role} public key encoding is invalid" + ) + try: + key = base64.urlsafe_b64decode( + raw_encoded + "=" * (-len(raw_encoded) % 4) + ) + except Exception as exc: + raise DumbMoneyWindowsServiceError( + f"{role} public key encoding is invalid" + ) from exc + if ( + len(key) != 32 + or hashlib.sha256(key).hexdigest() != key_id + ): + raise DumbMoneyWindowsServiceError( + f"{role} key id does not bind its Ed25519 key" + ) + result[key_id] = key + return result + + +def _fixed_path(value: Any, *, expected: Path, field: str) -> Path: + if not isinstance(value, str) or not value: + raise DumbMoneyWindowsServiceError(f"{field} must be an absolute path") + path = Path(value) + if not path.is_absolute() or path.resolve() != expected.resolve(): + raise DumbMoneyWindowsServiceError( + f"{field} differs from the fixed ProgramData path" + ) + return path.resolve() + + +def load_runner_config( + path: Path, + expected_sha256: str, + program_data: Path, +) -> DummyKalshiRunnerConfig: + """Load the fixed public config only after exact raw-byte pinning.""" + expected_digest = _digest(expected_sha256, field="config_sha256") + fixed = (program_data / CONFIG_RELATIVE_PATH).resolve() + if path.resolve() != fixed: + raise DumbMoneyWindowsServiceError( + "runner config path is not the fixed ProgramData path" + ) + try: + raw = path.read_bytes() + except OSError as exc: + raise DumbMoneyWindowsServiceError( + "pinned public runner config is unavailable" + ) from exc + if not 1 <= len(raw) <= MAXIMUM_CONFIG_BYTES: + raise DumbMoneyWindowsServiceError("runner config size is invalid") + if hashlib.sha256(raw).hexdigest() != expected_digest: + raise DumbMoneyWindowsServiceError( + "runner config does not match --config-sha256" + ) + payload = _strict_json(raw, label="runner config") + if set(payload) != _CONFIG_FIELDS: + raise DumbMoneyWindowsServiceError("runner config fields mismatch") + fixed_values = { + "schema": CONFIG_SCHEMA, + "service_name": SERVICE_NAME, + "core_endpoint_ref": CORE_ENDPOINT_REF, + "core_cell_token_target": CELL_TOKEN_TARGET_REF, + "kalshi_key_id_target": KALSHI_KEY_ID_TARGET_REF, + "kalshi_private_key_target": KALSHI_PRIVATE_KEY_TARGET_REF, + "readiness_signing_key_target": READINESS_KEY_TARGET_REF, + "start_mode": START_MODE, + "readiness_ref": READINESS_REF, + } + for field, expected in fixed_values.items(): + if payload[field] != expected: + raise DumbMoneyWindowsServiceError( + f"runner config {field} is not sealed" + ) + release_id = payload["release_id"] + if ( + not isinstance(release_id, str) + or release_id != release_id.strip() + or not release_id + ): + raise DumbMoneyWindowsServiceError("release_id is invalid") + core = _public_key_map( + payload["core_public_keys_base64url"], + role="Core", + ) + operator = _public_key_map( + payload["operator_public_keys_base64url"], + role="operator", + ) + promoter = _public_key_map( + payload["promoter_public_keys_base64url"], + role="promoter", + ) + research = _public_key_map( + payload["research_public_keys_base64url"], + role="research", + ) + evaluator = _public_key_map( + payload["evaluator_public_keys_base64url"], + role="evaluator", + maximum_keys=32, + ) + role_sets = [ + set(core), + set(operator), + set(promoter), + set(research), + set(evaluator), + ] + if len(set().union(*role_sets)) != sum(len(item) for item in role_sets): + raise DumbMoneyWindowsServiceError( + "Core/operator/promoter/research/evaluator signing roles overlap" + ) + subaccount = payload["kalshi_subaccount_number"] + if ( + isinstance(subaccount, bool) + or not isinstance(subaccount, int) + or subaccount != 0 + ): + raise DumbMoneyWindowsServiceError( + "kalshi_subaccount_number must be 0 until end-to-end " + "subaccount routing is sealed" + ) + poll_interval = payload["poll_interval_seconds"] + ttl = payload["readiness_ttl_seconds"] + truth_age = payload["broker_truth_max_age_seconds"] + if ( + isinstance(poll_interval, bool) + or not isinstance(poll_interval, int) + or not 5 <= poll_interval <= 300 + ): + raise DumbMoneyWindowsServiceError( + "poll_interval_seconds must be from 5 through 300" + ) + if ( + isinstance(ttl, bool) + or not isinstance(ttl, int) + or not 10 <= ttl <= 120 + ): + raise DumbMoneyWindowsServiceError( + "readiness_ttl_seconds must be from 10 through 120" + ) + if ( + isinstance(truth_age, bool) + or not isinstance(truth_age, int) + or not 5 <= truth_age <= 120 + ): + raise DumbMoneyWindowsServiceError( + "broker_truth_max_age_seconds must be from 5 through 120" + ) + return DummyKalshiRunnerConfig( + release_id=release_id, + core_readiness_path=_fixed_path( + payload["core_readiness_path"], + expected=program_data / CORE_READINESS_RELATIVE_PATH, + field="core_readiness_path", + ), + data_root=_fixed_path( + payload["data_root"], + expected=program_data / DATA_ROOT_RELATIVE_PATH, + field="data_root", + ), + readiness_path=_fixed_path( + payload["readiness_path"], + expected=program_data / READINESS_RELATIVE_PATH, + field="readiness_path", + ), + core_public_keys=core, + operator_public_keys=operator, + promoter_public_keys=promoter, + research_public_keys=research, + evaluator_public_keys=evaluator, + expected_account_hash=_digest( + payload["expected_account_hash"], + field="expected_account_hash", + ), + kalshi_subaccount_number=subaccount, + fund_lock_sha256=_digest( + payload["fund_lock_sha256"], + field="fund_lock_sha256", + ), + service_manifest_sha256=_digest( + payload["service_manifest_sha256"], + field="service_manifest_sha256", + ), + role_public_keys_sha256=_digest( + payload["role_public_keys_sha256"], + field="role_public_keys_sha256", + ), + core_runner_config_sha256=_digest( + payload["core_runner_config_sha256"], + field="core_runner_config_sha256", + ), + risk_policy_sha256=_digest( + payload["risk_policy_sha256"], + field="risk_policy_sha256", + ), + readiness_signer_public_key_sha256=_digest( + payload["readiness_signer_public_key_sha256"], + field="readiness_signer_public_key_sha256", + ), + poll_interval_seconds=poll_interval, + readiness_ttl_seconds=ttl, + broker_truth_max_age_seconds=truth_age, + config_sha256=expected_digest, + ) + + +def kalshi_account_hash(key_id: str, subaccount_number: int) -> str: + """Bind public config to the exact Credential Manager key and subaccount.""" + if not isinstance(key_id, str) or not _KEY_ID_RE.fullmatch(key_id): + raise DumbMoneyWindowsServiceError("Kalshi API key id is invalid") + if ( + isinstance(subaccount_number, bool) + or not isinstance(subaccount_number, int) + or subaccount_number != 0 + ): + raise DumbMoneyWindowsServiceError( + "Kalshi subaccount must be 0 until end-to-end routing is sealed" + ) + return hashlib.sha256( + canonical_json( + { + "schema": "dummy.kalshi-account-binding.v1", + "venue": "dummy_kalshi", + "api_key_id": key_id, + "subaccount_number": subaccount_number, + } + ).encode("utf-8") + ).hexdigest() + + +def _credential_target(reference: str, expected: str) -> str: + if reference != expected or not reference.startswith("credential-target:"): + raise DumbMoneyWindowsServiceError( + "credential reference is not the sealed target" + ) + return reference.removeprefix("credential-target:") + + +def _secret_text(raw: bytes, *, label: str, minimum: int = 16) -> str: + try: + value = raw.decode("utf-8") + except UnicodeDecodeError as exc: + raise DumbMoneyWindowsServiceError( + f"{label} credential is not UTF-8" + ) from exc + if ( + not minimum <= len(value) <= 65_536 + or value != value.strip() + or "\0" in value + ): + raise DumbMoneyWindowsServiceError( + f"{label} credential shape is invalid" + ) + return value + + +def _readiness_private_key(raw: bytes) -> Ed25519PrivateKey: + if len(raw) == 32: + return Ed25519PrivateKey.from_private_bytes(raw) + try: + key = serialization.load_pem_private_key(raw, password=None) + except (TypeError, ValueError) as exc: + raise DumbMoneyWindowsServiceError( + "readiness signing credential is not a private key" + ) from exc + if not isinstance(key, Ed25519PrivateKey): + raise DumbMoneyWindowsServiceError( + "readiness signing credential must be Ed25519" + ) + return key + + +def load_secrets( + provider: CredentialProvider, + config: DummyKalshiRunnerConfig, +) -> RunnerSecrets: + """Load exactly four sealed targets; there is no env or file fallback.""" + cell_token = _secret_text( + provider.read_bytes( + _credential_target( + CELL_TOKEN_TARGET_REF, + CELL_TOKEN_TARGET_REF, + ) + ), + label="cell bearer", + minimum=32, + ) + key_id = _secret_text( + provider.read_bytes( + _credential_target( + KALSHI_KEY_ID_TARGET_REF, + KALSHI_KEY_ID_TARGET_REF, + ) + ), + label="Kalshi key id", + minimum=8, + ) + if not _KEY_ID_RE.fullmatch(key_id): + raise DumbMoneyWindowsServiceError("Kalshi key id is invalid") + private_pem = provider.read_bytes( + _credential_target( + KALSHI_PRIVATE_KEY_TARGET_REF, + KALSHI_PRIVATE_KEY_TARGET_REF, + ) + ) + if not 128 <= len(private_pem) <= 65_536: + raise DumbMoneyWindowsServiceError( + "Kalshi private-key credential size is invalid" + ) + try: + kalshi_private_key = serialization.load_pem_private_key( + private_pem, + password=None, + ) + except (TypeError, ValueError) as exc: + raise DumbMoneyWindowsServiceError( + "Kalshi private-key credential is invalid" + ) from exc + if ( + not isinstance(kalshi_private_key, RSAPrivateKey) + or kalshi_private_key.key_size < 2_048 + ): + raise DumbMoneyWindowsServiceError( + "Kalshi private-key credential must be RSA with at least 2048 bits" + ) + readiness = _readiness_private_key( + provider.read_bytes( + _credential_target( + READINESS_KEY_TARGET_REF, + READINESS_KEY_TARGET_REF, + ) + ) + ) + readiness_public = readiness.public_key().public_bytes_raw() + if ( + hashlib.sha256(readiness_public).hexdigest() + != config.readiness_signer_public_key_sha256 + ): + raise DumbMoneyWindowsServiceError( + "readiness signing credential does not match public config" + ) + if ( + kalshi_account_hash(key_id, config.kalshi_subaccount_number) + != config.expected_account_hash + ): + raise DumbMoneyWindowsServiceError( + "Kalshi Credential Manager key/subaccount differs from account pin" + ) + return RunnerSecrets( + cell_token=cell_token, + kalshi_key_id=key_id, + kalshi_private_key_pem=bytes(private_pem), + readiness_private_key=readiness, + ) + + +def _parse_utc(value: Any, *, field: str) -> datetime: + if not isinstance(value, str) or not value.endswith("Z"): + raise DumbMoneyWindowsServiceError(f"{field} must be RFC3339 UTC") + try: + parsed = datetime.fromisoformat(value[:-1] + "+00:00") + except ValueError as exc: + raise DumbMoneyWindowsServiceError( + f"{field} must be RFC3339 UTC" + ) from exc + return parsed.astimezone(timezone.utc) + + +def _format_utc(value: datetime) -> str: + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _core_endpoint_from_readiness( + config: DummyKalshiRunnerConfig, + *, + now: datetime, +) -> CoreEndpoint: + try: + raw = config.core_readiness_path.read_bytes() + except OSError as exc: + raise DumbMoneyWindowsServiceError( + "signed Core readiness is unavailable" + ) from exc + if not 1 <= len(raw) <= MAXIMUM_READINESS_BYTES: + raise DumbMoneyWindowsServiceError( + "signed Core readiness size is invalid" + ) + envelope = _strict_json(raw, label="signed Core readiness") + from live_firewall.dumbmoney_capital import verify_signed_envelope + + try: + signed = verify_signed_envelope( + envelope, + trusted_public_keys=config.core_public_keys, + now=now, + expected_body_schema=READINESS_SCHEMA, + max_ttl=timedelta(seconds=120), + ) + except ValueError as exc: + raise DumbMoneyWindowsServiceError( + f"signed Core readiness is invalid: {exc}" + ) from exc + body = signed.body + if set(body) != _CORE_READINESS_FIELDS: + raise DumbMoneyWindowsServiceError( + "Core readiness descriptor fields mismatch" + ) + if ( + body["schema"] != READINESS_SCHEMA + or body["service_name"] != "DumbMoneyCore" + or body["release_id"] != config.release_id + ): + raise DumbMoneyWindowsServiceError( + "Core readiness identity or release mismatch" + ) + observed = _parse_utc(body["observed_at"], field="Core observed_at") + valid = _parse_utc(body["valid_until"], field="Core valid_until") + if ( + not observed <= now < valid + or signed.not_before != observed + or signed.expires_at != valid + ): + raise DumbMoneyWindowsServiceError( + "Core readiness window is invalid" + ) + if ( + body["fund_lock_sha256"] != config.fund_lock_sha256 + or body["service_manifest_sha256"] + != config.service_manifest_sha256 + ): + raise DumbMoneyWindowsServiceError("Core readiness release pin mismatch") + authority = body["authority"] + if authority != { + "broker": "NONE", + "mode": "OFFLINE", + "execution_enabled": False, + }: + raise DumbMoneyWindowsServiceError( + "Core readiness claims broker authority" + ) + health = body["health"] + if not isinstance(health, dict): + raise DumbMoneyWindowsServiceError("Core health is invalid") + expected_health = { + "role_public_keys_sha256": config.role_public_keys_sha256, + "runner_config_sha256": config.core_runner_config_sha256, + "risk_policy_sha256": config.risk_policy_sha256, + } + for field, expected in expected_health.items(): + if health.get(field) != expected: + raise DumbMoneyWindowsServiceError( + f"Core health pin mismatch: {field}" + ) + if health.get("status") not in {"READY", "DEGRADED"}: + raise DumbMoneyWindowsServiceError("Core readiness is blocked") + endpoint = body["endpoint"] + if not isinstance(endpoint, dict) or set(endpoint) != { + "transport", + "host", + "port", + "base_path", + }: + raise DumbMoneyWindowsServiceError("Core endpoint is invalid") + host = endpoint["host"] + port = endpoint["port"] + if ( + endpoint["transport"] != "http" + or host not in {"127.0.0.1", "::1"} + or endpoint["base_path"] != "/" + or isinstance(port, bool) + or not isinstance(port, int) + or not 1024 <= port <= 65_535 + ): + raise DumbMoneyWindowsServiceError( + "Core endpoint is not literal loopback HTTP" + ) + instance_id = body["instance_id"] + try: + if str(uuid.UUID(instance_id)) != instance_id: + raise ValueError + except (AttributeError, ValueError) as exc: + raise DumbMoneyWindowsServiceError( + "Core instance id is invalid" + ) from exc + base = f"http://[{host}]:{port}" if host == "::1" else f"http://{host}:{port}" + return CoreEndpoint( + base_url=base, + instance_id=instance_id, + observed_at=body["observed_at"], + valid_until=body["valid_until"], + ) + + +class LoopbackCoreTransport: + """No-proxy, no-redirect transport pinned to a validated Core endpoint.""" + + def __init__(self) -> None: + self._endpoint: CoreEndpoint | None = None + self._client = httpx.Client( + timeout=5.0, + follow_redirects=False, + trust_env=False, + ) + + def set_endpoint(self, endpoint: CoreEndpoint) -> None: + parsed = urlsplit(endpoint.base_url) + if ( + parsed.scheme != "http" + or parsed.hostname not in {"127.0.0.1", "::1"} + or parsed.username is not None + or parsed.password is not None + ): + raise DumbMoneyWindowsServiceError( + "Core transport endpoint is not literal loopback" + ) + self._endpoint = endpoint + + def _get( + self, + path: str, + headers: Mapping[str, str], + ) -> tuple[int, bytes]: + endpoint = self._endpoint + if endpoint is None: + raise DumbMoneyWindowsServiceError( + "Core endpoint has not been authenticated" + ) + if ( + not isinstance(path, str) + or not path.startswith("/") + or path.startswith("//") + or "\\" in path + ): + raise DumbMoneyWindowsServiceError("Core request path is invalid") + response = self._client.get( + f"{endpoint.base_url}{path}", + headers=dict(headers), + ) + return response.status_code, bytes(response.content) + + def command_get( + self, + path: str, + headers: Mapping[str, str], + ) -> CommandFeedResponse: + status, body = self._get(path, headers) + return CommandFeedResponse( + status_code=status, + body=body, + ) + + def contract_get( + self, + path: str, + headers: Mapping[str, str], + ) -> ContractResolutionResponse: + status, body = self._get(path, headers) + return ContractResolutionResponse( + status_code=status, + body=body, + ) + + def journal_anchor_post( + self, + path: str, + headers: Mapping[str, str], + body: bytes, + ) -> JournalAnchorResponse: + endpoint = self._endpoint + if endpoint is None: + raise DumbMoneyWindowsServiceError( + "Core endpoint has not been authenticated" + ) + if ( + path + != "/v1/cells/dummy_kalshi/journal-heads:anchor" + or not isinstance(body, bytes) + or not 1 <= len(body) <= 262_144 + ): + raise DumbMoneyWindowsServiceError( + "Core journal anchor request is invalid" + ) + response = self._client.post( + f"{endpoint.base_url}{path}", + headers=dict(headers), + content=body, + ) + return JournalAnchorResponse( + status_code=response.status_code, + body=bytes(response.content), + ) + + def liveness(self) -> None: + endpoint = self._endpoint + if endpoint is None: + raise DumbMoneyWindowsServiceError( + "Core endpoint has not been authenticated" + ) + response = self._client.get( + f"{endpoint.base_url}/health/live", + headers={"Accept": "application/json"}, + ) + if response.status_code != 200 or len(response.content) > 16_384: + raise DumbMoneyWindowsServiceError("Core liveness probe failed") + if _strict_json( + bytes(response.content), + label="Core liveness", + ) != {"schema": HEALTH_SCHEMA, "status": "LIVE"}: + raise DumbMoneyWindowsServiceError( + "Core liveness payload is invalid" + ) + + def close(self) -> None: + self._client.close() + + +def _atomic_write_json(path: Path, payload: Mapping[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name( + f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp" + ) + encoded = canonical_json(dict(payload)).encode("utf-8") + b"\n" + try: + with temporary.open("xb") as handle: + handle.write(encoded) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, path) + finally: + temporary.unlink(missing_ok=True) + + +class _HealthServer: + def __init__(self, state_reader: Callable[[], RunnerState]) -> None: + read_state = state_reader + + class Handler(BaseHTTPRequestHandler): + def do_GET(self) -> None: # noqa: N802 + if self.path == "/health/live": + self._write( + 200, + {"schema": HEALTH_SCHEMA, "status": "LIVE"}, + ) + return + if self.path == "/health/ready": + state = read_state() + ready = ( + state.health_status == "READY" + and state.reconciled_once + ) + self._write( + 200 if ready else 503, + { + "schema": HEALTH_SCHEMA, + "status": "READY" if ready else "NOT_READY", + "reason_codes": [] if ready else [state.reason], + }, + ) + return + self._write( + 404, + {"schema": HEALTH_SCHEMA, "status": "NOT_FOUND"}, + ) + + def _write(self, status: int, payload: Mapping[str, Any]) -> None: + encoded = canonical_json(dict(payload)).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", str(len(encoded))) + self.end_headers() + self.wfile.write(encoded) + + def log_message(self, _format: str, *_args: Any) -> None: + return + + self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + self.thread = threading.Thread( + target=self.server.serve_forever, + name=f"{SERVICE_NAME}-health", + daemon=True, + ) + + @property + def port(self) -> int: + return int(self.server.server_address[1]) + + def start(self) -> None: + self.thread.start() + + def close(self) -> None: + self.server.shutdown() + self.server.server_close() + self.thread.join(timeout=5.0) + + +class DumbMoneyDummyKalshiService: + """Continuous control/reconciliation supervisor; never a second order sink.""" + + def __init__( + self, + config: DummyKalshiRunnerConfig, + secrets: RunnerSecrets, + *, + core_transport: LoopbackCoreTransport, + broker_truth: BrokerTruthProvider, + execution_cycle: ExecutionCycle | None = None, + reconciliation_sweeper: ( + DumbMoneyKalshiReconciliationSweeper | None + ) = None, + journal_anchor_client: CoreJournalAnchorClient | None = None, + clock: Callable[[], datetime] = lambda: datetime.now(timezone.utc), + ) -> None: + self.config = config + self.secrets = secrets + self.core_transport = core_transport + self.broker_truth = broker_truth + self.execution_cycle = execution_cycle + self._reconciliation_sweeper = reconciliation_sweeper + self._journal_anchor_client = journal_anchor_client + self.clock = clock + self.instance_id = str(uuid.uuid4()) + self._state = RunnerState() + self._state_lock = threading.Lock() + self._readiness_sequence = 0 + self._health = _HealthServer(self.state) + self._components_ready = False + self._capital_adapter: CapitalEnvelopeAdapter | None = None + self._command_feed: CoreCommandFeedConsumer | None = None + self._capital_journal: SQLiteOperationalJournal | None = None + self._command_journal: SQLiteOperationalJournal | None = None + self._exposure_tracker: ExposureTracker | None = None + + def state(self) -> RunnerState: + with self._state_lock: + return RunnerState(**self._state.__dict__) + + def _set_state(self, **changes: Any) -> None: + with self._state_lock: + for field, value in changes.items(): + setattr(self._state, field, value) + + def _clock(self) -> datetime: + now = self.clock() + if now.tzinfo is None or now.utcoffset() is None: + raise DumbMoneyWindowsServiceError( + "runner clock must be timezone-aware" + ) + return now.astimezone(timezone.utc) + + def start(self) -> None: + """Publish blocked readiness without any Core or broker call.""" + self.config.data_root.mkdir(parents=True, exist_ok=True) + self.config.readiness_path.parent.mkdir(parents=True, exist_ok=True) + self._health.start() + self.write_readiness() + + def close(self) -> None: + try: + broker_close = getattr(self.broker_truth, "close", None) + if callable(broker_close): + broker_close() + finally: + try: + self._health.close() + finally: + try: + if self._command_journal is not None: + self._command_journal.close() + self._command_journal = None + finally: + try: + if self._capital_journal is not None: + self._capital_journal.close() + self._capital_journal = None + finally: + self.core_transport.close() + + def _initialize_components(self) -> None: + if self._components_ready: + return + capital_journal = SQLiteOperationalJournal( + self.config.data_root / "capital-operational.db", + now_fn=self._clock, + ) + if not capital_journal.healthy: + capital_journal.close() + raise DumbMoneyWindowsServiceError( + "capital operational journal is unhealthy" + ) + command_journal: SQLiteOperationalJournal | None = None + try: + resolver = CoreAuthorityContractResolver( + transport=self.core_transport.contract_get, + cell_token_provider=lambda: self.secrets.cell_token, + trusted_core_public_keys=self.config.core_public_keys, + trusted_research_public_keys=self.config.research_public_keys, + trusted_promoter_public_keys=self.config.promoter_public_keys, + trusted_evaluator_public_keys=self.config.evaluator_public_keys, + now_fn=self._clock, + ) + adapter = CapitalEnvelopeAdapter( + journal=capital_journal, + trusted_public_keys=self.config.core_public_keys, + expected_venue="dummy_kalshi", + expected_account_hash=self.config.expected_account_hash, + lineage_resolver=resolver, + trusted_broker_witness_public_keys={ + self.config.readiness_signer_public_key_sha256: ( + self.secrets.readiness_private_key.public_key() + ) + }, + now_fn=self._clock, + max_bootstrap_age=timedelta( + seconds=self.config.broker_truth_max_age_seconds + ), + ) + + def reduce_authority(_command: Any) -> None: + self._set_state( + mode=START_MODE, + health_status="BLOCKED", + reason="CORE_AUTHORITY_REDUCED", + execution_enabled=False, + ) + + def fail_closed(reason: str) -> None: + self._set_state( + mode=START_MODE, + health_status="BLOCKED", + reason=reason, + core_ready=False, + execution_enabled=False, + ) + + command_journal = SQLiteOperationalJournal( + self.config.data_root / "command-feed.db", + now_fn=self._clock, + ) + if not command_journal.healthy: + raise DumbMoneyWindowsServiceError( + "command-feed operational journal is unhealthy" + ) + command_feed = CoreCommandFeedConsumer( + transport=self.core_transport.command_get, + cell_token_provider=lambda: self.secrets.cell_token, + state_journal=command_journal, + trusted_operator_public_keys=self.config.operator_public_keys, + trusted_checkpoint_public_keys=self.config.core_public_keys, + capital_adapter=adapter, + kill_handler=reduce_authority, + mode_handler=reduce_authority, + fail_closed_handler=fail_closed, + now_fn=self._clock, + ) + exposure = ExposureTracker( + persist=True, + state_path=( + self.config.data_root + / "live-exposure-state.json" + ), + ) + if not exposure.state_healthy: + raise DumbMoneyWindowsServiceError( + "live exposure projection is unhealthy" + ) + if ( + self._reconciliation_sweeper is None + and isinstance( + self.broker_truth, + KalshiBrokerTruthProvider, + ) + ): + reader = KalshiReconciliationReader( + broker_truth=self.broker_truth, + witness_signing_private_key=( + self.secrets.readiness_private_key + ), + ) + self._reconciliation_sweeper = ( + DumbMoneyKalshiReconciliationSweeper( + capital_adapter=adapter, + broker_reader=reader, + exposure_tracker=exposure, + ) + ) + if self._journal_anchor_client is None: + self._journal_anchor_client = CoreJournalAnchorClient( + transport=self.core_transport.journal_anchor_post, + cell_token_provider=lambda: self.secrets.cell_token, + trusted_core_public_keys=self.config.core_public_keys, + expected_account_hash=( + self.config.expected_account_hash + ), + now_fn=self._clock, + ) + except BaseException: + if command_journal is not None: + command_journal.close() + capital_journal.close() + raise + self._capital_adapter = adapter + self._command_feed = command_feed + self._capital_journal = capital_journal + self._command_journal = command_journal + self._exposure_tracker = exposure + self._components_ready = True + + def _anchor_local_state(self) -> None: + client = self._journal_anchor_client + capital = self._capital_journal + command = self._command_journal + exposure = self._exposure_tracker + if ( + client is None + or capital is None + or command is None + or exposure is None + ): + raise DumbMoneyWindowsServiceError( + "rollback anchor components are unavailable" + ) + capital_sequence, capital_head = capital.head() + command_sequence, command_head = command.head() + exposure_sequence, exposure_head = exposure.anchor_head() + sources = ( + ( + "capital-operational", + "dummy.sqlite-operational-journal.v1", + capital_sequence, + capital_head, + ), + ( + "command-feed", + "dummy.sqlite-operational-journal.v1", + command_sequence, + command_head, + ), + ( + "live-exposure", + EXPOSURE_STATE_ANCHOR_SCHEMA, + exposure_sequence, + exposure_head, + ), + ) + for name, schema, sequence, head in sources: + client.anchor( + journal_name=name, + journal_schema=schema, + journal_sequence=sequence, + journal_head_sha256=head, + ) + + def _record_broker_truth(self, raw: Mapping[str, Any]) -> tuple[int, int]: + payload = json.loads(canonical_json(dict(raw))) + required = { + "schema", + "venue", + "account_hash", + "subaccount_number", + "observed_at", + "broker_snapshot_sha256", + "flat_book_observed", + "total_exposure_cents", + "open_order_count", + "market_exposure_cents", + "correlated_exposure_cents", + "unresolved_open_orders", + "unresolved_positions", + } + if set(payload) != required: + raise DumbMoneyWindowsServiceError( + "broker truth snapshot fields mismatch" + ) + if ( + payload["schema"] != "dummy.kalshi-broker-truth.v1" + or payload["venue"] != "dummy_kalshi" + or payload["account_hash"] != self.config.expected_account_hash + or payload["subaccount_number"] + != self.config.kalshi_subaccount_number + ): + raise DumbMoneyWindowsServiceError( + "broker truth identity mismatch" + ) + observed = _parse_utc( + payload["observed_at"], + field="broker truth observed_at", + ) + now = self._clock() + if ( + observed > now + timedelta(seconds=5) + or now - observed + > timedelta(seconds=self.config.broker_truth_max_age_seconds) + ): + raise DumbMoneyWindowsServiceError("broker truth is stale") + _digest( + payload["broker_snapshot_sha256"], + field="broker_snapshot_sha256", + ) + integers = ( + "total_exposure_cents", + "open_order_count", + "unresolved_open_orders", + "unresolved_positions", + ) + for field in integers: + value = payload[field] + if ( + isinstance(value, bool) + or not isinstance(value, int) + or value < 0 + ): + raise DumbMoneyWindowsServiceError( + f"broker truth {field} is invalid" + ) + unresolved_orders = payload["unresolved_open_orders"] + unresolved_positions = payload["unresolved_positions"] + flat = ( + payload["flat_book_observed"] is True + and payload["total_exposure_cents"] == 0 + and payload["open_order_count"] == 0 + and unresolved_orders == 0 + and unresolved_positions == 0 + and payload["market_exposure_cents"] == {} + and payload["correlated_exposure_cents"] == {} + ) + if payload["flat_book_observed"] is not flat: + raise DumbMoneyWindowsServiceError( + "broker flat-book projection is inconsistent" + ) + assert self._capital_adapter is not None + receipt = { + "schema": ( + "dummy.broker-bootstrap.flat.v1" + if flat + else "dummy.broker-bootstrap.inherited-exposure.v1" + ), + "receipt_id": sha256_json(payload), + "venue": payload["venue"], + "account_hash": payload["account_hash"], + "observed_at": payload["observed_at"], + "broker_snapshot_sha256": payload["broker_snapshot_sha256"], + "flat_book_observed": flat, + "total_exposure_cents": payload["total_exposure_cents"], + "open_order_count": payload["open_order_count"], + "market_exposure_cents": payload["market_exposure_cents"], + "correlated_exposure_cents": payload[ + "correlated_exposure_cents" + ], + } + self._capital_adapter.record_broker_bootstrap(receipt) + return unresolved_orders, unresolved_positions + + def run_once(self) -> Mapping[str, Any]: + """Poll controls and broker truth once; writes remain disabled on pass one.""" + now = self._clock() + prior = self.state() + try: + endpoint = _core_endpoint_from_readiness( + self.config, + now=now, + ) + self.core_transport.set_endpoint(endpoint) + self.core_transport.liveness() + self._initialize_components() + self._anchor_local_state() + assert self._command_feed is not None + poll = self._command_feed.poll_once() + core_ready = ( + poll.page_accepted + and poll.submission_allowed + and self._command_feed.submission_allowed() + ) + truth = self.broker_truth.snapshot() + unresolved_orders, unresolved_positions = ( + self._record_broker_truth(truth) + ) + if self._reconciliation_sweeper is not None: + sweep = self._reconciliation_sweeper.run_once() + unresolved_orders += int( + sweep["unresolved_reservations"] + ) + unresolved_positions += int( + sweep["unresolved_positions"] + ) + flat = unresolved_orders == 0 and unresolved_positions == 0 + reconciled = flat and core_ready + # Reconciliation-only is sticky for the first successful pass. + cycle = self.execution_cycle + may_execute = ( + prior.reconciled_once + and reconciled + and cycle is not None + ) + cycle_result: Mapping[str, Any] = { + "schema": "dummy.dumbmoney-execution-cycle.v1", + "status": "RECONCILIATION_COMPLETE" + if reconciled + else "RECONCILIATION_BLOCKED", + "broker_contacted": True, + "orders_submitted": 0, + "broker_snapshot_sha256": truth[ + "broker_snapshot_sha256" + ], + } + if may_execute: + assert self._capital_adapter is not None + assert cycle is not None + cycle_result = cycle( + capital_adapter=self._capital_adapter, + command_feed=self._command_feed, + broker_snapshot=truth, + ) + if set(cycle_result) != { + "schema", + "status", + "broker_contacted", + "orders_submitted", + "broker_snapshot_sha256", + }: + raise DumbMoneyWindowsServiceError( + "execution cycle result fields mismatch" + ) + if ( + cycle_result["schema"] + != "dummy.dumbmoney-execution-cycle.v1" + or cycle_result["status"] + not in {"COMPLETE", "BLOCKED"} + or not isinstance( + cycle_result["broker_contacted"], + bool, + ) + or isinstance(cycle_result["orders_submitted"], bool) + or not isinstance( + cycle_result["orders_submitted"], + int, + ) + or cycle_result["orders_submitted"] < 0 + or cycle_result["broker_snapshot_sha256"] + != truth["broker_snapshot_sha256"] + ): + raise DumbMoneyWindowsServiceError( + "execution cycle result is invalid" + ) + self._anchor_local_state() + execution_enabled = bool( + may_execute + and cycle_result.get("status") == "COMPLETE" + and unresolved_orders == 0 + and unresolved_positions == 0 + ) + cycle_blocked = ( + may_execute + and cycle_result.get("status") != "COMPLETE" + ) + reason = ( + "UNRESOLVED_BROKER_ORDERS_OR_POSITIONS" + if not flat + else ( + "CORE_CONTROLS_NOT_CURRENT" + if not core_ready + else ( + "EXECUTION_CYCLE_BLOCKED" + if cycle_blocked + else ( + "EXECUTION_CYCLE_NOT_SEALED" + if cycle is None + else ( + "INITIAL_RECONCILIATION_COMPLETE" + if not prior.reconciled_once + else "ALL_CURRENT_CONTROLS_ALLOW" + ) + ) + ) + ) + ) + self._set_state( + mode=( + "AGGRESSIVE_BOUNDED" + if execution_enabled + else START_MODE + ), + health_status=( + "READY" + if execution_enabled + else "BLOCKED" + ), + reason=reason, + core_ready=core_ready, + broker_truth_fresh=True, + rollback_anchor_current=True, + reconciled_once=reconciled, + execution_enabled=execution_enabled, + unresolved_open_orders=unresolved_orders, + unresolved_positions=unresolved_positions, + last_cycle_status=str( + cycle_result.get("status", "unknown") + ), + ) + self.write_readiness() + return cycle_result + except Exception as exc: + self._set_state( + mode=START_MODE, + health_status="BLOCKED", + reason=f"cycle_failed_closed:{type(exc).__name__}", + core_ready=False, + broker_truth_fresh=False, + rollback_anchor_current=False, + reconciled_once=False, + execution_enabled=False, + last_cycle_status="cycle_failed_closed", + ) + self.write_readiness() + raise + + def write_readiness(self) -> dict[str, Any]: + now = self._clock() + valid_until = now + timedelta( + seconds=self.config.readiness_ttl_seconds + ) + state = self.state() + self._readiness_sequence += 1 + body = { + "schema": READINESS_SCHEMA, + "service_name": SERVICE_NAME, + "release_id": self.config.release_id, + "instance_id": self.instance_id, + "process_id": os.getpid(), + "generation": self._readiness_sequence, + "observed_at": _format_utc(now), + "valid_until": _format_utc(valid_until), + "endpoint": { + "transport": "http", + "host": "127.0.0.1", + "port": self._health.port, + "base_path": "/", + }, + "fund_lock_sha256": self.config.fund_lock_sha256, + "service_manifest_sha256": ( + self.config.service_manifest_sha256 + ), + "authority": { + "broker": "KALSHI", + "mode": state.mode, + "execution_enabled": state.execution_enabled, + }, + "health": { + "status": state.health_status, + "reason_codes": [state.reason], + "core_ready": state.core_ready, + "broker_truth_fresh": state.broker_truth_fresh, + "rollback_anchor_current": ( + state.rollback_anchor_current + ), + "reconciled_once": state.reconciled_once, + "unresolved_open_orders": state.unresolved_open_orders, + "unresolved_positions": state.unresolved_positions, + "last_cycle_status": state.last_cycle_status, + "runner_config_sha256": self.config.config_sha256, + "readiness_public_key_sha256": ( + self.config.readiness_signer_public_key_sha256 + ), + }, + "capabilities": [ + "authenticated-promotion-resolution", + "broker-reconciliation", + "cell-command-checkpoints", + "canonical-live-firewall-only", + "core-journal-head-anchors", + ], + } + unsigned = { + "schema": SIGNED_ENVELOPE_SCHEMA, + "source_id": SERVICE_NAME, + "source_sequence": self._readiness_sequence, + "correlation_id": f"readiness-{self.instance_id}", + "causation_id": None, + "nonce": hashlib.sha256( + canonical_json( + { + "instance_id": self.instance_id, + "sequence": self._readiness_sequence, + "observed_at": body["observed_at"], + } + ).encode("utf-8") + ).hexdigest(), + "not_before": body["observed_at"], + "expires_at": body["valid_until"], + "body_schema": READINESS_SCHEMA, + "body_digest": sha256_json(body), + "body": body, + "signature_algorithm": "Ed25519", + "signer_key_id": ( + self.config.readiness_signer_public_key_sha256 + ), + } + unsigned["event_id"] = sha256_json(unsigned) + signature = self.secrets.readiness_private_key.sign( + canonical_json(unsigned).encode("utf-8") + ) + envelope = { + **unsigned, + "signature": base64.urlsafe_b64encode(signature) + .decode("ascii") + .rstrip("="), + } + _atomic_write_json(self.config.readiness_path, envelope) + return envelope + + def run_forever(self, stop_event: threading.Event) -> None: + self.start() + try: + while not stop_event.is_set(): + if stop_event.wait(self.config.poll_interval_seconds): + break + try: + self.run_once() + except Exception: + # run_once already persisted a short-lived signed blocked + # readiness descriptor. Continue polling for recovery. + continue + finally: + self.close() + + +class _DataRootLease: + def __init__(self, path: Path) -> None: + self.path = path + self.handle: Any = None + + def __enter__(self) -> "_DataRootLease": + self.path.parent.mkdir(parents=True, exist_ok=True) + self.handle = self.path.open("a+b") + try: + self.handle.seek(0) + import msvcrt + + msvcrt.locking(self.handle.fileno(), msvcrt.LK_NBLCK, 1) + except (ImportError, OSError) as exc: + self.handle.close() + raise DumbMoneyWindowsServiceError( + "another Dummy service owns the ProgramData root" + ) from exc + return self + + def __exit__(self, *_args: Any) -> None: + if self.handle is None or self.handle.closed: + return + try: + self.handle.seek(0) + import msvcrt + + msvcrt.locking(self.handle.fileno(), msvcrt.LK_UNLCK, 1) + finally: + self.handle.close() + + +def _program_data_path() -> Path: + value = os.environ.get("PROGRAMDATA", "") + if not value: + raise DumbMoneyWindowsServiceError( + "PROGRAMDATA is required for the sealed Windows service" + ) + path = Path(value).resolve() + if not path.is_absolute(): + raise DumbMoneyWindowsServiceError("PROGRAMDATA must be absolute") + return path + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog=SERVICE_NAME, + description="Sealed DumbMoney Dummy/Kalshi venue-cell service", + allow_abbrev=False, + ) + parser.add_argument("--core-endpoint-ref", required=True) + parser.add_argument("--core-cell-token-target", required=True) + parser.add_argument("--kalshi-key-id-target", required=True) + parser.add_argument("--kalshi-private-key-target", required=True) + parser.add_argument("--readiness-signing-key-target", required=True) + parser.add_argument("--start-mode", required=True) + parser.add_argument("--readiness-ref", required=True) + parser.add_argument("--config-sha256", required=True) + return parser + + +def _validate_cli(arguments: argparse.Namespace) -> None: + expected = { + "core_endpoint_ref": CORE_ENDPOINT_REF, + "core_cell_token_target": CELL_TOKEN_TARGET_REF, + "kalshi_key_id_target": KALSHI_KEY_ID_TARGET_REF, + "kalshi_private_key_target": KALSHI_PRIVATE_KEY_TARGET_REF, + "readiness_signing_key_target": READINESS_KEY_TARGET_REF, + "start_mode": START_MODE, + "readiness_ref": READINESS_REF, + } + for field, value in expected.items(): + if getattr(arguments, field) != value: + raise DumbMoneyWindowsServiceError( + f"sealed service argument mismatch: {field}" + ) + _digest(arguments.config_sha256, field="--config-sha256") + + +def main(argv: list[str] | None = None) -> int: + """Synchronous sealed-packaging entry point.""" + try: + arguments = _parser().parse_args(argv) + _validate_cli(arguments) + program_data = _program_data_path() + config = load_runner_config( + program_data / CONFIG_RELATIVE_PATH, + arguments.config_sha256, + program_data, + ) + secrets_value = load_secrets(WindowsCredentialManager(), config) + service = DumbMoneyDummyKalshiService( + config, + secrets_value, + core_transport=LoopbackCoreTransport(), + broker_truth=KalshiBrokerTruthProvider( + api_key_id=secrets_value.kalshi_key_id, + private_key_pem=secrets_value.kalshi_private_key_pem, + expected_account_hash=config.expected_account_hash, + subaccount_number=config.kalshi_subaccount_number, + ), + execution_cycle=SealedDisabledExecutionCycle(), + ) + stop_event = threading.Event() + + def stop(_signum: int, _frame: Any) -> None: + stop_event.set() + + for signal_name in ("SIGINT", "SIGTERM"): + candidate = getattr(signal, signal_name, None) + if candidate is not None: + signal.signal(candidate, stop) + with _DataRootLease(config.data_root / "service.lock"): + service.run_forever(stop_event) + return 0 + except KeyboardInterrupt: + return 0 + except ( + DumbMoneyWindowsServiceError, + KalshiBrokerTruthError, + OSError, + ValueError, + ) as exc: + print( + f"{SERVICE_NAME} failed closed: {type(exc).__name__}", + file=sys.stderr, + ) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) + + +__all__ = [ + "CELL_TOKEN_TARGET_REF", + "CONFIG_RELATIVE_PATH", + "CONFIG_SCHEMA", + "CORE_ENDPOINT_REF", + "DATA_ROOT_RELATIVE_PATH", + "DumbMoneyDummyKalshiService", + "DumbMoneyWindowsServiceError", + "DummyKalshiRunnerConfig", + "KALSHI_KEY_ID_TARGET_REF", + "KALSHI_PRIVATE_KEY_TARGET_REF", + "KalshiBrokerTruthProvider", + "LoopbackCoreTransport", + "READINESS_KEY_TARGET_REF", + "READINESS_REF", + "RunnerSecrets", + "SealedDisabledExecutionCycle", + "SERVICE_NAME", + "START_MODE", + "WindowsCredentialManager", + "kalshi_account_hash", + "load_runner_config", + "load_secrets", + "main", +] diff --git a/live_firewall/exposure_tracker.py b/live_firewall/exposure_tracker.py index ccce925b..3334be18 100644 --- a/live_firewall/exposure_tracker.py +++ b/live_firewall/exposure_tracker.py @@ -3,14 +3,24 @@ import json import math import os +from collections.abc import Callable from datetime import datetime, timezone, timedelta from pathlib import Path from typing import Any +from uuid import uuid4 +from core.interprocess_lock import InterprocessFileLock from core.ontology import Position +from autonomy.fees import kalshi_taker_fee_cents +from live_firewall.operational_journal import canonical_json, sha256_json DEFAULT_EXPOSURE_STATE_PATH = Path(__file__).resolve().parents[1] / "runtime" / "live_exposure_state.json" +EXPOSURE_STATE_ANCHOR_SCHEMA = "dummy.live-exposure-state.v1" +_FAIL_CLOSED_EXPOSURE_CENTS = 2**63 - 1 +_OPEN_ORDER_STATES = frozenset( + {"open", "submitting", "submit_outcome_unknown", "partially_filled"} +) class ExposureTracker: @@ -19,9 +29,15 @@ class ExposureTracker: def __init__(self, *, persist: bool = False, state_path: Path | None = None): configured_path = os.environ.get("DUMMY_EXPOSURE_STATE_PATH") self.state_path = state_path or (Path(configured_path) if configured_path else DEFAULT_EXPOSURE_STATE_PATH) + self._state_lock = InterprocessFileLock( + self.state_path.with_name(f"{self.state_path.name}.lock") + ) self.persist_enabled = persist + self._has_persisted_state = False + self._state_revision = 0 self.positions: dict[tuple[str, str], Position] = {} self.order_history: list[dict[str, Any]] = [] + self.reconciliation_history: list[dict[str, Any]] = [] self.open_orders: list[dict[str, Any]] = [] self.persistence_error: str | None = None if self.persist_enabled: @@ -35,6 +51,69 @@ def state_healthy(self) -> bool: def _position_key(position: Position) -> tuple[str, str]: return (position.contract_ticker, position.side.lower()) + @staticmethod + def _validate_position(position: Position) -> None: + if ( + not isinstance(position.market_ticker, str) + or not position.market_ticker.strip() + or not isinstance(position.contract_ticker, str) + or not position.contract_ticker.strip() + or position.side not in {"yes", "no"} + or isinstance(position.quantity, bool) + or not isinstance(position.quantity, int) + or position.quantity < 1 + or isinstance(position.avg_price_cents, bool) + or not isinstance(position.avg_price_cents, int) + or not 1 <= position.avg_price_cents <= 100 + or isinstance(position.unrealized_pnl_cents, bool) + or not isinstance(position.unrealized_pnl_cents, int) + ): + raise ValueError("invalid exposure position") + + @staticmethod + def _strict_persisted_position(raw: Any) -> Position: + required = { + "market_ticker", + "contract_ticker", + "side", + "quantity", + "avg_price_cents", + "unrealized_pnl_cents", + } + allowed = required | {"source_ts", "freshness_score"} + if ( + not isinstance(raw, dict) + or not required <= set(raw) <= allowed + or not all( + isinstance(raw[field], str) + for field in ("market_ticker", "contract_ticker", "side") + ) + or any( + isinstance(raw[field], bool) + or not isinstance(raw[field], int) + for field in ( + "quantity", + "avg_price_cents", + "unrealized_pnl_cents", + ) + ) + ): + raise ValueError("persisted position fields or types are invalid") + position = Position.model_validate(raw) + ExposureTracker._validate_position(position) + return position + + @staticmethod + def _strict_int(value: Any, *, minimum: int, maximum: int | None = None) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or value < minimum + or (maximum is not None and value > maximum) + ): + raise ValueError("persisted exposure integer is invalid") + return value + @staticmethod def _serialize_timestamp(value: Any) -> str: if isinstance(value, datetime): @@ -52,66 +131,304 @@ def _parse_timestamp(value: Any) -> datetime: return parsed.astimezone(timezone.utc) def _load(self) -> None: + try: + with self._state_lock.hold(): + self._load_locked() + except Exception as exc: + self.persistence_error = f"{type(exc).__name__}: {exc}" + + def _load_locked(self, *, require_existing: bool = False) -> bool: if not self.state_path.exists(): - return + if require_existing or self._has_persisted_state: + self.persistence_error = ( + "FileNotFoundError: persisted exposure state is missing" + ) + return False + self.positions = {} + self.order_history = [] + self.reconciliation_history = [] + self.open_orders = [] + self._state_revision = 0 + return True try: data = json.loads(self.state_path.read_text(encoding="utf-8")) if not isinstance(data, dict): raise ValueError("exposure state must be a JSON object") + legacy_fields = { + "positions", + "open_orders", + "order_history", + "updated_at", + } + reconciliation_fields = legacy_fields | { + "reconciliation_history" + } + current_fields = reconciliation_fields | {"state_revision"} + if frozenset(data) not in { + frozenset(legacy_fields), + frozenset(reconciliation_fields), + frozenset(current_fields), + }: + raise ValueError("exposure state fields mismatch") + self._parse_timestamp(data["updated_at"]) + state_revision = self._strict_int( + data.get("state_revision", 0), + minimum=0, + ) positions = data.get("positions", []) orders = data.get("open_orders", []) history = data.get("order_history", []) - if not isinstance(positions, list) or not isinstance(orders, list) or not isinstance(history, list): + reconciliations = data.get("reconciliation_history", []) + if ( + not isinstance(positions, list) + or not isinstance(orders, list) + or not isinstance(history, list) + or not isinstance(reconciliations, list) + ): raise ValueError("exposure state collections must be lists") loaded_positions: dict[tuple[str, str], Position] = {} for raw in positions: - position = Position.model_validate(raw) - loaded_positions[self._position_key(position)] = position + position = self._strict_persisted_position(raw) + key = self._position_key(position) + if key in loaded_positions: + raise ValueError("duplicate persisted position identity") + loaded_positions[key] = position loaded_history = [] for raw in history: if not isinstance(raw, dict): raise ValueError("order history item must be an object") - loaded_history.append({**raw, "ts": self._parse_timestamp(raw["ts"])}) + required_history = {"ts", "market", "size", "price_cents"} + allowed_history = required_history | {"client_order_id"} + if ( + not required_history <= set(raw) <= allowed_history + or not isinstance(raw["market"], str) + or not raw["market"].strip() + or ( + "client_order_id" in raw + and ( + not isinstance(raw["client_order_id"], str) + or not raw["client_order_id"].strip() + ) + ) + ): + raise ValueError("order history fields are invalid") + self._strict_int(raw["size"], minimum=1) + self._strict_int( + raw["price_cents"], + minimum=1, + maximum=99, + ) + loaded_history.append( + {**raw, "ts": self._parse_timestamp(raw["ts"])} + ) loaded_orders: list[dict[str, Any]] = [] + seen_order_identities: set[str] = set() for raw in orders: if not isinstance(raw, dict): raise ValueError("open order item must be an object") order = dict(raw) - order_id = str(order.get("order_id") or "").strip() - market = str(order.get("market") or "").strip() - size = int(order.get("size")) - price = int(order.get("price_cents")) - filled = int(order.get("filled_size", 0)) - remaining = int(order.get("remaining_size", size - filled)) + required_order = { + "order_id", + "market", + "contract", + "side", + "size", + "price_cents", + "filled_size", + "remaining_size", + "filled_cost_cents", + "state", + } + allowed_order = required_order | { + "client_order_id", + "reserved_at", + "accepted_at", + "last_reconciled_at", + "fee_reserve_cents", + } + if not required_order <= set(order) <= allowed_order: + raise ValueError("persisted open-order fields mismatch") + for field in ( + "order_id", + "market", + "contract", + "side", + "state", + ): + if ( + not isinstance(order[field], str) + or not order[field].strip() + ): + raise ValueError("persisted open-order identity is invalid") + order_id = order["order_id"] + market = order["market"] + contract = order["contract"] + side = order["side"] + state = order["state"] + size = self._strict_int(order["size"], minimum=1) + price = self._strict_int( + order["price_cents"], + minimum=1, + maximum=99, + ) + filled = self._strict_int(order["filled_size"], minimum=0) + remaining = self._strict_int( + order["remaining_size"], + minimum=0, + ) + filled_cost = self._strict_int( + order["filled_cost_cents"], + minimum=0, + ) + fee_reserve = self._strict_int( + order.get( + "fee_reserve_cents", + kalshi_taker_fee_cents( + price, + size, + market, + ), + ), + minimum=0, + ) + client_order_id = order.get("client_order_id") if ( - not order_id - or not market - or size < 1 - or not (1 <= price <= 99) - or filled < 0 - or remaining < 0 + side not in {"yes", "no"} + or state not in _OPEN_ORDER_STATES or filled + remaining != size + or filled_cost > filled * 100 + or (filled == 0 and filled_cost != 0) + or ( + client_order_id is not None + and ( + not isinstance(client_order_id, str) + or not client_order_id.strip() + ) + ) ): raise ValueError("invalid persisted open-order reservation") + identities = {order_id} + if client_order_id is not None: + identities.add(client_order_id) + if seen_order_identities & identities: + raise ValueError("duplicate persisted open-order identity") + seen_order_identities.update(identities) order.update({ "order_id": order_id, "market": market, + "contract": contract, + "side": side, "size": size, "price_cents": price, "filled_size": filled, "remaining_size": remaining, - "filled_cost_cents": int( - order.get("filled_cost_cents", filled * price) - ), - "state": str(order.get("state") or "open"), + "filled_cost_cents": filled_cost, + "fee_reserve_cents": fee_reserve, + "state": state, }) loaded_orders.append(order) + loaded_reconciliations: list[dict[str, Any]] = [] + seen_reconciliation_ids: set[str] = set() + for raw in reconciliations: + if not isinstance(raw, dict): + raise ValueError( + "reconciliation history item must be an object" + ) + kind = raw.get("kind") + reconciliation_id = raw.get("reconciliation_id") + if ( + not isinstance(reconciliation_id, str) + or len(reconciliation_id) != 64 + or any( + character not in "0123456789abcdef" + for character in reconciliation_id + ) + or reconciliation_id in seen_reconciliation_ids + ): + raise ValueError( + "reconciliation history identity is invalid" + ) + seen_reconciliation_ids.add(reconciliation_id) + if kind == "terminal_order": + required = { + "kind", + "reconciliation_id", + "order_id", + "client_order_id", + "contract", + "side", + "cumulative_size", + "avg_price_cents", + "terminal_state", + } + if set(raw) != required: + raise ValueError( + "terminal reconciliation fields mismatch" + ) + if ( + raw["side"] not in {"yes", "no"} + or raw["terminal_state"] + not in {"filled", "canceled", "expired", "rejected"} + or not isinstance(raw["order_id"], str) + or not raw["order_id"] + or not isinstance(raw["client_order_id"], str) + or not raw["client_order_id"] + or not isinstance(raw["contract"], str) + or not raw["contract"] + ): + raise ValueError( + "terminal reconciliation identity is invalid" + ) + cumulative = self._strict_int( + raw["cumulative_size"], + minimum=0, + ) + average = raw["avg_price_cents"] + if ( + cumulative == 0 + and average is not None + ) or ( + cumulative > 0 + and ( + isinstance(average, bool) + or not isinstance(average, int) + or not 1 <= average <= 100 + ) + ): + raise ValueError( + "terminal reconciliation price is invalid" + ) + elif kind == "position_close": + if set(raw) != { + "kind", + "reconciliation_id", + "contract", + "side", + } or ( + not isinstance(raw.get("contract"), str) + or not raw["contract"] + or raw.get("side") not in {"yes", "no"} + ): + raise ValueError( + "position-close reconciliation is invalid" + ) + else: + raise ValueError( + "reconciliation history kind is invalid" + ) + loaded_reconciliations.append(dict(raw)) self.positions = loaded_positions self.open_orders = loaded_orders self.order_history = loaded_history + self.reconciliation_history = loaded_reconciliations + self._state_revision = state_revision + self._has_persisted_state = True + self.persistence_error = None + return True except Exception as exc: # A corrupt prior state must block new live orders, not look empty. self.persistence_error = f"{type(exc).__name__}: {exc}" + return False def _payload(self) -> dict[str, Any]: return { @@ -121,55 +438,222 @@ def _payload(self) -> dict[str, Any]: {**order, "ts": self._serialize_timestamp(order["ts"])} for order in self.order_history[-10000:] ], + "reconciliation_history": self.reconciliation_history[-10000:], + "state_revision": self._state_revision, "updated_at": datetime.now(timezone.utc).isoformat(), } - def _persist(self) -> bool: + def anchor_head(self) -> tuple[int, str]: + """Return a monotonic revision and canonical semantic-state digest.""" + if self.persist_enabled and not self.refresh_persisted_state(): + raise RuntimeError( + "persisted exposure state is unavailable for anchoring" + ) + payload = { + "schema": EXPOSURE_STATE_ANCHOR_SCHEMA, + "state_revision": self._state_revision, + "positions": sorted( + ( + position.model_dump(mode="json") + for position in self.positions.values() + ), + key=lambda item: ( + str(item["contract_ticker"]), + str(item["side"]), + ), + ), + "open_orders": sorted( + ( + json.loads(canonical_json(order)) + for order in self.open_orders + ), + key=lambda item: ( + str(item.get("client_order_id") or ""), + str(item.get("order_id") or ""), + ), + ), + "order_history": [ + { + **order, + "ts": self._serialize_timestamp(order["ts"]), + } + for order in self.order_history[-10000:] + ], + "reconciliation_history": ( + self.reconciliation_history[-10000:] + ), + } + return self._state_revision, sha256_json(payload) + + def _persist_locked(self) -> bool: if not self.persist_enabled: return True - tmp = self.state_path.with_suffix(self.state_path.suffix + ".tmp") + tmp = self.state_path.with_name( + f".{self.state_path.name}.{os.getpid()}.{uuid4().hex}.tmp" + ) try: self.state_path.parent.mkdir(parents=True, exist_ok=True) - tmp.write_text(json.dumps(self._payload(), indent=2, sort_keys=True), encoding="utf-8") + encoded = json.dumps( + self._payload(), + indent=2, + sort_keys=True, + ) + with tmp.open("w", encoding="utf-8", newline="\n") as handle: + handle.write(encoded) + handle.flush() + os.fsync(handle.fileno()) os.replace(tmp, self.state_path) + # Flush the replaced file as well. On POSIX, also fsync the parent + # directory so the rename itself is durable. Windows does not + # expose directory handles through os.open, while os.fsync on the + # final file still maps to FlushFileBuffers. + # Windows requires a writable handle for FlushFileBuffers via + # ``os.fsync``; opening read-only raises ``EBADF`` even though no + # additional bytes are written here. + with self.state_path.open("r+b") as handle: + os.fsync(handle.fileno()) + if os.name != "nt": + directory_fd = os.open( + self.state_path.parent, + os.O_RDONLY, + ) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + self._has_persisted_state = True self.persistence_error = None return True except Exception as exc: self.persistence_error = f"{type(exc).__name__}: {exc}" return False + finally: + try: + tmp.unlink(missing_ok=True) + except OSError: + pass + + def _mutate_persisted_state( + self, + mutation: Callable[[], bool], + ) -> bool: + if self.persistence_error is not None: + return False + if not self.persist_enabled: + if not mutation(): + return False + self._state_revision += 1 + return True + try: + with self._state_lock.hold(): + if not self._load_locked(): + return False + if not mutation(): + return False + self._state_revision += 1 + return self._persist_locked() + except Exception as exc: + self.persistence_error = f"{type(exc).__name__}: {exc}" + return False + + def refresh_persisted_state( + self, + *, + require_existing: bool = False, + ) -> bool: + if not self.persist_enabled: + return self.persistence_error is None + if self.persistence_error is not None: + return False + try: + with self._state_lock.hold(): + return self._load_locked( + require_existing=require_existing + ) + except Exception as exc: + self.persistence_error = f"{type(exc).__name__}: {exc}" + return False def verify_persistence(self) -> bool: - """Preflight the state sink before any broker submission.""" + """Reload then replace only the exact newest cross-process state.""" if self.persistence_error is not None: # Never overwrite a corrupt/unknown prior position book with an # empty optimistic one. Resolution requires an operator repair. return False - return self._persist() - - def record_order(self, market_ticker: str, size: int, price_cents: int): - self.order_history.append({ - "ts": datetime.now(timezone.utc), - "market": market_ticker, - "size": size, - "price_cents": price_cents, - }) - self._persist() - - def update_position(self, position: Position): - self.positions[self._position_key(position)] = position - self._persist() - - def remove_position(self, ticker: str, side: str | None = None): + if not self.persist_enabled: + return True + try: + with self._state_lock.hold(): + if not self._load_locked(): + return False + return self._persist_locked() + except Exception as exc: + self.persistence_error = f"{type(exc).__name__}: {exc}" + return False + + def record_order( + self, + market_ticker: str, + size: int, + price_cents: int, + ) -> None: + if ( + not isinstance(market_ticker, str) + or not market_ticker.strip() + or isinstance(size, bool) + or not isinstance(size, int) + or size < 1 + or isinstance(price_cents, bool) + or not isinstance(price_cents, int) + or not 1 <= price_cents <= 99 + ): + raise ValueError("invalid order history record") + + def mutation() -> bool: + self.order_history.append({ + "ts": datetime.now(timezone.utc), + "market": market_ticker, + "size": size, + "price_cents": price_cents, + }) + return True + + self._mutate_persisted_state(mutation) + + def update_position(self, position: Position) -> None: + self._validate_position(position) + + def mutation() -> bool: + self.positions[self._position_key(position)] = position + return True + + self._mutate_persisted_state(mutation) + + def remove_position( + self, + ticker: str, + side: str | None = None, + ) -> None: normalized_side = side.lower() if side else None - self.positions = { - key: position - for key, position in self.positions.items() - if not ( - (position.contract_ticker == ticker or position.market_ticker == ticker) - and (normalized_side is None or position.side.lower() == normalized_side) - ) - } - self._persist() + + def mutation() -> bool: + self.positions = { + key: position + for key, position in self.positions.items() + if not ( + ( + position.contract_ticker == ticker + or position.market_ticker == ticker + ) + and ( + normalized_side is None + or position.side.lower() == normalized_side + ) + ) + } + return True + + self._mutate_persisted_state(mutation) def add_open_order( self, @@ -180,26 +664,50 @@ def add_open_order( *, contract_ticker: str | None = None, side: str | None = None, - ): - if not order_id or int(size) < 1 or not (1 <= int(price_cents) <= 99): + ) -> None: + contract = contract_ticker or market_ticker + if ( + not isinstance(order_id, str) + or not order_id.strip() + or not isinstance(market_ticker, str) + or not market_ticker.strip() + or not isinstance(contract, str) + or not contract.strip() + or side not in {"yes", "no"} + or isinstance(size, bool) + or not isinstance(size, int) + or size < 1 + or isinstance(price_cents, bool) + or not isinstance(price_cents, int) + or not 1 <= price_cents <= 99 + ): raise ValueError("invalid open-order reservation") - self.open_orders = [ - order for order in self.open_orders - if order.get("order_id") != order_id - ] - self.open_orders.append({ - "order_id": order_id, - "market": market_ticker, - "contract": contract_ticker or market_ticker, - "side": side, - "size": int(size), - "remaining_size": int(size), - "filled_size": 0, - "filled_cost_cents": 0, - "price_cents": int(price_cents), - "state": "open", - }) - self._persist() + + def mutation() -> bool: + self.open_orders = [ + order for order in self.open_orders + if order.get("order_id") != order_id + ] + self.open_orders.append({ + "order_id": order_id, + "market": market_ticker, + "contract": contract, + "side": side, + "size": int(size), + "remaining_size": int(size), + "filled_size": 0, + "filled_cost_cents": 0, + "fee_reserve_cents": kalshi_taker_fee_cents( + int(price_cents), + int(size), + market_ticker, + ), + "price_cents": int(price_cents), + "state": "open", + }) + return True + + self._mutate_persisted_state(mutation) def reserve_order_submission( self, @@ -217,45 +725,80 @@ def reserve_order_submission( reservation first ensures that process failure or an ambiguous transport result cannot make the next order assume zero exposure. """ - if self.persistence_error is not None: - return False if ( - not client_order_id - or int(size) < 1 - or not (1 <= int(price_cents) <= 99) + not isinstance(client_order_id, str) + or not client_order_id.strip() + or not isinstance(market_ticker, str) + or not market_ticker.strip() + or not isinstance(contract_ticker, str) + or not contract_ticker.strip() + or side not in {"yes", "no"} + or isinstance(size, bool) + or not isinstance(size, int) + or size < 1 + or isinstance(price_cents, bool) + or not isinstance(price_cents, int) + or not 1 <= price_cents <= 99 ): self.persistence_error = "invalid order-submission reservation" return False - if any( - order.get("order_id") == client_order_id - or order.get("client_order_id") == client_order_id - for order in self.open_orders - ): - self.persistence_error = "duplicate client order id" - return False - now = datetime.now(timezone.utc) - self.order_history.append({ - "ts": now, - "market": market_ticker, - "size": int(size), - "price_cents": int(price_cents), - "client_order_id": client_order_id, - }) - self.open_orders.append({ - "order_id": client_order_id, - "client_order_id": client_order_id, - "market": market_ticker, - "contract": contract_ticker or market_ticker, - "side": side, - "size": int(size), - "remaining_size": int(size), - "filled_size": 0, - "filled_cost_cents": 0, - "price_cents": int(price_cents), - "state": "submitting", - "reserved_at": now.isoformat(), - }) - return self._persist() + + def mutation() -> bool: + if any( + order.get("order_id") == client_order_id + or order.get("client_order_id") == client_order_id + for order in self.open_orders + ): + self.persistence_error = "duplicate client order id" + return False + now = datetime.now(timezone.utc) + self.order_history.append({ + "ts": now, + "market": market_ticker, + "size": int(size), + "price_cents": int(price_cents), + "client_order_id": client_order_id, + }) + self.open_orders.append({ + "order_id": client_order_id, + "client_order_id": client_order_id, + "market": market_ticker, + "contract": contract_ticker or market_ticker, + "side": side, + "size": int(size), + "remaining_size": int(size), + "filled_size": 0, + "filled_cost_cents": 0, + "fee_reserve_cents": kalshi_taker_fee_cents( + int(price_cents), + int(size), + market_ticker, + ), + "price_cents": int(price_cents), + "state": "submitting", + "reserved_at": now.isoformat(), + }) + return True + + return self._mutate_persisted_state(mutation) + + def submission_record( + self, + client_order_id: str, + ) -> dict[str, Any] | None: + """Return an existing durable/ambiguous submission without mutating it.""" + normalized = str(client_order_id).strip() + if not normalized: + return None + if self.persist_enabled and not self.refresh_persisted_state(): + return None + for order in self.open_orders: + if ( + str(order.get("order_id") or "") == normalized + or str(order.get("client_order_id") or "") == normalized + ): + return dict(order) + return None def confirm_open_order( self, @@ -263,32 +806,65 @@ def confirm_open_order( broker_order_id: str, ) -> bool: """Bind a pre-transport reservation to the broker order id.""" - if self.persistence_error is not None or not broker_order_id: + if ( + not isinstance(client_order_id, str) + or not client_order_id.strip() + or not isinstance(broker_order_id, str) + or not broker_order_id.strip() + ): return False - for order in self.open_orders: - if ( - order.get("order_id") == client_order_id - or order.get("client_order_id") == client_order_id + + def mutation() -> bool: + if any( + broker_order_id + in { + str(order.get("order_id") or ""), + str(order.get("client_order_id") or ""), + } + and client_order_id + not in { + str(order.get("order_id") or ""), + str(order.get("client_order_id") or ""), + } + for order in self.open_orders ): - order["client_order_id"] = client_order_id - order["order_id"] = broker_order_id - order["state"] = "open" - order["accepted_at"] = datetime.now(timezone.utc).isoformat() - return self._persist() - self.persistence_error = "accepted order has no durable reservation" - return False + self.persistence_error = "duplicate broker order id" + return False + for order in self.open_orders: + if ( + order.get("order_id") == client_order_id + or order.get("client_order_id") == client_order_id + ): + order["client_order_id"] = client_order_id + order["order_id"] = broker_order_id + order["state"] = "open" + order["accepted_at"] = ( + datetime.now(timezone.utc).isoformat() + ) + return True + self.persistence_error = ( + "accepted order has no durable reservation" + ) + return False + + return self._mutate_persisted_state(mutation) def mark_order_outcome_unknown(self, client_order_id: str) -> bool: """Keep an ambiguous submission reserved until reconciliation.""" - for order in self.open_orders: - if ( - order.get("order_id") == client_order_id - or order.get("client_order_id") == client_order_id - ): - order["state"] = "submit_outcome_unknown" - return self._persist() - self.persistence_error = "unknown submit outcome has no reservation" - return False + def mutation() -> bool: + for order in self.open_orders: + if ( + order.get("order_id") == client_order_id + or order.get("client_order_id") == client_order_id + ): + order["state"] = "submit_outcome_unknown" + return True + self.persistence_error = ( + "unknown submit outcome has no reservation" + ) + return False + + return self._mutate_persisted_state(mutation) def record_cumulative_fill( self, @@ -297,6 +873,7 @@ def record_cumulative_fill( avg_price_cents: int | None, *, terminal_state: str | None = None, + reconciliation_id: str | None = None, ) -> bool: """Apply a broker-witnessed cumulative fill without double counting. @@ -305,8 +882,74 @@ def record_cumulative_fill( unfilled remainder at the submitted LIMIT. Filled/canceled terminal states release only the remainder that the broker has resolved. """ - if self.persistence_error is not None: - return False + return self._mutate_persisted_state( + lambda: self._record_cumulative_fill_in_memory( + order_id, + cumulative_size, + avg_price_cents, + terminal_state=terminal_state, + reconciliation_id=reconciliation_id, + ) + ) + + def _record_cumulative_fill_in_memory( + self, + order_id: str, + cumulative_size: int, + avg_price_cents: int | None, + *, + terminal_state: str | None, + reconciliation_id: str | None, + ) -> bool: + normalized_reconciliation_id: str | None = None + if reconciliation_id is not None: + normalized_reconciliation_id = str(reconciliation_id) + if ( + len(normalized_reconciliation_id) != 64 + or any( + character not in "0123456789abcdef" + for character in normalized_reconciliation_id + ) + ): + self.persistence_error = ( + "terminal reconciliation identity is invalid" + ) + return False + prior_reconciliation = next( + ( + item + for item in self.reconciliation_history + if item.get("reconciliation_id") + == normalized_reconciliation_id + ), + None, + ) + expected_terminal = { + "kind": "terminal_order", + "reconciliation_id": normalized_reconciliation_id, + "order_id": str(order_id), + "client_order_id": "", + "contract": "", + "side": "", + "cumulative_size": int(cumulative_size), + "avg_price_cents": avg_price_cents, + "terminal_state": str(terminal_state or "").lower(), + } + if prior_reconciliation is not None: + comparable = { + **expected_terminal, + "client_order_id": prior_reconciliation.get( + "client_order_id" + ), + "contract": prior_reconciliation.get("contract"), + "side": prior_reconciliation.get("side"), + } + if prior_reconciliation == comparable: + return True + self.persistence_error = ( + "terminal reconciliation identity conflict" + ) + return False order = next( ( item for item in self.open_orders @@ -327,7 +970,13 @@ def record_cumulative_fill( self.persistence_error = "malformed open order during fill reconcile" return False terminal = str(terminal_state or "").lower() or None - if terminal not in {None, "filled", "canceled", "expired"}: + if terminal not in { + None, + "filled", + "canceled", + "expired", + "rejected", + }: self.persistence_error = "invalid terminal fill state" return False if not (prior_size <= cumulative_size <= original_size): @@ -397,63 +1046,213 @@ def record_cumulative_fill( ) order["last_reconciled_at"] = datetime.now(timezone.utc).isoformat() if terminal is not None: + if normalized_reconciliation_id is not None: + self.reconciliation_history.append( + { + "kind": "terminal_order", + "reconciliation_id": ( + normalized_reconciliation_id + ), + # Preserve the caller's lookup identity, not the + # broker-rebound id. A restart may replay the same + # witness by proposal/client id after the open order + # was removed, so this value must remain stable across + # the first application and every idempotent replay. + "order_id": str(order_id), + "client_order_id": str( + order.get("client_order_id") or order_id + ), + "contract": str(order.get("contract") or ""), + "side": str(order.get("side") or ""), + "cumulative_size": cumulative_size, + "avg_price_cents": avg_price_cents, + "terminal_state": terminal, + } + ) self.open_orders = [item for item in self.open_orders if item is not order] - return self._persist() + return True + + def record_position_close( + self, + ticker: str, + side: str, + *, + reconciliation_id: str, + ) -> bool: + """Persist one idempotent settlement-backed position removal.""" + normalized_id = str(reconciliation_id) + normalized_side = str(side).lower() + if ( + len(normalized_id) != 64 + or any( + character not in "0123456789abcdef" + for character in normalized_id + ) + or not isinstance(ticker, str) + or not ticker + or normalized_side not in {"yes", "no"} + ): + self.persistence_error = ( + "position-close reconciliation identity is invalid" + ) + return False - def remove_open_order(self, order_id: str): - self.open_orders = [ - order for order in self.open_orders - if order.get("order_id") != order_id - and order.get("client_order_id") != order_id - ] - self._persist() + expected = { + "kind": "position_close", + "reconciliation_id": normalized_id, + "contract": ticker, + "side": normalized_side, + } - def total_exposure_cents(self) -> int: - positions = sum( - p.quantity * p.avg_price_cents for p in self.positions.values() + def mutation() -> bool: + prior = next( + ( + item + for item in self.reconciliation_history + if item.get("reconciliation_id") == normalized_id + ), + None, + ) + if prior is not None: + if prior == expected: + return True + self.persistence_error = ( + "position-close reconciliation identity conflict" + ) + return False + self.positions.pop((ticker, normalized_side), None) + self.reconciliation_history.append(expected) + return True + + return self._mutate_persisted_state(mutation) + + def remove_open_order(self, order_id: str) -> None: + def mutation() -> bool: + self.open_orders = [ + order for order in self.open_orders + if order.get("order_id") != order_id + and order.get("client_order_id") != order_id + ] + return True + + self._mutate_persisted_state(mutation) + + def _refresh_for_risk_read(self) -> bool: + return ( + self.persistence_error is None + and ( + not self.persist_enabled + or self.refresh_persisted_state(require_existing=True) + ) ) - reservations = sum( - int(order.get("remaining_size", order.get("size", 0))) - * int(order.get("price_cents", 0)) - for order in self.open_orders + + def _bounded_exposure( + self, + positions: Any, + orders: Any, + ) -> int: + """Calculate notional or return a cap-breaking fail-closed sentinel.""" + try: + total = 0 + for position in positions: + self._validate_position(position) + total += position.quantity * position.avg_price_cents + for order in orders: + remaining = order.get("remaining_size") + price = order.get("price_cents") + if ( + isinstance(remaining, bool) + or not isinstance(remaining, int) + or remaining < 0 + or isinstance(price, bool) + or not isinstance(price, int) + or not 1 <= price <= 99 + ): + raise ValueError("invalid open-order risk fields") + total += remaining * price + fee_reserve = order.get("fee_reserve_cents", 0) + if ( + isinstance(fee_reserve, bool) + or not isinstance(fee_reserve, int) + or fee_reserve < 0 + ): + raise ValueError( + "invalid open-order fee reservation" + ) + total += fee_reserve + if total < 0: + raise ValueError("negative aggregate exposure") + return total + except (AttributeError, TypeError, ValueError) as exc: + self.persistence_error = ( + f"{type(exc).__name__}: invalid exposure risk state" + ) + return _FAIL_CLOSED_EXPOSURE_CENTS + + def total_exposure_cents(self) -> int: + if not self._refresh_for_risk_read(): + return _FAIL_CLOSED_EXPOSURE_CENTS + return self._bounded_exposure( + self.positions.values(), + self.open_orders, ) - return positions + reservations def market_exposure_cents(self, ticker: str) -> int: - positions = sum( - position.quantity * position.avg_price_cents - for position in self.positions.values() - if position.market_ticker == ticker - ) - reservations = sum( - int(order.get("remaining_size", order.get("size", 0))) - * int(order.get("price_cents", 0)) - for order in self.open_orders - if order.get("market") == ticker + if not self._refresh_for_risk_read(): + return _FAIL_CLOSED_EXPOSURE_CENTS + return self._bounded_exposure( + ( + position + for position in self.positions.values() + if position.market_ticker == ticker + ), + ( + order + for order in self.open_orders + if order.get("market") == ticker + ), ) - return positions + reservations def correlated_exposure_cents(self, ticker: str) -> int: + if not self._refresh_for_risk_read(): + return _FAIL_CLOSED_EXPOSURE_CENTS # Event-family proxy: Kalshi event/series prefix before the first '-'. prefix = ticker.split("-")[0].upper() - positions = sum( - p.quantity * p.avg_price_cents - for p in self.positions.values() - if p.market_ticker.upper().split("-")[0] == prefix - ) - reservations = sum( - int(order.get("remaining_size", order.get("size", 0))) - * int(order.get("price_cents", 0)) - for order in self.open_orders - if str(order.get("market", "")).upper().split("-")[0] == prefix + return self._bounded_exposure( + ( + position + for position in self.positions.values() + if position.market_ticker.upper().split("-")[0] == prefix + ), + ( + order + for order in self.open_orders + if str(order.get("market", "")).upper().split("-")[0] + == prefix + ), ) - return positions + reservations def orders_last_hour(self) -> int: + if not self._refresh_for_risk_read(): + return _FAIL_CLOSED_EXPOSURE_CENTS cutoff = datetime.now(timezone.utc) - timedelta(hours=1) - return len([o for o in self.order_history if self._parse_timestamp(o["ts"]) > cutoff]) + try: + return len( + [ + order + for order in self.order_history + if self._parse_timestamp(order["ts"]) > cutoff + ] + ) + except (KeyError, TypeError, ValueError) as exc: + self.persistence_error = ( + f"{type(exc).__name__}: invalid order history state" + ) + return _FAIL_CLOSED_EXPOSURE_CENTS def open_markets(self) -> int: + if not self._refresh_for_risk_read(): + return _FAIL_CLOSED_EXPOSURE_CENTS markets = {position.market_ticker for position in self.positions.values()} markets.update( str(order["market"]) @@ -463,8 +1262,26 @@ def open_markets(self) -> int: return len(markets) def open_order_count(self) -> int: + if not self._refresh_for_risk_read(): + return _FAIL_CLOSED_EXPOSURE_CENTS return len(self.open_orders) + def client_order_id_for(self, order_id: str) -> str | None: + """Resolve the durable proposal identity before terminal removal.""" + normalized = str(order_id).strip() + if not normalized: + return None + if self.persist_enabled and not self.refresh_persisted_state(): + return None + for order in self.open_orders: + if ( + str(order.get("order_id") or "") == normalized + or str(order.get("client_order_id") or "") == normalized + ): + candidate = str(order.get("client_order_id") or "").strip() + return candidate or None + return None + _PERSISTENT_TRACKER: ExposureTracker | None = None diff --git a/live_firewall/firewall.py b/live_firewall/firewall.py index 466a4cd8..5d388a73 100644 --- a/live_firewall/firewall.py +++ b/live_firewall/firewall.py @@ -1,12 +1,14 @@ import hashlib +import inspect import json import os +import secrets from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime, timezone, timedelta from decimal import Decimal from pathlib import Path -from typing import Any, Optional +from typing import Any, Callable, Optional from core.state import STATE from core.caps_authority import evaluate_caps_authority from core.config_loader import load_caps @@ -30,6 +32,7 @@ verify_model_influence_attestation, ) from forecasting.model_probability_authority import ModelProbabilityAuthorityRegistry +from live_firewall.dumbmoney_capital import CapitalEnvelopeAdapter REJECTED_ADAPTERS: set[str] = set() @@ -241,6 +244,8 @@ def __init__( require_autonomy_risk_state: bool = False, require_canary_readiness: bool = False, model_authority_registry: ModelProbabilityAuthorityRegistry | None = None, + capital_envelope_adapter: CapitalEnvelopeAdapter | None = None, + core_submission_authority: Callable[[], bool] | None = None, ): self.client = kalshi_client self.exposure = exposure_tracker @@ -251,6 +256,16 @@ def __init__( self.model_authority_registry = ( model_authority_registry or ModelProbabilityAuthorityRegistry() ) + # Additional deny-only DumbMoney bound. Absence preserves the + # standalone Dummy authority model; presence makes a valid signed grant + # mandatory without replacing any existing local gate. + self.capital_envelope_adapter = capital_envelope_adapter + if ( + core_submission_authority is not None + and not callable(core_submission_authority) + ): + raise TypeError("core_submission_authority must be callable") + self.core_submission_authority = core_submission_authority self.autonomy_risk_state_path = autonomy_risk_state_path or Path( os.environ.get( "DUMMY_AUTONOMY_LIVE_RISK_STATE_PATH", @@ -443,13 +458,374 @@ async def _trusted_sink_orderbook( def _mandatory_submit_authority( self, req: LiveOrderRequest, + *, + reservation_held: bool = False, + trusted_orderbook: OrderBook | None = None, + forecast: Forecast | None = None, ) -> FirewallVerdict: """Recheck every non-optional real-submit authority gate.""" + now = datetime.now(timezone.utc) + if ( + req.expiration_ts is not None + and req.expiration_ts <= int(now.timestamp()) + ): + return FirewallVerdict( + allow=False, + reason="Limit order expiry is not in the future", + rejected_by="order_expiry", + ) + if ( + self.capital_envelope_adapter is not None + and req.liquidity_role == "maker" + and req.expiration_ts is None + ): + return FirewallVerdict( + allow=False, + reason="Maker order lacks exchange-enforced expiration", + rejected_by="order_expiry", + ) + if self.capital_envelope_adapter is not None: + if self.core_submission_authority is None: + return FirewallVerdict( + allow=False, + reason="Current DumbMoney Core control authority is required", + rejected_by="core_command_authority", + ) + try: + core_allows = self.core_submission_authority() + except Exception: + core_allows = False + if core_allows is not True: + return FirewallVerdict( + allow=False, + reason="DumbMoney Core control authority is not current", + rejected_by="core_command_authority", + ) + if not STATE.refresh_persisted_state(): + return FirewallVerdict( + allow=False, + reason="Persisted risk/safety state unavailable", + rejected_by="risk_state", + ) + if STATE.kill_switch.active: + return FirewallVerdict( + allow=False, + reason="Kill switch active", + rejected_by="kill_switch", + ) + if STATE.emergency_stop.active: + return FirewallVerdict( + allow=False, + reason="Emergency stop active", + rejected_by="emergency_stop", + ) + if STATE.persistence_error or not STATE.verify_persistence(): + return FirewallVerdict( + allow=False, + reason="Persisted daily-loss state unavailable", + rejected_by="daily_loss_state", + ) + if ( + not self.exposure.state_healthy + or not self.exposure.verify_persistence() + ): + return FirewallVerdict( + allow=False, + reason="Persisted exposure state unavailable", + rejected_by="exposure_state", + ) + caps = load_caps() + if STATE.daily_loss_cents >= caps.max_daily_loss_cents: + return FirewallVerdict( + allow=False, + reason="Daily loss cap exceeded", + rejected_by="daily_loss_cap", + ) + if reservation_held: + order_value = req.price_cents * req.size + if order_value > caps.max_single_order_cents: + return FirewallVerdict( + allow=False, + reason="Single order cap exceeded after reservation", + rejected_by="single_order_cap", + ) + if ( + self.exposure.market_exposure_cents( + req.market_ticker + ) + > caps.max_market_exposure_cents + ): + return FirewallVerdict( + allow=False, + reason="Market exposure cap exceeded after reservation", + rejected_by="market_exposure_cap", + ) + if ( + self.exposure.total_exposure_cents() + > caps.max_total_live_exposure_cents + ): + return FirewallVerdict( + allow=False, + reason="Total exposure cap exceeded after reservation", + rejected_by="total_exposure_cap", + ) + if ( + self.exposure.correlated_exposure_cents( + req.market_ticker + ) + > caps.max_correlated_exposure_cents + ): + return FirewallVerdict( + allow=False, + reason=( + "Correlated exposure cap exceeded after reservation" + ), + rejected_by="correlated_exposure_cap", + ) + if self.exposure.open_markets() > caps.max_open_markets: + return FirewallVerdict( + allow=False, + reason="Max open markets exceeded after reservation", + rejected_by="open_markets", + ) + if ( + self.exposure.orders_last_hour() + > caps.max_orders_per_hour + ): + return FirewallVerdict( + allow=False, + reason="Order frequency cap exceeded after reservation", + rejected_by="frequency_cap", + ) + if ( + self.exposure.open_order_count() + > caps.max_orders_per_hour + ): + return FirewallVerdict( + allow=False, + reason=( + "Open order count exceeded after reservation" + ), + rejected_by="open_order_cap", + ) + if trusted_orderbook is not None: + freshness_clocks = [trusted_orderbook.timestamp] + if trusted_orderbook.received_at is not None: + freshness_clocks.append(trusted_orderbook.received_at) + if trusted_orderbook.source_ts is not None: + freshness_clocks.append(trusted_orderbook.source_ts) + if any( + stamp.tzinfo is None + or stamp < now - timedelta(seconds=30) + or stamp > now + timedelta(seconds=5) + for stamp in freshness_clocks + ): + return FirewallVerdict( + allow=False, + reason="Trusted broker depth expired before submission", + rejected_by="stale_data", + ) + caps = load_caps() + if not market_is_allowlisted(req.market_ticker, caps): + return FirewallVerdict( + allow=False, + reason="Market not allowlisted at final submit", + rejected_by="market_allowlist", + ) + if ( + req.side not in {"yes", "no"} + or req.size < 1 + or not 1 <= req.price_cents <= 99 + ): + return FirewallVerdict( + allow=False, + reason="Invalid final limit-order schema", + rejected_by="order_schema", + ) + if ( + trusted_orderbook.market_ticker != req.market_ticker + or trusted_orderbook.contract_ticker != req.contract_ticker + or not trusted_orderbook.bids + or not trusted_orderbook.asks + or any( + level.size <= 0 or not 1 <= level.price <= 99 + for level in ( + trusted_orderbook.bids + trusted_orderbook.asks + ) + ) + ): + return FirewallVerdict( + allow=False, + reason="Trusted broker depth failed final schema binding", + rejected_by="context_integrity", + ) + side_bids, side_asks = self._side_book_ladders( + req, + trusted_orderbook, + ) + if not side_bids or not side_asks: + return FirewallVerdict( + allow=False, + reason="Trusted side-specific depth is unavailable", + rejected_by="liquidity", + ) + best_bid = side_bids[0][0] + best_ask = side_asks[0][0] + spread = best_ask - best_bid + total_liquidity = sum( + level.size + for level in ( + trusted_orderbook.bids + trusted_orderbook.asks + ) + ) + if spread <= 0 or spread > caps.max_spread_cents: + return FirewallVerdict( + allow=False, + reason="Trusted spread violates current policy", + rejected_by="spread", + ) + if total_liquidity < caps.min_liquidity: + return FirewallVerdict( + allow=False, + reason="Trusted liquidity violates current policy", + rejected_by="liquidity", + ) + if req.liquidity_role == "maker": + if not best_bid <= req.price_cents < best_ask: + return FirewallVerdict( + allow=False, + reason="Maker order is not passive at final submit", + rejected_by="execution_role", + ) + else: + executable_size = sum( + size + for price, size in side_asks + if price <= req.price_cents + ) + if ( + req.price_cents < best_ask + or executable_size < req.size + ): + return FirewallVerdict( + allow=False, + reason="Final taker depth cannot execute the request", + rejected_by="executable_depth", + ) + if forecast is not None: + if ( + forecast.market_ticker != req.market_ticker + or forecast.contract_ticker != req.contract_ticker + or forecast.proof_reference + != req.forecast_proof_reference + ): + return FirewallVerdict( + allow=False, + reason="Forecast identity changed before final submit", + rejected_by="context_integrity", + ) + net_ev = self._net_ev_verdict(req, forecast, caps) + if not net_ev.allow: + return net_ev + if forecast.settlement_risk_score > Decimal("0.8"): + return FirewallVerdict( + allow=False, + reason="Settlement risk too high at final submit", + rejected_by="settlement_risk", + ) + model_influence = self._model_influence_verdict( + req, + forecast, + ) + if not model_influence.allow: + return model_influence risk_verdict = self._autonomy_risk_verdict(req, required=True) if not risk_verdict.allow: return risk_verdict + capital_verdict = self._capital_envelope_verdict( + req, + reservation_held=reservation_held, + ) + if not capital_verdict.allow: + return capital_verdict return self.live_authority_verdict() + def _capital_envelope_verdict( + self, + req: LiveOrderRequest, + *, + reservation_held: bool = False, + ) -> FirewallVerdict: + """Revalidate the optional signed external ceiling. + + The adapter can only reject or further reduce locally permitted risk. + It is deliberately evaluated after Dummy's own caps in ``evaluate``; + the central sink also calls it from mandatory authority checks so a + replaced preflight cannot bypass it. + """ + fields_present = any( + value is not None + for value in ( + req.capital_envelope_id, + req.capital_strategy_hash, + req.capital_passport_hash, + req.capital_promotion_hash, + req.capital_fencing_generation, + ) + ) + if self.capital_envelope_adapter is None: + if fields_present: + return FirewallVerdict( + allow=False, + reason="DumbMoney capital adapter unavailable", + rejected_by="capital_adapter", + ) + return FirewallVerdict( + allow=True, + reason="Standalone Dummy authority; no external capital adapter", + ) + if not all( + value is not None + for value in ( + req.capital_envelope_id, + req.capital_strategy_hash, + req.capital_passport_hash, + req.capital_promotion_hash, + req.capital_fencing_generation, + ) + ): + return FirewallVerdict( + allow=False, + reason="Signed DumbMoney capital binding is incomplete", + rejected_by="capital_envelope", + ) + try: + verdict = self.capital_envelope_adapter.evaluate_request( + req, + current_daily_loss_cents=max(0, int(STATE.daily_loss_cents)), + current_local_total_exposure_cents=( + self.exposure.total_exposure_cents() + ), + current_local_correlated_exposure_cents=( + self.exposure.correlated_exposure_cents( + req.market_ticker + ) + ), + current_local_open_orders=self.exposure.open_order_count(), + current_request_locally_reserved=reservation_held, + ) + except Exception as exc: + return FirewallVerdict( + allow=False, + reason=f"DumbMoney capital state unavailable:{type(exc).__name__}", + rejected_by="capital_envelope", + ) + return FirewallVerdict( + allow=verdict.allow, + reason=verdict.reason, + rejected_by=verdict.rejected_by, + ) + def _model_influence_verdict( self, req: LiveOrderRequest, @@ -613,6 +989,15 @@ def fail(by: str, reason: str) -> FirewallVerdict: return fail("order_schema", "Invalid limit order side, price, or size") if req.expiration_ts is not None and req.expiration_ts <= int(datetime.now(timezone.utc).timestamp()): return fail("order_expiry", "Limit order expiry is not in the future") + if ( + self.capital_envelope_adapter is not None + and req.liquidity_role == "maker" + and req.expiration_ts is None + ): + return fail( + "order_expiry", + "Maker order lacks exchange-enforced expiration", + ) if not os.environ.get("KALSHI_API_KEY_ID"): return fail("secrets", "API key missing") allowed = get_allowed_adapter_names() @@ -707,6 +1092,15 @@ def fail(by: str, reason: str) -> FirewallVerdict: return fail("frequency_cap", "Order frequency cap exceeded") if self.exposure.open_order_count() >= caps.max_orders_per_hour: return fail("open_order_cap", "Open order count exceeded") + # External capital grants are additional ceilings only. Dummy's local + # cap, exposure, loss, frequency, and order-count checks above always + # run first and can never be widened by the grant. + capital_verdict = self._capital_envelope_verdict(req) + if not capital_verdict.allow: + return fail( + capital_verdict.rejected_by or "capital_envelope", + capital_verdict.reason, + ) if forecast.settlement_risk_score > Decimal("0.8"): return fail("settlement_risk", "Settlement risk too high") model_influence_verdict = self._model_influence_verdict(req, forecast) @@ -726,24 +1120,95 @@ def _live_submit_enabled(self) -> bool: return is_live_submit_armed() def _build_order(self, req: LiveOrderRequest) -> dict[str, Any]: - # trade-api/v2 CreateOrder body: the limit price is side-specific - # (yes_price/no_price) and client_order_id is required. + # Create Order V2 uses one YES-denominated book. Buying NO is an ask + # at the complementary YES price. Fixed-point strings are constructed + # without binary floating-point conversion. + yes_price_cents = ( + req.price_cents if req.side == "yes" else 100 - req.price_cents + ) + maker = req.liquidity_role == "maker" order: dict[str, Any] = { "ticker": req.contract_ticker, - "side": req.side, - "action": "buy", - "type": "limit", - "count": req.size, "client_order_id": req.proposal_id, + "side": "bid" if req.side == "yes" else "ask", + "count": f"{req.size}.00", + "price": f"0.{yes_price_cents:02d}00", + "time_in_force": ( + "good_till_canceled" if maker else "fill_or_kill" + ), + "self_trade_prevention_type": "taker_at_cross", + "post_only": maker, + "cancel_order_on_pause": True, + "reduce_only": False, + "subaccount": 0, + "exchange_index": 0, } - if req.side == "no": - order["no_price"] = req.price_cents - else: - order["yes_price"] = req.price_cents - if req.expiration_ts is not None: - order["expiration_ts"] = req.expiration_ts + if maker and req.expiration_ts is not None: + order["expiration_time"] = req.expiration_ts return order + @staticmethod + def _validate_v2_create_response( + response: Any, + *, + request: LiveOrderRequest, + ) -> str: + """Validate the current Create Order V2 acknowledgement shape.""" + if not isinstance(response, Mapping): + raise ValueError("Create Order V2 response is not an object") + required = {"order_id", "fill_count", "remaining_count", "ts_ms"} + allowed = required | { + "client_order_id", + "average_fill_price", + "average_fee_paid", + } + if not required <= set(response) <= allowed: + raise ValueError("Create Order V2 response fields mismatch") + order_id = response["order_id"] + if not isinstance(order_id, str) or not order_id.strip(): + raise ValueError("Create Order V2 order_id is invalid") + client_order_id = response.get("client_order_id") + if ( + client_order_id is not None + and client_order_id != request.proposal_id + ): + raise ValueError( + "Create Order V2 client_order_id differs from proposal" + ) + try: + fill_count = Decimal(response["fill_count"]) + remaining_count = Decimal(response["remaining_count"]) + except Exception as exc: + raise ValueError( + "Create Order V2 counts are not fixed-point values" + ) from exc + if ( + not isinstance(response["fill_count"], str) + or not isinstance(response["remaining_count"], str) + or not fill_count.is_finite() + or not remaining_count.is_finite() + or fill_count < 0 + or remaining_count < 0 + or fill_count != fill_count.to_integral_value() + or remaining_count != remaining_count.to_integral_value() + or fill_count + remaining_count > Decimal(request.size) + ): + raise ValueError("Create Order V2 counts are invalid") + ts_ms = response["ts_ms"] + if ( + isinstance(ts_ms, bool) + or not isinstance(ts_ms, int) + or ts_ms < 1 + ): + raise ValueError("Create Order V2 ts_ms is invalid") + # Any immediate fill or canceled remainder needs broker reconciliation + # before local open-order state can be finalized. + if fill_count != 0 or remaining_count != Decimal(request.size): + raise ValueError( + "Create Order V2 acknowledgement requires fill reconciliation" + ) + return order_id.strip() + async def _verified_live_compliance_verdict( self, req: LiveOrderRequest, @@ -960,7 +1425,10 @@ async def submit(self, req: LiveOrderRequest, orderbook: OrderBook, forecast: Fo # Real submission never inherits relaxed evaluate/rehearsal defaults. # Check before authenticated reads, then check again after them so an # expiring authority/risk state cannot win a time-of-check race. - authority = self._mandatory_submit_authority(req) + authority = self._mandatory_submit_authority( + req, + forecast=forecast, + ) if not authority.allow: logger.info( "Live submit blocked by authority", @@ -1000,7 +1468,10 @@ async def submit(self, req: LiveOrderRequest, orderbook: OrderBook, forecast: Fo proof_reference=req.strategy_proof_reference, broker_contacted=False, ) - authority = self._mandatory_submit_authority(req) + authority = self._mandatory_submit_authority( + req, + forecast=forecast, + ) if not authority.allow: return LiveOrderResult( success=False, @@ -1009,6 +1480,57 @@ async def submit(self, req: LiveOrderRequest, orderbook: OrderBook, forecast: Fo broker_contacted=False, ) order = self._build_order(req) + capital_reservation_id: str | None = None + existing_submission = self.exposure.submission_record( + req.proposal_id + ) + if existing_submission is not None: + return LiveOrderResult( + success=False, + error=( + "EXISTING_SUBMISSION_REQUIRES_BROKER_RECONCILIATION" + ), + proof_reference=req.strategy_proof_reference, + broker_contacted=False, + ) + # Reserve the external envelope budget before the local exposure + # reservation. Both are durable and both must succeed. A crash between + # them leaves a conservative external reservation that an identical + # proposal can resume idempotently; it never creates broker authority. + if self.capital_envelope_adapter is not None: + try: + capital = self.capital_envelope_adapter.reserve_request( + req, + current_daily_loss_cents=max( + 0, int(STATE.daily_loss_cents) + ), + current_local_total_exposure_cents=( + self.exposure.total_exposure_cents() + ), + current_local_correlated_exposure_cents=( + self.exposure.correlated_exposure_cents( + req.market_ticker + ) + ), + current_local_open_orders=( + self.exposure.open_order_count() + ), + ) + except Exception as exc: + return LiveOrderResult( + success=False, + error=f"DUMBMONEY_CAPITAL_RESERVATION_FAILED:{type(exc).__name__}", + proof_reference=req.strategy_proof_reference, + broker_contacted=False, + ) + if not capital.allow: + return LiveOrderResult( + success=False, + error=capital.reason, + proof_reference=req.strategy_proof_reference, + broker_contacted=False, + ) + capital_reservation_id = capital.reservation_id # Reserve the full LIMIT notional durably before transport. A timeout # is an unknown broker outcome, not evidence that no order exists. if not self.exposure.reserve_order_submission( @@ -1019,25 +1541,204 @@ async def submit(self, req: LiveOrderRequest, orderbook: OrderBook, forecast: Fo contract_ticker=req.contract_ticker, side=req.side, ): + # The failure may be a concurrent/existing or ambiguously durable + # local reservation. Retain the external reservation until broker + # reconciliation; absence cannot be proven from a false return. + if capital_reservation_id is not None: + logger.warning( + "DumbMoney reservation retained after local failure", + extra={ + "component": "firewall", + "proposal_id": req.proposal_id, + }, + ) return LiveOrderResult( success=False, error="EXPOSURE_RESERVATION_FAILED", proof_reference=req.strategy_proof_reference, broker_contacted=False, ) - try: - from kalshi.client import _CENTRAL_FIREWALL_SUBMIT_CAPABILITY + from kalshi.client import _CENTRAL_FIREWALL_SUBMIT_CAPABILITY - resp = await self.client.create_order( - order, - _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + mutation_permit: object | None = None + prepare_mutation = ( + getattr(self.client, "prepare_order_mutation") + if inspect.getattr_static( + self.client, + "prepare_order_mutation", + None, ) - order_id = resp.get("order", {}).get("order_id") or resp.get("order_id") + is not None + else None + ) + cancel_mutation = ( + getattr(self.client, "cancel_prepared_order_mutation") + if inspect.getattr_static( + self.client, + "cancel_prepared_order_mutation", + None, + ) + is not None + else None + ) + if callable(prepare_mutation): + try: + # Any queue/limiter wait happens before the last Core + # resolution. The resulting permit authorizes exactly one + # socket attempt and cannot be reused after ambiguity. + mutation_permit = await prepare_mutation( + "POST", + "/portfolio/events/orders", + _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + ) + except Exception as exc: + return LiveOrderResult( + success=False, + error=( + "BROKER_MUTATION_PREPARATION_FAILED:" + f"{type(exc).__name__}" + ), + proof_reference=req.strategy_proof_reference, + broker_contacted=False, + ) + + def release_unused_mutation_permit() -> None: + nonlocal mutation_permit + if mutation_permit is None or not callable(cancel_mutation): + return + try: + cancel_mutation( + mutation_permit, + _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + ) + except Exception: + logger.exception( + "Unused broker mutation permit release failed", + extra={ + "component": "firewall", + "proposal_id": req.proposal_id, + }, + ) + finally: + mutation_permit = None + + # Reservation work can cross a short lease or operator-authority + # boundary. Recheck at the last synchronous point before broker I/O. + # If authority disappeared, retain both durable reservations: no + # broker call occurred, but automatically releasing ambiguous + # cross-process state could grant capacity to a concurrent proposal. + authority = self._mandatory_submit_authority( + req, + reservation_held=True, + trusted_orderbook=trusted_orderbook, + forecast=forecast, + ) + if not authority.allow: + release_unused_mutation_permit() + logger.info( + "Live submit blocked after durable reservation", + extra={ + "component": "firewall", + "proposal_id": req.proposal_id, + "rejected_by": authority.rejected_by, + "reason": authority.reason, + }, + ) + return LiveOrderResult( + success=False, + error=authority.reason, + proof_reference=req.strategy_proof_reference, + broker_contacted=False, + ) + if self.capital_envelope_adapter is not None: + if capital_reservation_id is None: + release_unused_mutation_permit() + return LiveOrderResult( + success=False, + error="DUMBMONEY_CAPITAL_RESERVATION_MISSING", + proof_reference=req.strategy_proof_reference, + broker_contacted=False, + ) + try: + claimant_nonce = secrets.token_hex(32) + if len(claimant_nonce) != 64: + raise ValueError("dispatch nonce length mismatch") + self.capital_envelope_adapter.claim_broker_dispatch( + req, + reservation_id=capital_reservation_id, + order=order, + claimant_nonce=claimant_nonce, + ) + except Exception as exc: + release_unused_mutation_permit() + logger.error( + "DumbMoney broker dispatch claim failed", + extra={ + "component": "firewall", + "proposal_id": req.proposal_id, + "error_type": type(exc).__name__, + }, + ) + return LiveOrderResult( + success=False, + error=( + "DUMBMONEY_DISPATCH_CLAIM_FAILED:" + f"{type(exc).__name__}" + ), + proof_reference=req.strategy_proof_reference, + broker_contacted=False, + ) + # The command feed and local safety journals are separate durable + # authorities. Recheck after consuming the one-shot dispatch claim + # and immediately before transport. A denial leaves the claim and + # both reservations intact for broker reconciliation; it never + # creates permission to retry the same proposal. + authority = self._mandatory_submit_authority( + req, + reservation_held=True, + trusted_orderbook=trusted_orderbook, + forecast=forecast, + ) + if not authority.allow: + release_unused_mutation_permit() + logger.info( + "Live submit blocked after dispatch claim", + extra={ + "component": "firewall", + "proposal_id": req.proposal_id, + "rejected_by": authority.rejected_by, + "reason": authority.reason, + }, + ) + return LiveOrderResult( + success=False, + error=authority.reason, + proof_reference=req.strategy_proof_reference, + broker_contacted=False, + ) + try: + create_kwargs: dict[str, Any] = { + "_capability": _CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + } + if mutation_permit is not None: + create_kwargs["_mutation_permit"] = mutation_permit + resp = await self.client.create_order(order, **create_kwargs) + mutation_permit = None + if self.capital_envelope_adapter is not None: + order_id = self._validate_v2_create_response( + resp, + request=req, + ) + else: + order_id = ( + resp.get("order", {}).get("order_id") + or resp.get("order_id") + ) if not order_id: self.exposure.mark_order_outcome_unknown(req.proposal_id) return LiveOrderResult( success=False, - error="broker_order_id_missing", + error="AMBIGUOUS_BROKER_OUTCOME:ORDER_ID_MISSING", proof_reference=req.strategy_proof_reference, broker_contacted=True, ) @@ -1052,6 +1753,9 @@ async def submit(self, req: LiveOrderRequest, orderbook: OrderBook, forecast: Fo broker_contacted=True, ) except Exception as exc: + # The permit is consumed before the socket call. A transport + # exception, timeout, or HTTP 429 cannot prove non-acceptance. + mutation_permit = None error_type = type(exc).__name__ self.exposure.mark_order_outcome_unknown(req.proposal_id) logger.error( @@ -1060,7 +1764,7 @@ async def submit(self, req: LiveOrderRequest, orderbook: OrderBook, forecast: Fo ) return LiveOrderResult( success=False, - error=f"broker_submit_failed:{error_type}", + error=f"AMBIGUOUS_BROKER_OUTCOME:{error_type}", proof_reference=req.strategy_proof_reference, broker_contacted=True, ) diff --git a/live_firewall/kalshi_broker_truth.py b/live_firewall/kalshi_broker_truth.py new file mode 100644 index 00000000..c1902d43 --- /dev/null +++ b/live_firewall/kalshi_broker_truth.py @@ -0,0 +1,725 @@ +"""Credential-owned, read-only Kalshi broker-truth projection for DumbMoney. + +The provider is intentionally narrower than the general Kalshi client. It +uses an injected RSA credential directly, pins the production origin and v2 +path, ignores ambient proxy configuration, performs no retries, and exposes +only a conservative reconciliation snapshot. No order mutation method exists +in this module. +""" + +from __future__ import annotations + +import base64 +import re +import threading +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from decimal import Decimal, InvalidOperation, ROUND_CEILING +from typing import Any + +import httpx +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey + +from kalshi.strict_json import load_strict_json_response +from live_firewall.operational_journal import canonical_json, sha256_json + + +KALSHI_PRODUCTION_ORIGIN = "https://external-api.kalshi.com" +KALSHI_API_PREFIX = "/trade-api/v2" +MAXIMUM_RESPONSE_BYTES = 8 * 1024 * 1024 +MAXIMUM_PAGES = 64 +MAXIMUM_RECORDS = 64_000 +REQUEST_TIMEOUT_SECONDS = 10.0 + +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_KEY_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,255}$") +_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$") +_TICKER_RE = re.compile(r"^[A-Z0-9][A-Z0-9._:-]{0,255}$") +_FIXED_POINT_RE = re.compile( + r"^-?(?:0|[1-9][0-9]{0,18})(?:\.[0-9]{1,6})?$" +) + + +class KalshiBrokerTruthError(RuntimeError): + """The authenticated broker snapshot could not be proven complete.""" + + +@dataclass(frozen=True) +class _BufferedResponse: + content: bytes + + +def _mapping(value: Any, *, field: str) -> Mapping[str, Any]: + if not isinstance(value, Mapping): + raise KalshiBrokerTruthError(f"{field} must be an object") + return value + + +def _list(value: Any, *, field: str) -> list[Any]: + if not isinstance(value, list): + raise KalshiBrokerTruthError(f"{field} must be an array") + return value + + +def _integer( + value: Any, + *, + field: str, + minimum: int = 0, + maximum: int = 2**63 - 1, +) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or value < minimum + or value > maximum + ): + raise KalshiBrokerTruthError(f"{field} is outside its integer domain") + return value + + +def _identifier(value: Any, *, field: str, ticker: bool = False) -> str: + pattern = _TICKER_RE if ticker else _IDENTIFIER_RE + if not isinstance(value, str) or not pattern.fullmatch(value): + raise KalshiBrokerTruthError(f"{field} is invalid") + return value + + +def _cursor(value: Any, *, field: str) -> str: + if ( + not isinstance(value, str) + or len(value) > 4_096 + or any(ord(character) < 0x20 for character in value) + ): + raise KalshiBrokerTruthError(f"{field} is invalid") + return value + + +def _fixed_point( + value: Any, + *, + field: str, + allow_negative: bool, +) -> Decimal: + if not isinstance(value, str) or not _FIXED_POINT_RE.fullmatch(value): + raise KalshiBrokerTruthError(f"{field} is not a fixed-point string") + try: + parsed = Decimal(value) + except InvalidOperation as exc: + raise KalshiBrokerTruthError(f"{field} is invalid") from exc + if not parsed.is_finite() or (not allow_negative and parsed < 0): + raise KalshiBrokerTruthError(f"{field} is outside its numeric domain") + if parsed.copy_abs() > Decimal("1000000000000000"): + raise KalshiBrokerTruthError(f"{field} exceeds the sealed maximum") + return parsed + + +def _dollars_to_conservative_cents(value: Decimal, *, field: str) -> int: + cents = int( + (value.copy_abs() * Decimal(100)).to_integral_value( + rounding=ROUND_CEILING + ) + ) + return _integer(cents, field=field) + + +def _whole_count(value: Any, *, field: str) -> int: + parsed = _fixed_point(value, field=field, allow_negative=False) + if parsed != parsed.to_integral_value(): + raise KalshiBrokerTruthError( + f"{field} is fractional for a whole-contract DumbMoney order" + ) + return _integer(int(parsed), field=field) + + +def _parse_utc_text(value: Any, *, field: str) -> datetime: + if ( + not isinstance(value, str) + or not value.endswith("Z") + or len(value) > 64 + ): + raise KalshiBrokerTruthError(f"{field} is invalid") + try: + parsed = datetime.fromisoformat(value[:-1] + "+00:00") + except ValueError as exc: + raise KalshiBrokerTruthError(f"{field} is invalid") from exc + if parsed.tzinfo is None or parsed.utcoffset() is None: + raise KalshiBrokerTruthError(f"{field} is timezone-naive") + return parsed.astimezone(timezone.utc) + + +def _format_utc(value: datetime) -> str: + rendered = value.astimezone(timezone.utc).isoformat( + timespec="microseconds" if value.microsecond else "seconds" + ) + return rendered.replace("+00:00", "Z") + + +class KalshiBrokerTruthProvider: + """Build a stable, fully paginated projection of one Kalshi subaccount.""" + + def __init__( + self, + *, + api_key_id: str, + private_key_pem: bytes, + expected_account_hash: str, + subaccount_number: int, + client: httpx.Client | None = None, + clock: Callable[[], datetime] = lambda: datetime.now(timezone.utc), + maximum_pages: int = MAXIMUM_PAGES, + maximum_records: int = MAXIMUM_RECORDS, + ) -> None: + if not isinstance(api_key_id, str) or not _KEY_ID_RE.fullmatch( + api_key_id + ): + raise KalshiBrokerTruthError("Kalshi API key id is invalid") + if not isinstance(private_key_pem, bytes): + raise KalshiBrokerTruthError("Kalshi private key must be bytes") + try: + private_key = serialization.load_pem_private_key( + private_key_pem, + password=None, + ) + except (TypeError, ValueError) as exc: + raise KalshiBrokerTruthError( + "Kalshi private key is invalid" + ) from exc + if ( + not isinstance(private_key, RSAPrivateKey) + or private_key.key_size < 2_048 + ): + raise KalshiBrokerTruthError( + "Kalshi private key must be RSA with at least 2048 bits" + ) + if ( + not isinstance(expected_account_hash, str) + or not _SHA256_RE.fullmatch(expected_account_hash) + ): + raise KalshiBrokerTruthError("expected account hash is invalid") + if ( + isinstance(subaccount_number, bool) + or not isinstance(subaccount_number, int) + or subaccount_number != 0 + ): + raise KalshiBrokerTruthError( + "only the sealed primary Kalshi subaccount is supported" + ) + binding_hash = sha256_json( + { + "schema": "dummy.kalshi-account-binding.v1", + "venue": "dummy_kalshi", + "api_key_id": api_key_id, + "subaccount_number": subaccount_number, + } + ) + if binding_hash != expected_account_hash: + raise KalshiBrokerTruthError( + "Kalshi key and subaccount do not match the account pin" + ) + if ( + isinstance(maximum_pages, bool) + or not isinstance(maximum_pages, int) + or not 1 <= maximum_pages <= MAXIMUM_PAGES + ): + raise KalshiBrokerTruthError("maximum_pages is invalid") + if ( + isinstance(maximum_records, bool) + or not isinstance(maximum_records, int) + or not 1 <= maximum_records <= MAXIMUM_RECORDS + ): + raise KalshiBrokerTruthError("maximum_records is invalid") + + self._api_key_id = api_key_id + self._private_key = private_key + self._expected_account_hash = expected_account_hash + self._subaccount_number = subaccount_number + self._clock = clock + self._maximum_pages = maximum_pages + self._maximum_records = maximum_records + self._lock = threading.Lock() + self._client = client or httpx.Client( + base_url=f"{KALSHI_PRODUCTION_ORIGIN}{KALSHI_API_PREFIX}/", + timeout=httpx.Timeout(REQUEST_TIMEOUT_SECONDS), + follow_redirects=False, + trust_env=False, + limits=httpx.Limits( + max_connections=1, + max_keepalive_connections=1, + ), + ) + + def _now(self) -> datetime: + value = self._clock() + if value.tzinfo is None or value.utcoffset() is None: + raise KalshiBrokerTruthError( + "broker-truth clock must be timezone-aware" + ) + return value.astimezone(timezone.utc) + + def _headers(self, endpoint_path: str) -> dict[str, str]: + timestamp = str(int(self._now().timestamp() * 1_000)) + signed_path = f"{KALSHI_API_PREFIX}/{endpoint_path.lstrip('/')}" + message = f"{timestamp}GET{signed_path}".encode("utf-8") + signature = self._private_key.sign( + message, + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.DIGEST_LENGTH, + ), + hashes.SHA256(), + ) + return { + "Accept": "application/json", + "KALSHI-ACCESS-KEY": self._api_key_id, + "KALSHI-ACCESS-SIGNATURE": base64.b64encode(signature).decode( + "ascii" + ), + "KALSHI-ACCESS-TIMESTAMP": timestamp, + } + + def _request( + self, + endpoint_path: str, + *, + params: Mapping[str, Any], + ) -> Mapping[str, Any]: + if endpoint_path not in { + "historical/fills", + "historical/orders", + "portfolio/balance", + "portfolio/fills", + "portfolio/positions", + "portfolio/orders", + "portfolio/settlements", + }: + raise KalshiBrokerTruthError("broker-truth endpoint is not allowed") + try: + with self._client.stream( + "GET", + endpoint_path, + params=dict(params), + headers=self._headers(endpoint_path), + ) as response: + response_url = response.request.url + expected_path = ( + f"{KALSHI_API_PREFIX}/{endpoint_path.lstrip('/')}" + ) + if ( + response_url.scheme != "https" + or response_url.host != "external-api.kalshi.com" + or response_url.port not in {None, 443} + or response_url.path != expected_path + ): + raise KalshiBrokerTruthError( + "Kalshi response origin or path is not sealed" + ) + length_header = response.headers.get("content-length") + if length_header is not None: + try: + content_length = int(length_header) + except ValueError as exc: + raise KalshiBrokerTruthError( + "Kalshi Content-Length is invalid" + ) from exc + if ( + content_length < 0 + or content_length > MAXIMUM_RESPONSE_BYTES + ): + raise KalshiBrokerTruthError( + "Kalshi response exceeds the byte limit" + ) + body = bytearray() + for chunk in response.iter_bytes(): + body.extend(chunk) + if len(body) > MAXIMUM_RESPONSE_BYTES: + raise KalshiBrokerTruthError( + "Kalshi response exceeds the byte limit" + ) + if response.status_code != 200: + raise KalshiBrokerTruthError( + "Kalshi read failed with " + f"HTTP status {response.status_code}" + ) + except KalshiBrokerTruthError: + raise + except (httpx.HTTPError, OSError) as exc: + raise KalshiBrokerTruthError( + "Kalshi read transport failed" + ) from exc + try: + payload = load_strict_json_response( + _BufferedResponse(bytes(body)), + maximum_bytes=MAXIMUM_RESPONSE_BYTES, + ) + except (TypeError, ValueError) as exc: + raise KalshiBrokerTruthError( + "Kalshi read response is not strict bounded JSON" + ) from exc + return _mapping(payload, field=f"Kalshi {endpoint_path} response") + + def _balance(self) -> dict[str, int | str]: + payload = self._request( + "portfolio/balance", + params={"subaccount": self._subaccount_number}, + ) + balance = _integer(payload.get("balance"), field="balance") + portfolio_value = _integer( + payload.get("portfolio_value"), + field="portfolio_value", + ) + updated_ts = _integer( + payload.get("updated_ts"), + field="updated_ts", + ) + balance_dollars = _fixed_point( + payload.get("balance_dollars"), + field="balance_dollars", + allow_negative=False, + ) + if balance_dollars * Decimal(100) != Decimal(balance): + raise KalshiBrokerTruthError( + "balance cents and fixed-point dollars disagree" + ) + return { + "balance_cents": balance, + "balance_dollars": str(balance_dollars), + "portfolio_value_cents": portfolio_value, + "updated_ts": updated_ts, + } + + def _positions(self) -> dict[str, list[dict[str, Any]]]: + market_positions: list[dict[str, Any]] = [] + event_positions: list[dict[str, Any]] = [] + seen_market: set[str] = set() + seen_event: set[str] = set() + seen_cursors: set[str] = set() + cursor = "" + + for _page in range(self._maximum_pages): + params: dict[str, Any] = { + "limit": 1_000, + "count_filter": "position", + "subaccount": self._subaccount_number, + } + if cursor: + params["cursor"] = cursor + payload = self._request("portfolio/positions", params=params) + markets = _list( + payload.get("market_positions"), + field="market_positions", + ) + events = _list( + payload.get("event_positions"), + field="event_positions", + ) + for index, raw in enumerate(markets): + item = _mapping( + raw, + field=f"market_positions[{index}]", + ) + ticker = _identifier( + item.get("ticker"), + field="market position ticker", + ticker=True, + ) + if ticker in seen_market: + raise KalshiBrokerTruthError( + "market position ticker is duplicated" + ) + seen_market.add(ticker) + position = _fixed_point( + item.get("position_fp"), + field=f"{ticker}.position_fp", + allow_negative=True, + ) + if position == 0: + raise KalshiBrokerTruthError( + "position-filtered response contains a zero position" + ) + exposure = _fixed_point( + item.get("market_exposure_dollars"), + field=f"{ticker}.market_exposure_dollars", + allow_negative=True, + ) + updated = item.get("last_updated_ts") + if ( + not isinstance(updated, str) + or not updated.endswith("Z") + or len(updated) > 64 + ): + raise KalshiBrokerTruthError( + "market position timestamp is invalid" + ) + try: + parsed_updated = datetime.fromisoformat( + updated[:-1] + "+00:00" + ) + except ValueError as exc: + raise KalshiBrokerTruthError( + "market position timestamp is invalid" + ) from exc + if ( + parsed_updated.tzinfo is None + or parsed_updated.utcoffset() is None + ): + raise KalshiBrokerTruthError( + "market position timestamp is naive" + ) + if parsed_updated > self._now() + timedelta(seconds=5): + raise KalshiBrokerTruthError( + "market position timestamp is in the future" + ) + market_positions.append( + { + "ticker": ticker, + "position_fp": str(position), + "market_exposure_cents": ( + _dollars_to_conservative_cents( + exposure, + field=f"{ticker}.market_exposure_cents", + ) + ), + "last_updated_ts": _format_utc(parsed_updated), + } + ) + for index, raw in enumerate(events): + item = _mapping( + raw, + field=f"event_positions[{index}]", + ) + event_ticker = _identifier( + item.get("event_ticker"), + field="event position ticker", + ticker=True, + ) + if event_ticker in seen_event: + raise KalshiBrokerTruthError( + "event position ticker is duplicated" + ) + seen_event.add(event_ticker) + exposure = _fixed_point( + item.get("event_exposure_dollars"), + field=f"{event_ticker}.event_exposure_dollars", + allow_negative=True, + ) + event_positions.append( + { + "event_ticker": event_ticker, + "event_exposure_cents": ( + _dollars_to_conservative_cents( + exposure, + field=( + f"{event_ticker}.event_exposure_cents" + ), + ) + ), + } + ) + if ( + len(market_positions) + len(event_positions) + > self._maximum_records + ): + raise KalshiBrokerTruthError( + "position snapshot exceeds the record limit" + ) + next_cursor = _cursor( + payload.get("cursor"), + field="positions cursor", + ) + if not next_cursor: + market_positions.sort(key=lambda item: item["ticker"]) + event_positions.sort(key=lambda item: item["event_ticker"]) + return { + "market_positions": market_positions, + "event_positions": event_positions, + } + if next_cursor == cursor or next_cursor in seen_cursors: + raise KalshiBrokerTruthError( + "positions cursor repeated" + ) + seen_cursors.add(next_cursor) + cursor = next_cursor + raise KalshiBrokerTruthError("positions pagination is incomplete") + + def _orders(self) -> list[dict[str, Any]]: + orders: list[dict[str, Any]] = [] + seen_orders: set[str] = set() + seen_cursors: set[str] = set() + cursor = "" + + for _page in range(self._maximum_pages): + params: dict[str, Any] = { + "status": "resting", + "limit": 1_000, + "subaccount": self._subaccount_number, + } + if cursor: + params["cursor"] = cursor + payload = self._request("portfolio/orders", params=params) + page_orders = _list(payload.get("orders"), field="orders") + for index, raw in enumerate(page_orders): + item = _mapping(raw, field=f"orders[{index}]") + order_id = _identifier( + item.get("order_id"), + field="order_id", + ) + if order_id in seen_orders: + raise KalshiBrokerTruthError("order_id is duplicated") + seen_orders.add(order_id) + ticker = _identifier( + item.get("ticker"), + field=f"{order_id}.ticker", + ticker=True, + ) + subaccount = _integer( + item.get("subaccount_number"), + field=f"{order_id}.subaccount_number", + maximum=63, + ) + if subaccount != self._subaccount_number: + raise KalshiBrokerTruthError( + "order belongs to a different subaccount" + ) + remaining = _fixed_point( + item.get("remaining_count_fp"), + field=f"{order_id}.remaining_count_fp", + allow_negative=False, + ) + if remaining <= 0: + raise KalshiBrokerTruthError( + "resting order has no remaining count" + ) + yes_price = _fixed_point( + item.get("yes_price_dollars"), + field=f"{order_id}.yes_price_dollars", + allow_negative=False, + ) + no_price = _fixed_point( + item.get("no_price_dollars"), + field=f"{order_id}.no_price_dollars", + allow_negative=False, + ) + if ( + yes_price > 1 + or no_price > 1 + or yes_price + no_price != 1 + ): + raise KalshiBrokerTruthError( + "order price pair is inconsistent" + ) + orders.append( + { + "order_id": order_id, + "ticker": ticker, + "subaccount_number": subaccount, + "remaining_count_fp": str(remaining), + "yes_price_dollars": str(yes_price), + "no_price_dollars": str(no_price), + } + ) + if len(orders) > self._maximum_records: + raise KalshiBrokerTruthError( + "order snapshot exceeds the record limit" + ) + next_cursor = _cursor( + payload.get("cursor"), + field="orders cursor", + ) + if not next_cursor: + orders.sort(key=lambda item: item["order_id"]) + return orders + if next_cursor == cursor or next_cursor in seen_cursors: + raise KalshiBrokerTruthError("orders cursor repeated") + seen_cursors.add(next_cursor) + cursor = next_cursor + raise KalshiBrokerTruthError("orders pagination is incomplete") + + def _stable_state(self) -> dict[str, Any]: + return { + "positions": self._positions(), + "resting_orders": self._orders(), + } + + def snapshot(self) -> Mapping[str, Any]: + """Return one stable account-scoped truth projection or fail closed.""" + if not self._lock.acquire(blocking=False): + raise KalshiBrokerTruthError( + "a broker-truth snapshot is already in progress" + ) + try: + balance = self._balance() + first_state = self._stable_state() + second_state = self._stable_state() + if canonical_json(first_state) != canonical_json(second_state): + raise KalshiBrokerTruthError( + "Kalshi positions or orders changed during reconciliation" + ) + observed_at = self._now() + market_positions = first_state["positions"]["market_positions"] + event_positions = first_state["positions"]["event_positions"] + resting_orders = first_state["resting_orders"] + if event_positions and not market_positions: + raise KalshiBrokerTruthError( + "event exposure exists without a market position" + ) + market_exposure = { + item["ticker"]: item["market_exposure_cents"] + for item in market_positions + } + correlated_exposure = { + item["event_ticker"]: item["event_exposure_cents"] + for item in event_positions + } + market_total = sum(market_exposure.values()) + correlated_total = sum(correlated_exposure.values()) + total_exposure = max(market_total, correlated_total) + if market_positions and not correlated_exposure: + correlated_exposure = { + "ACCOUNT_WIDE_UNGROUPED": total_exposure + } + flat = ( + not market_positions + and not resting_orders + and total_exposure == 0 + ) + digest_payload = { + "schema": "dummy.kalshi-broker-raw-snapshot.v1", + "venue": "dummy_kalshi", + "account_hash": self._expected_account_hash, + "subaccount_number": self._subaccount_number, + "observed_at": _format_utc(observed_at), + "balance": balance, + "state": first_state, + } + return { + "schema": "dummy.kalshi-broker-truth.v1", + "venue": "dummy_kalshi", + "account_hash": self._expected_account_hash, + "subaccount_number": self._subaccount_number, + "observed_at": digest_payload["observed_at"], + "broker_snapshot_sha256": sha256_json(digest_payload), + "flat_book_observed": flat, + "total_exposure_cents": total_exposure, + "open_order_count": len(resting_orders), + "market_exposure_cents": market_exposure, + "correlated_exposure_cents": correlated_exposure, + "unresolved_open_orders": len(resting_orders), + "unresolved_positions": len(market_positions), + } + finally: + self._lock.release() + + def close(self) -> None: + self._client.close() + + +__all__ = [ + "KALSHI_API_PREFIX", + "KALSHI_PRODUCTION_ORIGIN", + "KalshiBrokerTruthError", + "KalshiBrokerTruthProvider", +] diff --git a/live_firewall/kalshi_reconciliation.py b/live_firewall/kalshi_reconciliation.py new file mode 100644 index 00000000..2c1113ad --- /dev/null +++ b/live_firewall/kalshi_reconciliation.py @@ -0,0 +1,891 @@ +"""Read-only, signed Kalshi reconciliation witnesses for DumbMoney. + +The reader intentionally queries both current and historical portfolio tiers. +Kalshi partitions completed orders and fills at moving cutoffs, so consulting +only the live portfolio endpoints cannot prove absence or completeness after a +restart. Every collection is fully paginated, normalized, and read twice +before the local venue identity signs the resulting witness. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from datetime import timedelta +from decimal import Decimal, ROUND_CEILING +from typing import Any + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from live_firewall.dumbmoney_broker_witness import ( + SETTLEMENT_WITNESS_SCHEMA, + TERMINAL_WITNESS_SCHEMA, + sign_broker_witness, +) +from live_firewall.kalshi_broker_truth import ( + KalshiBrokerTruthError, + KalshiBrokerTruthProvider, + _cursor, + _dollars_to_conservative_cents, + _fixed_point, + _format_utc, + _identifier, + _integer, + _list, + _mapping, + _parse_utc_text, + _whole_count, +) +from live_firewall.operational_journal import canonical_json, sha256_json + + +class KalshiReconciliationReader: + """Produce signed terminal and settlement facts without mutation methods.""" + + def __init__( + self, + *, + broker_truth: KalshiBrokerTruthProvider, + witness_signing_private_key: Ed25519PrivateKey, + ) -> None: + self._broker_truth = broker_truth + self._witness_signing_private_key = witness_signing_private_key + + def _paginated_rows( + self, + endpoint_path: str, + *, + collection: str, + base_params: Mapping[str, Any], + ) -> list[Mapping[str, Any]]: + rows: list[Mapping[str, Any]] = [] + seen_cursors: set[str] = set() + cursor = "" + for _page in range(self._broker_truth._maximum_pages): + params = {"limit": 1_000, **dict(base_params)} + if cursor: + params["cursor"] = cursor + payload = self._broker_truth._request( + endpoint_path, + params=params, + ) + page = _list(payload.get(collection), field=collection) + for index, raw in enumerate(page): + rows.append( + _mapping(raw, field=f"{collection}[{index}]") + ) + if len(rows) > self._broker_truth._maximum_records: + raise KalshiBrokerTruthError( + f"{collection} snapshot exceeds the record limit" + ) + next_cursor = _cursor( + payload.get("cursor"), + field=f"{collection} cursor", + ) + if not next_cursor: + return rows + if next_cursor == cursor or next_cursor in seen_cursors: + raise KalshiBrokerTruthError( + f"{collection} cursor repeated" + ) + seen_cursors.add(next_cursor) + cursor = next_cursor + raise KalshiBrokerTruthError( + f"{collection} pagination is incomplete" + ) + + def _normalized_order( + self, + raw: Mapping[str, Any], + *, + source_tier: str, + ) -> dict[str, Any]: + order_id = _identifier(raw.get("order_id"), field="order_id") + client_order_id = _identifier( + raw.get("client_order_id"), + field=f"{order_id}.client_order_id", + ) + ticker = _identifier( + raw.get("ticker"), + field=f"{order_id}.ticker", + ticker=True, + ) + status = raw.get("status") + if status not in {"resting", "canceled", "executed"}: + raise KalshiBrokerTruthError("order status is unsupported") + if raw.get("action") != "buy": + raise KalshiBrokerTruthError( + "DumbMoney reconciliation observed a non-buy order" + ) + outcome_side = raw.get("outcome_side") + book_side = raw.get("book_side") + if outcome_side not in {"yes", "no"}: + raise KalshiBrokerTruthError("order outcome_side is invalid") + if book_side != ("bid" if outcome_side == "yes" else "ask"): + raise KalshiBrokerTruthError( + "order direction fields are inconsistent" + ) + subaccount = _integer( + raw.get("subaccount_number"), + field=f"{order_id}.subaccount_number", + maximum=63, + ) + initial_count = _whole_count( + raw.get("initial_count_fp"), + field=f"{order_id}.initial_count_fp", + ) + fill_count = _whole_count( + raw.get("fill_count_fp"), + field=f"{order_id}.fill_count_fp", + ) + remaining_count = _whole_count( + raw.get("remaining_count_fp"), + field=f"{order_id}.remaining_count_fp", + ) + if ( + initial_count < 1 + or fill_count > initial_count + or remaining_count > initial_count - fill_count + ): + raise KalshiBrokerTruthError("order counts are inconsistent") + yes_price = _fixed_point( + raw.get("yes_price_dollars"), + field=f"{order_id}.yes_price_dollars", + allow_negative=False, + ) + no_price = _fixed_point( + raw.get("no_price_dollars"), + field=f"{order_id}.no_price_dollars", + allow_negative=False, + ) + if ( + yes_price > 1 + or no_price > 1 + or yes_price + no_price != 1 + ): + raise KalshiBrokerTruthError( + "order price pair is inconsistent" + ) + taker_cost = _fixed_point( + raw.get("taker_fill_cost_dollars"), + field=f"{order_id}.taker_fill_cost_dollars", + allow_negative=False, + ) + maker_cost = _fixed_point( + raw.get("maker_fill_cost_dollars"), + field=f"{order_id}.maker_fill_cost_dollars", + allow_negative=False, + ) + taker_fees = _fixed_point( + raw.get("taker_fees_dollars"), + field=f"{order_id}.taker_fees_dollars", + allow_negative=False, + ) + maker_fees = _fixed_point( + raw.get("maker_fees_dollars"), + field=f"{order_id}.maker_fees_dollars", + allow_negative=False, + ) + created = _parse_utc_text( + raw.get("created_time"), + field=f"{order_id}.created_time", + ) + updated = _parse_utc_text( + raw.get("last_update_time"), + field=f"{order_id}.last_update_time", + ) + if ( + updated < created + or updated + > self._broker_truth._now() + timedelta(seconds=5) + ): + raise KalshiBrokerTruthError( + "order timestamps are inconsistent" + ) + return { + "order_id": order_id, + "client_order_id": client_order_id, + "ticker": ticker, + "status": status, + "outcome_side": outcome_side, + "book_side": book_side, + "subaccount_number": subaccount, + "initial_count": initial_count, + "fill_count": fill_count, + "remaining_count": remaining_count, + "yes_price_dollars": str(yes_price), + "no_price_dollars": str(no_price), + "fill_cost_dollars": str(taker_cost + maker_cost), + "fee_dollars": str(taker_fees + maker_fees), + "created_time": _format_utc(created), + "last_update_time": _format_utc(updated), + "source_tier": source_tier, + } + + @staticmethod + def _merge_tiered_row( + rows: dict[str, dict[str, Any]], + normalized: dict[str, Any], + *, + identity_field: str, + conflict_message: str, + ) -> None: + identity = str(normalized[identity_field]) + prior = rows.get(identity) + if prior is None: + rows[identity] = normalized + return + comparable_prior = { + key: value + for key, value in prior.items() + if key != "source_tier" + } + comparable_current = { + key: value + for key, value in normalized.items() + if key != "source_tier" + } + if comparable_prior != comparable_current: + raise KalshiBrokerTruthError(conflict_message) + prior["source_tier"] = "both" + + def _orders(self, ticker: str) -> list[dict[str, Any]]: + tiers = ( + ( + "portfolio", + "portfolio/orders", + { + "ticker": ticker, + "subaccount": self._broker_truth._subaccount_number, + }, + ), + ( + "historical", + "historical/orders", + {"ticker": ticker}, + ), + ) + by_id: dict[str, dict[str, Any]] = {} + for source_tier, endpoint, params in tiers: + for raw in self._paginated_rows( + endpoint, + collection="orders", + base_params=params, + ): + normalized = self._normalized_order( + raw, + source_tier=source_tier, + ) + if ( + normalized["subaccount_number"] + != self._broker_truth._subaccount_number + ): + if source_tier == "portfolio": + raise KalshiBrokerTruthError( + "live order belongs to a different subaccount" + ) + continue + self._merge_tiered_row( + by_id, + normalized, + identity_field="order_id", + conflict_message=( + "live and historical order rows conflict" + ), + ) + return sorted(by_id.values(), key=lambda item: item["order_id"]) + + def _normalized_fill( + self, + raw: Mapping[str, Any], + *, + source_tier: str, + ) -> dict[str, Any]: + fill_id = _identifier(raw.get("fill_id"), field="fill_id") + order_id = _identifier( + raw.get("order_id"), + field=f"{fill_id}.order_id", + ) + ticker = _identifier( + raw.get("ticker"), + field=f"{fill_id}.ticker", + ticker=True, + ) + market_ticker = _identifier( + raw.get("market_ticker"), + field=f"{fill_id}.market_ticker", + ticker=True, + ) + if raw.get("action") != "buy": + raise KalshiBrokerTruthError( + "DumbMoney reconciliation observed a non-buy fill" + ) + outcome_side = raw.get("outcome_side") + book_side = raw.get("book_side") + if outcome_side not in {"yes", "no"}: + raise KalshiBrokerTruthError("fill outcome_side is invalid") + if book_side != ("bid" if outcome_side == "yes" else "ask"): + raise KalshiBrokerTruthError( + "fill direction fields are inconsistent" + ) + subaccount = _integer( + raw.get("subaccount_number"), + field=f"{fill_id}.subaccount_number", + maximum=63, + ) + count = _whole_count( + raw.get("count_fp"), + field=f"{fill_id}.count_fp", + ) + if count < 1: + raise KalshiBrokerTruthError("fill count must be positive") + yes_price = _fixed_point( + raw.get("yes_price_dollars"), + field=f"{fill_id}.yes_price_dollars", + allow_negative=False, + ) + no_price = _fixed_point( + raw.get("no_price_dollars"), + field=f"{fill_id}.no_price_dollars", + allow_negative=False, + ) + if ( + yes_price > 1 + or no_price > 1 + or yes_price + no_price != 1 + ): + raise KalshiBrokerTruthError("fill price pair is inconsistent") + fee = _fixed_point( + raw.get("fee_cost"), + field=f"{fill_id}.fee_cost", + allow_negative=False, + ) + created = _parse_utc_text( + raw.get("created_time"), + field=f"{fill_id}.created_time", + ) + if ( + created + > self._broker_truth._now() + timedelta(seconds=5) + ): + raise KalshiBrokerTruthError( + "fill timestamp is in the future" + ) + price = yes_price if outcome_side == "yes" else no_price + return { + "fill_id": fill_id, + "order_id": order_id, + "ticker": ticker, + "market_ticker": market_ticker, + "outcome_side": outcome_side, + "book_side": book_side, + "subaccount_number": subaccount, + "count": count, + "price_dollars": str(price), + "cost_dollars": str(price * count), + "fee_dollars": str(fee), + "created_time": _format_utc(created), + "source_tier": source_tier, + } + + def _fills( + self, + *, + order_id: str, + ticker: str, + ) -> list[dict[str, Any]]: + tiers = ( + ( + "portfolio", + "portfolio/fills", + { + "order_id": order_id, + "subaccount": self._broker_truth._subaccount_number, + }, + ), + ( + "historical", + "historical/fills", + {"ticker": ticker}, + ), + ) + by_id: dict[str, dict[str, Any]] = {} + for source_tier, endpoint, params in tiers: + for raw in self._paginated_rows( + endpoint, + collection="fills", + base_params=params, + ): + normalized = self._normalized_fill( + raw, + source_tier=source_tier, + ) + if ( + normalized["subaccount_number"] + != self._broker_truth._subaccount_number + ): + if source_tier == "portfolio": + raise KalshiBrokerTruthError( + "live fill belongs to a different subaccount" + ) + continue + if normalized["order_id"] != order_id: + if source_tier == "portfolio": + raise KalshiBrokerTruthError( + "live fill query returned another order" + ) + continue + self._merge_tiered_row( + by_id, + normalized, + identity_field="fill_id", + conflict_message=( + "live and historical fill rows conflict" + ), + ) + return sorted(by_id.values(), key=lambda item: item["fill_id"]) + + def _terminal_projection( + self, + reservation: Mapping[str, Any], + ) -> dict[str, Any]: + proposal_id = _identifier( + reservation.get("proposal_id"), + field="reservation.proposal_id", + ) + ticker = _identifier( + reservation.get("contract_ticker"), + field="reservation.contract_ticker", + ticker=True, + ) + side = reservation.get("side") + if side not in {"yes", "no"}: + raise KalshiBrokerTruthError("reservation side is invalid") + size = _integer( + reservation.get("size"), + field="reservation.size", + minimum=1, + ) + price_cents = _integer( + reservation.get("price_cents"), + field="reservation.price_cents", + minimum=1, + maximum=99, + ) + orders = [ + order + for order in self._orders(ticker) + if order["client_order_id"] == proposal_id + ] + if len(orders) > 1: + raise KalshiBrokerTruthError( + "client_order_id resolves to multiple broker orders" + ) + if not orders: + return { + "state": "NOT_FOUND", + "proposal_id": proposal_id, + "ticker": ticker, + } + order = orders[0] + if ( + order["ticker"] != ticker + or order["outcome_side"] != side + or order["initial_count"] != size + or ( + Decimal( + str(order[f"{side}_price_dollars"]) + ) + * Decimal(100) + != Decimal(price_cents) + ) + ): + raise KalshiBrokerTruthError( + "broker order differs from the capital reservation" + ) + if order["status"] == "resting": + if order["remaining_count"] <= 0: + raise KalshiBrokerTruthError( + "resting order has no remaining count" + ) + return {"state": "RESTING", "order": order} + if order["remaining_count"] != 0: + raise KalshiBrokerTruthError( + "terminal order retains a remaining count" + ) + if ( + order["status"] == "executed" + and order["fill_count"] != size + ): + raise KalshiBrokerTruthError( + "executed order lacks a complete fill" + ) + fills = self._fills( + order_id=order["order_id"], + ticker=ticker, + ) + fill_count = sum(int(item["count"]) for item in fills) + fill_cost = sum( + ( + Decimal(str(item["cost_dollars"])) + for item in fills + ), + Decimal(0), + ) + fee_cost = sum( + ( + Decimal(str(item["fee_dollars"])) + for item in fills + ), + Decimal(0), + ) + if ( + fill_count != order["fill_count"] + or fill_cost != Decimal(str(order["fill_cost_dollars"])) + or fee_cost != Decimal(str(order["fee_dollars"])) + or any(item["ticker"] != ticker for item in fills) + or any(item["outcome_side"] != side for item in fills) + ): + raise KalshiBrokerTruthError( + "order and fill projections disagree" + ) + return { + "state": "TERMINAL", + "order": order, + "fills": fills, + "fill_count": fill_count, + "fill_cost_dollars": str(fill_cost), + "fee_dollars": str(fee_cost), + } + + def terminal_reconciliation_witness( + self, + reservation: Mapping[str, Any], + ) -> Mapping[str, Any] | None: + """Return a signed terminal witness, or retain unresolved authority.""" + first = self._terminal_projection(reservation) + second = self._terminal_projection(reservation) + if canonical_json(first) != canonical_json(second): + raise KalshiBrokerTruthError( + "order or fill state changed during reconciliation" + ) + if first["state"] != "TERMINAL": + return None + observed = self._broker_truth._now() + order = first["order"] + fill_count = int(first["fill_count"]) + fill_cost = Decimal(str(first["fill_cost_dollars"])) + fee = Decimal(str(first["fee_dollars"])) + average_price_cents = ( + int( + ( + fill_cost + * Decimal(100) + / Decimal(fill_count) + ).to_integral_value(rounding=ROUND_CEILING) + ) + if fill_count + else None + ) + projection_digest = sha256_json( + { + "schema": "dummy.kalshi-terminal-projection.v1", + "projection": first, + } + ) + invariant = { + "venue": "dummy_kalshi", + "account_hash": self._broker_truth._expected_account_hash, + "subaccount_number": ( + self._broker_truth._subaccount_number + ), + "reservation_id": reservation.get("reservation_id"), + "proposal_id": reservation.get("proposal_id"), + "order": first["order"], + "fills": first["fills"], + } + body = { + "schema": TERMINAL_WITNESS_SCHEMA, + "witness_id": sha256_json(invariant), + "venue": "dummy_kalshi", + "account_hash": self._broker_truth._expected_account_hash, + "subaccount_number": ( + self._broker_truth._subaccount_number + ), + "reservation_id": _identifier( + reservation.get("reservation_id"), + field="reservation.reservation_id", + ), + "proposal_id": order["client_order_id"], + "order_id": order["order_id"], + "market_ticker": _identifier( + reservation.get("market_ticker"), + field="reservation.market_ticker", + ticker=True, + ), + "contract_ticker": order["ticker"], + "side": order["outcome_side"], + "terminal_status": order["status"], + "initial_count": order["initial_count"], + "fill_count": fill_count, + "remaining_count": order["remaining_count"], + "fill_cost_cents": _dollars_to_conservative_cents( + fill_cost, + field="fill_cost_cents", + ), + "fee_cents": _dollars_to_conservative_cents( + fee, + field="fee_cents", + ), + "average_fill_price_cents": average_price_cents, + "fill_ids": [item["fill_id"] for item in first["fills"]], + "observed_at": _format_utc(observed), + "broker_projection_sha256": projection_digest, + } + return sign_broker_witness( + body, + private_key=self._witness_signing_private_key, + observed_at=observed, + correlation_id=order["client_order_id"], + ) + + def _settlements(self, ticker: str) -> list[dict[str, Any]]: + rows = self._paginated_rows( + "portfolio/settlements", + collection="settlements", + base_params={ + "ticker": ticker, + "subaccount": self._broker_truth._subaccount_number, + }, + ) + settlements: list[dict[str, Any]] = [] + seen: set[str] = set() + for raw in rows: + row_ticker = _identifier( + raw.get("ticker"), + field="settlement.ticker", + ticker=True, + ) + if row_ticker in seen: + raise KalshiBrokerTruthError( + "settlement ticker is duplicated" + ) + seen.add(row_ticker) + result = raw.get("market_result") + if result not in {"yes", "no"}: + raise KalshiBrokerTruthError( + "settlement result is invalid" + ) + settled = _parse_utc_text( + raw.get("settled_time"), + field=f"{row_ticker}.settled_time", + ) + if ( + settled + > self._broker_truth._now() + timedelta(seconds=5) + ): + raise KalshiBrokerTruthError( + "settlement timestamp is in the future" + ) + yes_count = _fixed_point( + raw.get("yes_count_fp"), + field=f"{row_ticker}.yes_count_fp", + allow_negative=False, + ) + no_count = _fixed_point( + raw.get("no_count_fp"), + field=f"{row_ticker}.no_count_fp", + allow_negative=False, + ) + yes_cost = _fixed_point( + raw.get("yes_total_cost_dollars"), + field=f"{row_ticker}.yes_total_cost_dollars", + allow_negative=False, + ) + no_cost = _fixed_point( + raw.get("no_total_cost_dollars"), + field=f"{row_ticker}.no_total_cost_dollars", + allow_negative=False, + ) + fee = _fixed_point( + raw.get("fee_cost"), + field=f"{row_ticker}.fee_cost", + allow_negative=False, + ) + revenue = _integer( + raw.get("revenue"), + field=f"{row_ticker}.revenue", + ) + settlements.append( + { + "ticker": row_ticker, + "event_ticker": _identifier( + raw.get("event_ticker"), + field=f"{row_ticker}.event_ticker", + ticker=True, + ), + "market_result": result, + "yes_count_fp": str(yes_count), + "no_count_fp": str(no_count), + "yes_total_cost_dollars": str(yes_cost), + "no_total_cost_dollars": str(no_cost), + "fee_cost_dollars": str(fee), + "revenue_cents": revenue, + "settled_time": _format_utc(settled), + } + ) + return sorted(settlements, key=lambda item: item["ticker"]) + + def _settlement_projection( + self, + position_exposure: Mapping[str, Any], + ) -> dict[str, Any]: + ticker = _identifier( + position_exposure.get("contract_ticker"), + field="position_exposure.contract_ticker", + ticker=True, + ) + positions = self._broker_truth._positions() + market_positions = [ + item + for item in positions["market_positions"] + if item["ticker"] == ticker + ] + settlements = [ + item + for item in self._settlements(ticker) + if item["ticker"] == ticker + ] + if market_positions: + return { + "state": "POSITION_OPEN", + "positions": market_positions, + "settlements": settlements, + } + if len(settlements) > 1: + raise KalshiBrokerTruthError( + "multiple settlement rows exist for one market" + ) + if not settlements: + return { + "state": "POSITION_ABSENT_UNSETTLED", + "positions": [], + "settlements": [], + } + side = position_exposure.get("side") + if side not in {"yes", "no"}: + raise KalshiBrokerTruthError( + "position exposure side is invalid" + ) + settlement = settlements[0] + position_observed = _parse_utc_text( + position_exposure.get("observed_at"), + field="position_exposure.observed_at", + ) + settlement_observed = _parse_utc_text( + settlement["settled_time"], + field="settlement.settled_time", + ) + if settlement_observed < position_observed: + raise KalshiBrokerTruthError( + "settlement predates the filled position exposure" + ) + settled_count = Decimal( + str(settlement[f"{side}_count_fp"]) + ) + fill_count = _integer( + position_exposure.get("fill_count"), + field="position_exposure.fill_count", + minimum=1, + ) + if settled_count < fill_count: + raise KalshiBrokerTruthError( + "settlement count does not cover the filled exposure" + ) + return { + "state": "SETTLED_POSITION_ABSENT", + "positions": [], + "settlement": settlement, + } + + def settlement_reconciliation_witness( + self, + position_exposure: Mapping[str, Any], + ) -> Mapping[str, Any] | None: + """Return a signed settlement/position-close witness when complete.""" + first = self._settlement_projection(position_exposure) + second = self._settlement_projection(position_exposure) + if canonical_json(first) != canonical_json(second): + raise KalshiBrokerTruthError( + "position or settlement state changed during reconciliation" + ) + if first["state"] != "SETTLED_POSITION_ABSENT": + return None + observed = self._broker_truth._now() + settlement = first["settlement"] + projection_digest = sha256_json( + { + "schema": "dummy.kalshi-settlement-projection.v1", + "projection": first, + } + ) + invariant = { + "venue": "dummy_kalshi", + "account_hash": self._broker_truth._expected_account_hash, + "subaccount_number": ( + self._broker_truth._subaccount_number + ), + "position_exposure_id": position_exposure.get( + "position_exposure_id" + ), + "settlement": settlement, + "position_absent": True, + } + body = { + "schema": SETTLEMENT_WITNESS_SCHEMA, + "witness_id": sha256_json(invariant), + "venue": "dummy_kalshi", + "account_hash": self._broker_truth._expected_account_hash, + "subaccount_number": ( + self._broker_truth._subaccount_number + ), + "position_exposure_id": _identifier( + position_exposure.get("position_exposure_id"), + field="position_exposure.position_exposure_id", + ), + "reservation_id": _identifier( + position_exposure.get("reservation_id"), + field="position_exposure.reservation_id", + ), + "proposal_id": _identifier( + position_exposure.get("proposal_id"), + field="position_exposure.proposal_id", + ), + "contract_ticker": settlement["ticker"], + "side": position_exposure.get("side"), + "fill_count": _integer( + position_exposure.get("fill_count"), + field="position_exposure.fill_count", + minimum=1, + ), + "market_result": settlement["market_result"], + "settled_at": settlement["settled_time"], + "revenue_cents": settlement["revenue_cents"], + "settlement_fee_cents": ( + _dollars_to_conservative_cents( + Decimal(str(settlement["fee_cost_dollars"])), + field="settlement_fee_cents", + ) + ), + "position_absent": True, + "observed_at": _format_utc(observed), + "broker_projection_sha256": projection_digest, + } + return sign_broker_witness( + body, + private_key=self._witness_signing_private_key, + observed_at=observed, + correlation_id=body["proposal_id"], + ) + + +__all__ = ["KalshiReconciliationReader"] diff --git a/live_firewall/operational_journal.py b/live_firewall/operational_journal.py new file mode 100644 index 00000000..bac4c225 --- /dev/null +++ b/live_firewall/operational_journal.py @@ -0,0 +1,414 @@ +"""Compact append-only operational journal and durable outbox. + +This journal is intentionally separate from Dummy's high-volume research +ledger. It records only low-volume authority, reservation, bootstrap, and +reconciliation facts needed at the live boundary. A broken hash chain is an +authority failure: callers can inspect the journal, but cannot append through +an unhealthy instance. +""" +from __future__ import annotations + +import hashlib +import json +import os +import threading +from contextlib import contextmanager +from collections.abc import Callable, Mapping +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, BinaryIO, Iterator, cast + + +OPERATIONAL_EVENT_SCHEMA = "dummy.operational-event.v1" + + +def canonical_json(value: Any) -> str: + """Return the shared DumbMoney canonical JSON representation.""" + return json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + allow_nan=False, + ) + + +def sha256_json(value: Any) -> str: + return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest() + + +class OperationalJournalError(RuntimeError): + """Raised when an append-only journal cannot prove its own history.""" + + +class AppendOnlyOperationalJournal: + """Hash-chained JSONL journal with serialized cross-process appends.""" + + def __init__( + self, + path: Path, + *, + now_fn: Callable[[], datetime] | None = None, + ) -> None: + self.path = Path(path) + self._now_fn = now_fn or (lambda: datetime.now(timezone.utc)) + self._lock = threading.RLock() + self._operation_lock = threading.RLock() + self._lock_path = self.path.with_name(f"{self.path.name}.lock") + self._operation_lock_path = self.path.with_name( + f"{self.path.name}.operation.lock" + ) + self._events: list[dict[str, Any]] = [] + self._error: str | None = None + self._load() + + @property + def healthy(self) -> bool: + return self._error is None + + @property + def error(self) -> str | None: + return self._error + + def events(self, *, kind: str | None = None) -> tuple[dict[str, Any], ...]: + with self._lock: + self._refresh_or_raise() + rows = list(self._events) + if kind is not None: + rows = [row for row in rows if row.get("kind") == kind] + # Canonical round-trip prevents callers from mutating journal state. + return tuple(json.loads(canonical_json(row)) for row in rows) + + def head(self) -> tuple[int, str]: + """Return the verified monotonic sequence and chain head.""" + with self._lock: + self._refresh_or_raise() + if not self._events: + return 0, "0" * 64 + latest = self._events[-1] + return int(latest["sequence"]), str(latest["event_sha256"]) + + def _load(self) -> None: + try: + with self._lock: + with self._interprocess_lock(): + self._reload_locked() + except Exception as exc: + self._error = f"{type(exc).__name__}: {exc}" + + @staticmethod + @contextmanager + def _lock_file(handle: BinaryIO) -> Iterator[None]: + handle.seek(0) + if os.name == "nt": + import msvcrt + + msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1) + try: + yield + finally: + handle.seek(0) + msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1) + return + + import fcntl + + fcntl.flock( # type: ignore[attr-defined] + handle.fileno(), + fcntl.LOCK_EX, # type: ignore[attr-defined] + ) + try: + yield + finally: + fcntl.flock( # type: ignore[attr-defined] + handle.fileno(), + fcntl.LOCK_UN, # type: ignore[attr-defined] + ) + + @contextmanager + def _interprocess_lock(self) -> Iterator[None]: + """Lock a stable sidecar byte for the full reload/append transaction.""" + self.path.parent.mkdir(parents=True, exist_ok=True) + with self._lock_path.open("a+b") as handle: + handle.seek(0, os.SEEK_END) + if handle.tell() == 0: + handle.write(b"\0") + handle.flush() + os.fsync(handle.fileno()) + with self._lock_file(handle): + yield + + @contextmanager + def serialized_operation(self) -> Iterator[None]: + """Serialize a caller-owned multi-step operation across processes. + + This lock is deliberately separate from the append lock so the caller + may safely perform journal reads/appends while holding it. + """ + with self._operation_lock: + self.path.parent.mkdir(parents=True, exist_ok=True) + with self._operation_lock_path.open("a+b") as handle: + handle.seek(0, os.SEEK_END) + if handle.tell() == 0: + handle.write(b"\0") + handle.flush() + os.fsync(handle.fileno()) + with self._lock_file(handle): + yield + + def _reload_locked(self) -> None: + raw = self.path.read_bytes() if self.path.exists() else b"" + if raw and not raw.endswith(b"\n"): + raise OperationalJournalError("journal has a partial trailing record") + previous = "0" * 64 + expected_sequence = 1 + rows: list[dict[str, Any]] = [] + for line in raw.splitlines(): + if not line: + raise OperationalJournalError("journal contains a blank record") + value = json.loads(line.decode("utf-8")) + if not isinstance(value, dict): + raise OperationalJournalError("journal record must be an object") + if value.get("schema") != OPERATIONAL_EVENT_SCHEMA: + raise OperationalJournalError("journal schema mismatch") + if value.get("sequence") != expected_sequence: + raise OperationalJournalError("journal sequence mismatch") + if value.get("previous_sha256") != previous: + raise OperationalJournalError("journal previous hash mismatch") + claimed = str(value.get("event_sha256") or "") + unsigned = { + key: item for key, item in value.items() if key != "event_sha256" + } + actual = sha256_json(unsigned) + if claimed != actual: + raise OperationalJournalError("journal event hash mismatch") + # Reject non-canonical encodings so two readers cannot hash + # semantically equivalent but byte-distinct histories. + if line.decode("utf-8") != canonical_json(value): + raise OperationalJournalError("journal record is not canonical JSON") + rows.append(value) + previous = claimed + expected_sequence += 1 + self._events = rows + + def _refresh_or_raise(self) -> None: + if self._error is not None: + raise OperationalJournalError( + f"operational journal unhealthy: {self._error}" + ) + try: + with self._interprocess_lock(): + self._reload_locked() + except Exception as exc: + self._error = f"{type(exc).__name__}: {exc}" + raise OperationalJournalError( + f"operational journal refresh failed: {type(exc).__name__}" + ) from exc + + def append( + self, + kind: str, + payload: Mapping[str, Any], + *, + outbox_id: str | None = None, + allow_existing_outbox: bool = True, + validate_existing: ( + Callable[[tuple[dict[str, Any], ...]], None] | None + ) = None, + validate_existing_latest_kinds: tuple[str, ...] | None = None, + ) -> dict[str, Any]: + """Append and fsync one fact. + + ``outbox_id`` is globally idempotent. Repeating the identical event + returns the original event; reusing the id for different content fails. + Callers acquiring one-shot authority may set + ``allow_existing_outbox=False`` so even an identical prior event is a + failed claim rather than apparent ownership. + ``validate_existing`` runs against an immutable current snapshot while + the cross-process writer lock is held, immediately before the append. + It must not call back into this journal. + When ``validate_existing_latest_kinds`` is supplied, that snapshot is + bounded to at most the latest row for each named kind. Omitting it + preserves the full-history callback contract. + """ + with self._lock: + if self._error is not None: + raise OperationalJournalError( + f"operational journal unhealthy: {self._error}" + ) + if not isinstance(kind, str) or not kind.strip(): + raise ValueError("journal kind is required") + normalized_payload = json.loads(canonical_json(dict(payload))) + normalized_outbox = str(outbox_id).strip() if outbox_id is not None else None + if normalized_outbox == "": + raise ValueError("outbox_id cannot be blank") + normalized_latest_kinds: tuple[str, ...] | None = None + if validate_existing_latest_kinds is not None: + if validate_existing is None: + raise ValueError( + "validate_existing_latest_kinds requires " + "validate_existing" + ) + if not isinstance(validate_existing_latest_kinds, tuple): + raise TypeError( + "validate_existing_latest_kinds must be a tuple" + ) + if not 1 <= len(validate_existing_latest_kinds) <= 64: + raise ValueError( + "validate_existing_latest_kinds must contain " + "1 to 64 kinds" + ) + latest_kinds: list[str] = [] + for latest_kind in validate_existing_latest_kinds: + if ( + not isinstance(latest_kind, str) + or not latest_kind.strip() + ): + raise ValueError( + "validation kind must be a non-blank string" + ) + if latest_kind not in latest_kinds: + latest_kinds.append(latest_kind) + normalized_latest_kinds = tuple(latest_kinds) + try: + with self._interprocess_lock(): + try: + self._reload_locked() + except Exception as exc: + self._error = f"{type(exc).__name__}: {exc}" + raise OperationalJournalError( + "operational journal integrity refresh failed" + ) from exc + if normalized_outbox is not None: + existing = next( + ( + row + for row in self._events + if row.get("outbox_id") == normalized_outbox + ), + None, + ) + if existing is not None: + if not allow_existing_outbox: + raise OperationalJournalError( + "outbox id is already claimed" + ) + if ( + existing.get("kind") == kind + and existing.get("payload") == normalized_payload + ): + return cast( + dict[str, Any], + json.loads(canonical_json(existing)), + ) + raise OperationalJournalError( + "outbox id reused with different content" + ) + if validate_existing is not None: + validation_rows = self._events + if normalized_latest_kinds is not None: + latest_by_kind: dict[str, dict[str, Any]] = {} + for row in self._events: + row_kind = row.get("kind") + if row_kind in normalized_latest_kinds: + latest_by_kind[str(row_kind)] = row + validation_rows = sorted( + latest_by_kind.values(), + key=lambda row: int(row["sequence"]), + ) + snapshot = tuple( + json.loads(canonical_json(row)) + for row in validation_rows + ) + validate_existing(snapshot) + + now = self._now_fn() + if now.tzinfo is None or now.utcoffset() is None: + raise ValueError("journal clock must be timezone-aware") + event: dict[str, Any] = { + "schema": OPERATIONAL_EVENT_SCHEMA, + "sequence": len(self._events) + 1, + "recorded_at": now.astimezone(timezone.utc) + .isoformat() + .replace("+00:00", "Z"), + "kind": kind, + "payload": normalized_payload, + "previous_sha256": ( + str(self._events[-1]["event_sha256"]) + if self._events + else "0" * 64 + ), + } + if normalized_outbox is not None: + event["outbox_id"] = normalized_outbox + event["event_sha256"] = sha256_json(event) + serialized = (canonical_json(event) + "\n").encode("utf-8") + try: + with self.path.open("ab") as journal_handle: + journal_handle.write(serialized) + journal_handle.flush() + os.fsync(journal_handle.fileno()) + except OSError as exc: + self._error = f"{type(exc).__name__}: {exc}" + raise OperationalJournalError( + f"operational journal append failed: {type(exc).__name__}" + ) from exc + self._events.append(event) + except OSError as exc: + self._error = f"{type(exc).__name__}: {exc}" + raise OperationalJournalError( + f"operational journal lock failed: {type(exc).__name__}" + ) from exc + return cast( + dict[str, Any], + json.loads(canonical_json(event)), + ) + + def acknowledge_outbox( + self, + outbox_id: str, + *, + acknowledgement: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + normalized = str(outbox_id).strip() + rows = self.events() + source = next( + (row for row in rows if row.get("outbox_id") == normalized), + None, + ) + if source is None: + raise OperationalJournalError("cannot acknowledge unknown outbox id") + payload = { + "outbox_id": normalized, + "source_event_sha256": source["event_sha256"], + "acknowledgement": dict(acknowledgement or {}), + } + return self.append( + "outbox.acknowledged", + payload, + outbox_id=f"ack:{normalized}", + ) + + def pending_outbox(self) -> tuple[dict[str, Any], ...]: + rows = self.events() + acknowledged = { + str(row.get("payload", {}).get("outbox_id")) + for row in rows + if row.get("kind") == "outbox.acknowledged" + } + return tuple( + json.loads(canonical_json(row)) + for row in rows + if row.get("outbox_id") + and row.get("kind") != "outbox.acknowledged" + and row["outbox_id"] not in acknowledged + ) + + +__all__ = [ + "AppendOnlyOperationalJournal", + "OPERATIONAL_EVENT_SCHEMA", + "OperationalJournalError", + "canonical_json", + "sha256_json", +] diff --git a/live_firewall/sqlite_operational_journal.py b/live_firewall/sqlite_operational_journal.py new file mode 100644 index 00000000..9b96718d --- /dev/null +++ b/live_firewall/sqlite_operational_journal.py @@ -0,0 +1,1215 @@ +"""Durable SQLite WAL implementation of Dummy's operational journal. + +The JSONL journal remains useful as a compact reference implementation. This +module provides the production storage shape: indexed reads, an indexed +outbox, and cross-process compare-and-append semantics under +``BEGIN IMMEDIATE``. Events retain the exact canonical event schema and hash +chain used by :mod:`live_firewall.operational_journal`. + +SQLite is used as a transactional append log, not as mutable application +state. Triggers reject event updates and deletes, and a singleton metadata row +tracks the committed sequence and hash-chain head. A restart performs a +streaming full-chain verification; normal operations verify only the known +head and any newly committed tail. + +The internal chain detects mutation, truncation against its current metadata, +and schema corruption. Like the JSONL implementation, it cannot by itself +distinguish a byte-for-byte rollback to an older valid database snapshot. +Deployment that must resist privileged whole-file rollback needs a monotonic +head anchor outside this database (for example, a Core-signed checkpoint). +""" +from __future__ import annotations + +import json +import re +import sqlite3 +import threading +from collections.abc import Callable, Iterator, Mapping +from contextlib import contextmanager +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, NoReturn, cast + +from live_firewall.operational_journal import ( + AppendOnlyOperationalJournal, + OPERATIONAL_EVENT_SCHEMA, + OperationalJournalError, + canonical_json, + sha256_json, +) + + +SQLITE_OPERATIONAL_JOURNAL_SCHEMA = "dummy.sqlite-operational-journal.v1" +_APPLICATION_ID = 0x444D4A31 # ASCII "DMJ1". +_SCHEMA_VERSION = 1 +_ZERO_SHA256 = "0" * 64 +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_EVENT_COLUMNS = ( + "sequence, kind, outbox_id, acknowledges_outbox_id, " + "previous_sha256, event_sha256, event_json" +) + +_CREATE_METADATA = f""" +CREATE TABLE journal_metadata ( + singleton INTEGER PRIMARY KEY CHECK (singleton = 1), + schema TEXT NOT NULL CHECK (schema = '{SQLITE_OPERATIONAL_JOURNAL_SCHEMA}'), + event_count INTEGER NOT NULL CHECK (event_count >= 0), + head_sha256 TEXT NOT NULL CHECK (length(head_sha256) = 64) +) +""".strip() + +_CREATE_EVENTS = """ +CREATE TABLE operational_events ( + sequence INTEGER PRIMARY KEY CHECK (sequence > 0), + kind TEXT NOT NULL CHECK (length(kind) > 0), + outbox_id TEXT UNIQUE, + acknowledges_outbox_id TEXT UNIQUE, + previous_sha256 TEXT NOT NULL CHECK (length(previous_sha256) = 64), + event_sha256 TEXT NOT NULL UNIQUE CHECK (length(event_sha256) = 64), + event_json TEXT NOT NULL +) +""".strip() + +_CREATE_KIND_INDEX = """ +CREATE INDEX operational_events_kind_sequence_idx +ON operational_events (kind, sequence) +""".strip() + +_CREATE_OUTBOX_INDEX = """ +CREATE INDEX operational_events_outbox_sequence_idx +ON operational_events (outbox_id, sequence) +WHERE outbox_id IS NOT NULL +""".strip() + +_CREATE_ACK_INDEX = """ +CREATE INDEX operational_events_ack_target_idx +ON operational_events (acknowledges_outbox_id) +WHERE acknowledges_outbox_id IS NOT NULL +""".strip() + +_CREATE_VALIDATE_INSERT_TRIGGER = """ +CREATE TRIGGER operational_events_validate_insert +BEFORE INSERT ON operational_events +BEGIN + SELECT CASE + WHEN NEW.sequence != ( + SELECT event_count + 1 + FROM journal_metadata + WHERE singleton = 1 + ) + THEN RAISE(ABORT, 'journal sequence mismatch') + END; + SELECT CASE + WHEN NEW.previous_sha256 != ( + SELECT head_sha256 + FROM journal_metadata + WHERE singleton = 1 + ) + THEN RAISE(ABORT, 'journal previous hash mismatch') + END; +END +""".strip() + +_CREATE_ADVANCE_HEAD_TRIGGER = """ +CREATE TRIGGER operational_events_advance_head +AFTER INSERT ON operational_events +BEGIN + UPDATE journal_metadata + SET event_count = NEW.sequence, + head_sha256 = NEW.event_sha256 + WHERE singleton = 1; +END +""".strip() + +_CREATE_NO_UPDATE_TRIGGER = """ +CREATE TRIGGER operational_events_no_update +BEFORE UPDATE ON operational_events +BEGIN + SELECT RAISE(ABORT, 'operational events are append-only'); +END +""".strip() + +_CREATE_NO_DELETE_TRIGGER = """ +CREATE TRIGGER operational_events_no_delete +BEFORE DELETE ON operational_events +BEGIN + SELECT RAISE(ABORT, 'operational events are append-only'); +END +""".strip() + +_SCHEMA_OBJECTS = { + "journal_metadata": ("table", _CREATE_METADATA), + "operational_events": ("table", _CREATE_EVENTS), + "operational_events_kind_sequence_idx": ("index", _CREATE_KIND_INDEX), + "operational_events_outbox_sequence_idx": ("index", _CREATE_OUTBOX_INDEX), + "operational_events_ack_target_idx": ("index", _CREATE_ACK_INDEX), + "operational_events_validate_insert": ( + "trigger", + _CREATE_VALIDATE_INSERT_TRIGGER, + ), + "operational_events_advance_head": ( + "trigger", + _CREATE_ADVANCE_HEAD_TRIGGER, + ), + "operational_events_no_update": ("trigger", _CREATE_NO_UPDATE_TRIGGER), + "operational_events_no_delete": ("trigger", _CREATE_NO_DELETE_TRIGGER), +} + + +def _normalized_sql(value: str) -> str: + return " ".join(value.split()).casefold() + + +def _validated_page( + *, + after_sequence: int, + limit: int | None, +) -> tuple[int, int | None]: + if isinstance(after_sequence, bool) or not isinstance(after_sequence, int): + raise TypeError("after_sequence must be an integer") + if after_sequence < 0: + raise ValueError("after_sequence cannot be negative") + if limit is not None: + if isinstance(limit, bool) or not isinstance(limit, int): + raise TypeError("limit must be an integer") + if limit <= 0: + raise ValueError("limit must be positive") + if limit > 9_223_372_036_854_775_807: + raise ValueError("limit exceeds SQLite integer range") + return after_sequence, limit + + +class SQLiteOperationalJournal(AppendOnlyOperationalJournal): + """Hash-chained operational journal backed by durable SQLite WAL. + + ``events`` and ``pending_outbox`` accept optional ``after_sequence`` and + ``limit`` arguments for bounded incremental consumers. Omitting them + preserves the return behavior of :class:`AppendOnlyOperationalJournal`. + The legacy ``validate_existing`` callback receives a real tuple for API + compatibility and therefore materializes history; high-volume consumers + should use indexed pages and a purpose-built transactional projection. + """ + + def __init__( + self, + path: Path, + *, + now_fn: Callable[[], datetime] | None = None, + busy_timeout_seconds: float = 5.0, + scan_batch_size: int = 256, + wal_autocheckpoint_pages: int = 256, + journal_size_limit_bytes: int = 64 * 1024 * 1024, + ) -> None: + self.path = Path(path) + self._now_fn = now_fn or (lambda: datetime.now(timezone.utc)) + if ( + isinstance(busy_timeout_seconds, bool) + or not isinstance(busy_timeout_seconds, (int, float)) + or not 0 < float(busy_timeout_seconds) <= 60 + ): + raise ValueError("busy_timeout_seconds must be in (0, 60]") + if ( + isinstance(scan_batch_size, bool) + or not isinstance(scan_batch_size, int) + or not 1 <= scan_batch_size <= 10_000 + ): + raise ValueError("scan_batch_size must be in [1, 10000]") + if ( + isinstance(wal_autocheckpoint_pages, bool) + or not isinstance(wal_autocheckpoint_pages, int) + or not 1 <= wal_autocheckpoint_pages <= 100_000 + ): + raise ValueError("wal_autocheckpoint_pages must be in [1, 100000]") + if ( + isinstance(journal_size_limit_bytes, bool) + or not isinstance(journal_size_limit_bytes, int) + or journal_size_limit_bytes < 1_048_576 + ): + raise ValueError("journal_size_limit_bytes must be at least 1 MiB") + + self._busy_timeout_seconds = float(busy_timeout_seconds) + self._scan_batch_size = scan_batch_size + self._wal_autocheckpoint_pages = wal_autocheckpoint_pages + self._journal_size_limit_bytes = journal_size_limit_bytes + self._lock = threading.RLock() + self._connection: sqlite3.Connection | None = None + self._error: str | None = None + self._closed = False + self._operation_depth = 0 + self._verified_sequence = 0 + self._verified_head = _ZERO_SHA256 + self._observed_data_version: int | None = None + self._observed_schema_version: int | None = None + + try: + self.path.parent.mkdir(parents=True, exist_ok=True) + self._connection = sqlite3.connect( + str(self.path), + timeout=self._busy_timeout_seconds, + isolation_level=None, + check_same_thread=False, + ) + self._connection.row_factory = sqlite3.Row + self._configure_connection(self._connection) + self._initialize_or_validate_schema(self._connection) + self._connection.execute("BEGIN") + try: + self._verify_full_chain_locked(self._connection) + ( + self._observed_data_version, + self._observed_schema_version, + ) = self._database_versions(self._connection) + self._connection.execute("COMMIT") + except BaseException: + self._rollback_quietly(self._connection) + raise + except Exception as exc: + self._error = f"{type(exc).__name__}: {exc}" + + @property + def healthy(self) -> bool: + with self._lock: + if self._error is not None or self._closed: + return False + try: + with self._transaction(write=False) as connection: + self._synchronize_integrity_locked(connection) + except Exception as exc: + if self._error is None: + self._error = f"{type(exc).__name__}: {exc}" + return False + return True + + @property + def error(self) -> str | None: + # Probe first so a metadata/head mutation is surfaced without requiring + # a separate read or append call. + _ = self.healthy + with self._lock: + if self._closed and self._error is None: + return "OperationalJournalError: operational journal is closed" + return self._error + + def close(self) -> None: + """Close this process-local connection. + + A journal must not be closed from inside ``serialized_operation``. + """ + with self._lock: + if self._closed: + return + if self._operation_depth: + raise OperationalJournalError( + "cannot close journal during serialized operation" + ) + connection = self._connection + self._connection = None + self._closed = True + if connection is not None: + connection.close() + + def __enter__(self) -> SQLiteOperationalJournal: + if not self.healthy: + raise OperationalJournalError( + f"operational journal unhealthy: {self.error}" + ) + return self + + def __exit__( + self, + exc_type: object, + exc_value: object, + traceback: object, + ) -> None: + del exc_type, exc_value, traceback + self.close() + + def events( + self, + *, + kind: str | None = None, + after_sequence: int = 0, + limit: int | None = None, + ) -> tuple[dict[str, Any], ...]: + """Return immutable event copies, optionally as a bounded page.""" + normalized_after, normalized_limit = _validated_page( + after_sequence=after_sequence, + limit=limit, + ) + if kind is not None and not isinstance(kind, str): + raise TypeError("kind must be a string or None") + + with self._lock: + self._raise_if_unavailable() + try: + with self._transaction(write=False) as connection: + self._synchronize_integrity_locked(connection) + clauses = ["sequence > ?"] + parameters: list[Any] = [normalized_after] + if kind is not None: + clauses.append("kind = ?") + parameters.append(kind) + sql = ( + f"SELECT {_EVENT_COLUMNS} FROM operational_events " + f"WHERE {' AND '.join(clauses)} ORDER BY sequence" + ) + if normalized_limit is not None: + sql += " LIMIT ?" + parameters.append(normalized_limit) + rows = connection.execute(sql, parameters).fetchall() + return tuple(self._copy_row(row) for row in rows) + except sqlite3.Error as exc: + self._poison_sqlite("operational journal read failed", exc) + + def head(self) -> tuple[int, str]: + """Return the integrity-verified committed sequence and chain head.""" + with self._lock: + self._raise_if_unavailable() + try: + with self._transaction(write=False) as connection: + self._synchronize_integrity_locked(connection) + return self._verified_sequence, self._verified_head + except sqlite3.Error as exc: + self._poison_sqlite( + "operational journal head read failed", + exc, + ) + + @contextmanager + def serialized_operation(self) -> Iterator[None]: + """Hold one cross-process writer transaction across caller operations.""" + with self._lock: + self._raise_if_unavailable() + if self._operation_depth: + self._operation_depth += 1 + try: + yield + finally: + self._operation_depth -= 1 + return + + connection = self._require_connection() + old_verified = self._verification_state() + begun = False + try: + connection.execute("BEGIN IMMEDIATE") + begun = True + self._synchronize_integrity_locked(connection) + self._operation_depth = 1 + try: + yield + finally: + self._operation_depth = 0 + connection.execute("COMMIT") + except sqlite3.Error as exc: + if begun: + self._rollback_quietly(connection) + self._restore_verification_state(old_verified) + self._poison_sqlite( + "operational journal serialized operation failed", + exc, + ) + except BaseException: + if begun: + self._rollback_quietly(connection) + self._restore_verification_state(old_verified) + raise + + def append( + self, + kind: str, + payload: Mapping[str, Any], + *, + outbox_id: str | None = None, + allow_existing_outbox: bool = True, + validate_existing: ( + Callable[[tuple[dict[str, Any], ...]], None] | None + ) = None, + validate_existing_latest_kinds: tuple[str, ...] | None = None, + ) -> dict[str, Any]: + """Atomically validate current history and append one durable event. + + ``validate_existing_latest_kinds`` is an opt-in bounded validation + view. When supplied, ``validate_existing`` receives at most the latest + event for each named kind, in sequence order. This supports atomic + compare-and-swap on high-frequency state streams without repeatedly + materializing the full journal. Omitting it preserves the reference + journal's full-history callback contract. + """ + if not isinstance(kind, str) or not kind.strip(): + raise ValueError("journal kind is required") + normalized_validation_kinds = self._validated_latest_kinds( + validate_existing=validate_existing, + kinds=validate_existing_latest_kinds, + ) + normalized_payload = cast( + dict[str, Any], + json.loads(canonical_json(dict(payload))), + ) + normalized_outbox = ( + str(outbox_id).strip() if outbox_id is not None else None + ) + if normalized_outbox == "": + raise ValueError("outbox_id cannot be blank") + + with self._lock: + self._raise_if_unavailable() + try: + with self._transaction(write=True) as connection: + self._synchronize_integrity_locked(connection) + return self._append_locked( + connection, + kind=kind, + payload=normalized_payload, + outbox_id=normalized_outbox, + allow_existing_outbox=allow_existing_outbox, + validate_existing=validate_existing, + validate_existing_latest_kinds=( + normalized_validation_kinds + ), + ) + except sqlite3.Error as exc: + self._poison_sqlite("operational journal append failed", exc) + + def acknowledge_outbox( + self, + outbox_id: str, + *, + acknowledgement: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + normalized = str(outbox_id).strip() + with self._lock: + self._raise_if_unavailable() + try: + with self._transaction(write=True) as connection: + self._synchronize_integrity_locked(connection) + source_row = connection.execute( + f"SELECT {_EVENT_COLUMNS} FROM operational_events " + "WHERE outbox_id = ?", + (normalized,), + ).fetchone() + if source_row is None: + raise OperationalJournalError( + "cannot acknowledge unknown outbox id" + ) + source = self._copy_row(source_row) + payload = cast( + dict[str, Any], + json.loads( + canonical_json( + { + "outbox_id": normalized, + "source_event_sha256": source[ + "event_sha256" + ], + "acknowledgement": dict( + acknowledgement or {} + ), + } + ) + ), + ) + return self._append_locked( + connection, + kind="outbox.acknowledged", + payload=payload, + outbox_id=f"ack:{normalized}", + allow_existing_outbox=True, + validate_existing=None, + validate_existing_latest_kinds=None, + ) + except sqlite3.Error as exc: + self._poison_sqlite( + "operational journal acknowledgement failed", + exc, + ) + + def pending_outbox( + self, + *, + after_sequence: int = 0, + limit: int | None = None, + ) -> tuple[dict[str, Any], ...]: + """Return unacknowledged outbox events as an indexed bounded page.""" + normalized_after, normalized_limit = _validated_page( + after_sequence=after_sequence, + limit=limit, + ) + with self._lock: + self._raise_if_unavailable() + try: + with self._transaction(write=False) as connection: + self._synchronize_integrity_locked(connection) + sql = ( + f"SELECT {_EVENT_COLUMNS} " + "FROM operational_events AS source " + "WHERE source.sequence > ? " + "AND source.outbox_id IS NOT NULL " + "AND source.kind != 'outbox.acknowledged' " + "AND NOT EXISTS (" + " SELECT 1 FROM operational_events AS acknowledgement " + " WHERE acknowledgement.acknowledges_outbox_id = " + " source.outbox_id" + ") " + "ORDER BY source.sequence" + ) + parameters: list[Any] = [normalized_after] + if normalized_limit is not None: + sql += " LIMIT ?" + parameters.append(normalized_limit) + rows = connection.execute(sql, parameters).fetchall() + return tuple(self._copy_row(row) for row in rows) + except sqlite3.Error as exc: + self._poison_sqlite( + "operational journal outbox read failed", + exc, + ) + + @contextmanager + def _transaction( + self, + *, + write: bool, + ) -> Iterator[sqlite3.Connection]: + connection = self._require_connection() + if self._operation_depth: + yield connection + return + + old_verified = self._verification_state() + begun = False + try: + connection.execute("BEGIN IMMEDIATE" if write else "BEGIN") + begun = True + yield connection + connection.execute("COMMIT") + except BaseException: + if begun: + self._rollback_quietly(connection) + self._restore_verification_state(old_verified) + raise + + def _append_locked( + self, + connection: sqlite3.Connection, + *, + kind: str, + payload: dict[str, Any], + outbox_id: str | None, + allow_existing_outbox: bool, + validate_existing: ( + Callable[[tuple[dict[str, Any], ...]], None] | None + ), + validate_existing_latest_kinds: tuple[str, ...] | None, + ) -> dict[str, Any]: + if outbox_id is not None: + existing_row = connection.execute( + f"SELECT {_EVENT_COLUMNS} FROM operational_events " + "WHERE outbox_id = ?", + (outbox_id,), + ).fetchone() + if existing_row is not None: + existing = self._copy_row(existing_row) + if not allow_existing_outbox: + raise OperationalJournalError( + "outbox id is already claimed" + ) + if ( + existing.get("kind") == kind + and existing.get("payload") == payload + ): + return existing + raise OperationalJournalError( + "outbox id reused with different content" + ) + + if validate_existing is not None: + if validate_existing_latest_kinds is None: + validation_rows = self._all_events_locked(connection) + else: + validation_rows = self._latest_kinds_locked( + connection, + validate_existing_latest_kinds, + ) + validate_existing(validation_rows) + + now = self._now_fn() + if now.tzinfo is None or now.utcoffset() is None: + raise ValueError("journal clock must be timezone-aware") + sequence = self._verified_sequence + 1 + event: dict[str, Any] = { + "schema": OPERATIONAL_EVENT_SCHEMA, + "sequence": sequence, + "recorded_at": now.astimezone(timezone.utc) + .isoformat() + .replace("+00:00", "Z"), + "kind": kind, + "payload": payload, + "previous_sha256": self._verified_head, + } + if outbox_id is not None: + event["outbox_id"] = outbox_id + event["event_sha256"] = sha256_json(event) + event_json = canonical_json(event) + acknowledgement_target = self._acknowledgement_target(event) + + connection.execute( + """ + INSERT INTO operational_events ( + sequence, + kind, + outbox_id, + acknowledges_outbox_id, + previous_sha256, + event_sha256, + event_json + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + sequence, + kind, + outbox_id, + acknowledgement_target, + self._verified_head, + event["event_sha256"], + event_json, + ), + ) + metadata = self._metadata_locked(connection) + if ( + metadata[0] != sequence + or metadata[1] != event["event_sha256"] + ): + self._integrity_failure("journal head trigger did not advance") + self._verified_sequence = sequence + self._verified_head = str(event["event_sha256"]) + return cast(dict[str, Any], json.loads(event_json)) + + def _all_events_locked( + self, + connection: sqlite3.Connection, + ) -> tuple[dict[str, Any], ...]: + cursor = connection.execute( + f"SELECT {_EVENT_COLUMNS} FROM operational_events " + "ORDER BY sequence" + ) + events: list[dict[str, Any]] = [] + while True: + rows = cursor.fetchmany(self._scan_batch_size) + if not rows: + break + events.extend(self._copy_row(row) for row in rows) + return tuple(events) + + def _latest_kinds_locked( + self, + connection: sqlite3.Connection, + kinds: tuple[str, ...], + ) -> tuple[dict[str, Any], ...]: + rows: list[dict[str, Any]] = [] + for kind in kinds: + row = connection.execute( + f"SELECT {_EVENT_COLUMNS} FROM operational_events " + "WHERE kind = ? ORDER BY sequence DESC LIMIT 1", + (kind,), + ).fetchone() + if row is not None: + rows.append(self._copy_row(row)) + rows.sort(key=lambda event: int(event["sequence"])) + return tuple(rows) + + @staticmethod + def _validated_latest_kinds( + *, + validate_existing: ( + Callable[[tuple[dict[str, Any], ...]], None] | None + ), + kinds: tuple[str, ...] | None, + ) -> tuple[str, ...] | None: + if kinds is None: + return None + if validate_existing is None: + raise ValueError( + "validate_existing_latest_kinds requires validate_existing" + ) + if not isinstance(kinds, tuple): + raise TypeError( + "validate_existing_latest_kinds must be a tuple" + ) + if not 1 <= len(kinds) <= 64: + raise ValueError( + "validate_existing_latest_kinds must contain 1 to 64 kinds" + ) + normalized: list[str] = [] + for kind in kinds: + if not isinstance(kind, str) or not kind.strip(): + raise ValueError("validation kind must be a non-blank string") + if kind not in normalized: + normalized.append(kind) + return tuple(normalized) + + def _copy_row(self, row: sqlite3.Row) -> dict[str, Any]: + event = self._decode_row(row) + return cast(dict[str, Any], json.loads(canonical_json(event))) + + def _decode_row( + self, + row: sqlite3.Row, + *, + expected_sequence: int | None = None, + expected_previous: str | None = None, + ) -> dict[str, Any]: + event_json = row["event_json"] + if not isinstance(event_json, str): + self._integrity_failure("journal event JSON is not text") + try: + value = json.loads(event_json) + except (TypeError, ValueError) as exc: + self._integrity_failure( + f"journal event JSON is invalid: {type(exc).__name__}" + ) + if not isinstance(value, dict): + self._integrity_failure("journal event must be an object") + event = cast(dict[str, Any], value) + try: + is_canonical = canonical_json(event) == event_json + except (TypeError, ValueError) as exc: + self._integrity_failure( + f"journal event cannot be canonicalized: {type(exc).__name__}" + ) + if not is_canonical: + self._integrity_failure("journal event is not canonical JSON") + if event.get("schema") != OPERATIONAL_EVENT_SCHEMA: + self._integrity_failure("journal event schema mismatch") + + sequence = event.get("sequence") + if isinstance(sequence, bool) or not isinstance(sequence, int): + self._integrity_failure("journal event sequence is invalid") + if sequence != row["sequence"]: + self._integrity_failure("journal event sequence column mismatch") + if expected_sequence is not None and sequence != expected_sequence: + self._integrity_failure("journal sequence mismatch") + + kind = event.get("kind") + if kind != row["kind"]: + self._integrity_failure("journal event kind column mismatch") + event_outbox = event.get("outbox_id") + if event_outbox != row["outbox_id"]: + self._integrity_failure("journal event outbox column mismatch") + + previous = event.get("previous_sha256") + if previous != row["previous_sha256"]: + self._integrity_failure( + "journal event previous hash column mismatch" + ) + if expected_previous is not None and previous != expected_previous: + self._integrity_failure("journal previous hash mismatch") + if not isinstance(previous, str) or _SHA256_RE.fullmatch(previous) is None: + self._integrity_failure("journal previous hash is invalid") + + claimed = event.get("event_sha256") + if claimed != row["event_sha256"]: + self._integrity_failure("journal event hash column mismatch") + if not isinstance(claimed, str) or _SHA256_RE.fullmatch(claimed) is None: + self._integrity_failure("journal event hash is invalid") + unsigned = { + key: item for key, item in event.items() if key != "event_sha256" + } + if sha256_json(unsigned) != claimed: + self._integrity_failure("journal event hash mismatch") + + if self._acknowledgement_target(event) != row[ + "acknowledges_outbox_id" + ]: + self._integrity_failure( + "journal acknowledgement target column mismatch" + ) + return event + + @staticmethod + def _acknowledgement_target(event: Mapping[str, Any]) -> str | None: + if event.get("kind") != "outbox.acknowledged": + return None + payload = event.get("payload") + if not isinstance(payload, Mapping): + return "None" + return str(payload.get("outbox_id")) + + def _synchronize_integrity_locked( + self, + connection: sqlite3.Connection, + ) -> None: + try: + self._validate_database_identity_locked(connection) + except OperationalJournalError as exc: + self._integrity_failure(str(exc)) + data_version, schema_version = self._database_versions(connection) + schema_changed = ( + self._observed_schema_version is not None + and schema_version != self._observed_schema_version + ) + data_changed = ( + self._observed_data_version is not None + and data_version != self._observed_data_version + ) + if schema_changed: + try: + self._validate_schema_objects_locked(connection) + except OperationalJournalError as exc: + self._integrity_failure(str(exc)) + if data_changed or schema_changed: + self._verify_full_chain_locked(connection) + self._observed_data_version = data_version + self._observed_schema_version = schema_version + return + + count, head = self._metadata_locked(connection) + if count < self._verified_sequence: + self._integrity_failure("journal event count moved backwards") + if count == self._verified_sequence: + if head != self._verified_head: + self._integrity_failure("journal head metadata mismatch") + if count == 0: + unexpected = connection.execute( + "SELECT 1 FROM operational_events LIMIT 1" + ).fetchone() + if unexpected is not None: + self._integrity_failure( + "journal contains events behind an empty head" + ) + self._observed_data_version = data_version + self._observed_schema_version = schema_version + return + head_row = connection.execute( + f"SELECT {_EVENT_COLUMNS} FROM operational_events " + "WHERE sequence = ?", + (count,), + ).fetchone() + if head_row is None: + self._integrity_failure("journal head event is missing") + decoded = self._decode_row( + cast(sqlite3.Row, head_row), + expected_sequence=count, + ) + if decoded["event_sha256"] != head: + self._integrity_failure("journal head event hash mismatch") + self._observed_data_version = data_version + self._observed_schema_version = schema_version + return + + previous = self._verified_head + expected_sequence = self._verified_sequence + 1 + if self._verified_sequence: + anchor = connection.execute( + f"SELECT {_EVENT_COLUMNS} FROM operational_events " + "WHERE sequence = ?", + (self._verified_sequence,), + ).fetchone() + if anchor is None: + self._integrity_failure("verified journal anchor is missing") + anchor_event = self._decode_row( + cast(sqlite3.Row, anchor), + expected_sequence=self._verified_sequence, + ) + if anchor_event["event_sha256"] != self._verified_head: + self._integrity_failure("verified journal anchor changed") + + cursor = connection.execute( + f"SELECT {_EVENT_COLUMNS} FROM operational_events " + "WHERE sequence > ? ORDER BY sequence", + (self._verified_sequence,), + ) + observed = self._verified_sequence + while True: + rows = cursor.fetchmany(self._scan_batch_size) + if not rows: + break + for row in rows: + event = self._decode_row( + row, + expected_sequence=expected_sequence, + expected_previous=previous, + ) + previous = str(event["event_sha256"]) + observed = expected_sequence + expected_sequence += 1 + if observed != count: + self._integrity_failure("journal metadata count mismatch") + if previous != head: + self._integrity_failure("journal metadata head mismatch") + self._verified_sequence = count + self._verified_head = head + self._observed_data_version = data_version + self._observed_schema_version = schema_version + + def _verify_full_chain_locked( + self, + connection: sqlite3.Connection, + ) -> None: + quick_check = connection.execute("PRAGMA quick_check(1)").fetchone() + if quick_check is None or str(quick_check[0]).casefold() != "ok": + self._integrity_failure("SQLite quick_check failed") + + expected_sequence = 1 + previous = _ZERO_SHA256 + cursor = connection.execute( + f"SELECT {_EVENT_COLUMNS} FROM operational_events " + "ORDER BY sequence" + ) + while True: + rows = cursor.fetchmany(self._scan_batch_size) + if not rows: + break + for row in rows: + event = self._decode_row( + row, + expected_sequence=expected_sequence, + expected_previous=previous, + ) + previous = str(event["event_sha256"]) + expected_sequence += 1 + observed_count = expected_sequence - 1 + metadata_count, metadata_head = self._metadata_locked(connection) + if metadata_count != observed_count: + self._integrity_failure("journal metadata count mismatch") + if metadata_head != previous: + self._integrity_failure("journal metadata head mismatch") + self._verified_sequence = observed_count + self._verified_head = previous + + def _metadata_locked( + self, + connection: sqlite3.Connection, + ) -> tuple[int, str]: + rows = connection.execute( + "SELECT schema, event_count, head_sha256 " + "FROM journal_metadata WHERE singleton = 1" + ).fetchall() + if len(rows) != 1: + self._integrity_failure("journal metadata row is missing") + row = rows[0] + if row["schema"] != SQLITE_OPERATIONAL_JOURNAL_SCHEMA: + self._integrity_failure("journal metadata schema mismatch") + count = row["event_count"] + head = row["head_sha256"] + if isinstance(count, bool) or not isinstance(count, int) or count < 0: + self._integrity_failure("journal metadata count is invalid") + if not isinstance(head, str) or _SHA256_RE.fullmatch(head) is None: + self._integrity_failure("journal metadata head is invalid") + if count == 0 and head != _ZERO_SHA256: + self._integrity_failure("empty journal head is not zero") + if count > 0 and head == _ZERO_SHA256: + self._integrity_failure("non-empty journal head is zero") + return count, head + + def _configure_connection(self, connection: sqlite3.Connection) -> None: + timeout_ms = max(1, int(self._busy_timeout_seconds * 1000)) + connection.execute(f"PRAGMA busy_timeout = {timeout_ms}") + mode = connection.execute("PRAGMA journal_mode = WAL").fetchone() + if mode is None or str(mode[0]).casefold() != "wal": + raise OperationalJournalError( + "SQLite refused WAL journal mode" + ) + connection.execute("PRAGMA synchronous = FULL") + connection.execute("PRAGMA fullfsync = ON") + connection.execute("PRAGMA checkpoint_fullfsync = ON") + connection.execute("PRAGMA foreign_keys = ON") + connection.execute("PRAGMA trusted_schema = OFF") + connection.execute("PRAGMA temp_store = MEMORY") + connection.execute( + f"PRAGMA wal_autocheckpoint = {self._wal_autocheckpoint_pages}" + ) + connection.execute( + f"PRAGMA journal_size_limit = {self._journal_size_limit_bytes}" + ) + + def _initialize_or_validate_schema( + self, + connection: sqlite3.Connection, + ) -> None: + connection.execute("BEGIN IMMEDIATE") + try: + tables = { + str(row[0]) + for row in connection.execute( + "SELECT name FROM sqlite_master " + "WHERE type = 'table' AND name NOT LIKE 'sqlite_%'" + ) + } + application_id = int( + connection.execute("PRAGMA application_id").fetchone()[0] + ) + user_version = int( + connection.execute("PRAGMA user_version").fetchone()[0] + ) + if not tables: + if application_id not in (0, _APPLICATION_ID): + raise OperationalJournalError( + "SQLite application id mismatch" + ) + if user_version not in (0, _SCHEMA_VERSION): + raise OperationalJournalError( + "SQLite schema version mismatch" + ) + for _object_type, statement in _SCHEMA_OBJECTS.values(): + connection.execute(statement) + connection.execute( + "INSERT INTO journal_metadata " + "(singleton, schema, event_count, head_sha256) " + "VALUES (1, ?, 0, ?)", + (SQLITE_OPERATIONAL_JOURNAL_SCHEMA, _ZERO_SHA256), + ) + connection.execute(f"PRAGMA application_id = {_APPLICATION_ID}") + connection.execute(f"PRAGMA user_version = {_SCHEMA_VERSION}") + else: + if application_id != _APPLICATION_ID: + raise OperationalJournalError( + "SQLite application id mismatch" + ) + if user_version != _SCHEMA_VERSION: + raise OperationalJournalError( + "SQLite schema version mismatch" + ) + if tables != {"journal_metadata", "operational_events"}: + raise OperationalJournalError( + "SQLite journal contains unexpected tables" + ) + self._validate_schema_objects_locked(connection) + connection.execute("COMMIT") + except BaseException: + self._rollback_quietly(connection) + raise + + def _validate_schema_objects_locked( + self, + connection: sqlite3.Connection, + ) -> None: + named_objects = { + str(row["name"]) + for row in connection.execute( + "SELECT name FROM sqlite_master " + "WHERE type IN ('table', 'index', 'trigger', 'view') " + "AND name NOT LIKE 'sqlite_%' " + "AND sql IS NOT NULL" + ) + } + if named_objects != set(_SCHEMA_OBJECTS): + unexpected = sorted(named_objects - set(_SCHEMA_OBJECTS)) + missing = sorted(set(_SCHEMA_OBJECTS) - named_objects) + raise OperationalJournalError( + "SQLite journal schema object set mismatch: " + f"unexpected={unexpected}, missing={missing}" + ) + rows = connection.execute( + "SELECT type, name, sql FROM sqlite_master " + "WHERE name IN ({})".format( + ",".join("?" for _ in _SCHEMA_OBJECTS) + ), + tuple(_SCHEMA_OBJECTS), + ).fetchall() + actual = { + str(row["name"]): ( + str(row["type"]), + _normalized_sql(str(row["sql"])), + ) + for row in rows + } + for name, (object_type, statement) in _SCHEMA_OBJECTS.items(): + expected = (object_type, _normalized_sql(statement)) + if actual.get(name) != expected: + raise OperationalJournalError( + f"SQLite journal schema object mismatch: {name}" + ) + + @staticmethod + def _validate_database_identity_locked( + connection: sqlite3.Connection, + ) -> None: + application_id = int( + connection.execute("PRAGMA application_id").fetchone()[0] + ) + if application_id != _APPLICATION_ID: + raise OperationalJournalError("SQLite application id mismatch") + user_version = int( + connection.execute("PRAGMA user_version").fetchone()[0] + ) + if user_version != _SCHEMA_VERSION: + raise OperationalJournalError("SQLite schema version mismatch") + + @staticmethod + def _database_versions( + connection: sqlite3.Connection, + ) -> tuple[int, int]: + data_version = int( + connection.execute("PRAGMA data_version").fetchone()[0] + ) + schema_version = int( + connection.execute("PRAGMA schema_version").fetchone()[0] + ) + return data_version, schema_version + + def _verification_state( + self, + ) -> tuple[int, str, int | None, int | None]: + return ( + self._verified_sequence, + self._verified_head, + self._observed_data_version, + self._observed_schema_version, + ) + + def _restore_verification_state( + self, + state: tuple[int, str, int | None, int | None], + ) -> None: + ( + self._verified_sequence, + self._verified_head, + self._observed_data_version, + self._observed_schema_version, + ) = state + + def _raise_if_unavailable(self) -> None: + if self._closed: + raise OperationalJournalError("operational journal is closed") + if self._error is not None: + raise OperationalJournalError( + f"operational journal unhealthy: {self._error}" + ) + + def _require_connection(self) -> sqlite3.Connection: + self._raise_if_unavailable() + if self._connection is None: + raise OperationalJournalError( + "operational journal connection is unavailable" + ) + return self._connection + + def _integrity_failure(self, message: str) -> NoReturn: + self._error = f"OperationalJournalError: {message}" + raise OperationalJournalError(message) + + def _poison_sqlite( + self, + message: str, + exc: sqlite3.Error, + ) -> NoReturn: + self._error = f"{type(exc).__name__}: {exc}" + raise OperationalJournalError( + f"{message}: {type(exc).__name__}" + ) from exc + + @staticmethod + def _rollback_quietly(connection: sqlite3.Connection) -> None: + try: + if connection.in_transaction: + connection.execute("ROLLBACK") + except sqlite3.Error: + pass + + +__all__ = [ + "SQLITE_OPERATIONAL_JOURNAL_SCHEMA", + "SQLiteOperationalJournal", +] diff --git a/predator_mesh/brokers/kalshi_livebrokerfirewall_adapter.py b/predator_mesh/brokers/kalshi_livebrokerfirewall_adapter.py index 10780555..0f13f405 100644 --- a/predator_mesh/brokers/kalshi_livebrokerfirewall_adapter.py +++ b/predator_mesh/brokers/kalshi_livebrokerfirewall_adapter.py @@ -81,19 +81,16 @@ def __init__( self.max_order_notional_cents = int(max_order_notional_cents) self._attempted = False - # Use the same defaults as kalshi.signer so the request URL and the - # signature prefix stay aligned. - self._base = _env("KALSHI_API_BASE") or kalshi.signer.BASE - self._version = _env("KALSHI_API_VERSION") or kalshi.signer.VERSION + # This retired adapter retains authenticated status reads only. Keep + # those reads on the same reviewed production origin and signature + # prefix as KalshiClient; ambient service variables are not endpoint + # authority. + self._base = kalshi.signer.BASE + self._version = kalshi.signer.VERSION self._kalshi = KalshiClient() if httpx_client is not None: self._kalshi.client = httpx_client - else: - self._kalshi.client = httpx.AsyncClient( - base_url=f"{self._base}/{self._version}".rstrip("/"), - timeout=10, - ) # ------------------------------------------------------------------ # Environment / credentials diff --git a/tests/fixtures/dumbmoney/signed-capital-envelope.v1.json b/tests/fixtures/dumbmoney/signed-capital-envelope.v1.json new file mode 100644 index 00000000..7327a1dc --- /dev/null +++ b/tests/fixtures/dumbmoney/signed-capital-envelope.v1.json @@ -0,0 +1,50 @@ +{ + "fixture_schema": "dumbmoney.signed-capital-envelope-fixture.v1", + "public_key_base64url": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg", + "signing_payload_sha256": "97f8ecdf0d6b48b310c06cdeb37654fb171f4fa62bf91b427965191643d7ee1e", + "envelope": { + "schema": "dumbmoney.signed-envelope.v1", + "source_id": "dumbmoney-core", + "source_sequence": 1, + "event_id": "95fd19cc7bcbc89fe3c7985ee04d502704344b2f09c1eb6dce575b41c0ae579c", + "correlation_id": "fixture-capital-001", + "causation_id": null, + "nonce": "fixture-nonce-001", + "not_before": "2099-01-01T00:00:00Z", + "expires_at": "2099-01-01T00:01:00Z", + "body_schema": "dumbmoney.capital-envelope.v1", + "body_digest": "fcf86b4c257c0fea9ba65051366b76738563598d4217c4950ef3cf63ed1927d0", + "body": { + "schema": "dumbmoney.capital-envelope.v1", + "envelope_id": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "mandate_id": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "venue": "dopey_robinhood", + "account_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "strategy_hashes": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "passport_hashes": [ + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "promotion_hashes": [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ], + "authorized_instruments": [ + "equity:ACME" + ], + "authorized_mode": "LIVE", + "max_order_risk_cents": 100, + "max_open_risk_cents": 1250, + "max_correlated_risk_cents": 300, + "max_daily_loss_cents": 300, + "max_open_orders": 5, + "fencing_generation": 7, + "policy_epoch": 1, + "not_before": "2099-01-01T00:00:00Z", + "expires_at": "2099-01-01T00:01:00Z" + }, + "signature_algorithm": "Ed25519", + "signer_key_id": "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c", + "signature": "ga9nH3SEc6uGk8U_Q-tqWXa76n_-OSq8VhUAPHlNdTp5jzGmcZkN8JIjXuhpidniz4NOuTjpPWUE9vvtiqQ7CQ" + } +} diff --git a/tests/test_autonomy_pipeline.py b/tests/test_autonomy_pipeline.py index afb4112c..eeca2580 100644 --- a/tests/test_autonomy_pipeline.py +++ b/tests/test_autonomy_pipeline.py @@ -648,6 +648,10 @@ def live_authority_verdict(self): async def submit(self, request, orderbook, forecast): return self._result + class FakeCapital: + def binding_for(self, **_kwargs): + return {} + from core.ontology import LiveOrderResult accepted = LiveOrderResult( @@ -659,6 +663,7 @@ async def submit(self, request, orderbook, forecast): kill_path=tmp_path / "KILL", risk_state_path=brain.state_path, exchange_status_fn=lambda: {"exchange_active": True, "trading_active": True}, + capital_envelope_adapter=FakeCapital(), ) monkeypatch.setattr(executor, "_make_firewall", lambda: FakeFirewall(accepted)) outcome = asyncio.run(executor.execute(decision, market=market)) @@ -672,6 +677,7 @@ async def submit(self, request, orderbook, forecast): SessionMode.LIVE, session_path=session, kill_path=tmp_path / "KILL", risk_state_path=brain.state_path, exchange_status_fn=lambda: {"exchange_active": True, "trading_active": True}, + capital_envelope_adapter=FakeCapital(), ) monkeypatch.setattr(executor2, "_make_firewall", lambda: FakeFirewall(rejected)) outcome2 = asyncio.run(executor2.execute(decision, market=market)) @@ -685,6 +691,7 @@ async def submit(self, request, orderbook, forecast): SessionMode.LIVE, session_path=session, kill_path=tmp_path / "KILL", risk_state_path=brain.state_path, exchange_status_fn=lambda: {"exchange_active": True, "trading_active": True}, + capital_envelope_adapter=FakeCapital(), ) monkeypatch.setattr(executor3, "_make_firewall", lambda: FakeFirewall(local)) outcome3 = asyncio.run(executor3.execute(decision, market=market)) diff --git a/tests/test_dumbmoney_capital_envelope.py b/tests/test_dumbmoney_capital_envelope.py new file mode 100644 index 00000000..966d47bf --- /dev/null +++ b/tests/test_dumbmoney_capital_envelope.py @@ -0,0 +1,1969 @@ +from __future__ import annotations + +import base64 +import copy +import hashlib +import json +import threading +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timedelta, timezone +from decimal import Decimal +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from autonomy.kill_reconciliation import queue_kill_reconciliation +from autonomy.brain import PredatorBrain +from autonomy.ontology import OutcomeKind, SessionMode, TradeOutcome +from core.ontology import ( + AccountMode, + CapConfig, + Forecast, + FirewallVerdict, + LiveOrderRequest, + OrderBook, + OrderBookLevel, +) +from core.state import DummyState +from live_firewall.dumbmoney_capital import ( + CAPITAL_ENVELOPE_SCHEMA, + SIGNED_ENVELOPE_SCHEMA, + CapitalEnvelopeAdapter, + flat_book_receipt, + inherited_exposure_receipt, + strategy_binding_hash, + verify_signed_capital_envelope, +) +from live_firewall.dumbmoney_broker_witness import ( + SETTLEMENT_WITNESS_SCHEMA, + TERMINAL_WITNESS_SCHEMA, + sign_broker_witness, +) +from live_firewall.dumbmoney_reconciliation import ( + DumbMoneyKalshiReconciliationSweeper, +) +from live_firewall.exposure_tracker import ExposureTracker +from live_firewall.firewall import LiveBrokerFirewall +from live_firewall.operational_journal import ( + AppendOnlyOperationalJournal, + OperationalJournalError, + canonical_json, + sha256_json, +) + + +NOW = datetime(2026, 7, 26, 20, 0, 0, tzinfo=timezone.utc) +ACCOUNT_HASH = hashlib.sha256(b"dummy-kalshi-fixture-account").hexdigest() +STRATEGY_REFERENCE = "autonomy_strategy_v2:test-decision" +PASSPORT_REFERENCE = "autonomy_forecast:test-decision" +STRATEGY_HASH = strategy_binding_hash(STRATEGY_REFERENCE) +PASSPORT_HASH = strategy_binding_hash(PASSPORT_REFERENCE) +PROMOTION_HASH = hashlib.sha256(b"promotion:fixture").hexdigest() +AUTHORIZED_INSTRUMENT = "event_contract:KXTEST-26JUL" +PRIVATE_KEY = Ed25519PrivateKey.from_private_bytes(bytes(range(32))) +PUBLIC_KEY = PRIVATE_KEY.public_key().public_bytes_raw() +SIGNER_KEY_ID = hashlib.sha256(PUBLIC_KEY).hexdigest() +WITNESS_PRIVATE_KEY = Ed25519PrivateKey.from_private_bytes( + bytes(range(31, -1, -1)) +) +WITNESS_PUBLIC_KEY = ( + WITNESS_PRIVATE_KEY.public_key().public_bytes_raw() +) +WITNESS_KEY_ID = hashlib.sha256(WITNESS_PUBLIC_KEY).hexdigest() + + +class _FixtureLineageResolver: + def resolve_lineage( + self, + *, + capital, + strategy_hash, + passport_hash, + promotion_hash, + authorized_instrument, + expected_binding=None, + ): + evidence = { + "strategy_hash": strategy_hash, + "passport_hash": passport_hash, + "promotion_hash": promotion_hash, + "authorized_instrument": authorized_instrument, + "maximum_loss_cents": min( + 10_000, + capital.max_order_risk_cents, + ), + "policy_epoch": capital.policy_epoch, + "capital_envelope_id": capital.envelope_id, + "capital_event_id": capital.event_id, + "capital_body_digest": sha256_json(capital.body), + "capital_fencing_generation": capital.fencing_generation, + "passport_event_id": _hash(f"passport-event:{passport_hash}"), + "promotion_event_id": _hash(f"promotion-event:{promotion_hash}"), + "passport_signer_key_id": _hash("research-key"), + "promotion_signer_key_id": _hash("promoter-key"), + "expires_at": _z(capital.expires_at), + "passport_resolution_sha256": _hash("passport-resolution"), + "promotion_resolution_sha256": _hash("promotion-resolution"), + } + if expected_binding is not None: + expected = dict(expected_binding) + expected.pop("passport_resolution_sha256", None) + expected.pop("promotion_resolution_sha256", None) + actual = dict(evidence) + actual.pop("passport_resolution_sha256", None) + actual.pop("promotion_resolution_sha256", None) + if expected != actual: + raise ValueError("fixture lineage binding changed") + return SimpleNamespace(evidence=lambda: evidence) + + +LINEAGE_RESOLVER = _FixtureLineageResolver() + + +def _z(value: datetime) -> str: + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _hash(label: str) -> str: + return hashlib.sha256(label.encode("utf-8")).hexdigest() + + +def _signed_envelope( + *, + source_sequence: int = 1, + nonce: str = "dummy-nonce-1", + fencing_generation: int = 1, + venue: str = "dummy_kalshi", + account_hash: str = ACCOUNT_HASH, + not_before: datetime = NOW - timedelta(seconds=10), + expires_at: datetime = NOW + timedelta(seconds=50), + max_order_risk_cents: int = 500, + max_open_risk_cents: int = 1_000, + max_correlated_risk_cents: int = 750, + max_daily_loss_cents: int = 300, + max_open_orders: int = 5, + policy_epoch: int = 1, + strategy_hashes: list[str] | None = None, + passport_hashes: list[str] | None = None, + promotion_hashes: list[str] | None = None, + authorized_instruments: list[str] | None = None, +) -> dict: + body = { + "schema": CAPITAL_ENVELOPE_SCHEMA, + "envelope_id": _hash( + f"envelope:{source_sequence}:{nonce}:{fencing_generation}" + ), + "mandate_id": _hash("mandate:fixture"), + "venue": venue, + "account_hash": account_hash, + "strategy_hashes": ( + [STRATEGY_HASH] if strategy_hashes is None else strategy_hashes + ), + "passport_hashes": ( + [PASSPORT_HASH] if passport_hashes is None else passport_hashes + ), + "promotion_hashes": ( + [PROMOTION_HASH] if promotion_hashes is None else promotion_hashes + ), + "authorized_instruments": ( + [AUTHORIZED_INSTRUMENT] + if authorized_instruments is None + else authorized_instruments + ), + "authorized_mode": "LIVE", + "max_order_risk_cents": max_order_risk_cents, + "max_open_risk_cents": max_open_risk_cents, + "max_correlated_risk_cents": max_correlated_risk_cents, + "max_daily_loss_cents": max_daily_loss_cents, + "max_open_orders": max_open_orders, + "fencing_generation": fencing_generation, + "policy_epoch": policy_epoch, + "not_before": _z(not_before), + "expires_at": _z(expires_at), + } + event_material = { + "schema": SIGNED_ENVELOPE_SCHEMA, + "source_id": "dumbmoney-core", + "source_sequence": source_sequence, + "correlation_id": "dummy-fixture-correlation", + "causation_id": None, + "nonce": nonce, + "not_before": body["not_before"], + "expires_at": body["expires_at"], + "body_schema": CAPITAL_ENVELOPE_SCHEMA, + "body_digest": sha256_json(body), + "body": body, + "signature_algorithm": "Ed25519", + "signer_key_id": SIGNER_KEY_ID, + } + wrapper = { + **event_material, + "event_id": sha256_json(event_material), + } + wrapper["signature"] = ( + base64.urlsafe_b64encode( + PRIVATE_KEY.sign(canonical_json(wrapper).encode("utf-8")) + ) + .decode("ascii") + .rstrip("=") + ) + return wrapper + + +def _adapter( + tmp_path: Path, + *, + now: datetime = NOW, +) -> tuple[CapitalEnvelopeAdapter, AppendOnlyOperationalJournal]: + def clock() -> datetime: + return now + + journal = AppendOnlyOperationalJournal( + tmp_path / "operational.jsonl", + now_fn=clock, + ) + adapter = CapitalEnvelopeAdapter( + journal=journal, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=LINEAGE_RESOLVER, + trusted_broker_witness_public_keys={ + WITNESS_KEY_ID: WITNESS_PUBLIC_KEY + }, + now_fn=clock, + ) + return adapter, journal + + +def _record_flat(adapter: CapitalEnvelopeAdapter) -> None: + adapter.record_broker_bootstrap( + flat_book_receipt( + receipt_id=_hash("flat-receipt"), + venue="dummy_kalshi", + account_hash=ACCOUNT_HASH, + observed_at=_z(NOW), + broker_snapshot_sha256=_hash("flat-broker-snapshot"), + ) + ) + + +def _request( + wrapper: dict, + *, + proposal_id: str = "proposal-1", + market_ticker: str = "KXTEST-26JUL", + price_cents: int = 50, + size: int = 1, +) -> LiveOrderRequest: + return LiveOrderRequest( + proposal_id=proposal_id, + market_ticker=market_ticker, + contract_ticker=market_ticker, + side="yes", + price_cents=price_cents, + size=size, + strategy_proof_reference=STRATEGY_REFERENCE, + forecast_proof_reference=PASSPORT_REFERENCE, + adapter_name="kalshi_live_firewall_adapter", + expiration_ts=int( + (datetime.now(timezone.utc) + timedelta(hours=1)).timestamp() + ), + capital_envelope_id=wrapper["body"]["envelope_id"], + capital_strategy_hash=STRATEGY_HASH, + capital_passport_hash=PASSPORT_HASH, + capital_promotion_hash=PROMOTION_HASH, + capital_fencing_generation=wrapper["body"]["fencing_generation"], + ) + + +def _terminal_witness( + reservation: dict, + *, + fill_count: int = 1, + terminal_status: str = "executed", +) -> dict: + fill_ids = ["fill-1"] if fill_count else [] + fill_cost = fill_count * int(reservation["price_cents"]) + fee = 2 if fill_count else 0 + body = { + "schema": TERMINAL_WITNESS_SCHEMA, + "witness_id": _hash( + f"terminal:{reservation['reservation_id']}:{fill_count}" + ), + "venue": "dummy_kalshi", + "account_hash": ACCOUNT_HASH, + "subaccount_number": 0, + "reservation_id": reservation["reservation_id"], + "proposal_id": reservation["proposal_id"], + "order_id": "broker-order-1", + "market_ticker": reservation["market_ticker"], + "contract_ticker": reservation["contract_ticker"], + "side": reservation["side"], + "terminal_status": terminal_status, + "initial_count": reservation["size"], + "fill_count": fill_count, + "remaining_count": 0, + "fill_cost_cents": fill_cost, + "fee_cents": fee, + "average_fill_price_cents": ( + int(reservation["price_cents"]) if fill_count else None + ), + "fill_ids": fill_ids, + "observed_at": _z(NOW), + "broker_projection_sha256": _hash( + f"terminal-projection:{reservation['reservation_id']}" + ), + } + return sign_broker_witness( + body, + private_key=WITNESS_PRIVATE_KEY, + observed_at=NOW, + correlation_id=str(reservation["proposal_id"]), + ) + + +def _settlement_witness(position: dict) -> dict: + body = { + "schema": SETTLEMENT_WITNESS_SCHEMA, + "witness_id": _hash( + f"settlement:{position['position_exposure_id']}" + ), + "venue": "dummy_kalshi", + "account_hash": ACCOUNT_HASH, + "subaccount_number": 0, + "position_exposure_id": position["position_exposure_id"], + "reservation_id": position["reservation_id"], + "proposal_id": position["proposal_id"], + "contract_ticker": position["contract_ticker"], + "side": position["side"], + "fill_count": position["fill_count"], + "market_result": "yes", + "settled_at": _z(NOW), + "revenue_cents": 100, + "settlement_fee_cents": 0, + "position_absent": True, + "observed_at": _z(NOW), + "broker_projection_sha256": _hash( + f"settlement-projection:{position['position_exposure_id']}" + ), + } + return sign_broker_witness( + body, + private_key=WITNESS_PRIVATE_KEY, + observed_at=NOW, + correlation_id=str(position["proposal_id"]), + ) + + +def test_shared_blunder_fixture_verifies_byte_for_byte(): + fixture = json.loads( + ( + Path(__file__).parent + / "fixtures" + / "dumbmoney" + / "signed-capital-envelope.v1.json" + ).read_text(encoding="utf-8") + ) + wrapper = fixture["envelope"] + public_key = base64.urlsafe_b64decode( + fixture["public_key_base64url"] + + "=" * (-len(fixture["public_key_base64url"]) % 4) + ) + signed_material = { + key: value for key, value in wrapper.items() if key != "signature" + } + assert hashlib.sha256( + canonical_json(signed_material).encode("utf-8") + ).hexdigest() == fixture["signing_payload_sha256"] + + verified = verify_signed_capital_envelope( + wrapper, + trusted_public_keys={wrapper["signer_key_id"]: public_key}, + expected_venue="dopey_robinhood", + expected_account_hash=wrapper["body"]["account_hash"], + now=datetime(2099, 1, 1, 0, 0, 30, tzinfo=timezone.utc), + ) + + assert verified.event_id == wrapper["event_id"] + assert verified.envelope_id == wrapper["body"]["envelope_id"] + assert verified.fencing_generation == 7 + assert verified.promotion_hashes == ("f" * 64,) + assert verified.authorized_instruments == ("equity:ACME",) + + +def test_expired_envelope_and_bad_signature_fail_closed(): + wrapper = _signed_envelope() + with pytest.raises(ValueError, match="expired"): + verify_signed_capital_envelope( + wrapper, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + now=NOW + timedelta(minutes=2), + ) + + tampered = copy.deepcopy(wrapper) + tampered["signature"] = ( + ("A" if wrapper["signature"][0] != "A" else "B") + + wrapper["signature"][1:] + ) + with pytest.raises(ValueError, match="signature invalid"): + verify_signed_capital_envelope( + tampered, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + now=NOW, + ) + + +@pytest.mark.parametrize( + ("venue", "account_hash", "message"), + [ + ("dopey_robinhood", ACCOUNT_HASH, "venue mismatch"), + ("dummy_kalshi", _hash("wrong-account"), "account mismatch"), + ], +) +def test_signed_envelope_is_bound_to_exact_venue_and_account( + venue, + account_hash, + message, +): + wrapper = _signed_envelope(venue=venue, account_hash=account_hash) + with pytest.raises(ValueError, match=message): + verify_signed_capital_envelope( + wrapper, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + now=NOW, + ) + + +def test_capital_envelope_requires_complete_lineage_and_ordered_limits(): + mismatched_lineage = _signed_envelope( + promotion_hashes=sorted( + [PROMOTION_HASH, _hash("promotion:extra")] + ) + ) + with pytest.raises(ValueError, match="hash counts must match"): + verify_signed_capital_envelope( + mismatched_lineage, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + now=NOW, + ) + + multiple_lineages = _signed_envelope( + strategy_hashes=sorted([STRATEGY_HASH, _hash("strategy:extra")]), + passport_hashes=sorted([PASSPORT_HASH, _hash("passport:extra")]), + promotion_hashes=sorted( + [PROMOTION_HASH, _hash("promotion:extra")] + ), + ) + with pytest.raises(ValueError, match="exactly one"): + verify_signed_capital_envelope( + multiple_lineages, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + now=NOW, + ) + + bad_limit_order = _signed_envelope( + max_order_risk_cents=500, + max_correlated_risk_cents=499, + ) + with pytest.raises(ValueError, match="internally inconsistent"): + verify_signed_capital_envelope( + bad_limit_order, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + now=NOW, + ) + + +def test_exact_instrument_and_promotion_are_rechecked_at_sink(tmp_path): + adapter, _ = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + + unauthorized_instrument = _request( + wrapper, + market_ticker="KXOTHER-26JUL", + ) + instrument_verdict = adapter.evaluate_request( + unauthorized_instrument, + current_daily_loss_cents=0, + ) + assert instrument_verdict.allow is False + assert "instrument is not authorized" in instrument_verdict.reason + + wrong_promotion = _request(wrapper).model_copy( + update={"capital_promotion_hash": _hash("wrong-promotion")} + ) + promotion_verdict = adapter.evaluate_request( + wrong_promotion, + current_daily_loss_cents=0, + ) + assert promotion_verdict.allow is False + assert "promotion hash mismatch" in promotion_verdict.reason + + +def test_replay_sequence_nonce_and_fencing_are_fail_closed(tmp_path): + adapter, journal = _adapter(tmp_path) + first = _signed_envelope() + adapter.accept_signed_envelope(first) + adapter.accept_signed_envelope(first) + assert len(journal.events(kind="capital.envelope.accepted")) == 1 + + with pytest.raises(ValueError, match="nonce replay"): + adapter.accept_signed_envelope( + _signed_envelope(source_sequence=2, nonce="dummy-nonce-1") + ) + with pytest.raises(ValueError, match="source sequence replay"): + adapter.accept_signed_envelope( + _signed_envelope(source_sequence=1, nonce="dummy-nonce-2") + ) + + newest = _signed_envelope( + source_sequence=2, + nonce="dummy-nonce-2", + fencing_generation=9, + ) + adapter.accept_signed_envelope(newest) + with pytest.raises(ValueError, match="not strictly increasing"): + adapter.accept_signed_envelope( + _signed_envelope( + source_sequence=3, + nonce="dummy-nonce-3", + fencing_generation=8, + ) + ) + + _record_flat(adapter) + stale = adapter.evaluate_request(first_request := _request(first), current_daily_loss_cents=0) + assert first_request.capital_fencing_generation == 1 + assert stale.allow is False + assert "stale" in stale.reason + + +def test_equal_fence_split_brain_is_rejected(tmp_path): + adapter, _ = _adapter(tmp_path) + adapter.accept_signed_envelope( + _signed_envelope( + source_sequence=1, + nonce="equal-fence-a", + fencing_generation=4, + ) + ) + + with pytest.raises(ValueError, match="not strictly increasing"): + adapter.accept_signed_envelope( + _signed_envelope( + source_sequence=2, + nonce="equal-fence-b", + fencing_generation=4, + ) + ) + + +def test_two_adapters_cannot_accept_conflicting_equal_fences(tmp_path): + path = tmp_path / "shared-capital.jsonl" + first_journal = AppendOnlyOperationalJournal(path, now_fn=lambda: NOW) + second_journal = AppendOnlyOperationalJournal(path, now_fn=lambda: NOW) + first_adapter = CapitalEnvelopeAdapter( + journal=first_journal, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=LINEAGE_RESOLVER, + now_fn=lambda: NOW, + ) + second_adapter = CapitalEnvelopeAdapter( + journal=second_journal, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=LINEAGE_RESOLVER, + now_fn=lambda: NOW, + ) + first = _signed_envelope( + source_sequence=1, + nonce="concurrent-fence-a", + fencing_generation=1, + ) + second = _signed_envelope( + source_sequence=2, + nonce="concurrent-fence-b", + fencing_generation=1, + ) + barrier = threading.Barrier(3) + + def attempt(adapter, envelope): + barrier.wait() + try: + adapter.accept_signed_envelope(envelope) + except ValueError: + return "rejected" + return "accepted" + + with ThreadPoolExecutor(max_workers=2) as executor: + futures = [ + executor.submit(attempt, first_adapter, first), + executor.submit(attempt, second_adapter, second), + ] + barrier.wait() + outcomes = [future.result() for future in futures] + + assert sorted(outcomes) == ["accepted", "rejected"] + restarted = AppendOnlyOperationalJournal(path, now_fn=lambda: NOW) + accepted = restarted.events(kind="capital.envelope.accepted") + assert restarted.healthy is True + assert len(accepted) == 1 + assert [row["payload"]["fencing_generation"] for row in accepted] == [1] + + +def test_policy_epoch_cannot_regress_under_a_higher_fence(tmp_path): + adapter, _ = _adapter(tmp_path) + adapter.accept_signed_envelope( + _signed_envelope( + source_sequence=1, + nonce="policy-two", + fencing_generation=1, + policy_epoch=2, + ) + ) + + with pytest.raises(ValueError, match="policy epoch regressed"): + adapter.accept_signed_envelope( + _signed_envelope( + source_sequence=2, + nonce="policy-one", + fencing_generation=2, + policy_epoch=1, + ) + ) + + +def test_expired_newer_fence_never_resurrects_older_grant(tmp_path): + clock_state = {"now": NOW} + + def clock() -> datetime: + return clock_state["now"] + + journal = AppendOnlyOperationalJournal( + tmp_path / "operational.jsonl", + now_fn=clock, + ) + adapter = CapitalEnvelopeAdapter( + journal=journal, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=LINEAGE_RESOLVER, + now_fn=clock, + ) + older = _signed_envelope( + source_sequence=1, + nonce="older-fence", + fencing_generation=1, + expires_at=NOW + timedelta(seconds=50), + ) + newer = _signed_envelope( + source_sequence=2, + nonce="newer-fence", + fencing_generation=2, + expires_at=NOW + timedelta(seconds=20), + ) + adapter.accept_signed_envelope(older) + adapter.accept_signed_envelope(newer) + _record_flat(adapter) + clock_state["now"] = NOW + timedelta(seconds=30) + + with pytest.raises(ValueError, match="no active capital envelope"): + adapter.binding_for( + strategy_hash=STRATEGY_HASH, + passport_hash=PASSPORT_HASH, + authorized_instrument=AUTHORIZED_INSTRUMENT, + ) + stale = adapter.evaluate_request( + _request(older), + current_daily_loss_cents=0, + ) + assert stale.allow is False + assert "stale" in stale.reason + + +def test_missing_local_exposure_is_never_treated_as_flat(tmp_path): + adapter, _ = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + + verdict = adapter.evaluate_request( + _request(wrapper), + current_daily_loss_cents=0, + ) + + assert verdict.allow is False + assert verdict.rejected_by == "capital_broker_bootstrap" + assert "local absence is not flat" in verdict.reason + + +def test_daily_loss_capacity_charges_proposed_order_at_exact_boundary(tmp_path): + adapter, _ = _adapter(tmp_path) + wrapper = _signed_envelope(max_daily_loss_cents=300) + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper, price_cents=100) + + exact = adapter.evaluate_request( + request, + current_daily_loss_cents=200, + ) + over = adapter.evaluate_request( + request, + current_daily_loss_cents=201, + ) + + assert exact.allow is True + assert over.allow is False + assert "daily loss capacity" in over.reason + + +def test_broker_bootstrap_cannot_replay_an_older_snapshot(tmp_path): + adapter, journal = _adapter(tmp_path) + current = flat_book_receipt( + receipt_id=_hash("bootstrap-current"), + venue="dummy_kalshi", + account_hash=ACCOUNT_HASH, + observed_at=_z(NOW), + broker_snapshot_sha256=_hash("bootstrap-current-snapshot"), + ) + first = adapter.record_broker_bootstrap(current) + repeated = adapter.record_broker_bootstrap(current) + assert repeated["event_sha256"] == first["event_sha256"] + + older = flat_book_receipt( + receipt_id=_hash("bootstrap-older"), + venue="dummy_kalshi", + account_hash=ACCOUNT_HASH, + observed_at=_z(NOW - timedelta(seconds=1)), + broker_snapshot_sha256=_hash("bootstrap-older-snapshot"), + ) + with pytest.raises(ValueError, match="strictly increasing"): + adapter.record_broker_bootstrap(older) + assert len(journal.events(kind="broker.bootstrap.recorded")) == 1 + + +def test_two_adapters_cannot_record_conflicting_bootstrap_observations( + tmp_path, +): + path = tmp_path / "shared-bootstrap.jsonl" + adapters = [ + CapitalEnvelopeAdapter( + journal=AppendOnlyOperationalJournal( + path, + now_fn=lambda: NOW, + ), + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=LINEAGE_RESOLVER, + now_fn=lambda: NOW, + ) + for _ in range(2) + ] + receipts = [ + flat_book_receipt( + receipt_id=_hash(f"concurrent-bootstrap:{index}"), + venue="dummy_kalshi", + account_hash=ACCOUNT_HASH, + observed_at=_z(NOW), + broker_snapshot_sha256=_hash( + f"concurrent-bootstrap-snapshot:{index}" + ), + ) + for index in range(2) + ] + barrier = threading.Barrier(3) + + def attempt(adapter, receipt): + barrier.wait() + try: + adapter.record_broker_bootstrap(receipt) + except ValueError: + return "rejected" + return "accepted" + + with ThreadPoolExecutor(max_workers=2) as executor: + futures = [ + executor.submit(attempt, adapter, receipt) + for adapter, receipt in zip(adapters, receipts, strict=True) + ] + barrier.wait() + outcomes = [future.result() for future in futures] + + assert sorted(outcomes) == ["accepted", "rejected"] + restarted = AppendOnlyOperationalJournal(path, now_fn=lambda: NOW) + assert restarted.healthy is True + assert len(restarted.events(kind="broker.bootstrap.recorded")) == 1 + + +def test_inherited_broker_exposure_consumes_grant_before_new_risk(tmp_path): + adapter, _ = _adapter(tmp_path) + wrapper = _signed_envelope( + max_order_risk_cents=100, + max_open_risk_cents=100, + max_correlated_risk_cents=100, + ) + adapter.accept_signed_envelope(wrapper) + adapter.record_broker_bootstrap( + inherited_exposure_receipt( + receipt_id=_hash("inherited-receipt"), + venue="dummy_kalshi", + account_hash=ACCOUNT_HASH, + observed_at=_z(NOW), + broker_snapshot_sha256=_hash("inherited-snapshot"), + total_exposure_cents=80, + open_order_count=1, + market_exposure_cents={"KXTEST-26JUL": 80}, + correlated_exposure_cents={"KXTEST": 80}, + ) + ) + + verdict = adapter.evaluate_request( + _request(wrapper, price_cents=30), + current_daily_loss_cents=0, + ) + + assert verdict.allow is False + assert verdict.rejected_by == "capital_limits" + assert "open risk" in verdict.reason + + +def test_post_bootstrap_fill_cannot_hide_behind_inherited_exposure(tmp_path): + adapter, _ = _adapter(tmp_path) + wrapper = _signed_envelope( + max_order_risk_cents=100, + max_open_risk_cents=100, + max_correlated_risk_cents=100, + ) + adapter.accept_signed_envelope(wrapper) + adapter.record_broker_bootstrap( + inherited_exposure_receipt( + receipt_id=_hash("inherited-plus-fill-receipt"), + venue="dummy_kalshi", + account_hash=ACCOUNT_HASH, + observed_at=_z(NOW), + broker_snapshot_sha256=_hash("inherited-plus-fill-snapshot"), + total_exposure_cents=80, + open_order_count=1, + market_exposure_cents={"KXTEST-26JUL": 80}, + correlated_exposure_cents={"KXTEST": 80}, + ) + ) + + verdict = adapter.reserve_request( + _request(wrapper, price_cents=20), + current_daily_loss_cents=0, + current_local_total_exposure_cents=10, + current_local_correlated_exposure_cents=10, + ) + + assert verdict.allow is False + assert verdict.rejected_by == "capital_limits" + assert "open risk" in verdict.reason + + +def test_reservation_is_crash_safe_and_idempotent(tmp_path): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + + first = adapter.reserve_request(request, current_daily_loss_cents=0) + assert first.allow is True + assert first.reservation_id + + reloaded_journal = AppendOnlyOperationalJournal( + journal.path, + now_fn=lambda: NOW, + ) + reloaded = CapitalEnvelopeAdapter( + journal=reloaded_journal, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=LINEAGE_RESOLVER, + now_fn=lambda: NOW, + ) + second = reloaded.reserve_request(request, current_daily_loss_cents=0) + + assert second.allow is True + assert second.reservation_id == first.reservation_id + assert ( + len(reloaded_journal.events(kind="capital.reservation.created")) == 1 + ) + conflict = reloaded.reserve_request( + _request(wrapper, size=2), + current_daily_loss_cents=0, + ) + assert conflict.allow is False + assert conflict.rejected_by == "capital_idempotency" + + +def test_two_adapters_cannot_overreserve_same_capital_ceiling(tmp_path): + first_adapter, first_journal = _adapter(tmp_path) + wrapper = _signed_envelope( + max_order_risk_cents=617, + max_open_risk_cents=1_000, + max_correlated_risk_cents=1_000, + max_daily_loss_cents=2_000, + max_open_orders=5, + ) + first_adapter.accept_signed_envelope(wrapper) + _record_flat(first_adapter) + second_adapter = CapitalEnvelopeAdapter( + journal=AppendOnlyOperationalJournal( + first_journal.path, + now_fn=lambda: NOW, + ), + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=LINEAGE_RESOLVER, + now_fn=lambda: NOW, + ) + first_request = _request( + wrapper, + proposal_id="concurrent-proposal-a", + price_cents=60, + size=10, + ) + second_request = _request( + wrapper, + proposal_id="concurrent-proposal-b", + price_cents=60, + size=10, + ) + barrier = threading.Barrier(2) + first_verdict = first_adapter._verdict + second_verdict = second_adapter._verdict + + def gated_first(*args, **kwargs): + result = first_verdict(*args, **kwargs) + barrier.wait(timeout=5) + return result + + def gated_second(*args, **kwargs): + result = second_verdict(*args, **kwargs) + barrier.wait(timeout=5) + return result + + first_adapter._verdict = gated_first + second_adapter._verdict = gated_second + with ThreadPoolExecutor(max_workers=2) as executor: + futures = [ + executor.submit( + first_adapter.reserve_request, + first_request, + current_daily_loss_cents=0, + ), + executor.submit( + second_adapter.reserve_request, + second_request, + current_daily_loss_cents=0, + ), + ] + verdicts = [future.result() for future in futures] + + assert sorted(verdict.allow for verdict in verdicts) == [False, True] + restarted = AppendOnlyOperationalJournal( + first_journal.path, + now_fn=lambda: NOW, + ) + reservations = restarted.events(kind="capital.reservation.created") + assert restarted.healthy is True + assert len(reservations) == 1 + assert sum(int(row["payload"]["risk_cents"]) for row in reservations) == 617 + + +def test_cross_process_dispatch_claim_is_one_shot(tmp_path): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + reservation = adapter.reserve_request( + request, + current_daily_loss_cents=0, + ) + assert reservation.reservation_id + order = { + "ticker": request.contract_ticker, + "client_order_id": request.proposal_id, + "side": "bid", + "count": "1.00", + "price": "0.5000", + "time_in_force": "good_till_canceled", + "self_trade_prevention_type": "taker_at_cross", + "post_only": True, + "cancel_order_on_pause": True, + "reduce_only": False, + "subaccount": 0, + "exchange_index": 0, + "expiration_time": request.expiration_ts, + } + second_journal = AppendOnlyOperationalJournal( + journal.path, + now_fn=lambda: NOW, + ) + second_adapter = CapitalEnvelopeAdapter( + journal=second_journal, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=LINEAGE_RESOLVER, + now_fn=lambda: NOW, + ) + barrier = threading.Barrier(2) + + def claim( + candidate: CapitalEnvelopeAdapter, + nonce: str, + ): + barrier.wait(timeout=5) + try: + return candidate.claim_broker_dispatch( + request, + reservation_id=reservation.reservation_id, + order=order, + claimant_nonce=nonce, + ) + except (ValueError, OperationalJournalError) as exc: + return exc + + with ThreadPoolExecutor(max_workers=2) as executor: + results = [ + future.result() + for future in ( + executor.submit(claim, adapter, "1" * 64), + executor.submit(claim, second_adapter, "2" * 64), + ) + ] + + successes = [ + result + for result in results + if isinstance(result, dict) + ] + failures = [ + result + for result in results + if isinstance(result, (ValueError, OperationalJournalError)) + ] + assert len(successes) == 1 + assert successes[0]["kind"] == "capital.dispatch.claimed" + assert len(failures) == 1 + assert "already" in str(failures[0]) or "claimed" in str(failures[0]) + assert len( + second_journal.events(kind="capital.dispatch.claimed") + ) == 1 + assert len(journal.events(kind="capital.dispatch.claimed")) == 1 + with pytest.raises(ValueError, match="disabled pending sealed CAS proof"): + adapter.release_after_local_reservation_failure( + request, + reservation_id=reservation.reservation_id, + reason="caller claims transport did not occur", + ) + assert not journal.events(kind="capital.reservation.released") + + +def test_dispatch_claim_rejects_payload_drift_and_cancel_only(tmp_path): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + reservation = adapter.reserve_request( + request, + current_daily_loss_cents=0, + ) + assert reservation.reservation_id + order = { + "ticker": request.contract_ticker, + "client_order_id": request.proposal_id, + "side": "bid", + "count": "1.00", + "price": "0.5100", + "time_in_force": "good_till_canceled", + "self_trade_prevention_type": "taker_at_cross", + "post_only": True, + "cancel_order_on_pause": True, + "reduce_only": False, + "subaccount": 0, + "exchange_index": 0, + "expiration_time": request.expiration_ts, + } + with pytest.raises(ValueError, match="exact authorized wire payload"): + adapter.claim_broker_dispatch( + request, + reservation_id=reservation.reservation_id, + order=order, + claimant_nonce="3" * 64, + ) + + order["price"] = "0.5000" + adapter.enter_cancel_only( + reason="operator kill", + kill_asserted_at=_z(NOW), + ) + with pytest.raises(ValueError, match="cancel-only"): + adapter.claim_broker_dispatch( + request, + reservation_id=reservation.reservation_id, + order=order, + claimant_nonce="4" * 64, + ) + assert len(journal.events(kind="capital.dispatch.claimed")) == 0 + + +def test_existing_reservation_rechecks_new_broker_exposure(tmp_path): + adapter, _journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + reservation = adapter.reserve_request( + request, + current_daily_loss_cents=0, + ) + assert reservation.allow is True + adapter.record_broker_bootstrap( + inherited_exposure_receipt( + receipt_id=_hash("new-inherited-receipt"), + venue="dummy_kalshi", + account_hash=ACCOUNT_HASH, + observed_at=_z(NOW + timedelta(seconds=1)), + broker_snapshot_sha256=_hash("new-inherited-snapshot"), + total_exposure_cents=980, + open_order_count=1, + market_exposure_cents={request.market_ticker: 980}, + correlated_exposure_cents={"KXTEST": 980}, + ) + ) + + verdict = adapter.evaluate_request( + request, + current_daily_loss_cents=0, + current_local_total_exposure_cents=52, + current_local_correlated_exposure_cents=52, + current_local_open_orders=1, + current_request_locally_reserved=True, + ) + + assert verdict.allow is False + assert verdict.rejected_by == "capital_limits" + assert verdict.reason == "DumbMoney maximum open risk exceeded" + + +@pytest.mark.asyncio +async def test_local_exposure_failure_retains_capital_without_broker_contact( + tmp_path, +): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + tracker = ExposureTracker() + tracker.reserve_order_submission = Mock(return_value=False) + client = SimpleNamespace(create_order=AsyncMock()) + firewall = LiveBrokerFirewall( + client, + tracker, + capital_envelope_adapter=adapter, + ) + allow = FirewallVerdict(allow=True, reason="fixture") + firewall.evaluate = AsyncMock(return_value=allow) + firewall._mandatory_submit_authority = Mock(return_value=allow) + firewall._verified_live_compliance_verdict = AsyncMock(return_value=allow) + firewall._trusted_sink_orderbook = AsyncMock( + return_value=(SimpleNamespace(), allow) + ) + + result = await firewall.submit(request, SimpleNamespace(), None) + + assert result.success is False + assert result.error == "EXPOSURE_RESERVATION_FAILED" + assert result.broker_contacted is False + client.create_order.assert_not_awaited() + releases = journal.events(kind="capital.reservation.released") + assert len(journal.events(kind="capital.reservation.created")) == 1 + assert not journal.events(kind="capital.local_reservation.failed") + assert releases == () + reused = adapter.evaluate_request( + request, + current_daily_loss_cents=0, + ) + assert reused.allow is True + assert reused.reservation_id is not None + + +@pytest.mark.asyncio +async def test_authority_loss_after_reservation_never_contacts_broker( + tmp_path, +): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + tracker = ExposureTracker() + client = SimpleNamespace(create_order=AsyncMock()) + firewall = LiveBrokerFirewall( + client, + tracker, + capital_envelope_adapter=adapter, + ) + allow = FirewallVerdict(allow=True, reason="fixture") + revoked = FirewallVerdict( + allow=False, + reason="Kill switch active", + rejected_by="kill_switch", + ) + firewall.evaluate = AsyncMock(return_value=allow) + firewall._mandatory_submit_authority = Mock( + side_effect=[allow, allow, revoked] + ) + firewall._verified_live_compliance_verdict = AsyncMock( + return_value=allow + ) + firewall._trusted_sink_orderbook = AsyncMock( + return_value=(SimpleNamespace(), allow) + ) + + result = await firewall.submit(request, SimpleNamespace(), None) + + assert result.success is False + assert result.error == "Kill switch active" + assert result.broker_contacted is False + client.create_order.assert_not_awaited() + assert len(journal.events(kind="capital.reservation.created")) == 1 + assert journal.events(kind="capital.reservation.released") == () + assert tracker.open_order_count() == 1 + + +@pytest.mark.asyncio +async def test_authority_loss_after_dispatch_claim_never_contacts_broker( + tmp_path, +): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + tracker = ExposureTracker() + client = SimpleNamespace(create_order=AsyncMock()) + firewall = LiveBrokerFirewall( + client, + tracker, + capital_envelope_adapter=adapter, + ) + allow = FirewallVerdict(allow=True, reason="fixture") + revoked = FirewallVerdict( + allow=False, + reason="Core command authority expired", + rejected_by="core_command_authority", + ) + firewall.evaluate = AsyncMock(return_value=allow) + firewall._mandatory_submit_authority = Mock( + side_effect=[allow, allow, allow, revoked] + ) + firewall._verified_live_compliance_verdict = AsyncMock( + return_value=allow + ) + firewall._trusted_sink_orderbook = AsyncMock( + return_value=(SimpleNamespace(), allow) + ) + + result = await firewall.submit(request, SimpleNamespace(), None) + + assert result.success is False + assert result.error == "Core command authority expired" + assert result.broker_contacted is False + client.create_order.assert_not_awaited() + assert len(journal.events(kind="capital.dispatch.claimed")) == 1 + assert len(journal.events(kind="capital.reservation.created")) == 1 + assert journal.events(kind="capital.reservation.released") == () + assert tracker.open_order_count() == 1 + + +@pytest.mark.asyncio +async def test_existing_ambiguous_submission_is_reconciliation_only( + tmp_path, +): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + capital = adapter.reserve_request(request, current_daily_loss_cents=0) + assert capital.allow is True + tracker = ExposureTracker() + assert tracker.reserve_order_submission( + request.proposal_id, + request.market_ticker, + request.size, + request.price_cents, + contract_ticker=request.contract_ticker, + side=request.side, + ) + client = SimpleNamespace(create_order=AsyncMock()) + firewall = LiveBrokerFirewall( + client, + tracker, + capital_envelope_adapter=adapter, + ) + allow = FirewallVerdict(allow=True, reason="fixture") + firewall.evaluate = AsyncMock(return_value=allow) + firewall._mandatory_submit_authority = Mock(return_value=allow) + firewall._verified_live_compliance_verdict = AsyncMock( + return_value=allow + ) + firewall._trusted_sink_orderbook = AsyncMock( + return_value=(SimpleNamespace(), allow) + ) + + result = await firewall.submit(request, SimpleNamespace(), None) + + assert result.success is False + assert ( + result.error + == "EXISTING_SUBMISSION_REQUIRES_BROKER_RECONCILIATION" + ) + assert result.broker_contacted is False + client.create_order.assert_not_awaited() + assert journal.events(kind="capital.reservation.released") == () + assert tracker.submission_record(request.proposal_id) is not None + assert tracker.state_healthy is True + + +def test_fee_inclusive_reservation_records_notional_and_worst_case_fee( + tmp_path, +): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + + verdict = adapter.reserve_request( + _request(wrapper, price_cents=50, size=2), + current_daily_loss_cents=0, + ) + + assert verdict.allow is True + payload = journal.events( + kind="capital.reservation.created" + )[0]["payload"] + assert payload["notional_cents"] == 100 + assert payload["fee_reserve_cents"] == 4 + assert payload["risk_cents"] == 104 + + +def test_signed_terminal_and_settlement_witnesses_release_in_order( + tmp_path, +): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope( + max_order_risk_cents=100, + max_open_risk_cents=100, + max_correlated_risk_cents=100, + ) + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + reservation = adapter.reserve_request( + request, + current_daily_loss_cents=0, + ) + assert reservation.reservation_id is not None + adapter.claim_broker_dispatch( + request, + reservation_id=reservation.reservation_id, + order=adapter._expected_broker_order(request), + claimant_nonce="9" * 64, + ) + pending = adapter.pending_reconciliation_reservations() + assert len(pending) == 1 + terminal_wrapper = _terminal_witness(pending[0]) + tracker = ExposureTracker( + persist=True, + state_path=tmp_path / "live-exposure.json", + ) + assert tracker.reserve_order_submission( + request.proposal_id, + request.market_ticker, + request.size, + request.price_cents, + contract_ticker=request.contract_ticker, + side=request.side, + ) + assert tracker.confirm_open_order( + request.proposal_id, + "broker-order-1", + ) + + class Reader: + settlement: dict | None = None + + def terminal_reconciliation_witness(self, _reservation): + return terminal_wrapper + + def settlement_reconciliation_witness(self, _position): + return self.settlement + + reader = Reader() + sweeper = DumbMoneyKalshiReconciliationSweeper( + capital_adapter=adapter, + broker_reader=reader, + exposure_tracker=tracker, + ) + + first = sweeper.run_once() + + assert first["status"] == "BLOCKED" + assert first["terminal_witnesses_recorded"] == 1 + assert first["unresolved_positions"] == 1 + assert adapter.pending_reconciliation_reservations() == () + positions = adapter.active_position_exposures() + assert len(positions) == 1 + assert positions[0]["risk_cents"] == 52 + assert tracker.open_order_count() == 0 + assert tracker.positions[ + (request.contract_ticker, request.side) + ].quantity == 1 + blocked = adapter.evaluate_request( + _request(wrapper, proposal_id="proposal-after-fill"), + current_daily_loss_cents=0, + ) + assert blocked.allow is False + assert blocked.reason == "DumbMoney maximum open risk exceeded" + + reader.settlement = _settlement_witness(positions[0]) + second = sweeper.run_once() + + assert second["status"] == "COMPLETE" + assert second["settlement_witnesses_recorded"] == 1 + assert adapter.active_position_exposures() == () + assert tracker.positions == {} + allowed = adapter.evaluate_request( + _request(wrapper, proposal_id="proposal-after-settlement"), + current_daily_loss_cents=0, + ) + assert allowed.allow is True + assert len( + journal.events( + kind="capital.terminal_reconciliation.witnessed" + ) + ) == 1 + assert len( + journal.events( + kind="capital.settlement_reconciliation.witnessed" + ) + ) == 1 + + # A restart sees no pending authority and does not duplicate releases. + restarted_journal = AppendOnlyOperationalJournal( + journal.path, + now_fn=lambda: NOW, + ) + restarted = CapitalEnvelopeAdapter( + journal=restarted_journal, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + trusted_broker_witness_public_keys={ + WITNESS_KEY_ID: WITNESS_PUBLIC_KEY + }, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=LINEAGE_RESOLVER, + now_fn=lambda: NOW, + ) + assert restarted.pending_reconciliation_reservations() == () + assert restarted.active_position_exposures() == () + + +def test_terminal_witness_tamper_or_wrong_signer_never_releases( + tmp_path, +): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + reservation = adapter.reserve_request( + request, + current_daily_loss_cents=0, + ) + assert reservation.reservation_id is not None + adapter.claim_broker_dispatch( + request, + reservation_id=reservation.reservation_id, + order=adapter._expected_broker_order(request), + claimant_nonce="8" * 64, + ) + terminal = _terminal_witness( + adapter.pending_reconciliation_reservations()[0] + ) + tampered = copy.deepcopy(terminal) + tampered["body"]["fill_cost_cents"] += 1 + + with pytest.raises(ValueError, match="digest"): + adapter.record_signed_terminal_reconciliation(tampered) + + wrong_key = Ed25519PrivateKey.from_private_bytes(bytes([7]) * 32) + wrong = sign_broker_witness( + terminal["body"], + private_key=wrong_key, + observed_at=NOW, + correlation_id=request.proposal_id, + ) + with pytest.raises(ValueError, match="trusted"): + adapter.record_signed_terminal_reconciliation(wrong) + + assert journal.events(kind="capital.reservation.released") == () + assert adapter.pending_reconciliation_reservations() + + +def test_caller_authored_terminal_receipts_never_release_capital(tmp_path): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope( + max_order_risk_cents=100, + max_open_risk_cents=100, + max_correlated_risk_cents=100, + ) + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper, price_cents=80) + reservation = adapter.reserve_request( + request, + current_daily_loss_cents=0, + ) + assert reservation.reservation_id + + with pytest.raises(ValueError, match="sealed broker witness"): + adapter.release_from_terminal_reconciliation( + proposal_id=request.proposal_id, + terminal_status="ambiguous", + reconciliation_receipt={ + "schema": "dummy.terminal-order-reconciliation.v1", + "terminal_status": "ambiguous", + "proposal_id": request.proposal_id, + "order_id": "broker-order-1", + "broker_contacted": True, + "local_exposure_projection_persisted": True, + }, + ) + with pytest.raises(ValueError, match="sealed broker witness"): + adapter.release_from_terminal_reconciliation( + proposal_id=request.proposal_id, + terminal_status="filled", + reconciliation_receipt={ + "schema": "dummy.terminal-order-reconciliation.v1", + "terminal_status": "filled", + "proposal_id": request.proposal_id, + "order_id": "broker-order-1", + "broker_contacted": False, + "local_exposure_projection_persisted": True, + }, + ) + assert not journal.events(kind="capital.reservation.released") + + receipt = { + "schema": "dummy.terminal-order-reconciliation.v1", + "terminal_status": "filled", + "proposal_id": request.proposal_id, + "broker_contacted": True, + "order_id": "broker-order-1", + "fill_count": 1, + "fill_price_cents": request.price_cents, + "local_exposure_projection_persisted": True, + } + with pytest.raises(ValueError, match="sealed broker witness"): + adapter.release_from_terminal_reconciliation( + proposal_id=request.proposal_id, + terminal_status="filled", + reconciliation_receipt=receipt, + ) + + reloaded_journal = AppendOnlyOperationalJournal( + journal.path, + now_fn=lambda: NOW, + ) + reloaded = CapitalEnvelopeAdapter( + journal=reloaded_journal, + trusted_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=LINEAGE_RESOLVER, + now_fn=lambda: NOW, + ) + with pytest.raises(ValueError, match="sealed broker witness"): + reloaded.release_from_terminal_reconciliation( + proposal_id=request.proposal_id, + terminal_status="filled", + reconciliation_receipt=receipt, + ) + assert not reloaded_journal.events( + kind="capital.terminal_reconciliation.witnessed" + ) + assert not reloaded_journal.events(kind="capital.reservation.released") + assert not reloaded_journal.events(kind="capital.position.exposure.recorded") + replay = reloaded.reserve_request( + request, + current_daily_loss_cents=0, + ) + assert replay.allow is True + assert replay.reservation_id == reservation.reservation_id + + # Filled risk remains charged in the independent capital journal even if a + # different process has an empty or stale local position book. + next_request = _request( + wrapper, + proposal_id="proposal-2", + price_cents=30, + ) + blocked = reloaded.evaluate_request( + next_request, + current_daily_loss_cents=0, + ) + assert blocked.allow is False + assert "open risk" in blocked.reason + + +def test_brain_releases_only_after_persisted_terminal_broker_witness( + tmp_path, + monkeypatch, +): + import live_firewall.exposure_tracker as tracker_module + + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + assert adapter.reserve_request( + request, + current_daily_loss_cents=0, + ).allow + tracker = ExposureTracker() + assert tracker.reserve_order_submission( + request.proposal_id, + request.market_ticker, + request.size, + request.price_cents, + contract_ticker=request.contract_ticker, + side=request.side, + ) + assert tracker.confirm_open_order(request.proposal_id, "broker-order-1") + monkeypatch.setattr( + tracker_module, + "get_persistent_exposure_tracker", + lambda: tracker, + ) + brain = SimpleNamespace( + mode=SessionMode.LIVE, + executor=SimpleNamespace(capital_envelope_adapter=adapter), + ) + outcome = TradeOutcome( + decision_id="test-decision", + market_ticker=request.market_ticker, + kind=OutcomeKind.FILLED, + order_id="broker-order-1", + fill_count=1, + fill_price_cents=request.price_cents, + pnl_cents=None, + broker_contacted=True, + detail={"state": "filled"}, + created_at=_z(NOW), + ) + + PredatorBrain._sync_live_exposure(brain, [outcome]) + + assert not journal.events(kind="capital.reservation.released") + assert not journal.events(kind="capital.position.exposure.recorded") + assert tracker.open_order_count() == 0 + assert tracker.total_exposure_cents() == request.price_cents + + +def test_brain_retains_capital_without_terminal_status_witness( + tmp_path, + monkeypatch, +): + import live_firewall.exposure_tracker as tracker_module + + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + assert adapter.reserve_request( + request, + current_daily_loss_cents=0, + ).allow + tracker = ExposureTracker() + assert tracker.reserve_order_submission( + request.proposal_id, + request.market_ticker, + request.size, + request.price_cents, + contract_ticker=request.contract_ticker, + side=request.side, + ) + assert tracker.confirm_open_order(request.proposal_id, "broker-order-1") + monkeypatch.setattr( + tracker_module, + "get_persistent_exposure_tracker", + lambda: tracker, + ) + brain = SimpleNamespace( + mode=SessionMode.LIVE, + executor=SimpleNamespace(capital_envelope_adapter=adapter), + ) + ambiguous_cancel = TradeOutcome( + decision_id="test-decision", + market_ticker=request.market_ticker, + kind=OutcomeKind.CANCELED, + order_id="broker-order-1", + fill_count=0, + fill_price_cents=request.price_cents, + pnl_cents=None, + broker_contacted=True, + detail={"reason": "cancel_requested_without_terminal_status"}, + created_at=_z(NOW), + ) + + PredatorBrain._sync_live_exposure(brain, [ambiguous_cancel]) + + assert not journal.events(kind="capital.reservation.released") + + +def test_cancel_only_degraded_mode_never_invents_cancel_authority(tmp_path): + adapter, journal = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + assert adapter.evaluate_request(request, current_daily_loss_cents=0).allow + + receipt = adapter.enter_cancel_only( + reason="kill switch asserted", + kill_asserted_at=_z(NOW), + ) + + assert receipt["payload"]["submission_authority"] is False + assert receipt["payload"]["cancel_authority"] is False + assert receipt["payload"]["broker_contacted"] is False + assert not journal.events(kind="cancel.reconciliation.requested") + blocked = adapter.evaluate_request(request, current_daily_loss_cents=0) + assert blocked.allow is False + assert blocked.rejected_by == "capital_cancel_only" + + with pytest.raises(ValueError, match="opaque receipt"): + adapter.enter_cancel_only( + reason="unverified cancel authority supplied", + kill_asserted_at=_z(NOW), + cancel_authorized=True, + cancel_authority_receipt="opaque-authority-claim", + ) + assert not journal.events(kind="cancel.reconciliation.requested") + + +def test_kill_queue_mirrors_cancel_only_without_cancel_command(tmp_path): + adapter, journal = _adapter(tmp_path) + + receipt = queue_kill_reconciliation( + kill_asserted_at=_z(NOW), + receipt_path=tmp_path / "kill-reconciliation.json", + capital_envelope_adapter=adapter, + ) + + assert receipt["submission_authority"] is False + assert receipt["cancel_authority"] is False + assert receipt["broker_contacted"] is False + assert receipt["dumbmoney_operational_status"] == "CANCEL_AUTHORITY_REQUIRED" + assert len(journal.events(kind="authority.cancel_only")) == 1 + assert not journal.events(kind="cancel.reconciliation.requested") + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("local_order_cap", "grant_order_cap", "expected_rejected_by"), + [ + (100, 500, "single_order_cap"), + (500, 100, "capital_limits"), + ], +) +async def test_firewall_intersects_local_and_signed_caps( + tmp_path, + monkeypatch, + local_order_cap, + grant_order_cap, + expected_rejected_by, +): + import live_firewall.firewall as firewall_module + + adapter, _ = _adapter(tmp_path) + wrapper = _signed_envelope( + max_order_risk_cents=grant_order_cap, + max_open_risk_cents=1_000, + max_correlated_risk_cents=1_000, + ) + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper, price_cents=50, size=3) + caps = CapConfig( + max_single_order_cents=local_order_cap, + max_market_exposure_cents=2_000, + max_correlated_exposure_cents=2_000, + max_total_live_exposure_cents=2_000, + max_daily_loss_cents=1_000, + max_open_markets=10, + max_orders_per_hour=10, + allowed_markets=[request.market_ticker], + max_spread_cents=5, + min_liquidity=10, + min_edge_bps=50, + ) + monkeypatch.setattr(firewall_module, "load_caps", lambda: caps) + monkeypatch.setattr( + firewall_module, + "get_allowed_adapter_names", + lambda: {"kalshi_live_firewall_adapter"}, + ) + monkeypatch.setenv("KALSHI_API_KEY_ID", "fixture-key-id") + state = DummyState() + state.set_mode(AccountMode.AUTONOMOUS_LIVE_CAPPED) + monkeypatch.setattr(firewall_module, "STATE", state) + firewall = LiveBrokerFirewall( + None, + ExposureTracker(), + capital_envelope_adapter=adapter, + ) + book = OrderBook( + market_ticker=request.market_ticker, + contract_ticker=request.contract_ticker, + bids=[OrderBookLevel(price=48, size=20)], + asks=[OrderBookLevel(price=52, size=20)], + timestamp=datetime.now(timezone.utc), + ) + forecast = Forecast( + market_ticker=request.market_ticker, + contract_ticker=request.contract_ticker, + event_title="Fixture", + contract_title="Fixture yes", + market_implied_probability=Decimal("0.50"), + dummy_probability=Decimal("0.80"), + probability_delta=Decimal("0.30"), + confidence_score=Decimal("0.80"), + uncertainty_band=(Decimal("0.75"), Decimal("0.85")), + expected_edge=Decimal("0.25"), + edge_after_fees=Decimal("0.20"), + freshness_score=Decimal("1"), + liquidity_score=Decimal("1"), + spread_score=Decimal("1"), + orderbook_depth_score=Decimal("1"), + settlement_risk_score=Decimal("0.1"), + source_summary="fixture", + model_summary="fixture", + calibration_notes="fixture", + timestamp=datetime.now(timezone.utc), + expiration=datetime.now(timezone.utc) + timedelta(hours=1), + strategy_references=[STRATEGY_REFERENCE], + proof_reference=PASSPORT_REFERENCE, + ) + + verdict = await firewall.evaluate(request, book, forecast) + + assert verdict.allow is False + assert verdict.rejected_by == expected_rejected_by + + +def test_final_firewall_authority_requires_current_core_command_state( + tmp_path, +): + adapter, _ = _adapter(tmp_path) + wrapper = _signed_envelope() + adapter.accept_signed_envelope(wrapper) + _record_flat(adapter) + request = _request(wrapper) + firewall = LiveBrokerFirewall( + None, + ExposureTracker(), + capital_envelope_adapter=adapter, + core_submission_authority=lambda: False, + ) + + verdict = firewall._mandatory_submit_authority(request) + + assert verdict.allow is False + assert verdict.rejected_by == "core_command_authority" + + +def test_operational_journal_outbox_is_append_only_and_replayable(tmp_path): + path = tmp_path / "journal.jsonl" + journal = AppendOnlyOperationalJournal(path, now_fn=lambda: NOW) + first = journal.append( + "fixture.event", + {"value": 1}, + outbox_id="fixture-outbox", + ) + repeated = journal.append( + "fixture.event", + {"value": 1}, + outbox_id="fixture-outbox", + ) + assert repeated["event_sha256"] == first["event_sha256"] + assert len(journal.pending_outbox()) == 1 + + journal.acknowledge_outbox( + "fixture-outbox", + acknowledgement={"receiver": "dumbmoney-core"}, + ) + assert journal.pending_outbox() == () + + reloaded = AppendOnlyOperationalJournal(path, now_fn=lambda: NOW) + assert reloaded.healthy is True + assert len(reloaded.events()) == 2 + + +def test_operational_journal_serializes_two_preloaded_instances(tmp_path): + path = tmp_path / "shared-journal.jsonl" + first_writer = AppendOnlyOperationalJournal(path, now_fn=lambda: NOW) + second_writer = AppendOnlyOperationalJournal(path, now_fn=lambda: NOW) + + first = first_writer.append("writer.a", {"value": 1}) + second = second_writer.append("writer.b", {"value": 2}) + third = first_writer.append("writer.a", {"value": 3}) + + assert [first["sequence"], second["sequence"], third["sequence"]] == [1, 2, 3] + assert second["previous_sha256"] == first["event_sha256"] + assert third["previous_sha256"] == second["event_sha256"] + assert [row["kind"] for row in second_writer.events()] == [ + "writer.a", + "writer.b", + "writer.a", + ] + + restarted = AppendOnlyOperationalJournal(path, now_fn=lambda: NOW) + assert restarted.healthy is True + assert [row["sequence"] for row in restarted.events()] == [1, 2, 3] diff --git a/tests/test_dumbmoney_command_feed.py b/tests/test_dumbmoney_command_feed.py new file mode 100644 index 00000000..2638015a --- /dev/null +++ b/tests/test_dumbmoney_command_feed.py @@ -0,0 +1,1163 @@ +from __future__ import annotations + +import base64 +import copy +import hashlib +import json +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timedelta, timezone +from pathlib import Path + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from live_firewall.dumbmoney_capital import ( + CAPITAL_ENVELOPE_SCHEMA, + SIGNED_ENVELOPE_SCHEMA, + CapitalEnvelopeAdapter, +) +from live_firewall.dumbmoney_command_feed import ( + DESIRED_MODE_SCHEMA, + KILL_STATE_SCHEMA, + CommandFeedResponse, + CoreCommandFeedConsumer, + ZERO_DIGEST, +) +from live_firewall.operational_journal import ( + AppendOnlyOperationalJournal, + canonical_json, + sha256_json, +) + + +NOW = datetime(2026, 7, 26, 21, 0, 0, tzinfo=timezone.utc) +CORE_PRIVATE_KEY = Ed25519PrivateKey.from_private_bytes(bytes(range(32))) +CORE_PUBLIC_KEY = CORE_PRIVATE_KEY.public_key().public_bytes_raw() +CORE_SIGNER_KEY_ID = hashlib.sha256(CORE_PUBLIC_KEY).hexdigest() +OPERATOR_PRIVATE_KEY = Ed25519PrivateKey.from_private_bytes( + bytes(range(1, 33)) +) +OPERATOR_PUBLIC_KEY = OPERATOR_PRIVATE_KEY.public_key().public_bytes_raw() +OPERATOR_SIGNER_KEY_ID = hashlib.sha256(OPERATOR_PUBLIC_KEY).hexdigest() +ACCOUNT_HASH = hashlib.sha256(b"dummy-command-feed-account").hexdigest() +STRATEGY_HASH = hashlib.sha256(b"strategy").hexdigest() +PASSPORT_HASH = hashlib.sha256(b"passport").hexdigest() +PROMOTION_HASH = hashlib.sha256(b"promotion").hexdigest() +INSTRUMENT = "event_contract:KXTEST-26JUL" +REQUEST_NONCE_1 = "1" * 64 +REQUEST_NONCE_2 = "2" * 64 +REQUEST_NONCE_3 = "3" * 64 + + +class _UnusedLineageResolver: + def resolve_lineage(self, **_kwargs): + raise AssertionError("command-feed tests must not resolve live lineage") + + +def _hash(label: str) -> str: + return hashlib.sha256(label.encode("utf-8")).hexdigest() + + +def _z(value: datetime) -> str: + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _sign( + body: dict, + *, + source_sequence: int, + not_before: datetime, + expires_at: datetime, + signer_role: str | None = None, +) -> dict: + role = signer_role or ( + "core" if body["schema"] == CAPITAL_ENVELOPE_SCHEMA else "operator" + ) + if role == "core": + private_key = CORE_PRIVATE_KEY + signer_key_id = CORE_SIGNER_KEY_ID + elif role == "operator": + private_key = OPERATOR_PRIVATE_KEY + signer_key_id = OPERATOR_SIGNER_KEY_ID + else: + raise ValueError(f"unsupported signer role: {role}") + identity = { + "schema": SIGNED_ENVELOPE_SCHEMA, + "source_id": "dumbmoney-control", + "source_sequence": source_sequence, + "correlation_id": "dummy-command-feed-test", + "causation_id": None, + "nonce": f"command-nonce-{source_sequence}", + "not_before": _z(not_before), + "expires_at": _z(expires_at), + "body_schema": body["schema"], + "body_digest": sha256_json(body), + "body": body, + "signature_algorithm": "Ed25519", + "signer_key_id": signer_key_id, + } + wrapper = {**identity, "event_id": sha256_json(identity)} + wrapper["signature"] = ( + base64.urlsafe_b64encode( + private_key.sign(canonical_json(wrapper).encode("utf-8")) + ) + .decode("ascii") + .rstrip("=") + ) + return wrapper + + +def _kill( + *, + sequence: int, + active: bool, + generation: int, + not_before: datetime = NOW - timedelta(seconds=10), + expires_at: datetime = NOW + timedelta(minutes=5), + signer_role: str | None = None, +) -> dict: + body = { + "schema": KILL_STATE_SCHEMA, + "active": active, + "generation": generation, + "reason": "test kill state", + "changed_by": "test-operator", + "policy_epoch": 1, + "changed_at": _z(min(NOW, expires_at - timedelta(seconds=1))), + } + return _sign( + body, + source_sequence=sequence, + not_before=not_before, + expires_at=expires_at, + signer_role=signer_role, + ) + + +def _mode( + *, + sequence: int, + mode: str, + revision: int, + not_before: datetime = NOW - timedelta(seconds=10), + expires_at: datetime = NOW + timedelta(minutes=5), + signer_role: str | None = None, +) -> dict: + body = { + "schema": DESIRED_MODE_SCHEMA, + "venue": "dummy_kalshi", + "mode": mode, + "revision": revision, + "reason": "test desired mode", + "policy_epoch": 1, + "not_before": _z(not_before), + "expires_at": _z(expires_at), + } + return _sign( + body, + source_sequence=sequence, + not_before=not_before, + expires_at=expires_at, + signer_role=signer_role, + ) + + +def _capital( + *, + sequence: int, + fence: int = 1, + not_before: datetime = NOW - timedelta(seconds=10), + expires_at: datetime = NOW + timedelta(seconds=50), + signer_role: str | None = None, +) -> dict: + body = { + "schema": CAPITAL_ENVELOPE_SCHEMA, + "envelope_id": _hash(f"capital-envelope:{sequence}"), + "mandate_id": _hash("mandate"), + "venue": "dummy_kalshi", + "account_hash": ACCOUNT_HASH, + "strategy_hashes": [STRATEGY_HASH], + "passport_hashes": [PASSPORT_HASH], + "promotion_hashes": [PROMOTION_HASH], + "authorized_instruments": [INSTRUMENT], + "authorized_mode": "LIVE", + "max_order_risk_cents": 100, + "max_open_risk_cents": 500, + "max_correlated_risk_cents": 300, + "max_daily_loss_cents": 200, + "max_open_orders": 3, + "fencing_generation": fence, + "policy_epoch": 1, + "not_before": _z(not_before), + "expires_at": _z(expires_at), + } + return _sign( + body, + source_sequence=sequence, + not_before=not_before, + expires_at=expires_at, + signer_role=signer_role, + ) + + +def _command( + envelope: dict, + *, + global_sequence: int, +) -> dict: + return { + "global_sequence": global_sequence, + "event_id": envelope["event_id"], + "event_digest": ZERO_DIGEST, + "body_schema": envelope["body_schema"], + "valid_now": False, + "transport_window_current": False, + "authority_effect": "HISTORICAL_ONLY", + "ledger_proof": {}, + "envelope": envelope, + } + + +def _parse_z(value: str) -> datetime: + return datetime.fromisoformat(value[:-1] + "+00:00") + + +def _decorate_commands( + commands: list[dict], + *, + after_sequence: int, + after_digest: str, + observed_at: datetime, +) -> None: + previous_command: dict | None = None + source_heads: dict[str, dict] = {} + for command in commands: + envelope = command["envelope"] + body = envelope["body"] + wrapper_not_before = _parse_z(envelope["not_before"]) + wrapper_expires_at = _parse_z(envelope["expires_at"]) + transport_current = ( + wrapper_not_before <= observed_at < wrapper_expires_at + ) + if envelope["body_schema"] == KILL_STATE_SCHEMA: + body_current = ( + _parse_z(body["changed_at"]) + <= observed_at + timedelta(seconds=5) + ) + positive_grant = body["active"] is False + elif envelope["body_schema"] in { + DESIRED_MODE_SCHEMA, + CAPITAL_ENVELOPE_SCHEMA, + }: + body_current = ( + _parse_z(body["not_before"]) + <= observed_at + < _parse_z(body["expires_at"]) + ) + positive_grant = ( + envelope["body_schema"] == CAPITAL_ENVELOPE_SCHEMA + or body["mode"] == "LIVE" + ) + else: + body_current = transport_current + positive_grant = True + valid_now = transport_current and body_current + command["transport_window_current"] = transport_current + command["valid_now"] = valid_now + command["authority_effect"] = ( + "APPLY_FAIL_CLOSED" + if not positive_grant + else "APPLY_POSITIVE" + if valid_now + else "HISTORICAL_ONLY" + ) + + sequence = command["global_sequence"] + if sequence == 1: + previous_global_digest = ZERO_DIGEST + elif sequence == after_sequence + 1: + previous_global_digest = after_digest + elif ( + previous_command is not None + and sequence == previous_command["global_sequence"] + 1 + ): + previous_global_digest = previous_command["event_digest"] + else: + previous_global_digest = _hash( + f"unprojected-global-event:{sequence - 1}" + ) + + source_id = envelope["source_id"] + source_sequence = envelope["source_sequence"] + prior_source = source_heads.get(source_id) + if source_sequence == 1: + previous_source_digest = ZERO_DIGEST + elif ( + prior_source is not None + and source_sequence + == prior_source["envelope"]["source_sequence"] + 1 + ): + previous_source_digest = prior_source["event_digest"] + elif sequence == after_sequence + 1: + previous_source_digest = after_digest + else: + previous_source_digest = _hash( + f"unprojected-source-event:{source_id}:{source_sequence - 1}" + ) + + received_at = observed_at - timedelta(seconds=1) + record_observed_at = max(received_at, wrapper_not_before) + event_material = { + "global_sequence": sequence, + "event_id": envelope["event_id"], + "source_id": source_id, + "source_sequence": source_sequence, + "signer_key_id": envelope["signer_key_id"], + "nonce": envelope["nonce"], + "event_schema": envelope["body_schema"], + "observed_at": _z(record_observed_at), + "received_at": _z(received_at), + "correlation_id": envelope["correlation_id"], + "causation_id": envelope["causation_id"], + "payload_digest": envelope["body_digest"], + "previous_source_digest": previous_source_digest, + "previous_global_digest": previous_global_digest, + } + event_digest = sha256_json(event_material) + command["event_digest"] = event_digest + command["ledger_proof"] = { + "schema": "dumbmoney.ledger-event-proof.v1", + **event_material, + "event_digest": event_digest, + } + source_heads[source_id] = command + previous_command = command + + +def _checkpoint_signature(checkpoint: dict) -> dict: + signature = CORE_PRIVATE_KEY.sign( + canonical_json(checkpoint).encode("utf-8") + ) + return { + "algorithm": "Ed25519", + "signer_key_id": CORE_SIGNER_KEY_ID, + "signature": ( + base64.urlsafe_b64encode(signature) + .decode("ascii") + .rstrip("=") + ), + } + + +def _page( + commands: list[dict], + *, + after_sequence: int = 0, + after_digest: str = ZERO_DIGEST, + next_sequence: int | None = None, + next_digest: str | None = None, + ledger_head_sequence: int | None = None, + ledger_head_digest: str | None = None, + required_action: str, + observed_at: datetime = NOW, + request_nonce: str = REQUEST_NONCE_1, +) -> bytes: + _decorate_commands( + commands, + after_sequence=after_sequence, + after_digest=after_digest, + observed_at=observed_at, + ) + resolved_next = ( + commands[-1]["global_sequence"] + if next_sequence is None and commands + else after_sequence + if next_sequence is None + else next_sequence + ) + if next_digest is not None: + resolved_digest = next_digest + elif commands and commands[-1]["global_sequence"] == resolved_next: + resolved_digest = commands[-1]["event_digest"] + elif resolved_next == after_sequence: + resolved_digest = after_digest + else: + resolved_digest = _hash(f"ledger-event:{resolved_next}") + head = resolved_next if ledger_head_sequence is None else ledger_head_sequence + resolved_head_digest = ( + ledger_head_digest + if ledger_head_digest is not None + else resolved_digest + if head == resolved_next + else _hash(f"ledger-head:{head}") + ) + ordered_commands = [ + { + key: copy.deepcopy(item) + for key, item in command.items() + if key != "envelope" + } + for command in commands + ] + checkpoint = { + "schema": "dumbmoney.cell-command-checkpoint.v1", + "cell_id": "dummy_kalshi", + "request_nonce": request_nonce, + "after_sequence": after_sequence, + "after_digest": after_digest, + "ordered_commands": ordered_commands, + "next_sequence": resolved_next, + "next_digest": resolved_digest, + "ledger_head_sequence": head, + "ledger_head_digest": resolved_head_digest, + "observed_at": _z(observed_at), + "required_action": required_action, + } + value = { + "schema": "dumbmoney.cell-command-page.v1", + "cell_id": "dummy_kalshi", + "request_nonce": request_nonce, + "observed_at": _z(observed_at), + "after_sequence": after_sequence, + "after_digest": after_digest, + "next_sequence": resolved_next, + "next_digest": resolved_digest, + "ledger_head_sequence": head, + "ledger_head_digest": resolved_head_digest, + "has_more": resolved_next < head, + "required_action": required_action, + "commands": commands, + "checkpoint": checkpoint, + "checkpoint_signature": _checkpoint_signature(checkpoint), + } + return canonical_json(value).encode("utf-8") + + +class _Transport: + def __init__(self, responses: list[CommandFeedResponse | Exception]): + self.responses = list(responses) + self.calls: list[tuple[str, dict[str, str]]] = [] + + def __call__(self, path: str, headers: dict[str, str]) -> CommandFeedResponse: + self.calls.append((path, dict(headers))) + response = self.responses.pop(0) + if isinstance(response, Exception): + raise response + return response + + +def _consumer( + tmp_path: Path, + transport: _Transport, + *, + kill_events: list | None = None, + mode_events: list | None = None, + failures: list | None = None, + state_journal: AppendOnlyOperationalJournal | None = None, + capital_adapter: CapitalEnvelopeAdapter | None = None, + now_fn=None, + request_nonces: list[str] | None = None, +) -> tuple[ + CoreCommandFeedConsumer, + AppendOnlyOperationalJournal, + CapitalEnvelopeAdapter, + list, + list, + list, +]: + clock = now_fn or (lambda: NOW) + nonce_values = iter( + request_nonces + or [REQUEST_NONCE_1, REQUEST_NONCE_2, REQUEST_NONCE_3] + ) + state = state_journal or AppendOnlyOperationalJournal( + tmp_path / "command-feed.jsonl", + now_fn=clock, + ) + adapter = capital_adapter or CapitalEnvelopeAdapter( + journal=AppendOnlyOperationalJournal( + tmp_path / "capital.jsonl", + now_fn=lambda: NOW, + ), + trusted_public_keys={CORE_SIGNER_KEY_ID: CORE_PUBLIC_KEY}, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + lineage_resolver=_UnusedLineageResolver(), + now_fn=clock, + ) + kills = [] if kill_events is None else kill_events + modes = [] if mode_events is None else mode_events + closed = [] if failures is None else failures + consumer = CoreCommandFeedConsumer( + transport=transport, + cell_token_provider=lambda: "dummy-cell-secret-token", + state_journal=state, + trusted_operator_public_keys={ + OPERATOR_SIGNER_KEY_ID: OPERATOR_PUBLIC_KEY + }, + trusted_checkpoint_public_keys={ + CORE_SIGNER_KEY_ID: CORE_PUBLIC_KEY + }, + capital_adapter=adapter, + kill_handler=kills.append, + mode_handler=modes.append, + fail_closed_handler=closed.append, + now_fn=clock, + request_nonce_fn=lambda: next(nonce_values), + ) + return consumer, state, adapter, kills, modes, closed + + +def test_signed_kill_is_delivered_and_restart_uses_durable_cursor(tmp_path): + kill = _command(_kill(sequence=1, active=True, generation=1), global_sequence=1) + first_transport = _Transport( + [ + CommandFeedResponse( + 200, + _page([kill], required_action="CANCEL_AND_RECONCILE"), + ) + ] + ) + consumer, state, adapter, kills, _, closed = _consumer( + tmp_path, + first_transport, + ) + + first = consumer.poll_once() + + assert first.page_accepted is True + assert first.cursor_sequence == 1 + assert first.submission_allowed is False + assert len(kills) == 1 + assert kills[0].body["active"] is True + assert closed + + next_transport = _Transport( + [ + CommandFeedResponse( + 200, + _page( + [], + after_sequence=1, + after_digest=kill["event_digest"], + required_action="CANCEL_AND_RECONCILE", + ), + ) + ] + ) + restarted, *_ = _consumer( + tmp_path, + next_transport, + state_journal=AppendOnlyOperationalJournal( + state.path, + now_fn=lambda: NOW, + ), + capital_adapter=adapter, + ) + second = restarted.poll_once() + + assert second.page_accepted is True + assert f"after=1&cursor={kill['event_digest']}" in next_transport.calls[0][0] + assert restarted.snapshot()["kill_envelope"]["body"]["active"] is True + + +def test_two_consumers_cannot_commit_from_the_same_cursor(tmp_path): + command = _command( + _kill(sequence=1, active=True, generation=1), + global_sequence=1, + ) + response = CommandFeedResponse( + 200, + _page([command], required_action="CANCEL_AND_RECONCILE"), + ) + state_path = tmp_path / "shared-command-feed.jsonl" + first, *_ = _consumer( + tmp_path, + _Transport([response]), + state_journal=AppendOnlyOperationalJournal( + state_path, + now_fn=lambda: NOW, + ), + ) + second, *_ = _consumer( + tmp_path, + _Transport([response]), + state_journal=AppendOnlyOperationalJournal( + state_path, + now_fn=lambda: NOW, + ), + ) + + with ThreadPoolExecutor(max_workers=2) as executor: + results = list( + executor.map(lambda consumer: consumer.poll_once(), [first, second]) + ) + + assert sorted(result.page_accepted for result in results) == [False, True] + restarted = AppendOnlyOperationalJournal( + state_path, + now_fn=lambda: NOW, + ) + states = restarted.events(kind="dumbmoney.command-feed.state") + assert restarted.healthy is True + assert len(states) == 2 + assert states[-1]["payload"]["cursor"]["sequence"] == 1 + assert states[-1]["payload"]["status"] == "FAIL_CLOSED" + + +def test_mode_and_capital_are_dispatched_without_positive_callbacks(tmp_path): + capital = _command(_capital(sequence=1), global_sequence=1) + mode = _command( + _mode(sequence=2, mode="PAUSED", revision=1), + global_sequence=2, + ) + inactive_kill = _command( + _kill(sequence=3, active=False, generation=1), + global_sequence=3, + ) + transport = _Transport( + [ + CommandFeedResponse( + 200, + _page( + [capital, mode, inactive_kill], + required_action="PAUSE_NEW_RISK", + ), + ) + ] + ) + consumer, _, adapter, kills, modes, _ = _consumer(tmp_path, transport) + + result = consumer.poll_once() + + assert result.page_accepted is True + assert result.commands_applied == 3 + assert result.submission_allowed is False + assert kills == [] + assert [item.body["mode"] for item in modes] == ["PAUSED"] + accepted = adapter.journal.events(kind="capital.envelope.accepted") + assert len(accepted) == 1 + assert accepted[0]["payload"]["wrapper"] == capital["envelope"] + + +def test_current_live_mode_kill_clear_and_capital_create_durable_readiness( + tmp_path, +): + commands = [ + _command(_capital(sequence=1), global_sequence=1), + _command( + _mode(sequence=2, mode="LIVE", revision=1), + global_sequence=2, + ), + _command( + _kill(sequence=3, active=False, generation=1), + global_sequence=3, + ), + ] + transport = _Transport( + [ + CommandFeedResponse( + 200, + _page(commands, required_action="APPLY_SIGNED_CONTROLS"), + ) + ] + ) + consumer, _, adapter, kills, modes, closed = _consumer( + tmp_path, + transport, + ) + + result = consumer.poll_once() + + assert result.page_accepted is True + assert result.commands_applied == 3 + assert result.submission_allowed is True + assert consumer.submission_allowed() is True + assert consumer.snapshot()["status"] == "READY" + assert len(adapter.journal.events(kind="capital.envelope.accepted")) == 1 + assert kills == [] + assert modes == [] + assert closed == [] + + +def test_expired_kill_clear_cannot_leave_submission_ready(tmp_path): + clock = [NOW] + commands = [ + _command( + _mode( + sequence=1, + mode="LIVE", + revision=1, + expires_at=NOW + timedelta(minutes=5), + ), + global_sequence=1, + ), + _command( + _kill( + sequence=2, + active=False, + generation=1, + expires_at=NOW + timedelta(seconds=5), + ), + global_sequence=2, + ), + ] + transport = _Transport( + [ + CommandFeedResponse( + 200, + _page(commands, required_action="APPLY_SIGNED_CONTROLS"), + ) + ] + ) + consumer, *_ = _consumer( + tmp_path, + transport, + now_fn=lambda: clock[0], + ) + + assert consumer.poll_once().submission_allowed is True + clock[0] = NOW + timedelta(seconds=6) + + assert consumer.submission_allowed() is False + + +def test_positive_controls_expiring_during_get_never_commit_ready(tmp_path): + clock = [NOW] + commands = [ + _command( + _mode( + sequence=1, + mode="LIVE", + revision=1, + expires_at=NOW + timedelta(seconds=1), + ), + global_sequence=1, + ), + _command( + _kill( + sequence=2, + active=False, + generation=1, + expires_at=NOW + timedelta(seconds=1), + ), + global_sequence=2, + ), + ] + response = CommandFeedResponse( + 200, + _page(commands, required_action="APPLY_SIGNED_CONTROLS"), + ) + + class SlowTransport: + def __call__(self, path, headers): + clock[0] = NOW + timedelta(seconds=2) + return response + + consumer, *_ = _consumer( + tmp_path, + SlowTransport(), + now_fn=lambda: clock[0], + ) + + result = consumer.poll_once() + + assert result.page_accepted is True + assert result.cursor_sequence == 2 + assert result.submission_allowed is False + assert consumer.snapshot()["status"] == "FAIL_CLOSED" + + +@pytest.mark.parametrize( + "tamper_kind", + ["unsigned_page_field", "signed_checkpoint", "command_projection"], +) +def test_checkpoint_tampering_never_advances_the_cursor( + tmp_path, + tamper_kind, +): + command = _command( + _kill(sequence=1, active=True, generation=1), + global_sequence=1, + ) + raw_page = _page( + [command], + required_action="CANCEL_AND_RECONCILE", + ) + page = json.loads(raw_page) + if tamper_kind == "unsigned_page_field": + page["required_action"] = "PAUSE_NEW_RISK" + elif tamper_kind == "signed_checkpoint": + page["checkpoint"]["required_action"] = "PAUSE_NEW_RISK" + else: + page["commands"][0]["authority_effect"] = "HISTORICAL_ONLY" + transport = _Transport( + [ + CommandFeedResponse( + 200, + canonical_json(page).encode("utf-8"), + ) + ] + ) + consumer, _, _, kills, _, closed = _consumer(tmp_path, transport) + + result = consumer.poll_once() + + assert result.page_accepted is False + assert result.cursor_sequence == 0 + assert kills == [] + assert len(closed) == 1 + + +def test_core_signed_malformed_ledger_proof_is_recomputed_and_rejected( + tmp_path, +): + command = _command( + _kill(sequence=1, active=True, generation=1), + global_sequence=1, + ) + page = json.loads( + _page([command], required_action="CANCEL_AND_RECONCILE") + ) + replacement = _hash("malformed-payload") + page["commands"][0]["ledger_proof"]["payload_digest"] = replacement + page["checkpoint"]["ordered_commands"][0]["ledger_proof"][ + "payload_digest" + ] = replacement + page["checkpoint_signature"] = _checkpoint_signature(page["checkpoint"]) + transport = _Transport( + [ + CommandFeedResponse( + 200, + canonical_json(page).encode("utf-8"), + ) + ] + ) + consumer, _, _, kills, _, closed = _consumer(tmp_path, transport) + + result = consumer.poll_once() + + assert result.page_accepted is False + assert result.cursor_sequence == 0 + assert kills == [] + assert len(closed) == 1 + + +def test_page_replayed_under_a_different_request_nonce_is_rejected(tmp_path): + command = _command( + _kill(sequence=1, active=True, generation=1), + global_sequence=1, + ) + captured_page = _page( + [command], + required_action="CANCEL_AND_RECONCILE", + request_nonce=REQUEST_NONCE_1, + ) + transport = _Transport([CommandFeedResponse(200, captured_page)]) + consumer, _, _, kills, _, closed = _consumer( + tmp_path, + transport, + request_nonces=[REQUEST_NONCE_2], + ) + + result = consumer.poll_once() + + assert result.page_accepted is False + assert result.cursor_sequence == 0 + assert kills == [] + assert f"request_nonce={REQUEST_NONCE_2}" in transport.calls[0][0] + assert len(closed) == 1 + + +@pytest.mark.parametrize("schema", ["kill", "capital"]) +def test_command_signer_roles_are_separate_and_fail_closed(tmp_path, schema): + if schema == "kill": + envelope = _kill( + sequence=1, + active=True, + generation=1, + signer_role="core", + ) + required_action = "CANCEL_AND_RECONCILE" + else: + envelope = _capital( + sequence=1, + signer_role="operator", + ) + required_action = "PAUSE_NEW_RISK" + command = _command(envelope, global_sequence=1) + transport = _Transport( + [ + CommandFeedResponse( + 200, + _page([command], required_action=required_action), + ) + ] + ) + consumer, _, adapter, kills, modes, closed = _consumer( + tmp_path, + transport, + ) + + result = consumer.poll_once() + + assert result.page_accepted is False + assert result.cursor_sequence == 0 + assert kills == [] + assert modes == [] + assert adapter.journal.events(kind="capital.envelope.accepted") == () + assert len(closed) == 1 + + +def test_backlog_and_irrelevant_ledger_gap_require_explicit_second_poll( + tmp_path, +): + mode = _command( + _mode(sequence=1, mode="LIVE", revision=1), + global_sequence=2, + ) + kill_clear = _command( + _kill(sequence=2, active=False, generation=1), + global_sequence=3, + ) + transport = _Transport( + [ + CommandFeedResponse( + 200, + _page( + [mode], + next_sequence=2, + ledger_head_sequence=3, + required_action="APPLY_SIGNED_CONTROLS", + ), + ), + CommandFeedResponse( + 200, + _page( + [kill_clear], + after_sequence=2, + after_digest=mode["event_digest"], + required_action="APPLY_SIGNED_CONTROLS", + request_nonce=REQUEST_NONCE_2, + ), + ), + ] + ) + consumer, *_ = _consumer(tmp_path, transport) + + catching_up = consumer.poll_once() + ready = consumer.poll_once() + + assert catching_up.page_accepted is True + assert catching_up.cursor_sequence == 2 + assert catching_up.submission_allowed is False + assert ready.page_accepted is True + assert ready.cursor_sequence == 3 + assert ready.submission_allowed is True + assert len(transport.calls) == 2 + + +@pytest.mark.parametrize( + "failure_kind", + ["tamper", "digest", "gap", "stale", "future", "unknown"], +) +def test_tamper_gap_and_stale_pages_retain_cursor_and_fail_closed( + tmp_path, + failure_kind, +): + envelope = _kill(sequence=1, active=True, generation=1) + command = _command(envelope, global_sequence=1) + if failure_kind == "tamper": + command["envelope"] = copy.deepcopy(envelope) + command["envelope"]["body"]["reason"] = "tampered" + body = _page([command], required_action="CANCEL_AND_RECONCILE") + elif failure_kind == "digest": + body = _page( + [command], + next_digest=_hash("wrong-ledger-digest"), + required_action="CANCEL_AND_RECONCILE", + ) + elif failure_kind == "gap": + body = _page( + [], + next_sequence=251, + next_digest=_hash("ledger-event:251"), + ledger_head_sequence=251, + required_action="PAUSE_NEW_RISK", + ) + elif failure_kind == "stale": + body = _page( + [], + required_action="PAUSE_NEW_RISK", + observed_at=NOW - timedelta(minutes=2), + ) + elif failure_kind == "future": + body = _page( + [], + required_action="PAUSE_NEW_RISK", + observed_at=NOW + timedelta(seconds=6), + ) + else: + command["body_schema"] = "dumbmoney.unknown-command.v1" + body = _page([command], required_action="CANCEL_AND_RECONCILE") + transport = _Transport([CommandFeedResponse(200, body)]) + consumer, _, _, kills, _, closed = _consumer(tmp_path, transport) + + result = consumer.poll_once() + + assert result.page_accepted is False + assert result.cursor_sequence == 0 + assert result.cursor_digest == ZERO_DIGEST + assert kills == [] + assert len(closed) == 1 + + +@pytest.mark.parametrize( + "response", + [ + CommandFeedResponse(401, b"not inspected"), + RuntimeError("loopback unavailable"), + ], +) +def test_auth_and_transport_failures_make_one_get_and_never_advance( + tmp_path, + response, +): + transport = _Transport([response]) + consumer, _, _, _, _, closed = _consumer(tmp_path, transport) + + result = consumer.poll_once() + + assert result.page_accepted is False + assert result.cursor_sequence == 0 + assert len(transport.calls) == 1 + assert transport.calls[0][1] == { + "Authorization": "Bearer dummy-cell-secret-token" + } + assert len(closed) == 1 + + +def test_expired_active_kill_advances_cursor_and_survives_restart_failure( + tmp_path, +): + expired = _kill( + sequence=1, + active=True, + generation=4, + not_before=NOW - timedelta(minutes=10), + expires_at=NOW - timedelta(minutes=5), + ) + command = _command(expired, global_sequence=1) + expired_clear = _command( + _kill( + sequence=2, + active=False, + generation=5, + not_before=NOW - timedelta(minutes=4), + expires_at=NOW - timedelta(minutes=3), + ), + global_sequence=2, + ) + transport = _Transport( + [ + CommandFeedResponse( + 200, + _page([command], required_action="CANCEL_AND_RECONCILE"), + ), + CommandFeedResponse( + 200, + _page( + [expired_clear], + after_sequence=1, + after_digest=command["event_digest"], + required_action="CANCEL_AND_RECONCILE", + request_nonce=REQUEST_NONCE_2, + ), + ), + ] + ) + consumer, state, adapter, kills, _, _ = _consumer(tmp_path, transport) + + result = consumer.poll_once() + + assert result.page_accepted is True + assert result.cursor_sequence == 1 + assert result.commands_applied == 1 + assert len(kills) == 1 + assert consumer.snapshot()["kill_generation_high_water"] == 4 + ignored_clear = consumer.poll_once() + assert ignored_clear.page_accepted is True + assert ignored_clear.cursor_sequence == 2 + assert ignored_clear.submission_allowed is False + assert consumer.snapshot()["kill_generation_high_water"] == 5 + assert consumer.snapshot()["kill_envelope"]["body"]["active"] is True + + failed_transport = _Transport([OSError("offline")]) + restarted, *_ = _consumer( + tmp_path, + failed_transport, + state_journal=AppendOnlyOperationalJournal( + state.path, + now_fn=lambda: NOW, + ), + capital_adapter=adapter, + ) + failed = restarted.poll_once() + + assert failed.page_accepted is False + assert failed.cursor_sequence == 2 + assert restarted.snapshot()["kill_envelope"]["body"]["active"] is True + assert restarted.submission_allowed() is False + + +def test_expired_pause_persists_while_live_and_future_capital_never_activate( + tmp_path, +): + expired_mode = _mode( + sequence=1, + mode="LIVE", + revision=7, + not_before=NOW - timedelta(minutes=10), + expires_at=NOW - timedelta(minutes=5), + ) + future_capital = _capital( + sequence=2, + fence=9, + not_before=NOW + timedelta(minutes=1), + expires_at=NOW + timedelta(minutes=2), + ) + expired_pause = _mode( + sequence=3, + mode="PAUSED", + revision=8, + not_before=NOW - timedelta(minutes=8), + expires_at=NOW - timedelta(minutes=4), + ) + commands = [ + _command(expired_mode, global_sequence=1), + _command(future_capital, global_sequence=2), + _command(expired_pause, global_sequence=3), + ] + transport = _Transport( + [ + CommandFeedResponse( + 200, + _page(commands, required_action="PAUSE_NEW_RISK"), + ) + ] + ) + consumer, _, adapter, _, modes, closed = _consumer(tmp_path, transport) + + result = consumer.poll_once() + snapshot = consumer.snapshot() + + assert result.page_accepted is True + assert result.cursor_sequence == 3 + assert result.commands_applied == 1 + assert result.submission_allowed is False + assert snapshot["desired_mode_envelope"]["body"]["mode"] == "PAUSED" + assert snapshot["mode_revision_high_water"] == 8 + assert snapshot["capital_fencing_high_water"] == 9 + assert adapter.journal.events(kind="capital.envelope.accepted") == () + assert [item.body["mode"] for item in modes] == ["PAUSED"] + assert closed diff --git a/tests/test_dumbmoney_execution_cycle.py b/tests/test_dumbmoney_execution_cycle.py new file mode 100644 index 00000000..48befdcd --- /dev/null +++ b/tests/test_dumbmoney_execution_cycle.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import hashlib + +import pytest + +from live_firewall.dumbmoney_execution_cycle import ( + SealedDisabledExecutionCycle, +) + + +def _snapshot() -> dict: + return { + "schema": "dummy.kalshi-broker-truth.v1", + "venue": "dummy_kalshi", + "account_hash": hashlib.sha256(b"account").hexdigest(), + "subaccount_number": 0, + "observed_at": "2026-07-26T22:00:00Z", + "broker_snapshot_sha256": hashlib.sha256(b"snapshot").hexdigest(), + "flat_book_observed": True, + "total_exposure_cents": 0, + "open_order_count": 0, + "market_exposure_cents": {}, + "correlated_exposure_cents": {}, + "unresolved_open_orders": 0, + "unresolved_positions": 0, + } + + +def test_sealed_cycle_is_snapshot_bound_and_has_no_submission_surface() -> None: + cycle = SealedDisabledExecutionCycle() + snapshot = _snapshot() + + result = cycle( + capital_adapter=object(), # type: ignore[arg-type] + command_feed=object(), # type: ignore[arg-type] + broker_snapshot=snapshot, + ) + + assert result == { + "schema": "dummy.dumbmoney-execution-cycle.v1", + "status": "BLOCKED", + "broker_contacted": False, + "orders_submitted": 0, + "broker_snapshot_sha256": snapshot["broker_snapshot_sha256"], + } + assert cycle.submission_capable is False + assert not hasattr(cycle, "broker") + assert not hasattr(cycle, "submit") + + +def test_sealed_cycle_rejects_non_exact_or_unbound_snapshot() -> None: + cycle = SealedDisabledExecutionCycle() + extra = {**_snapshot(), "unexpected": True} + with pytest.raises(ValueError, match="fields mismatch"): + cycle( + capital_adapter=object(), # type: ignore[arg-type] + command_feed=object(), # type: ignore[arg-type] + broker_snapshot=extra, + ) + + malformed = {**_snapshot(), "broker_snapshot_sha256": "not-a-digest"} + with pytest.raises(ValueError, match="digest is invalid"): + cycle( + capital_adapter=object(), # type: ignore[arg-type] + command_feed=object(), # type: ignore[arg-type] + broker_snapshot=malformed, + ) diff --git a/tests/test_dumbmoney_journal_anchor.py b/tests/test_dumbmoney_journal_anchor.py new file mode 100644 index 00000000..30be2d17 --- /dev/null +++ b/tests/test_dumbmoney_journal_anchor.py @@ -0,0 +1,302 @@ +from __future__ import annotations + +import base64 +import hashlib +import json +from datetime import datetime, timedelta, timezone +from typing import Any + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, +) + +from live_firewall.dumbmoney_journal_anchor import ( + ANCHOR_BODY_SCHEMA, + ANCHOR_CHECKPOINT_SCHEMA, + ANCHOR_REQUEST_SCHEMA, + ANCHOR_RESPONSE_SCHEMA, + CoreJournalAnchorClient, + JournalAnchorError, + JournalAnchorResponse, +) +from live_firewall.operational_journal import canonical_json, sha256_json + + +NOW = datetime(2026, 7, 26, 22, 0, 0, tzinfo=timezone.utc) +ACCOUNT_HASH = hashlib.sha256(b"dummy-account").hexdigest() +PRIVATE_KEY = Ed25519PrivateKey.from_private_bytes(bytes(range(32))) +PUBLIC_KEY = PRIVATE_KEY.public_key().public_bytes_raw() +SIGNER_KEY_ID = hashlib.sha256(PUBLIC_KEY).hexdigest() +ZERO_DIGEST = "0" * 64 + + +def _z(value: datetime) -> str: + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _hash(label: str) -> str: + return hashlib.sha256(label.encode("utf-8")).hexdigest() + + +def _signature(value: dict[str, Any]) -> str: + return ( + base64.urlsafe_b64encode( + PRIVATE_KEY.sign(canonical_json(value).encode("utf-8")) + ) + .decode("ascii") + .rstrip("=") + ) + + +def _signed_anchor( + body: dict[str, Any], + *, + observed_at: datetime, +) -> dict[str, Any]: + identity = { + "schema": "dumbmoney.signed-envelope.v1", + "source_id": "dumbmoney-core", + "source_sequence": 1, + "correlation_id": "journal-anchor:dummy_kalshi:test-journal", + "causation_id": None, + "nonce": _hash("anchor-event-nonce"), + "not_before": _z(observed_at), + "expires_at": _z(observed_at + timedelta(seconds=120)), + "body_schema": ANCHOR_BODY_SCHEMA, + "body_digest": sha256_json(body), + "body": body, + "signature_algorithm": "Ed25519", + "signer_key_id": SIGNER_KEY_ID, + } + envelope = {**identity, "event_id": sha256_json(identity)} + envelope["signature"] = _signature(envelope) + return envelope + + +def _response( + request: dict[str, Any], + *, + observed_at: datetime = NOW, + reused: bool = False, +) -> dict[str, Any]: + previous = ZERO_DIGEST + anchor_identity = { + "schema": ANCHOR_BODY_SCHEMA, + "venue": "dummy_kalshi", + "account_hash": request["account_hash"], + "journal_name": request["journal_name"], + "journal_schema": request["journal_schema"], + "journal_stream_id": request["journal_stream_id"], + "journal_sequence": request["journal_sequence"], + "journal_head_sha256": request["journal_head_sha256"], + "previous_anchor_body_digest": previous, + } + body = { + **anchor_identity, + "anchor_id": sha256_json(anchor_identity), + "anchored_at": _z(observed_at), + } + envelope = _signed_anchor(body, observed_at=observed_at) + event_material = { + "global_sequence": 1, + "event_id": envelope["event_id"], + "source_id": envelope["source_id"], + "source_sequence": envelope["source_sequence"], + "signer_key_id": envelope["signer_key_id"], + "nonce": envelope["nonce"], + "event_schema": envelope["body_schema"], + "observed_at": _z(observed_at), + "received_at": _z(observed_at), + "correlation_id": envelope["correlation_id"], + "causation_id": envelope["causation_id"], + "payload_digest": envelope["body_digest"], + "previous_source_digest": ZERO_DIGEST, + "previous_global_digest": ZERO_DIGEST, + } + event_digest = sha256_json(event_material) + proof = { + "schema": "dumbmoney.ledger-event-proof.v1", + **event_material, + "event_digest": event_digest, + } + checkpoint = { + "schema": ANCHOR_CHECKPOINT_SCHEMA, + "cell_id": "dummy_kalshi", + "request_nonce": request["request_nonce"], + "account_hash": request["account_hash"], + "journal_name": request["journal_name"], + "journal_schema": request["journal_schema"], + "journal_stream_id": request["journal_stream_id"], + "journal_sequence": request["journal_sequence"], + "journal_head_sha256": request["journal_head_sha256"], + "anchor_body_digest": envelope["body_digest"], + "anchor_event_digest": event_digest, + "reused": reused, + "observed_at": _z(observed_at), + } + return { + "schema": ANCHOR_RESPONSE_SCHEMA, + "cell_id": "dummy_kalshi", + "request_nonce": request["request_nonce"], + "observed_at": _z(observed_at), + "reused": reused, + "anchor_envelope": envelope, + "ledger_proof": proof, + "checkpoint": checkpoint, + "checkpoint_signature": { + "algorithm": "Ed25519", + "signer_key_id": SIGNER_KEY_ID, + "signature": _signature(checkpoint), + }, + } + + +class _Transport: + def __init__( + self, + mutate: Any = None, + *, + status_code: int = 200, + ) -> None: + self.mutate = mutate + self.status_code = status_code + self.calls: list[tuple[str, dict[str, str], dict[str, Any]]] = [] + + def __call__( + self, + path: str, + headers: dict[str, str], + body: bytes, + ) -> JournalAnchorResponse: + request = json.loads(body) + self.calls.append((path, dict(headers), request)) + payload = _response(request) + if self.mutate is not None: + self.mutate(payload) + return JournalAnchorResponse( + status_code=self.status_code, + body=canonical_json(payload).encode("utf-8"), + ) + + +def _client(transport: Any, *, now: datetime = NOW) -> CoreJournalAnchorClient: + return CoreJournalAnchorClient( + transport=transport, + cell_token_provider=lambda: "t" * 64, + trusted_core_public_keys={SIGNER_KEY_ID: PUBLIC_KEY}, + expected_account_hash=ACCOUNT_HASH, + request_nonce_fn=lambda: _hash("request-nonce"), + now_fn=lambda: now, + ) + + +def test_exact_core_anchor_checkpoint_and_ledger_proof_are_accepted() -> None: + transport = _Transport() + client = _client(transport) + head = _hash("journal-head") + + result = client.anchor( + journal_name="capital-operational", + journal_schema="dummy.sqlite-operational-journal.v1", + journal_sequence=7, + journal_head_sha256=head, + ) + + assert result.journal_sequence == 7 + assert result.journal_head_sha256 == head + assert result.reused is False + path, headers, request = transport.calls[0] + assert path == "/v1/cells/dummy_kalshi/journal-heads:anchor" + assert headers == { + "Authorization": f"Bearer {'t' * 64}", + "Content-Type": "application/json", + } + assert request["schema"] == ANCHOR_REQUEST_SCHEMA + assert request["journal_stream_id"] == client.stream_id( + "capital-operational" + ) + + +def test_checkpoint_tamper_fails_closed() -> None: + def mutate(payload: dict[str, Any]) -> None: + payload["checkpoint"]["journal_sequence"] = 6 + + with pytest.raises( + JournalAnchorError, + match="signature is invalid", + ): + _client(_Transport(mutate)).anchor( + journal_name="capital-operational", + journal_schema="dummy.sqlite-operational-journal.v1", + journal_sequence=7, + journal_head_sha256=_hash("journal-head"), + ) + + +def test_envelope_tamper_is_normalized_to_anchor_error() -> None: + def mutate(payload: dict[str, Any]) -> None: + payload["anchor_envelope"]["body"]["journal_sequence"] = 6 + + with pytest.raises( + JournalAnchorError, + match="anchor envelope is invalid", + ): + _client(_Transport(mutate)).anchor( + journal_name="capital-operational", + journal_schema="dummy.sqlite-operational-journal.v1", + journal_sequence=7, + journal_head_sha256=_hash("journal-head"), + ) + + +def test_stale_checkpoint_and_core_rejection_fail_closed() -> None: + def stale( + path: str, + headers: dict[str, str], + body: bytes, + ) -> JournalAnchorResponse: + del path, headers + request = json.loads(body) + payload = _response( + request, + observed_at=NOW - timedelta(seconds=31), + ) + return JournalAnchorResponse( + 200, + canonical_json(payload).encode("utf-8"), + ) + + with pytest.raises(JournalAnchorError, match="is not current"): + _client(stale).anchor( + journal_name="command-feed", + journal_schema="dummy.sqlite-operational-journal.v1", + journal_sequence=0, + journal_head_sha256=ZERO_DIGEST, + ) + + with pytest.raises(JournalAnchorError, match="rejected"): + _client(_Transport(status_code=409)).anchor( + journal_name="command-feed", + journal_schema="dummy.sqlite-operational-journal.v1", + journal_sequence=0, + journal_head_sha256=ZERO_DIGEST, + ) + + +def test_sequence_zero_and_zero_head_must_match() -> None: + client = _client(_Transport()) + with pytest.raises(JournalAnchorError, match="zero head"): + client.anchor( + journal_name="command-feed", + journal_schema="dummy.sqlite-operational-journal.v1", + journal_sequence=0, + journal_head_sha256=_hash("not-empty"), + ) + with pytest.raises(JournalAnchorError, match="zero head"): + client.anchor( + journal_name="command-feed", + journal_schema="dummy.sqlite-operational-journal.v1", + journal_sequence=1, + journal_head_sha256=ZERO_DIGEST, + ) diff --git a/tests/test_dumbmoney_kalshi_broker_truth.py b/tests/test_dumbmoney_kalshi_broker_truth.py new file mode 100644 index 00000000..7abc31b1 --- /dev/null +++ b/tests/test_dumbmoney_kalshi_broker_truth.py @@ -0,0 +1,831 @@ +from __future__ import annotations + +import base64 +import hashlib +from collections import Counter +from datetime import datetime, timezone +from pathlib import Path +from types import SimpleNamespace +from typing import Any +from unittest.mock import Mock + +import httpx +import pytest +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from live_firewall import dumbmoney_windows_service as windows_service +from live_firewall.dumbmoney_capital import verify_signed_envelope +from live_firewall.kalshi_broker_truth import ( + KALSHI_API_PREFIX, + KALSHI_PRODUCTION_ORIGIN, + KalshiBrokerTruthError, + KalshiBrokerTruthProvider, +) +from live_firewall.kalshi_reconciliation import ( + KalshiReconciliationReader, +) + + +NOW = datetime(2026, 7, 26, 22, 0, tzinfo=timezone.utc) +KEY_ID = "test-key-id" +ACCOUNT_HASH = windows_service.kalshi_account_hash(KEY_ID, 0) + + +@pytest.fixture(scope="module") +def private_key() -> rsa.RSAPrivateKey: + return rsa.generate_private_key(public_exponent=65_537, key_size=2_048) + + +@pytest.fixture(scope="module") +def private_key_pem(private_key: rsa.RSAPrivateKey) -> bytes: + return private_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + + +def _client( + handler: Any, +) -> httpx.Client: + return httpx.Client( + base_url=f"{KALSHI_PRODUCTION_ORIGIN}{KALSHI_API_PREFIX}/", + transport=httpx.MockTransport(handler), + trust_env=False, + follow_redirects=False, + ) + + +def _provider( + private_key_pem: bytes, + handler: Any, + **kwargs: Any, +) -> KalshiBrokerTruthProvider: + return KalshiBrokerTruthProvider( + api_key_id=KEY_ID, + private_key_pem=private_key_pem, + expected_account_hash=ACCOUNT_HASH, + subaccount_number=0, + client=_client(handler), + clock=lambda: NOW, + **kwargs, + ) + + +def _flat_handler(requests: list[httpx.Request]) -> Any: + def handler(request: httpx.Request) -> httpx.Response: + requests.append(request) + if request.url.path.endswith("/portfolio/balance"): + payload = { + "balance": 123, + "balance_dollars": "1.2300", + "portfolio_value": 0, + "updated_ts": 1_721_000_000, + "balance_breakdown": [], + } + elif request.url.path.endswith("/portfolio/positions"): + payload = { + "market_positions": [], + "event_positions": [], + "cursor": "", + } + elif request.url.path.endswith("/portfolio/orders"): + payload = {"orders": [], "cursor": ""} + else: + raise AssertionError(f"unexpected path: {request.url.path}") + return httpx.Response(200, json=payload, request=request) + + return handler + + +def test_flat_snapshot_is_signed_scoped_stable_and_network_read_only( + private_key: rsa.RSAPrivateKey, + private_key_pem: bytes, +) -> None: + requests: list[httpx.Request] = [] + provider = _provider(private_key_pem, _flat_handler(requests)) + try: + snapshot = provider.snapshot() + finally: + provider.close() + + assert snapshot == { + "schema": "dummy.kalshi-broker-truth.v1", + "venue": "dummy_kalshi", + "account_hash": ACCOUNT_HASH, + "subaccount_number": 0, + "observed_at": "2026-07-26T22:00:00Z", + "broker_snapshot_sha256": snapshot["broker_snapshot_sha256"], + "flat_book_observed": True, + "total_exposure_cents": 0, + "open_order_count": 0, + "market_exposure_cents": {}, + "correlated_exposure_cents": {}, + "unresolved_open_orders": 0, + "unresolved_positions": 0, + } + assert len(snapshot["broker_snapshot_sha256"]) == 64 + assert [request.method for request in requests] == ["GET"] * 5 + assert Counter(request.url.path for request in requests) == { + f"{KALSHI_API_PREFIX}/portfolio/balance": 1, + f"{KALSHI_API_PREFIX}/portfolio/positions": 2, + f"{KALSHI_API_PREFIX}/portfolio/orders": 2, + } + for request in requests: + assert request.url.host == "external-api.kalshi.com" + assert request.url.scheme == "https" + assert request.url.params["subaccount"] == "0" + assert request.headers["KALSHI-ACCESS-KEY"] == KEY_ID + timestamp = request.headers["KALSHI-ACCESS-TIMESTAMP"] + signature = request.headers["KALSHI-ACCESS-SIGNATURE"] + private_key.public_key().verify( + base64.b64decode(signature), + f"{timestamp}GET{request.url.path}".encode(), + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.DIGEST_LENGTH, + ), + hashes.SHA256(), + ) + positions = [ + request + for request in requests + if request.url.path.endswith("/portfolio/positions") + ] + orders = [ + request + for request in requests + if request.url.path.endswith("/portfolio/orders") + ] + assert all(request.url.params["limit"] == "1000" for request in positions) + assert all( + request.url.params["count_filter"] == "position" + for request in positions + ) + assert all(request.url.params["status"] == "resting" for request in orders) + + +def test_paginated_exposure_is_complete_and_conservative( + private_key_pem: bytes, +) -> None: + calls: Counter[tuple[str, str]] = Counter() + + def handler(request: httpx.Request) -> httpx.Response: + path = request.url.path + cursor = request.url.params.get("cursor", "") + calls[(path, cursor)] += 1 + if path.endswith("/portfolio/balance"): + payload = { + "balance": 500, + "balance_dollars": "5.0000", + "portfolio_value": 25, + "updated_ts": 1_721_000_001, + } + elif path.endswith("/portfolio/positions") and not cursor: + payload = { + "market_positions": [ + { + "ticker": "KXTEST-A", + "position_fp": "1.00", + "market_exposure_dollars": "0.1234", + "last_updated_ts": "2026-07-26T21:59:00Z", + } + ], + "event_positions": [ + { + "event_ticker": "KXTEST", + "event_exposure_dollars": "0.2000", + } + ], + "cursor": "positions-next", + } + elif path.endswith("/portfolio/positions"): + assert cursor == "positions-next" + payload = { + "market_positions": [ + { + "ticker": "KXOTHER-B", + "position_fp": "-2.00", + "market_exposure_dollars": "-0.0001", + "last_updated_ts": "2026-07-26T21:59:30Z", + } + ], + "event_positions": [ + { + "event_ticker": "KXOTHER", + "event_exposure_dollars": "0.0500", + } + ], + "cursor": "", + } + elif path.endswith("/portfolio/orders") and not cursor: + payload = { + "orders": [ + { + "order_id": "order-a", + "ticker": "KXTEST-A", + "remaining_count_fp": "1.00", + "yes_price_dollars": "0.4000", + "no_price_dollars": "0.6000", + "subaccount_number": 0, + } + ], + "cursor": "orders-next", + } + elif path.endswith("/portfolio/orders"): + assert cursor == "orders-next" + payload = { + "orders": [ + { + "order_id": "order-b", + "ticker": "KXOTHER-B", + "remaining_count_fp": "2.00", + "yes_price_dollars": "0.7500", + "no_price_dollars": "0.2500", + "subaccount_number": 0, + } + ], + "cursor": "", + } + else: + raise AssertionError(path) + return httpx.Response(200, json=payload, request=request) + + provider = _provider(private_key_pem, handler) + try: + snapshot = provider.snapshot() + finally: + provider.close() + + assert snapshot["flat_book_observed"] is False + assert snapshot["total_exposure_cents"] == 25 + assert snapshot["market_exposure_cents"] == { + "KXOTHER-B": 1, + "KXTEST-A": 13, + } + assert snapshot["correlated_exposure_cents"] == { + "KXOTHER": 5, + "KXTEST": 20, + } + assert snapshot["open_order_count"] == 2 + assert snapshot["unresolved_open_orders"] == 2 + assert snapshot["unresolved_positions"] == 2 + assert calls[ + (f"{KALSHI_API_PREFIX}/portfolio/positions", "") + ] == 2 + assert calls[ + (f"{KALSHI_API_PREFIX}/portfolio/positions", "positions-next") + ] == 2 + assert calls[ + (f"{KALSHI_API_PREFIX}/portfolio/orders", "") + ] == 2 + assert calls[ + (f"{KALSHI_API_PREFIX}/portfolio/orders", "orders-next") + ] == 2 + + +def test_snapshot_rejects_state_that_changes_between_double_reads( + private_key_pem: bytes, +) -> None: + position_reads = 0 + + def handler(request: httpx.Request) -> httpx.Response: + nonlocal position_reads + if request.url.path.endswith("/portfolio/balance"): + payload = { + "balance": 100, + "balance_dollars": "1.0000", + "portfolio_value": 0, + "updated_ts": 1, + } + elif request.url.path.endswith("/portfolio/orders"): + payload = {"orders": [], "cursor": ""} + else: + position_reads += 1 + markets = ( + [] + if position_reads == 1 + else [ + { + "ticker": "KXCHANGED", + "position_fp": "1.00", + "market_exposure_dollars": "0.1000", + "last_updated_ts": "2026-07-26T21:59:00Z", + } + ] + ) + payload = { + "market_positions": markets, + "event_positions": [], + "cursor": "", + } + return httpx.Response(200, json=payload, request=request) + + provider = _provider(private_key_pem, handler) + try: + with pytest.raises( + KalshiBrokerTruthError, + match="changed during reconciliation", + ): + provider.snapshot() + finally: + provider.close() + + +@pytest.mark.parametrize( + ("market_positions", "event_positions", "message"), + [ + ( + [ + { + "ticker": "KXFUTURE", + "position_fp": "1.00", + "market_exposure_dollars": "0.1000", + "last_updated_ts": "2026-07-26T22:00:06Z", + } + ], + [], + "timestamp is in the future", + ), + ( + [], + [ + { + "event_ticker": "KXORPHAN", + "event_exposure_dollars": "0.1000", + } + ], + "event exposure exists without a market position", + ), + ], +) +def test_snapshot_rejects_internally_inconsistent_position_evidence( + private_key_pem: bytes, + market_positions: list[dict[str, Any]], + event_positions: list[dict[str, Any]], + message: str, +) -> None: + def handler(request: httpx.Request) -> httpx.Response: + if request.url.path.endswith("/portfolio/balance"): + payload = { + "balance": 100, + "balance_dollars": "1.0000", + "portfolio_value": 0, + "updated_ts": 1, + } + elif request.url.path.endswith("/portfolio/positions"): + payload = { + "market_positions": market_positions, + "event_positions": event_positions, + "cursor": "", + } + else: + payload = {"orders": [], "cursor": ""} + return httpx.Response(200, json=payload, request=request) + + provider = _provider(private_key_pem, handler) + try: + with pytest.raises(KalshiBrokerTruthError, match=message): + provider.snapshot() + finally: + provider.close() + + +def test_snapshot_rejects_incomplete_pagination( + private_key_pem: bytes, +) -> None: + def handler(request: httpx.Request) -> httpx.Response: + if request.url.path.endswith("/portfolio/balance"): + payload = { + "balance": 100, + "balance_dollars": "1.0000", + "portfolio_value": 0, + "updated_ts": 1, + } + else: + payload = { + "market_positions": [], + "event_positions": [], + "cursor": "still-more", + } + return httpx.Response(200, json=payload, request=request) + + provider = _provider( + private_key_pem, + handler, + maximum_pages=1, + ) + try: + with pytest.raises( + KalshiBrokerTruthError, + match="positions pagination is incomplete", + ): + provider.snapshot() + finally: + provider.close() + + +@pytest.mark.parametrize( + "body", + [ + b'{"balance":100,"balance":200,"balance_dollars":"1.0000",' + b'"portfolio_value":0,"updated_ts":1}', + b'{"balance":NaN,"balance_dollars":"1.0000",' + b'"portfolio_value":0,"updated_ts":1}', + ], +) +def test_snapshot_rejects_non_strict_broker_json( + private_key_pem: bytes, + body: bytes, +) -> None: + def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response( + 200, + content=body, + headers={"Content-Type": "application/json"}, + request=request, + ) + + provider = _provider(private_key_pem, handler) + try: + with pytest.raises( + KalshiBrokerTruthError, + match="strict bounded JSON", + ): + provider.snapshot() + finally: + provider.close() + + +def test_snapshot_rejects_order_from_another_subaccount( + private_key_pem: bytes, +) -> None: + def handler(request: httpx.Request) -> httpx.Response: + if request.url.path.endswith("/portfolio/balance"): + payload = { + "balance": 100, + "balance_dollars": "1.0000", + "portfolio_value": 0, + "updated_ts": 1, + } + elif request.url.path.endswith("/portfolio/positions"): + payload = { + "market_positions": [], + "event_positions": [], + "cursor": "", + } + else: + payload = { + "orders": [ + { + "order_id": "wrong-account", + "ticker": "KXTEST", + "remaining_count_fp": "1.00", + "yes_price_dollars": "0.5000", + "no_price_dollars": "0.5000", + "subaccount_number": 1, + } + ], + "cursor": "", + } + return httpx.Response(200, json=payload, request=request) + + provider = _provider(private_key_pem, handler) + try: + with pytest.raises( + KalshiBrokerTruthError, + match="different subaccount", + ): + provider.snapshot() + finally: + provider.close() + + +def test_production_constructor_is_proxy_isolated_and_origin_pinned( + monkeypatch: pytest.MonkeyPatch, + private_key_pem: bytes, +) -> None: + constructed = Mock() + monkeypatch.setattr( + "live_firewall.kalshi_broker_truth.httpx.Client", + constructed, + ) + KalshiBrokerTruthProvider( + api_key_id=KEY_ID, + private_key_pem=private_key_pem, + expected_account_hash=ACCOUNT_HASH, + subaccount_number=0, + clock=lambda: NOW, + ) + + assert constructed.call_count == 1 + kwargs = constructed.call_args.kwargs + assert kwargs["base_url"] == ( + "https://external-api.kalshi.com/trade-api/v2/" + ) + assert kwargs["trust_env"] is False + assert kwargs["follow_redirects"] is False + assert kwargs["limits"].max_connections == 1 + + +def test_service_main_wires_credential_owned_truth_without_contacting_broker( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + private_key_pem: bytes, +) -> None: + config = SimpleNamespace( + data_root=tmp_path / "data", + expected_account_hash=ACCOUNT_HASH, + kalshi_subaccount_number=0, + ) + secrets = SimpleNamespace( + kalshi_key_id=KEY_ID, + kalshi_private_key_pem=private_key_pem, + ) + provider = object() + captured: dict[str, Any] = {} + + monkeypatch.setenv("PROGRAMDATA", str(tmp_path)) + monkeypatch.setattr( + windows_service, + "load_runner_config", + lambda *_args: config, + ) + monkeypatch.setattr( + windows_service, + "load_secrets", + lambda *_args: secrets, + ) + monkeypatch.setattr( + windows_service, + "WindowsCredentialManager", + lambda: object(), + ) + monkeypatch.setattr( + windows_service, + "LoopbackCoreTransport", + lambda: object(), + ) + + def truth_factory(**kwargs: Any) -> object: + captured["truth_kwargs"] = kwargs + return provider + + class Service: + def __init__( + self, + _config: Any, + _secrets: Any, + **kwargs: Any, + ) -> None: + captured["service_kwargs"] = kwargs + + def run_forever(self, _stop_event: Any) -> None: + return + + monkeypatch.setattr( + windows_service, + "KalshiBrokerTruthProvider", + truth_factory, + ) + monkeypatch.setattr( + windows_service, + "DumbMoneyDummyKalshiService", + Service, + ) + monkeypatch.setattr(windows_service.signal, "signal", lambda *_args: None) + + result = windows_service.main( + [ + "--core-endpoint-ref", + windows_service.CORE_ENDPOINT_REF, + "--core-cell-token-target", + windows_service.CELL_TOKEN_TARGET_REF, + "--kalshi-key-id-target", + windows_service.KALSHI_KEY_ID_TARGET_REF, + "--kalshi-private-key-target", + windows_service.KALSHI_PRIVATE_KEY_TARGET_REF, + "--readiness-signing-key-target", + windows_service.READINESS_KEY_TARGET_REF, + "--start-mode", + windows_service.START_MODE, + "--readiness-ref", + windows_service.READINESS_REF, + "--config-sha256", + "b" * 64, + ] + ) + + assert result == 0 + assert captured["truth_kwargs"] == { + "api_key_id": KEY_ID, + "private_key_pem": private_key_pem, + "expected_account_hash": ACCOUNT_HASH, + "subaccount_number": 0, + } + assert captured["service_kwargs"]["broker_truth"] is provider + cycle = captured["service_kwargs"]["execution_cycle"] + assert isinstance(cycle, windows_service.SealedDisabledExecutionCycle) + assert cycle.submission_capable is False + + +def test_service_close_closes_broker_truth( + monkeypatch: pytest.MonkeyPatch, +) -> None: + source = ( + Path(windows_service.__file__) + .read_text(encoding="utf-8") + .replace("\r\n", "\n") + ) + assert 'getattr(self.broker_truth, "close", None)' in source + assert "broker_close()" in source + + +def test_terminal_reader_combines_live_and_historical_fills_and_signs( + private_key_pem: bytes, +) -> None: + signing_key = Ed25519PrivateKey.from_private_bytes(bytes([6]) * 32) + public_key = signing_key.public_key().public_bytes_raw() + signer_id = hashlib.sha256(public_key).hexdigest() + requests: list[httpx.Request] = [] + + order = { + "order_id": "broker-order-1", + "user_id": "fixture-user", + "client_order_id": "proposal-1", + "ticker": "KXTEST-26JUL", + "side": "yes", + "action": "buy", + "outcome_side": "yes", + "book_side": "bid", + "type": "limit", + "status": "executed", + "yes_price_dollars": "0.500000", + "no_price_dollars": "0.500000", + "fill_count_fp": "1.00", + "remaining_count_fp": "0.00", + "initial_count_fp": "1.00", + "taker_fill_cost_dollars": "0.500000", + "maker_fill_cost_dollars": "0.000000", + "taker_fees_dollars": "0.020000", + "maker_fees_dollars": "0.000000", + "expiration_time": "2026-07-26T22:30:00Z", + "created_time": "2026-07-26T21:59:00Z", + "last_update_time": "2026-07-26T21:59:30Z", + "self_trade_prevention_type": "taker_at_cross", + "order_group_id": "", + "cancel_order_on_pause": True, + "subaccount_number": 0, + } + fill = { + "fill_id": "fill-1", + "trade_id": "trade-1", + "order_id": "broker-order-1", + "ticker": "KXTEST-26JUL", + "market_ticker": "KXTEST-26JUL", + "side": "yes", + "action": "buy", + "outcome_side": "yes", + "book_side": "bid", + "count_fp": "1.00", + "yes_price_dollars": "0.500000", + "no_price_dollars": "0.500000", + "is_taker": True, + "fee_cost": "0.020000", + "created_time": "2026-07-26T21:59:30Z", + "subaccount_number": 0, + "ts": 1_721_000_000, + } + + def handler(request: httpx.Request) -> httpx.Response: + requests.append(request) + if request.url.path.endswith("/orders"): + payload = {"orders": [order], "cursor": ""} + elif request.url.path.endswith("/fills"): + payload = {"fills": [fill], "cursor": ""} + else: + raise AssertionError(request.url.path) + return httpx.Response(200, json=payload, request=request) + + provider = _provider(private_key_pem, handler) + reader = KalshiReconciliationReader( + broker_truth=provider, + witness_signing_private_key=signing_key, + ) + reservation = { + "reservation_id": hashlib.sha256(b"reservation").hexdigest(), + "proposal_id": "proposal-1", + "market_ticker": "KXTEST-26JUL", + "contract_ticker": "KXTEST-26JUL", + "side": "yes", + "size": 1, + "price_cents": 50, + } + try: + with pytest.raises( + KalshiBrokerTruthError, + match="differs from the capital reservation", + ): + reader.terminal_reconciliation_witness( + {**reservation, "price_cents": 49} + ) + requests.clear() + wrapper = reader.terminal_reconciliation_witness(reservation) + finally: + provider.close() + + assert wrapper is not None + signed = verify_signed_envelope( + wrapper, + trusted_public_keys={signer_id: public_key}, + expected_body_schema=( + "dummy.kalshi-order-terminal-witness.v1" + ), + require_active=False, + ) + assert signed.body["terminal_status"] == "executed" + assert signed.body["fill_count"] == 1 + assert signed.body["fill_cost_cents"] == 50 + assert signed.body["fee_cents"] == 2 + assert signed.body["average_fill_price_cents"] == 50 + assert signed.body["fill_ids"] == ["fill-1"] + assert Counter(request.url.path for request in requests) == { + f"{KALSHI_API_PREFIX}/portfolio/orders": 2, + f"{KALSHI_API_PREFIX}/historical/orders": 2, + f"{KALSHI_API_PREFIX}/portfolio/fills": 2, + f"{KALSHI_API_PREFIX}/historical/fills": 2, + } + live_requests = [ + request for request in requests + if "/portfolio/" in request.url.path + ] + assert all( + request.url.params["subaccount"] == "0" + for request in live_requests + ) + + +def test_settlement_reader_requires_stable_position_absence( + private_key_pem: bytes, +) -> None: + signing_key = Ed25519PrivateKey.from_private_bytes(bytes([5]) * 32) + position_reads = 0 + + def handler(request: httpx.Request) -> httpx.Response: + nonlocal position_reads + if request.url.path.endswith("/positions"): + position_reads += 1 + payload = { + "market_positions": [], + "event_positions": [], + "cursor": "", + } + elif request.url.path.endswith("/settlements"): + payload = { + "settlements": [ + { + "ticker": "KXTEST-26JUL", + "event_ticker": "KXTEST", + "market_result": "yes", + "yes_count_fp": "1.00", + "yes_total_cost_dollars": "0.500000", + "no_count_fp": "0.00", + "no_total_cost_dollars": "0.000000", + "revenue": 100, + "settled_time": "2026-07-26T21:59:59Z", + "fee_cost": "0.020000", + "value": 100, + } + ], + "cursor": "", + } + else: + raise AssertionError(request.url.path) + return httpx.Response(200, json=payload, request=request) + + provider = _provider(private_key_pem, handler) + reader = KalshiReconciliationReader( + broker_truth=provider, + witness_signing_private_key=signing_key, + ) + position = { + "position_exposure_id": hashlib.sha256(b"position").hexdigest(), + "reservation_id": hashlib.sha256(b"reservation").hexdigest(), + "proposal_id": "proposal-1", + "contract_ticker": "KXTEST-26JUL", + "side": "yes", + "fill_count": 1, + "observed_at": "2026-07-26T21:59:30Z", + } + try: + wrapper = reader.settlement_reconciliation_witness(position) + finally: + provider.close() + + assert wrapper is not None + assert wrapper["body"]["position_absent"] is True + assert wrapper["body"]["market_result"] == "yes" + assert wrapper["body"]["settlement_fee_cents"] == 2 + assert position_reads == 2 diff --git a/tests/test_dumbmoney_lineage.py b/tests/test_dumbmoney_lineage.py new file mode 100644 index 00000000..f645e5f2 --- /dev/null +++ b/tests/test_dumbmoney_lineage.py @@ -0,0 +1,573 @@ +from __future__ import annotations + +import base64 +import hashlib +import json +from dataclasses import replace +from datetime import datetime, timedelta, timezone +from urllib.parse import parse_qs, urlsplit + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from live_firewall.dumbmoney_capital import ( + CAPITAL_ENVELOPE_SCHEMA, + SIGNED_ENVELOPE_SCHEMA, + verify_signed_capital_envelope, +) +from live_firewall.dumbmoney_lineage import ( + ALPHA_PASSPORT_SCHEMA, + CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA, + CONTRACT_RESOLUTION_SCHEMA, + PROMOTION_CERTIFICATE_SCHEMA, + ContractResolutionResponse, + CoreAuthorityContractResolver, + LineageResolutionError, +) +from live_firewall.operational_journal import canonical_json, sha256_json + + +NOW = datetime(2026, 7, 26, 22, 30, 0, tzinfo=timezone.utc) +ACCOUNT_HASH = "c" * 64 +STRATEGY_HASH = "a" * 64 +INSTRUMENT = "event_contract:KXTEST-26JUL" +ZERO_DIGEST = "0" * 64 + + +def _z(value: datetime) -> str: + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _sign( + private_key: Ed25519PrivateKey, + body: dict, + *, + source_id: str, + source_sequence: int = 1, + not_before: datetime = NOW - timedelta(seconds=10), + expires_at: datetime = NOW + timedelta(minutes=10), +) -> dict: + public = private_key.public_key().public_bytes_raw() + identity = { + "schema": SIGNED_ENVELOPE_SCHEMA, + "source_id": source_id, + "source_sequence": source_sequence, + "correlation_id": "dummy-lineage-fixture", + "causation_id": None, + "nonce": f"{source_id}-nonce-{source_sequence}", + "not_before": _z(not_before), + "expires_at": _z(expires_at), + "body_schema": body["schema"], + "body_digest": sha256_json(body), + "body": body, + "signature_algorithm": "Ed25519", + "signer_key_id": hashlib.sha256(public).hexdigest(), + } + wrapper = {**identity, "event_id": sha256_json(identity)} + wrapper["signature"] = ( + base64.urlsafe_b64encode( + private_key.sign(canonical_json(wrapper).encode()) + ) + .decode() + .rstrip("=") + ) + return wrapper + + +def _passport() -> dict: + return { + "schema": ALPHA_PASSPORT_SCHEMA, + "passport_id": "passport-dummy-1", + "strategy_lineage_id": "lineage-dummy-1", + "venue": "dummy_kalshi", + "strategy_hash": STRATEGY_HASH, + "artifact_hashes": ["1" * 64], + "evidence_verdict_hashes": ["2" * 64], + "intended_instruments": [INSTRUMENT], + "maximum_loss_cents": 100, + "evidence_class": "FORWARD", + "created_at": _z(NOW - timedelta(days=1)), + "expires_at": _z(NOW + timedelta(days=1)), + } + + +def _promotion(passport_digest: str, *, policy_epoch: int = 1) -> dict: + return { + "schema": PROMOTION_CERTIFICATE_SCHEMA, + "certificate_id": "promotion-dummy-1", + "passport_digest": passport_digest, + "verdict_digests": ["3" * 64], + "stage": "EXPLORATORY_LIVE", + "venue": "dummy_kalshi", + "instruments": [INSTRUMENT], + "maximum_loss_cents": 100, + "rollback_triggers": ["AMBIGUOUS_OUTCOME", "KILL_ACTIVE"], + "policy_epoch": policy_epoch, + "not_before": _z(NOW - timedelta(days=1)), + "expires_at": _z(NOW + timedelta(days=1)), + } + + +def _capital(passport_digest: str, promotion_digest: str) -> dict: + return { + "schema": CAPITAL_ENVELOPE_SCHEMA, + "envelope_id": "e" * 64, + "mandate_id": "d" * 64, + "venue": "dummy_kalshi", + "account_hash": ACCOUNT_HASH, + "strategy_hashes": [STRATEGY_HASH], + "passport_hashes": [passport_digest], + "promotion_hashes": [promotion_digest], + "authorized_instruments": [INSTRUMENT], + "authorized_mode": "LIVE", + "max_order_risk_cents": 100, + "max_open_risk_cents": 500, + "max_correlated_risk_cents": 300, + "max_daily_loss_cents": 300, + "max_open_orders": 5, + "fencing_generation": 7, + "policy_epoch": 1, + "not_before": _z(NOW - timedelta(seconds=5)), + "expires_at": _z(NOW + timedelta(seconds=60)), + } + + +def _proof(envelope: dict, *, sequence: int) -> dict: + received = NOW - timedelta(seconds=1) + observed = max( + received, + datetime.fromisoformat( + envelope["not_before"].replace("Z", "+00:00") + ), + ) + proof = { + "schema": "dumbmoney.ledger-event-proof.v1", + "global_sequence": sequence, + "event_id": envelope["event_id"], + "source_id": envelope["source_id"], + "source_sequence": envelope["source_sequence"], + "signer_key_id": envelope["signer_key_id"], + "nonce": envelope["nonce"], + "event_schema": envelope["body_schema"], + "observed_at": _z(observed), + "received_at": _z(received), + "correlation_id": envelope["correlation_id"], + "causation_id": envelope["causation_id"], + "payload_digest": envelope["body_digest"], + "previous_source_digest": ZERO_DIGEST, + "previous_global_digest": ( + ZERO_DIGEST if sequence == 1 else "9" * 64 + ), + } + proof["event_digest"] = sha256_json( + { + key: value + for key, value in proof.items() + if key != "schema" + } + ) + return proof + + +def _resolution( + core: Ed25519PrivateKey, + envelope: dict, + *, + nonce: str, + capital_digest: str, + sequence: int, + authority_state: dict, +) -> dict: + checkpoint = { + "schema": CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA, + "cell_id": "dummy_kalshi", + "request_nonce": nonce, + "requested_body_digest": envelope["body_digest"], + "capital_envelope_digest": capital_digest, + "fencing_generation": 7, + "observed_at": _z(NOW), + "contract_schema": envelope["body_schema"], + "transport_window_current": True, + "body_window_current": True, + "eligible_live_input": True, + "authority_state": authority_state, + "ledger_proof": _proof(envelope, sequence=sequence), + "envelope": envelope, + } + public = core.public_key().public_bytes_raw() + signature = core.sign(canonical_json(checkpoint).encode()) + return { + "schema": CONTRACT_RESOLUTION_SCHEMA, + **{ + key: value + for key, value in checkpoint.items() + if key != "schema" + }, + "checkpoint": checkpoint, + "checkpoint_signature": { + "algorithm": "Ed25519", + "signer_key_id": hashlib.sha256(public).hexdigest(), + "signature": base64.urlsafe_b64encode(signature) + .decode() + .rstrip("="), + }, + } + + +class _Transport: + def __init__( + self, + core: Ed25519PrivateKey, + capital_digest: str, + envelopes: dict[str, dict], + authority_state: dict, + *, + mutate=None, + after_call=None, + resign_mutation: bool = False, + ) -> None: + self.core = core + self.capital_digest = capital_digest + self.envelopes = envelopes + self.authority_state = authority_state + self.mutate = mutate + self.after_call = after_call + self.resign_mutation = resign_mutation + self.requests: list[tuple[str, dict[str, str]]] = [] + + def __call__(self, path: str, headers) -> ContractResolutionResponse: + self.requests.append((path, dict(headers))) + parsed = urlsplit(path) + query = parse_qs(parsed.query) + assert query["capital_envelope_digest"] == [self.capital_digest] + digest = parsed.path.rsplit("/", 1)[-1] + envelope = self.envelopes[digest] + response = _resolution( + self.core, + envelope, + nonce=query["request_nonce"][0], + capital_digest=self.capital_digest, + sequence=( + 1 + if envelope["body_schema"] == ALPHA_PASSPORT_SCHEMA + else 2 + ), + authority_state=self.authority_state, + ) + if self.mutate is not None: + response = self.mutate(response) + if self.resign_mutation: + checkpoint = { + "schema": CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA, + **{ + key: value + for key, value in response.items() + if key + not in { + "schema", + "checkpoint", + "checkpoint_signature", + } + }, + } + public = self.core.public_key().public_bytes_raw() + response["checkpoint"] = checkpoint + response["checkpoint_signature"] = { + "algorithm": "Ed25519", + "signer_key_id": hashlib.sha256(public).hexdigest(), + "signature": base64.urlsafe_b64encode( + self.core.sign(canonical_json(checkpoint).encode()) + ) + .decode() + .rstrip("="), + } + if self.after_call is not None: + self.after_call() + return ContractResolutionResponse( + status_code=200, + body=json.dumps(response, separators=(",", ":")).encode(), + ) + + +def _runtime( + *, + promotion_role: str = "promoter", + mutate=None, + clock=None, + resign_mutation: bool = False, +): + core = Ed25519PrivateKey.generate() + research = Ed25519PrivateKey.generate() + promoter = Ed25519PrivateKey.generate() + evaluator_integrity = Ed25519PrivateKey.generate() + evaluator_statistics = Ed25519PrivateKey.generate() + passport = _sign(research, _passport(), source_id="research") + promotion_key = research if promotion_role == "research" else promoter + promotion = _sign( + promotion_key, + _promotion(passport["body_digest"]), + source_id="promoter", + ) + capital_wrapper = _sign( + core, + _capital(passport["body_digest"], promotion["body_digest"]), + source_id="dumbmoney-core", + not_before=NOW - timedelta(seconds=5), + expires_at=NOW + timedelta(seconds=60), + ) + core_public = core.public_key().public_bytes_raw() + capital = verify_signed_capital_envelope( + capital_wrapper, + trusted_public_keys={ + hashlib.sha256(core_public).hexdigest(): core_public + }, + expected_venue="dummy_kalshi", + expected_account_hash=ACCOUNT_HASH, + now=NOW, + ) + capital = replace( + capital, + ledger_event_digest="c" * 64, + ledger_global_sequence=3, + ) + evaluator_publics = [ + evaluator_integrity.public_key().public_bytes_raw(), + evaluator_statistics.public_key().public_bytes_raw(), + ] + authority_state = { + "schema": "dumbmoney.cell-authority-state.v1", + "evaluated_at": _z(NOW), + "authority_valid_until": _z(NOW + timedelta(seconds=30)), + "policy_epoch": capital.policy_epoch, + "mandate_id": capital.body["mandate_id"], + "mandate_event_digest": "4" * 64, + "kill_clear": True, + "kill_generation": 1, + "kill_event_digest": "5" * 64, + "desired_mode": "LIVE", + "desired_mode_revision": 1, + "desired_mode_event_digest": "6" * 64, + "capital_envelope_digest": sha256_json(capital.body), + "capital_event_digest": capital.ledger_event_digest, + "fencing_generation": capital.fencing_generation, + "strategy_hash": STRATEGY_HASH, + "passport_digest": passport["body_digest"], + "passport_event_digest": _proof( + passport, + sequence=1, + )["event_digest"], + "promotion_digest": promotion["body_digest"], + "promotion_event_digest": _proof( + promotion, + sequence=2, + )["event_digest"], + "verdicts": [ + { + "verdict_digest": "7" * 64, + "verdict_event_digest": "8" * 64, + "verdict_id": "verdict-integrity-1", + "court": "integrity", + "decision": "PASS", + "signer_key_id": hashlib.sha256( + evaluator_publics[0] + ).hexdigest(), + "evaluated_at": _z(NOW - timedelta(seconds=5)), + "expires_at": _z(NOW + timedelta(minutes=5)), + "transport_expires_at": _z(NOW + timedelta(minutes=5)), + }, + { + "verdict_digest": "9" * 64, + "verdict_event_digest": "a" * 64, + "verdict_id": "verdict-statistics-1", + "court": "statistics", + "decision": "PASS", + "signer_key_id": hashlib.sha256( + evaluator_publics[1] + ).hexdigest(), + "evaluated_at": _z(NOW - timedelta(seconds=4)), + "expires_at": _z(NOW + timedelta(minutes=5)), + "transport_expires_at": _z(NOW + timedelta(minutes=5)), + }, + ], + "ledger_head_sequence": 4, + "ledger_head_digest": "b" * 64, + } + transport = _Transport( + core, + sha256_json(capital.body), + { + passport["body_digest"]: passport, + promotion["body_digest"]: promotion, + }, + authority_state, + mutate=mutate, + after_call=( + None if clock is None else clock.get("after_call") + ), + resign_mutation=resign_mutation, + ) + research_public = research.public_key().public_bytes_raw() + promoter_public = promoter.public_key().public_bytes_raw() + resolver = CoreAuthorityContractResolver( + transport=transport, + cell_token_provider=lambda: "t" * 64, + trusted_core_public_keys={ + hashlib.sha256(core_public).hexdigest(): core_public + }, + trusted_research_public_keys={ + hashlib.sha256(research_public).hexdigest(): research_public + }, + trusted_promoter_public_keys={ + hashlib.sha256(promoter_public).hexdigest(): promoter_public + }, + trusted_evaluator_public_keys={ + hashlib.sha256(public).hexdigest(): public + for public in evaluator_publics + }, + now_fn=(lambda: NOW) if clock is None else clock["now"], + ) + return resolver, capital, transport + + +def test_nonce_fence_and_role_bound_lineage_resolves() -> None: + resolver, capital, transport = _runtime() + + binding = resolver.resolve_lineage( + capital=capital, + strategy_hash=STRATEGY_HASH, + passport_hash=capital.passport_hashes[0], + promotion_hash=capital.promotion_hashes[0], + authorized_instrument=INSTRUMENT, + ) + + assert binding.capital_body_digest == sha256_json(capital.body) + assert binding.capital_fencing_generation == 7 + assert binding.authorized_instrument == INSTRUMENT + assert len(transport.requests) == 2 + nonces = [ + parse_qs(urlsplit(path).query)["request_nonce"][0] + for path, _ in transport.requests + ] + assert len(set(nonces)) == 2 + assert all( + headers["Authorization"] == f"Bearer {'t' * 64}" + for _, headers in transport.requests + ) + + +def test_wrong_role_promotion_is_rejected() -> None: + resolver, capital, _ = _runtime(promotion_role="research") + + with pytest.raises(LineageResolutionError, match="signer"): + resolver.resolve_lineage( + capital=capital, + strategy_hash=STRATEGY_HASH, + passport_hash=capital.passport_hashes[0], + promotion_hash=capital.promotion_hashes[0], + authorized_instrument=INSTRUMENT, + ) + + +def test_unsigned_capital_digest_transplant_is_rejected() -> None: + def mutate(response: dict) -> dict: + response["capital_envelope_digest"] = "8" * 64 + return response + + resolver, capital, _ = _runtime(mutate=mutate) + + with pytest.raises(LineageResolutionError, match="checkpoint"): + resolver.resolve_lineage( + capital=capital, + strategy_hash=STRATEGY_HASH, + passport_hash=capital.passport_hashes[0], + promotion_hash=capital.promotion_hashes[0], + authorized_instrument=INSTRUMENT, + ) + + +def test_slow_transport_cannot_return_expired_resolution() -> None: + state = {"value": NOW} + + def after_call() -> None: + state["value"] = NOW + timedelta(minutes=2) + + resolver, capital, _ = _runtime( + clock={ + "now": lambda: state["value"], + "after_call": after_call, + } + ) + + with pytest.raises(LineageResolutionError, match="stale"): + resolver.resolve_lineage( + capital=capital, + strategy_hash=STRATEGY_HASH, + passport_hash=capital.passport_hashes[0], + promotion_hash=capital.promotion_hashes[0], + authorized_instrument=INSTRUMENT, + ) + + +def test_core_signed_authority_rejects_unsealed_evaluator() -> None: + def mutate(response: dict) -> dict: + response["authority_state"]["verdicts"][0][ + "signer_key_id" + ] = "f" * 64 + return response + + resolver, capital, _ = _runtime( + mutate=mutate, + resign_mutation=True, + ) + + with pytest.raises(LineageResolutionError, match="signer is not sealed"): + resolver.resolve_lineage( + capital=capital, + strategy_hash=STRATEGY_HASH, + passport_hash=capital.passport_hashes[0], + promotion_hash=capital.promotion_hashes[0], + authorized_instrument=INSTRUMENT, + ) + + +def test_passport_and_promotion_require_one_authority_state() -> None: + calls = 0 + + def mutate(response: dict) -> dict: + nonlocal calls + calls += 1 + if calls == 2: + response["authority_state"]["kill_generation"] = 2 + return response + + resolver, capital, _ = _runtime( + mutate=mutate, + resign_mutation=True, + ) + + with pytest.raises(LineageResolutionError, match="one authority state"): + resolver.resolve_lineage( + capital=capital, + strategy_hash=STRATEGY_HASH, + passport_hash=capital.passport_hashes[0], + promotion_hash=capital.promotion_hashes[0], + authorized_instrument=INSTRUMENT, + ) + + +def test_capital_requires_authenticated_core_ledger_event() -> None: + resolver, capital, _ = _runtime() + unbound = replace( + capital, + ledger_event_digest=None, + ledger_global_sequence=None, + ) + + with pytest.raises(LineageResolutionError, match="ledger binding"): + resolver.resolve_lineage( + capital=unbound, + strategy_hash=STRATEGY_HASH, + passport_hash=capital.passport_hashes[0], + promotion_hash=capital.promotion_hashes[0], + authorized_instrument=INSTRUMENT, + ) diff --git a/tests/test_dumbmoney_windows_service.py b/tests/test_dumbmoney_windows_service.py new file mode 100644 index 00000000..d2e14f2e --- /dev/null +++ b/tests/test_dumbmoney_windows_service.py @@ -0,0 +1,692 @@ +from __future__ import annotations + +import base64 +import hashlib +import json +import sqlite3 +from datetime import datetime, timezone +from pathlib import Path + +import pytest +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from autonomy.executor import Executor +from autonomy.ontology import SessionMode +from live_firewall.dumbmoney_capital import verify_signed_envelope +from live_firewall.dumbmoney_command_feed import CommandFeedPollResult +from live_firewall.dumbmoney_windows_service import ( + CELL_TOKEN_TARGET_REF, + CONFIG_RELATIVE_PATH, + CONFIG_SCHEMA, + CORE_ENDPOINT_REF, + CORE_READINESS_RELATIVE_PATH, + DATA_ROOT_RELATIVE_PATH, + KALSHI_KEY_ID_TARGET_REF, + KALSHI_PRIVATE_KEY_TARGET_REF, + READINESS_KEY_TARGET_REF, + READINESS_REF, + READINESS_RELATIVE_PATH, + READINESS_SCHEMA, + SERVICE_NAME, + START_MODE, + CoreEndpoint, + DumbMoneyDummyKalshiService, + DumbMoneyWindowsServiceError, + DummyKalshiRunnerConfig, + RunnerSecrets, + kalshi_account_hash, + load_runner_config, + load_secrets, +) + + +NOW = datetime(2026, 7, 26, 22, 0, 0, tzinfo=timezone.utc) + + +def _hash(label: str) -> str: + return hashlib.sha256(label.encode("utf-8")).hexdigest() + + +def _key(seed: int) -> tuple[str, bytes, str]: + private = Ed25519PrivateKey.from_private_bytes(bytes([seed]) * 32) + public = private.public_key().public_bytes_raw() + return ( + hashlib.sha256(public).hexdigest(), + public, + base64.urlsafe_b64encode(public).decode("ascii").rstrip("="), + ) + + +def _config( + tmp_path: Path, + *, + readiness_private: Ed25519PrivateKey, + key_id: str = "kalshi-fixture-key", +) -> DummyKalshiRunnerConfig: + core_id, core, _ = _key(1) + operator_id, operator, _ = _key(2) + promoter_id, promoter, _ = _key(3) + research_id, research, _ = _key(4) + evaluator_id, evaluator, _ = _key(6) + readiness_id = hashlib.sha256( + readiness_private.public_key().public_bytes_raw() + ).hexdigest() + return DummyKalshiRunnerConfig( + release_id="release-fixture-1", + core_readiness_path=tmp_path / "core-readiness.json", + data_root=tmp_path / "data", + readiness_path=tmp_path / "readiness.json", + core_public_keys={core_id: core}, + operator_public_keys={operator_id: operator}, + promoter_public_keys={promoter_id: promoter}, + research_public_keys={research_id: research}, + evaluator_public_keys={evaluator_id: evaluator}, + expected_account_hash=kalshi_account_hash(key_id, 0), + kalshi_subaccount_number=0, + fund_lock_sha256=_hash("fund-lock"), + service_manifest_sha256=_hash("manifest"), + role_public_keys_sha256=_hash("roles"), + core_runner_config_sha256=_hash("core-config"), + risk_policy_sha256=_hash("risk"), + readiness_signer_public_key_sha256=readiness_id, + poll_interval_seconds=5, + readiness_ttl_seconds=30, + broker_truth_max_age_seconds=30, + config_sha256=_hash("runner-config"), + ) + + +def _public_config_payload( + program_data: Path, + *, + readiness_private: Ed25519PrivateKey, + key_id: str, +) -> dict: + core_id, _, core = _key(1) + operator_id, _, operator = _key(2) + promoter_id, _, promoter = _key(3) + research_id, _, research = _key(4) + evaluator_id, _, evaluator = _key(6) + return { + "schema": CONFIG_SCHEMA, + "service_name": SERVICE_NAME, + "release_id": "release-fixture-1", + "core_endpoint_ref": CORE_ENDPOINT_REF, + "core_readiness_path": str( + (program_data / CORE_READINESS_RELATIVE_PATH).resolve() + ), + "core_cell_token_target": CELL_TOKEN_TARGET_REF, + "kalshi_key_id_target": KALSHI_KEY_ID_TARGET_REF, + "kalshi_private_key_target": KALSHI_PRIVATE_KEY_TARGET_REF, + "readiness_signing_key_target": READINESS_KEY_TARGET_REF, + "start_mode": START_MODE, + "data_root": str((program_data / DATA_ROOT_RELATIVE_PATH).resolve()), + "readiness_ref": READINESS_REF, + "readiness_path": str( + (program_data / READINESS_RELATIVE_PATH).resolve() + ), + "core_public_keys_base64url": {core_id: core}, + "operator_public_keys_base64url": {operator_id: operator}, + "promoter_public_keys_base64url": {promoter_id: promoter}, + "research_public_keys_base64url": {research_id: research}, + "evaluator_public_keys_base64url": {evaluator_id: evaluator}, + "expected_account_hash": kalshi_account_hash(key_id, 0), + "kalshi_subaccount_number": 0, + "fund_lock_sha256": _hash("fund-lock"), + "service_manifest_sha256": _hash("manifest"), + "role_public_keys_sha256": _hash("roles"), + "core_runner_config_sha256": _hash("core-config"), + "risk_policy_sha256": _hash("risk"), + "readiness_signer_public_key_sha256": hashlib.sha256( + readiness_private.public_key().public_bytes_raw() + ).hexdigest(), + "poll_interval_seconds": 5, + "readiness_ttl_seconds": 30, + "broker_truth_max_age_seconds": 30, + } + + +class _Credentials: + def __init__(self, values: dict[str, bytes]) -> None: + self.values = values + self.targets: list[str] = [] + + def read_bytes(self, target: str) -> bytes: + self.targets.append(target) + return self.values[target] + + +def test_config_is_raw_byte_pinned_and_secrets_use_only_four_targets( + tmp_path: Path, +) -> None: + program_data = tmp_path / "ProgramData" + readiness = Ed25519PrivateKey.from_private_bytes(bytes([9]) * 32) + key_id = "kalshi-fixture-key" + payload = _public_config_payload( + program_data, + readiness_private=readiness, + key_id=key_id, + ) + raw = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode() + config_path = program_data / CONFIG_RELATIVE_PATH + config_path.parent.mkdir(parents=True) + config_path.write_bytes(raw) + config = load_runner_config( + config_path, + hashlib.sha256(raw).hexdigest(), + program_data, + ) + + rsa_private = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + pem = rsa_private.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + provider = _Credentials( + { + "DumbMoney/DummyCellToken": b"t" * 64, + "DumbMoney/KalshiApiKeyId": key_id.encode(), + "DumbMoney/KalshiPrivateKeyPem": pem, + "DumbMoney/DummyReadinessEd25519": bytes([9]) * 32, + } + ) + secrets = load_secrets(provider, config) + + assert secrets.cell_token == "t" * 64 + assert secrets.kalshi_key_id == key_id + assert provider.targets == [ + "DumbMoney/DummyCellToken", + "DumbMoney/KalshiApiKeyId", + "DumbMoney/KalshiPrivateKeyPem", + "DumbMoney/DummyReadinessEd25519", + ] + + +class _NoNetworkTransport: + def __init__(self) -> None: + self.calls = 0 + self.endpoint: CoreEndpoint | None = None + + def set_endpoint(self, endpoint: CoreEndpoint) -> None: + self.endpoint = endpoint + + def liveness(self) -> None: + self.calls += 1 + + def contract_get(self, *_args, **_kwargs): + raise AssertionError("contract transport must not run during setup") + + def command_get(self, *_args, **_kwargs): + raise AssertionError("command transport must not run during setup") + + def journal_anchor_post(self, *_args, **_kwargs): + raise AssertionError("anchor transport must not run during setup") + + def close(self) -> None: + return + + +class _BrokerTruth: + def __init__(self, snapshot: dict) -> None: + self.value = snapshot + self.calls = 0 + + def snapshot(self) -> dict: + self.calls += 1 + return dict(self.value) + + +def _secrets(readiness: Ed25519PrivateKey) -> RunnerSecrets: + return RunnerSecrets( + cell_token="t" * 64, + kalshi_key_id="kalshi-fixture-key", + kalshi_private_key_pem=b"unused-in-unit-service", + readiness_private_key=readiness, + ) + + +def test_startup_is_network_silent_and_readiness_is_signed_blocked( + tmp_path: Path, +) -> None: + readiness = Ed25519PrivateKey.from_private_bytes(bytes([9]) * 32) + config = _config(tmp_path, readiness_private=readiness) + transport = _NoNetworkTransport() + broker = _BrokerTruth({}) + service = DumbMoneyDummyKalshiService( + config, + _secrets(readiness), + core_transport=transport, # type: ignore[arg-type] + broker_truth=broker, + clock=lambda: NOW, + ) + try: + service.start() + envelope = json.loads(config.readiness_path.read_text()) + verified = verify_signed_envelope( + envelope, + trusted_public_keys={ + config.readiness_signer_public_key_sha256: ( + readiness.public_key().public_bytes_raw() + ) + }, + now=NOW, + expected_body_schema=READINESS_SCHEMA, + ) + + assert transport.calls == 0 + assert broker.calls == 0 + assert verified.body["authority"] == { + "broker": "KALSHI", + "mode": START_MODE, + "execution_enabled": False, + } + assert verified.body["health"]["status"] == "BLOCKED" + finally: + service.close() + + +def test_service_components_use_sqlite_wal_journals_and_close_them( + tmp_path: Path, +) -> None: + readiness = Ed25519PrivateKey.from_private_bytes(bytes([9]) * 32) + config = _config(tmp_path, readiness_private=readiness) + transport = _NoNetworkTransport() + service = DumbMoneyDummyKalshiService( + config, + _secrets(readiness), + core_transport=transport, # type: ignore[arg-type] + broker_truth=_BrokerTruth({}), + clock=lambda: NOW, + ) + service.start() + try: + service._initialize_components() + capital_path = config.data_root / "capital-operational.db" + command_path = config.data_root / "command-feed.db" + assert capital_path.is_file() + assert command_path.is_file() + assert not (config.data_root / "capital-operational.jsonl").exists() + assert not (config.data_root / "command-feed.jsonl").exists() + + for path in (capital_path, command_path): + with sqlite3.connect(path) as connection: + assert connection.execute( + "PRAGMA journal_mode" + ).fetchone()[0] == "wal" + assert connection.execute( + "SELECT event_count FROM journal_metadata " + "WHERE singleton = 1" + ).fetchone() == (0,) + finally: + service.close() + + assert service._capital_journal is None + assert service._command_journal is None + + +def test_legacy_live_executor_cannot_construct_firewall_without_dumbmoney( + tmp_path: Path, +) -> None: + executor = Executor( + SessionMode.LIVE, + session_path=tmp_path / "session.json", + kill_path=tmp_path / "KILL", + capital_envelope_adapter=None, + ) + + with pytest.raises(RuntimeError, match="mandatory for LIVE"): + executor._make_firewall() + + +class _CommandFeed: + def poll_once(self) -> CommandFeedPollResult: + return CommandFeedPollResult( + page_accepted=True, + cursor_sequence=1, + cursor_digest="1" * 64, + commands_applied=1, + required_action="APPLY_SIGNED_CONTROLS", + submission_allowed=True, + reason="fixture current", + ) + + def submission_allowed(self) -> bool: + return True + + +class _CapitalAdapter: + def __init__(self) -> None: + self.receipts: list[dict] = [] + + def record_broker_bootstrap(self, receipt: dict) -> None: + self.receipts.append(receipt) + + +class _JournalHead: + def head(self) -> tuple[int, str]: + return 0, "0" * 64 + + def close(self) -> None: + return + + +class _ExposureHead: + def anchor_head(self) -> tuple[int, str]: + return 0, "0" * 64 + + +class _JournalAnchorClient: + def __init__(self) -> None: + self.calls: list[dict] = [] + + def anchor(self, **kwargs) -> None: + self.calls.append(kwargs) + + +class _RejectingJournalAnchorClient: + def anchor(self, **_kwargs) -> None: + raise RuntimeError("Core rejected rolled-back journal") + + +def _install_anchor_fixtures( + service: DumbMoneyDummyKalshiService, +) -> _JournalAnchorClient: + client = _JournalAnchorClient() + service._capital_journal = _JournalHead() # type: ignore[assignment] + service._command_journal = _JournalHead() # type: ignore[assignment] + service._exposure_tracker = _ExposureHead() # type: ignore[assignment] + service._journal_anchor_client = client # type: ignore[assignment] + return client + + +class _ReconciliationSweeper: + def __init__( + self, + *, + unresolved_reservations: int, + unresolved_positions: int, + ) -> None: + self.unresolved_reservations = unresolved_reservations + self.unresolved_positions = unresolved_positions + self.calls = 0 + + def run_once(self) -> dict: + self.calls += 1 + return { + "unresolved_reservations": self.unresolved_reservations, + "unresolved_positions": self.unresolved_positions, + } + + +def test_anchor_rejection_blocks_before_broker_contact( + tmp_path: Path, + monkeypatch, +) -> None: + readiness = Ed25519PrivateKey.from_private_bytes(bytes([9]) * 32) + config = _config(tmp_path, readiness_private=readiness) + transport = _NoNetworkTransport() + broker = _BrokerTruth({}) + service = DumbMoneyDummyKalshiService( + config, + _secrets(readiness), + core_transport=transport, # type: ignore[arg-type] + broker_truth=broker, + journal_anchor_client=( # type: ignore[arg-type] + _RejectingJournalAnchorClient() + ), + clock=lambda: NOW, + ) + service._components_ready = True + service._capital_adapter = _CapitalAdapter() # type: ignore[assignment] + service._command_feed = _CommandFeed() # type: ignore[assignment] + service._capital_journal = _JournalHead() # type: ignore[assignment] + service._command_journal = _JournalHead() # type: ignore[assignment] + service._exposure_tracker = _ExposureHead() # type: ignore[assignment] + monkeypatch.setattr( + "live_firewall.dumbmoney_windows_service._core_endpoint_from_readiness", + lambda *_args, **_kwargs: CoreEndpoint( + base_url="http://127.0.0.1:8765", + instance_id="00000000-0000-0000-0000-000000000001", + observed_at="2026-07-26T22:00:00Z", + valid_until="2026-07-26T22:01:00Z", + ), + ) + try: + service.start() + with pytest.raises( + RuntimeError, + match="rolled-back journal", + ): + service.run_once() + state = service.state() + assert broker.calls == 0 + assert state.health_status == "BLOCKED" + assert state.rollback_anchor_current is False + assert state.execution_enabled is False + finally: + service.close() + + +def test_pending_restart_reconciliation_blocks_otherwise_flat_book( + tmp_path: Path, + monkeypatch, +) -> None: + readiness = Ed25519PrivateKey.from_private_bytes(bytes([9]) * 32) + config = _config(tmp_path, readiness_private=readiness) + transport = _NoNetworkTransport() + broker = _BrokerTruth({ + "schema": "dummy.kalshi-broker-truth.v1", + "venue": "dummy_kalshi", + "account_hash": config.expected_account_hash, + "subaccount_number": 0, + "observed_at": "2026-07-26T22:00:00Z", + "broker_snapshot_sha256": _hash("flat-with-pending"), + "flat_book_observed": True, + "total_exposure_cents": 0, + "open_order_count": 0, + "market_exposure_cents": {}, + "correlated_exposure_cents": {}, + "unresolved_open_orders": 0, + "unresolved_positions": 0, + }) + sweeper = _ReconciliationSweeper( + unresolved_reservations=1, + unresolved_positions=0, + ) + service = DumbMoneyDummyKalshiService( + config, + _secrets(readiness), + core_transport=transport, # type: ignore[arg-type] + broker_truth=broker, + reconciliation_sweeper=sweeper, # type: ignore[arg-type] + clock=lambda: NOW, + ) + service._components_ready = True + service._capital_adapter = _CapitalAdapter() # type: ignore[assignment] + service._command_feed = _CommandFeed() # type: ignore[assignment] + anchors = _install_anchor_fixtures(service) + monkeypatch.setattr( + "live_firewall.dumbmoney_windows_service._core_endpoint_from_readiness", + lambda *_args, **_kwargs: CoreEndpoint( + base_url="http://127.0.0.1:8765", + instance_id="00000000-0000-0000-0000-000000000001", + observed_at="2026-07-26T22:00:00Z", + valid_until="2026-07-26T22:01:00Z", + ), + ) + try: + service.start() + result = service.run_once() + state = service.state() + + assert result["status"] == "RECONCILIATION_BLOCKED" + assert state.reason == "UNRESOLVED_BROKER_ORDERS_OR_POSITIONS" + assert state.unresolved_open_orders == 1 + assert state.execution_enabled is False + assert sweeper.calls == 1 + assert len(anchors.calls) == 6 + finally: + service.close() + + +def test_unresolved_broker_book_stays_reconciliation_only( + tmp_path: Path, + monkeypatch, +) -> None: + readiness = Ed25519PrivateKey.from_private_bytes(bytes([9]) * 32) + config = _config(tmp_path, readiness_private=readiness) + transport = _NoNetworkTransport() + snapshot_body = { + "schema": "dummy.kalshi-broker-truth.v1", + "venue": "dummy_kalshi", + "account_hash": config.expected_account_hash, + "subaccount_number": 0, + "observed_at": "2026-07-26T22:00:00Z", + "broker_snapshot_sha256": _hash("broker-snapshot"), + "flat_book_observed": False, + "total_exposure_cents": 100, + "open_order_count": 1, + "market_exposure_cents": {"KXTEST": 100}, + "correlated_exposure_cents": {"KXTEST": 100}, + "unresolved_open_orders": 1, + "unresolved_positions": 1, + } + broker = _BrokerTruth(snapshot_body) + service = DumbMoneyDummyKalshiService( + config, + _secrets(readiness), + core_transport=transport, # type: ignore[arg-type] + broker_truth=broker, + execution_cycle=lambda **_kwargs: {"orders_submitted": 1}, + clock=lambda: NOW, + ) + adapter = _CapitalAdapter() + service._components_ready = True + service._capital_adapter = adapter # type: ignore[assignment] + service._command_feed = _CommandFeed() # type: ignore[assignment] + anchors = _install_anchor_fixtures(service) + monkeypatch.setattr( + "live_firewall.dumbmoney_windows_service._core_endpoint_from_readiness", + lambda *_args, **_kwargs: CoreEndpoint( + base_url="http://127.0.0.1:8765", + instance_id="00000000-0000-0000-0000-000000000001", + observed_at="2026-07-26T22:00:00Z", + valid_until="2026-07-26T22:01:00Z", + ), + ) + try: + service.start() + result = service.run_once() + state = service.state() + + assert result["status"] == "RECONCILIATION_BLOCKED" + assert state.mode == START_MODE + assert state.execution_enabled is False + assert state.reconciled_once is False + assert state.unresolved_open_orders == 1 + assert state.unresolved_positions == 1 + assert len(adapter.receipts) == 1 + assert len(anchors.calls) == 6 + finally: + service.close() + + +def test_second_pass_blocked_cycle_cannot_publish_ready( + tmp_path: Path, + monkeypatch, +) -> None: + readiness = Ed25519PrivateKey.from_private_bytes(bytes([9]) * 32) + config = _config(tmp_path, readiness_private=readiness) + transport = _NoNetworkTransport() + broker = _BrokerTruth({ + "schema": "dummy.kalshi-broker-truth.v1", + "venue": "dummy_kalshi", + "account_hash": config.expected_account_hash, + "subaccount_number": 0, + "observed_at": "2026-07-26T22:00:00Z", + "broker_snapshot_sha256": _hash("flat-broker-snapshot"), + "flat_book_observed": True, + "total_exposure_cents": 0, + "open_order_count": 0, + "market_exposure_cents": {}, + "correlated_exposure_cents": {}, + "unresolved_open_orders": 0, + "unresolved_positions": 0, + }) + cycle_calls: list[dict] = [] + + def blocked_cycle(**kwargs): + cycle_calls.append(kwargs) + return { + "schema": "dummy.dumbmoney-execution-cycle.v1", + "status": "BLOCKED", + "broker_contacted": False, + "orders_submitted": 0, + "broker_snapshot_sha256": _hash("flat-broker-snapshot"), + } + + service = DumbMoneyDummyKalshiService( + config, + _secrets(readiness), + core_transport=transport, # type: ignore[arg-type] + broker_truth=broker, + execution_cycle=blocked_cycle, + clock=lambda: NOW, + ) + adapter = _CapitalAdapter() + service._components_ready = True + service._capital_adapter = adapter # type: ignore[assignment] + service._command_feed = _CommandFeed() # type: ignore[assignment] + anchors = _install_anchor_fixtures(service) + monkeypatch.setattr( + "live_firewall.dumbmoney_windows_service._core_endpoint_from_readiness", + lambda *_args, **_kwargs: CoreEndpoint( + base_url="http://127.0.0.1:8765", + instance_id="00000000-0000-0000-0000-000000000001", + observed_at="2026-07-26T22:00:00Z", + valid_until="2026-07-26T22:01:00Z", + ), + ) + try: + service.start() + first = service.run_once() + first_state = service.state() + assert first["status"] == "RECONCILIATION_COMPLETE" + assert first_state.health_status == "BLOCKED" + assert first_state.reason == "INITIAL_RECONCILIATION_COMPLETE" + assert first_state.execution_enabled is False + assert first_state.rollback_anchor_current is True + result = service.run_once() + state = service.state() + + assert result["status"] == "BLOCKED" + assert state.health_status == "BLOCKED" + assert state.reason == "EXECUTION_CYCLE_BLOCKED" + assert state.execution_enabled is False + assert state.rollback_anchor_current is True + assert len(cycle_calls) == 1 + assert cycle_calls[0]["broker_snapshot"] == broker.value + assert len(anchors.calls) == 12 + + service.execution_cycle = lambda **_kwargs: { + "schema": "dummy.dumbmoney-execution-cycle.v1", + "status": "BLOCKED", + "broker_contacted": False, + "orders_submitted": 0, + "broker_snapshot_sha256": _hash("different-snapshot"), + } + with pytest.raises( + DumbMoneyWindowsServiceError, + match="execution cycle result is invalid", + ): + service.run_once() + assert service.state().execution_enabled is False + assert service.state().broker_truth_fresh is False + finally: + service.close() diff --git a/tests/test_kalshi_client.py b/tests/test_kalshi_client.py index 01699898..20420b68 100644 --- a/tests/test_kalshi_client.py +++ b/tests/test_kalshi_client.py @@ -7,10 +7,65 @@ from kalshi.signer import sign_request from kalshi.error_classifier import classify, KalshiErrorCategory + +def _json_response(payload): + return httpx.Response( + 200, + json=payload, + request=httpx.Request("GET", "https://kalshi.invalid/test"), + ) + + @pytest.fixture def client(): return KalshiClient() + +def test_production_client_rejects_ambient_endpoint_redirect(monkeypatch): + monkeypatch.setenv("KALSHI_API_BASE", "https://attacker.invalid") + monkeypatch.setenv("KALSHI_API_VERSION", "trade-api/v999") + + with pytest.raises(RuntimeError, match="KALSHI_ENDPOINT_OVERRIDE_REJECTED"): + KalshiClient() + + +def test_production_client_allows_only_exact_endpoint_pin(monkeypatch): + monkeypatch.setenv( + "KALSHI_API_BASE", + "https://external-api.kalshi.com/", + ) + monkeypatch.setenv("KALSHI_API_VERSION", "/trade-api/v2/") + + pinned = KalshiClient() + assert str(pinned.client.base_url) == ( + "https://external-api.kalshi.com/trade-api/v2/" + ) + + +def test_production_client_disables_environment_proxy_trust(monkeypatch): + monkeypatch.delenv("KALSHI_API_BASE", raising=False) + monkeypatch.delenv("KALSHI_API_VERSION", raising=False) + + with patch("kalshi.client.httpx.AsyncClient") as async_client: + KalshiClient() + + assert async_client.call_args.kwargs["trust_env"] is False + + +def test_signer_path_prefix_ignores_ambient_version_redirect(monkeypatch): + monkeypatch.setenv("KALSHI_API_KEY_ID", "test-key") + monkeypatch.setenv("KALSHI_API_VERSION", "trade-api/v999") + private_key = MagicMock() + private_key.sign.return_value = b"signature" + + with patch("kalshi.signer.load_private_key", return_value=private_key): + sign_request("GET", "/markets") + + signed_message = private_key.sign.call_args.args[0].decode("utf-8") + assert signed_message.endswith("GET/trade-api/v2/markets") + assert "v999" not in signed_message + + @pytest.mark.asyncio async def test_sign_request_requires_key_id(): os.environ.pop("KALSHI_API_KEY_ID", None) @@ -22,9 +77,7 @@ async def test_get_orderbook_parsing(client): mock_resp = {"orderbook": {"bids": [{"price": 50, "count": 10}], "asks": [{"price": 55, "count": 5}]}} with patch("kalshi.client.sign_request", return_value={"KALSHI-ACCESS-KEY": "test", "KALSHI-ACCESS-SIGNATURE": "sig", "KALSHI-ACCESS-TIMESTAMP": "ts"}) as _: with patch.object(client.client, "request", new_callable=AsyncMock) as m: - m.return_value.status_code = 200 - m.return_value.json = MagicMock(return_value=mock_resp) - m.return_value.raise_for_status = MagicMock() + m.return_value = _json_response(mock_resp) ob = await client.get_orderbook("MARKET") assert ob.bids[0].price == 50 assert ob.asks[0].size == 5 @@ -38,14 +91,12 @@ async def test_get_orderbook_parsing(client): async def test_get_orderbook_sorts_and_rejects_missing_book(client): with patch("kalshi.client.sign_request", return_value={"KALSHI-ACCESS-KEY": "test", "KALSHI-ACCESS-SIGNATURE": "sig", "KALSHI-ACCESS-TIMESTAMP": "ts"}): with patch.object(client.client, "request", new_callable=AsyncMock) as request: - request.return_value.status_code = 200 - request.return_value.raise_for_status = MagicMock() - request.return_value.json = MagicMock(return_value={"orderbook": {"bids": [{"price": 48, "count": 1}, {"price": 40, "count": 1}], "asks": [{"price": 60, "count": 1}, {"price": 52, "count": 1}]}}) + request.return_value = _json_response({"orderbook": {"bids": [{"price": 48, "count": 1}, {"price": 40, "count": 1}], "asks": [{"price": 60, "count": 1}, {"price": 52, "count": 1}]}}) book = await client.get_orderbook("MARKET") assert [level.price for level in book.bids] == [40, 48] assert [level.price for level in book.asks] == [52, 60] - request.return_value.json = MagicMock(return_value={"orderbook": {}}) + request.return_value = _json_response({"orderbook": {}}) with pytest.raises(ValueError, match="missing"): await client.get_orderbook("MARKET") @@ -88,12 +139,16 @@ async def test_create_order_signs_json_body(client): expected_body = '{"count":10,"price":50,"side":"yes","ticker":"MARKET"}' with patch("kalshi.client.sign_request") as mock_sign: with patch.object(client.client, "request", new_callable=AsyncMock) as m: - m.return_value.status_code = 200 - m.return_value.json = MagicMock(return_value={"status": "success"}) - m.return_value.raise_for_status = MagicMock() + m.return_value = _json_response({"status": "success"}) + permit = await client.prepare_order_mutation( + "POST", + "/portfolio/events/orders", + _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + ) await client.create_order( order, _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + _mutation_permit=permit, ) mock_sign.assert_called_once() signed_body = mock_sign.call_args[0][2] @@ -118,6 +173,9 @@ async def test_direct_create_order_without_central_capability_fails_before_trans ("method", "path"), [ ("POST", "/portfolio/orders"), + ("POST", "/portfolio/events/orders"), + ("DELETE", "/portfolio/events/orders/order-123"), + ("POST", "/portfolio/events/orders/order-123/amend"), ("post", "/portfolio/orders?client_order_id=ignored"), ("PATCH", "/portfolio/orders/order-123"), ( @@ -157,7 +215,7 @@ async def test_raw_order_mutation_rejects_forged_capability_before_transport(cli ): await client._request( "POST", - "/portfolio/orders", + "/portfolio/events/orders", json={"ticker": "MARKET"}, _capability=object(), ) @@ -176,14 +234,18 @@ async def test_authorized_raw_order_mutation_consumes_capability_locally(client) }, ): with patch.object(client.client, "request", new_callable=AsyncMock) as request: - request.return_value.status_code = 200 - request.return_value.json = MagicMock(return_value={"order": {"order_id": "1"}}) - request.return_value.raise_for_status = MagicMock() + request.return_value = _json_response({"order": {"order_id": "1"}}) + permit = await client.prepare_order_mutation( + "POST", + "/portfolio/events/orders", + _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + ) await client._request( "POST", - "/portfolio/orders", + "/portfolio/events/orders", json={"ticker": "MARKET"}, _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + _mutation_permit=permit, ) request.assert_awaited_once() assert "_capability" not in request.await_args.kwargs @@ -200,9 +262,7 @@ async def test_read_only_order_requests_remain_available_without_capability(clie }, ): with patch.object(client.client, "request", new_callable=AsyncMock) as request: - request.return_value.status_code = 200 - request.return_value.json = MagicMock(return_value={"orders": []}) - request.return_value.raise_for_status = MagicMock() + request.return_value = _json_response({"orders": []}) result = await client.get_orders() assert result == {"orders": []} request.assert_awaited_once() @@ -229,3 +289,85 @@ async def test_rate_limiter_retries_connect_error(client): with pytest.raises(httpx.ConnectError): await client.get_markets() assert m.call_count == client.limiter.max_retries + 1 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "failure", + [ + httpx.ConnectError("ambiguous connection failure"), + httpx.ReadTimeout("ambiguous response timeout"), + ], +) +async def test_order_mutation_transport_is_exactly_one_attempt(client, failure): + client.limiter.base_delay = 0 + with patch( + "kalshi.client.sign_request", + return_value={ + "KALSHI-ACCESS-KEY": "test", + "KALSHI-ACCESS-SIGNATURE": "sig", + "KALSHI-ACCESS-TIMESTAMP": "ts", + }, + ): + with patch.object( + client.client, + "request", + new_callable=AsyncMock, + ) as request: + request.side_effect = failure + permit = await client.prepare_order_mutation( + "POST", + "/portfolio/events/orders", + _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + ) + with pytest.raises(type(failure)): + await client.create_order( + { + "ticker": "MARKET", + "client_order_id": "proposal-1", + }, + _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + _mutation_permit=permit, + ) + assert request.await_count == 1 + + +@pytest.mark.asyncio +async def test_order_mutation_http_429_is_not_retried(client): + response = httpx.Response( + 429, + request=httpx.Request( + "POST", + "https://kalshi.invalid/portfolio/events/orders", + ), + json={"error": "rate limited"}, + ) + with patch( + "kalshi.client.sign_request", + return_value={ + "KALSHI-ACCESS-KEY": "test", + "KALSHI-ACCESS-SIGNATURE": "sig", + "KALSHI-ACCESS-TIMESTAMP": "ts", + }, + ): + with patch.object( + client.client, + "request", + new_callable=AsyncMock, + return_value=response, + ) as request: + permit = await client.prepare_order_mutation( + "POST", + "/portfolio/events/orders", + _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + ) + with pytest.raises(httpx.HTTPStatusError): + await client.create_order( + { + "ticker": "MARKET", + "client_order_id": "proposal-1", + }, + _capability=_CENTRAL_FIREWALL_SUBMIT_CAPABILITY, + _mutation_permit=permit, + ) + assert request.await_count == 1 diff --git a/tests/test_kalshi_livebrokerfirewall_adapter.py b/tests/test_kalshi_livebrokerfirewall_adapter.py index 44402ff9..15c2d11e 100644 --- a/tests/test_kalshi_livebrokerfirewall_adapter.py +++ b/tests/test_kalshi_livebrokerfirewall_adapter.py @@ -45,8 +45,6 @@ def kalshi_env(monkeypatch, rsa_private_key_pem: str) -> dict[str, str]: env: dict[str, str] = { "KALSHI_API_KEY_ID": "test-key-id", "KALSHI_API_PRIVATE_KEY_PEM": rsa_private_key_pem, - "KALSHI_API_BASE": "http://kalshi.test", - "KALSHI_API_VERSION": "trade-api/v2", } for name, value in env.items(): monkeypatch.setenv(name, value) diff --git a/tests/test_kalshi_order_payload_schema.py b/tests/test_kalshi_order_payload_schema.py index 06fb82be..1919f0c7 100644 --- a/tests/test_kalshi_order_payload_schema.py +++ b/tests/test_kalshi_order_payload_schema.py @@ -50,7 +50,7 @@ def test_required_fields_present(): assert payload["count"] == 1 -def test_firewall_build_order_uses_side_specific_price(): +def test_firewall_build_order_uses_event_order_v2_fixed_point_wire(): from core.ontology import LiveOrderRequest from live_firewall.firewall import LiveBrokerFirewall from live_firewall.exposure_tracker import ExposureTracker @@ -66,9 +66,14 @@ def test_firewall_build_order_uses_side_specific_price(): strategy_proof_reference="s", forecast_proof_reference="f", adapter_name="a", + expiration_ts=1_900_000_000, ) order = firewall._build_order(req) - assert order["yes_price"] == 12 - assert "price" not in order + assert order["side"] == "bid" + assert order["price"] == "0.1200" + assert order["count"] == "1.00" assert order["client_order_id"] == "prop-1" - assert order["type"] == "limit" + assert order["time_in_force"] == "good_till_canceled" + assert order["post_only"] is True + assert order["expiration_time"] == 1_900_000_000 + assert order["subaccount"] == 0 diff --git a/tests/test_kalshi_strict_json.py b/tests/test_kalshi_strict_json.py new file mode 100644 index 00000000..042e9c8d --- /dev/null +++ b/tests/test_kalshi_strict_json.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import httpx +import pytest + +from kalshi.strict_json import ( + StrictJSONResponseError, + load_strict_json_response, +) + + +def _response(body: bytes) -> httpx.Response: + return httpx.Response( + 200, + content=body, + request=httpx.Request("GET", "https://kalshi.invalid/test"), + ) + + +def test_accepts_nested_strict_utf8_json() -> None: + payload = load_strict_json_response( + _response('{"market":{"ticker":"KX-✓"},"prices":[1,2]}'.encode()) + ) + + assert payload == { + "market": {"ticker": "KX-✓"}, + "prices": [1, 2], + } + + +@pytest.mark.parametrize( + "body", + [ + b'{"ticker":"A","ticker":"B"}', + b'{"market":{"ticker":"A","ticker":"B"}}', + b'[{"ticker":"A","ticker":"B"}]', + ], +) +def test_rejects_duplicate_keys_at_every_object_depth(body: bytes) -> None: + with pytest.raises(StrictJSONResponseError, match="duplicate JSON key"): + load_strict_json_response(_response(body)) + + +@pytest.mark.parametrize( + "token", + ["NaN", "Infinity", "-Infinity", "1e999999"], +) +def test_rejects_non_finite_numeric_extensions(token: str) -> None: + with pytest.raises(StrictJSONResponseError, match="non-finite"): + load_strict_json_response(_response(f'{{"price":{token}}}'.encode())) + + +def test_rejects_invalid_utf8() -> None: + with pytest.raises(StrictJSONResponseError, match="strict UTF-8 JSON"): + load_strict_json_response(_response(b'{"ticker":"\xff"}')) + + +def test_rejects_response_over_explicit_byte_limit_before_decoding() -> None: + with pytest.raises(StrictJSONResponseError, match="exceeds 7 byte limit"): + load_strict_json_response(_response(b'{"a":1} '), maximum_bytes=7) + + +@pytest.mark.asyncio +async def test_kalshi_client_rejects_duplicate_response_keys(monkeypatch) -> None: + from kalshi.client import KalshiClient + + client = KalshiClient() + + async def fake_request(*_args, **_kwargs): + return _response(b'{"markets":[],"markets":[{"ticker":"forged"}]}') + + monkeypatch.setattr(client.client, "request", fake_request) + monkeypatch.setattr( + "kalshi.client.sign_request", + lambda *_args, **_kwargs: {}, + ) + + with pytest.raises(StrictJSONResponseError, match="duplicate JSON key"): + await client.get_markets() + + +def test_public_reconciler_rejects_non_finite_response(monkeypatch) -> None: + from autonomy import reconciler + + monkeypatch.setattr( + reconciler, + "_public_get", + lambda *_args, **_kwargs: _response( + b'{"market":{"ticker":"KX","last_price":NaN}}' + ), + ) + + with pytest.raises(StrictJSONResponseError, match="non-finite"): + reconciler.default_fetch_market_result("KX") diff --git a/tests/test_live_cap_firewall_rehearsal.py b/tests/test_live_cap_firewall_rehearsal.py index 527f77d2..91536e90 100644 --- a/tests/test_live_cap_firewall_rehearsal.py +++ b/tests/test_live_cap_firewall_rehearsal.py @@ -131,7 +131,10 @@ async def test_rehearsal_blocks_by_default(): assert verdict.would_submit is False assert verdict.blocked_reason == "live_submit_disabled" assert verdict.order is not None - assert verdict.order["type"] == "limit" + assert verdict.order["time_in_force"] == "good_till_canceled" + assert verdict.order["post_only"] is True + assert verdict.order["price"] == "0.5000" + assert verdict.order["count"] == "1.00" @pytest.mark.asyncio diff --git a/tests/test_live_firewall.py b/tests/test_live_firewall.py index f53ea93c..1657117b 100644 --- a/tests/test_live_firewall.py +++ b/tests/test_live_firewall.py @@ -463,7 +463,7 @@ async def test_submit_creates_order_and_tracks_exposure(): assert len(exposure.order_history) == 1 assert exposure.open_order_count() == 1 assert exposure.positions == {} - assert exposure.total_exposure_cents() == 50 + assert exposure.total_exposure_cents() == 52 client.create_order.assert_awaited_once() diff --git a/tests/test_live_firewall_exposure_truth.py b/tests/test_live_firewall_exposure_truth.py index 3e335f1e..e74e52c9 100644 --- a/tests/test_live_firewall_exposure_truth.py +++ b/tests/test_live_firewall_exposure_truth.py @@ -18,6 +18,10 @@ def _pass_non_metadata_gates(monkeypatch, firewall: LiveBrokerFirewall) -> None: + monkeypatch.setattr( + "live_firewall.firewall.load_caps", + lambda: CapConfig(allowed_markets=["MARKET"]), + ) monkeypatch.setattr( firewall, "evaluate", @@ -28,6 +32,16 @@ def _pass_non_metadata_gates(monkeypatch, firewall: LiveBrokerFirewall) -> None: "_autonomy_risk_verdict", lambda request, required=False: ALLOW, ) + monkeypatch.setattr( + firewall, + "_net_ev_verdict", + lambda request, forecast, caps: ALLOW, + ) + monkeypatch.setattr( + firewall, + "_model_influence_verdict", + lambda request, forecast: ALLOW, + ) monkeypatch.setattr( firewall, "_canary_readiness_verdict", @@ -69,7 +83,7 @@ async def test_accepted_order_is_reserved_but_not_a_position(monkeypatch) -> Non assert exposure.open_order_count() == 1 assert exposure.open_orders[0]["state"] == "open" assert exposure.open_orders[0]["remaining_size"] == 1 - assert exposure.total_exposure_cents() == 50 + assert exposure.total_exposure_cents() == 52 assert len(exposure.order_history) == 1 client.create_order.assert_awaited_once() @@ -91,11 +105,11 @@ async def test_unknown_transport_keeps_full_conservative_reservation(monkeypatch assert result.success is False assert result.broker_contacted is True - assert result.error == "broker_submit_failed:TimeoutError" + assert result.error == "AMBIGUOUS_BROKER_OUTCOME:TimeoutError" assert exposure.positions == {} assert exposure.open_order_count() == 1 assert exposure.open_orders[0]["state"] == "submit_outcome_unknown" - assert exposure.total_exposure_cents() == 50 + assert exposure.total_exposure_cents() == 52 def test_partial_fills_are_cumulative_and_reserve_the_remainder() -> None: @@ -109,26 +123,26 @@ def test_partial_fills_are_cumulative_and_reserve_the_remainder() -> None: side="yes", ) assert exposure.positions == {} - assert exposure.total_exposure_cents() == 240 + assert exposure.total_exposure_cents() == 247 assert exposure.record_cumulative_fill("broker-1", 1, 50) is True position = exposure.positions[("MARKET", "yes")] assert position.quantity == 1 assert position.avg_price_cents == 50 assert exposure.open_orders[0]["remaining_size"] == 3 - assert exposure.total_exposure_cents() == 50 + 3 * 60 + assert exposure.total_exposure_cents() == 50 + 3 * 60 + 7 assert exposure.record_cumulative_fill("broker-1", 3, 54) is True position = exposure.positions[("MARKET", "yes")] assert position.quantity == 3 assert position.avg_price_cents == 54 assert exposure.open_orders[0]["remaining_size"] == 1 - assert exposure.total_exposure_cents() == 3 * 54 + 60 + assert exposure.total_exposure_cents() == 3 * 54 + 60 + 7 # Replaying the same cumulative witness cannot double count the fill. assert exposure.record_cumulative_fill("broker-1", 3, 54) is True assert exposure.positions[("MARKET", "yes")].quantity == 3 - assert exposure.total_exposure_cents() == 3 * 54 + 60 + assert exposure.total_exposure_cents() == 3 * 54 + 60 + 7 # A witnessed cancel releases only the unfilled remainder. assert exposure.record_cumulative_fill( @@ -141,14 +155,102 @@ def test_partial_fills_are_cumulative_and_reserve_the_remainder() -> None: def test_stale_reservation_survives_persistence_reload(tmp_path) -> None: state_path = tmp_path / "exposure.json" first = ExposureTracker(persist=True, state_path=state_path) - assert first.reserve_order_submission("client-1", "MARKET", 2, 49) is True + empty_head = first.anchor_head() + assert empty_head[0] == 0 + assert first.reserve_order_submission( + "client-1", + "MARKET", + 2, + 49, + contract_ticker="MARKET", + side="yes", + ) is True + reserved_head = first.anchor_head() + assert reserved_head[0] == 1 + assert reserved_head[1] != empty_head[1] reloaded = ExposureTracker(persist=True, state_path=state_path) assert reloaded.state_healthy is True + assert reloaded.anchor_head() == reserved_head assert reloaded.open_order_count() == 1 assert reloaded.open_orders[0]["state"] == "submitting" - assert reloaded.total_exposure_cents() == 98 + assert reloaded.total_exposure_cents() == 102 + + +def test_exposure_anchor_exposes_byte_rollback_as_revision_regression( + tmp_path, +) -> None: + state_path = tmp_path / "exposure.json" + tracker = ExposureTracker(persist=True, state_path=state_path) + assert tracker.reserve_order_submission( + "client-1", + "MARKET", + 1, + 49, + contract_ticker="MARKET", + side="yes", + ) + earlier_bytes = state_path.read_bytes() + earlier_head = tracker.anchor_head() + assert tracker.confirm_open_order("client-1", "broker-1") + later_head = tracker.anchor_head() + assert later_head[0] > earlier_head[0] + assert later_head[1] != earlier_head[1] + + state_path.write_bytes(earlier_bytes) + rolled_back = ExposureTracker(persist=True, state_path=state_path) + assert rolled_back.state_healthy is True + assert rolled_back.anchor_head() == earlier_head + + +def test_terminal_reconciliation_is_idempotent_after_restart(tmp_path) -> None: + state_path = tmp_path / "exposure.json" + first = ExposureTracker(persist=True, state_path=state_path) + assert first.reserve_order_submission( + "proposal-1", + "MARKET", + 1, + 50, + contract_ticker="MARKET", + side="yes", + ) is True + assert first.confirm_open_order("proposal-1", "broker-1") is True + reconciliation_id = "a" * 64 + assert first.record_cumulative_fill( + "proposal-1", + 1, + 50, + terminal_state="filled", + reconciliation_id=reconciliation_id, + ) is True + + reloaded = ExposureTracker(persist=True, state_path=state_path) + assert reloaded.record_cumulative_fill( + "proposal-1", + 1, + 50, + terminal_state="filled", + reconciliation_id=reconciliation_id, + ) is True + assert reloaded.state_healthy is True + assert reloaded.open_order_count() == 0 + assert reloaded.positions[("MARKET", "yes")].quantity == 1 + + settlement_id = "b" * 64 + assert reloaded.record_position_close( + "MARKET", + "yes", + reconciliation_id=settlement_id, + ) is True + settled = ExposureTracker(persist=True, state_path=state_path) + assert settled.record_position_close( + "MARKET", + "yes", + reconciliation_id=settlement_id, + ) is True + assert settled.state_healthy is True + assert settled.positions == {} @pytest.mark.asyncio diff --git a/tests/test_live_kalshi.py b/tests/test_live_kalshi.py index f75b62de..2037cafb 100644 --- a/tests/test_live_kalshi.py +++ b/tests/test_live_kalshi.py @@ -2,8 +2,9 @@ import os from datetime import datetime, timezone from pathlib import Path -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch +import httpx import pytest from core import secret_guard @@ -28,9 +29,11 @@ def mock_sign(): def _mock_response(data): m = AsyncMock() - m.return_value.status_code = 200 - m.return_value.json = MagicMock(return_value=data) - m.return_value.raise_for_status = MagicMock() + m.return_value = httpx.Response( + 200, + json=data, + request=httpx.Request("GET", "https://kalshi.invalid/test"), + ) return m diff --git a/tests/test_reconciler_transport_safety.py b/tests/test_reconciler_transport_safety.py new file mode 100644 index 00000000..7f73e609 --- /dev/null +++ b/tests/test_reconciler_transport_safety.py @@ -0,0 +1,35 @@ +from unittest.mock import MagicMock, patch + +import autonomy.reconciler as reconciler +import httpx + + +def test_public_base_ignores_ambient_endpoint_redirect(monkeypatch): + monkeypatch.setenv("KALSHI_API_BASE", "https://attacker.invalid") + monkeypatch.setenv("KALSHI_API_VERSION", "trade-api/v999") + + assert ( + reconciler._public_base() + == "https://external-api.kalshi.com/trade-api/v2" + ) + + +def test_public_fetch_disables_environment_proxy_trust(): + response = httpx.Response( + 200, + json={"market": {"ticker": "TEST"}}, + request=httpx.Request("GET", "https://kalshi.invalid/markets/TEST"), + ) + client = MagicMock() + client.__enter__.return_value = client + client.get.return_value = response + + with patch("httpx.Client", return_value=client) as client_type: + result = reconciler.default_fetch_market_result("TEST") + + assert result == {"ticker": "TEST"} + assert client_type.call_args.kwargs["trust_env"] is False + assert client_type.call_args.kwargs["base_url"] == ( + "https://external-api.kalshi.com/trade-api/v2" + ) + client.get.assert_called_once_with("/markets/TEST", params=None) diff --git a/tests/test_risk_state_persistence.py b/tests/test_risk_state_persistence.py index a9071cb2..63630da4 100644 --- a/tests/test_risk_state_persistence.py +++ b/tests/test_risk_state_persistence.py @@ -2,6 +2,12 @@ from __future__ import annotations +import json +import threading +from concurrent.futures import ThreadPoolExecutor + +import pytest + from core.kalshi_market_validator import validate_ticker_shape from core.ontology import CapConfig, Position from core.state import DummyState @@ -34,6 +40,71 @@ def test_daily_realized_loss_persists_and_is_idempotent(tmp_path): assert DummyState(persist=True, state_path=path).daily_loss_cents == 150 +def test_concurrent_stale_states_merge_daily_losses(tmp_path): + path = tmp_path / "risk_state.json" + first = DummyState(persist=True, state_path=path) + second = DummyState(persist=True, state_path=path) + barrier = threading.Barrier(2) + + def record(state: DummyState, loss: int, settlement_id: str) -> bool: + barrier.wait(timeout=5) + return state.record_realized_pnl( + -loss, + settlement_id=settlement_id, + ) + + with ThreadPoolExecutor(max_workers=2) as executor: + results = [ + future.result() + for future in ( + executor.submit(record, first, 100, "settlement-a"), + executor.submit(record, second, 200, "settlement-b"), + ) + ] + + assert results == [True, True] + reloaded = DummyState(persist=True, state_path=path) + assert reloaded.daily_loss_cents == 300 + payload = json.loads(path.read_text(encoding="utf-8")) + assert payload["processed_settlement_ids"] == [ + "settlement-a", + "settlement-b", + ] + + +def test_stale_risk_verifier_cannot_erase_kill_or_newer_loss(tmp_path): + path = tmp_path / "risk_state.json" + stale = DummyState(persist=True, state_path=path) + writer = DummyState(persist=True, state_path=path) + + assert writer.record_realized_pnl( + -125, + settlement_id="settlement-a", + ) + assert writer.enable_kill_switch("operator stop") + assert stale.verify_persistence() is True + + reloaded = DummyState(persist=True, state_path=path) + assert reloaded.kill_switch.active is True + assert reloaded.kill_switch.reason == "operator stop" + assert reloaded.daily_loss_cents == 125 + + +def test_stale_kill_clear_cannot_clear_newer_reassertion(tmp_path): + path = tmp_path / "risk_state.json" + initial = DummyState(persist=True, state_path=path) + assert initial.enable_kill_switch("first stop") + stale = DummyState(persist=True, state_path=path) + writer = DummyState(persist=True, state_path=path) + assert writer.enable_kill_switch("newer stop") + + assert stale.disable_kill_switch() is False + + reloaded = DummyState(persist=True, state_path=path) + assert reloaded.kill_switch.active is True + assert reloaded.kill_switch.reason == "newer stop" + + def test_corrupt_daily_loss_state_is_not_treated_as_zero(tmp_path): path = tmp_path / "risk_state.json" path.write_text("{", encoding="utf-8") @@ -60,8 +131,8 @@ def test_exposure_persists_both_sides_without_overwrite(tmp_path): ("KXBTC-EVENT", "no"), } # Filled positions reserve 40c + 60c and the still-open YES order reserves - # another 40c. Counting both prevents capital from being double-spent. - assert reloaded.total_exposure_cents() == 140 + # another 40c plus the conservative 2c fee reserve. + assert reloaded.total_exposure_cents() == 142 assert reloaded.open_order_count() == 1 reloaded.remove_open_order("order-1") @@ -71,6 +142,100 @@ def test_exposure_persists_both_sides_without_overwrite(tmp_path): assert set(final.positions) == {("KXBTC-EVENT", "no")} +def test_stale_exposure_verifier_cannot_erase_reservation(tmp_path): + path = tmp_path / "exposure.json" + writer = ExposureTracker(persist=True, state_path=path) + stale = ExposureTracker(persist=True, state_path=path) + + assert writer.reserve_order_submission( + "order-a", + "KXBTC-EVENT", + 1, + 40, + contract_ticker="KXBTC-EVENT", + side="yes", + ) + assert stale.verify_persistence() is True + + reloaded = ExposureTracker(persist=True, state_path=path) + assert [ + order["client_order_id"] for order in reloaded.open_orders + ] == ["order-a"] + + +def test_concurrent_exposure_reservations_merge_and_dedupe(tmp_path): + path = tmp_path / "exposure.json" + first = ExposureTracker(persist=True, state_path=path) + second = ExposureTracker(persist=True, state_path=path) + barrier = threading.Barrier(2) + + def reserve( + tracker: ExposureTracker, + client_order_id: str, + ) -> bool: + barrier.wait(timeout=5) + return tracker.reserve_order_submission( + client_order_id, + "KXBTC-EVENT", + 1, + 40, + contract_ticker="KXBTC-EVENT", + side="yes", + ) + + with ThreadPoolExecutor(max_workers=2) as executor: + distinct_results = [ + future.result() + for future in ( + executor.submit(reserve, first, "order-a"), + executor.submit(reserve, second, "order-b"), + ) + ] + assert distinct_results == [True, True] + reloaded = ExposureTracker(persist=True, state_path=path) + assert { + order["client_order_id"] for order in reloaded.open_orders + } == {"order-a", "order-b"} + assert reloaded.total_exposure_cents() == 84 + + duplicate_first = ExposureTracker( + persist=True, + state_path=tmp_path / "dedupe.json", + ) + duplicate_second = ExposureTracker( + persist=True, + state_path=tmp_path / "dedupe.json", + ) + duplicate_barrier = threading.Barrier(2) + + def reserve_duplicate(tracker: ExposureTracker) -> bool: + duplicate_barrier.wait(timeout=5) + return tracker.reserve_order_submission( + "same-order", + "KXBTC-EVENT", + 1, + 40, + contract_ticker="KXBTC-EVENT", + side="yes", + ) + + with ThreadPoolExecutor(max_workers=2) as executor: + duplicate_results = [ + future.result() + for future in ( + executor.submit(reserve_duplicate, duplicate_first), + executor.submit(reserve_duplicate, duplicate_second), + ) + ] + assert sorted(duplicate_results) == [False, True] + deduped = ExposureTracker( + persist=True, + state_path=tmp_path / "dedupe.json", + ) + assert len(deduped.open_orders) == 1 + assert deduped.open_orders[0]["client_order_id"] == "same-order" + + def test_corrupt_exposure_state_fails_closed(tmp_path): path = tmp_path / "exposure.json" path.write_text("not-json", encoding="utf-8") @@ -80,6 +245,52 @@ def test_corrupt_exposure_state_fails_closed(tmp_path): assert tracker.state_healthy is False +def test_negative_persisted_position_cannot_offset_live_reservations(tmp_path): + path = tmp_path / "negative-exposure.json" + path.write_text( + json.dumps( + { + "positions": [ + { + "market_ticker": "KXBTC-EVENT", + "contract_ticker": "KXBTC-EVENT", + "side": "yes", + "quantity": -10, + "avg_price_cents": 50, + "unrealized_pnl_cents": 0, + "source_ts": None, + "freshness_score": None, + } + ], + "open_orders": [], + "order_history": [], + "updated_at": "2026-07-26T22:00:00+00:00", + } + ), + encoding="utf-8", + ) + + tracker = ExposureTracker(persist=True, state_path=path) + + assert tracker.state_healthy is False + assert tracker.total_exposure_cents() == 2**63 - 1 + + +def test_position_mutation_rejects_negative_risk() -> None: + tracker = ExposureTracker() + negative = Position( + market_ticker="KXBTC-EVENT", + contract_ticker="KXBTC-EVENT", + side="yes", + quantity=-1, + avg_price_cents=50, + unrealized_pnl_cents=0, + ) + + with pytest.raises(ValueError, match="invalid exposure position"): + tracker.update_position(negative) + + def test_blocked_category_matching_is_case_insensitive(): caps = CapConfig(blocked_categories=["kxpolitics"]) diff --git a/tests/test_sqlite_operational_journal.py b/tests/test_sqlite_operational_journal.py new file mode 100644 index 00000000..14929ba4 --- /dev/null +++ b/tests/test_sqlite_operational_journal.py @@ -0,0 +1,528 @@ +from __future__ import annotations + +import json +import multiprocessing +import sqlite3 +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import pytest + +from live_firewall.operational_journal import ( + AppendOnlyOperationalJournal, + OPERATIONAL_EVENT_SCHEMA, + OperationalJournalError, + canonical_json, + sha256_json, +) +from live_firewall.sqlite_operational_journal import ( + SQLITE_OPERATIONAL_JOURNAL_SCHEMA, + SQLiteOperationalJournal, +) + + +NOW = datetime(2026, 7, 26, 12, 30, tzinfo=timezone.utc) + + +def _claim_in_process( + database_path: str, + worker_id: int, + start: Any, + results: Any, +) -> None: + journal = SQLiteOperationalJournal( + Path(database_path), + now_fn=lambda: NOW, + busy_timeout_seconds=10, + ) + try: + if not journal.healthy: + results.put(("unhealthy", journal.error)) + return + if not start.wait(timeout=15): + results.put(("timeout", worker_id)) + return + + def no_prior_claim(rows: tuple[dict[str, Any], ...]) -> None: + if any(row.get("kind") == "dispatch.claimed" for row in rows): + raise OperationalJournalError("dispatch is already claimed") + + event = journal.append( + "dispatch.claimed", + {"worker_id": worker_id}, + outbox_id=f"dispatch-claim:{worker_id}", + allow_existing_outbox=False, + validate_existing=no_prior_claim, + validate_existing_latest_kinds=("dispatch.claimed",), + ) + results.put(("committed", event["sequence"])) + except OperationalJournalError as exc: + results.put(("rejected", str(exc))) + finally: + journal.close() + + +def test_restart_preserves_chain_idempotency_and_outbox_state( + tmp_path: Path, +) -> None: + path = tmp_path / "operational.db" + journal = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + assert journal.healthy, journal.error + + source = journal.append( + "capital.reserved", + {"reservation_id": "reservation-1", "cents": 125}, + outbox_id="reservation:1", + ) + assert ( + journal.append( + "capital.reserved", + {"reservation_id": "reservation-1", "cents": 125}, + outbox_id="reservation:1", + ) + == source + ) + with pytest.raises( + OperationalJournalError, + match="reused with different content", + ): + journal.append( + "capital.reserved", + {"reservation_id": "reservation-1", "cents": 126}, + outbox_id="reservation:1", + ) + + other = journal.append( + "capital.reserved", + {"reservation_id": "reservation-2", "cents": 75}, + outbox_id="reservation:2", + ) + acknowledgement = journal.acknowledge_outbox( + "reservation:1", + acknowledgement={"receipt_id": "receipt-1"}, + ) + assert journal.pending_outbox() == (other,) + journal.close() + + restarted = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + assert restarted.healthy, restarted.error + rows = restarted.events() + assert [row["sequence"] for row in rows] == [1, 2, 3] + assert rows[0] == source + assert rows[2] == acknowledgement + assert rows[2]["previous_sha256"] == rows[1]["event_sha256"] + assert rows[0]["schema"] == OPERATIONAL_EVENT_SCHEMA + assert restarted.pending_outbox() == (other,) + + with pytest.raises(OperationalJournalError, match="already claimed"): + restarted.append( + "capital.reserved", + {"reservation_id": "reservation-1", "cents": 125}, + outbox_id="reservation:1", + allow_existing_outbox=False, + ) + assert restarted.healthy + restarted.close() + + +def test_event_contract_matches_jsonl_reference(tmp_path: Path) -> None: + sqlite_journal = SQLiteOperationalJournal( + tmp_path / "contract.db", + now_fn=lambda: NOW, + ) + jsonl_journal = AppendOnlyOperationalJournal( + tmp_path / "contract.jsonl", + now_fn=lambda: NOW, + ) + assert sqlite_journal.head() == (0, "0" * 64) + assert jsonl_journal.head() == (0, "0" * 64) + + sqlite_source = sqlite_journal.append( + "broker.bootstrap.recorded", + {"receipt_id": "bootstrap-1", "orders": []}, + outbox_id="bootstrap:1", + ) + jsonl_source = jsonl_journal.append( + "broker.bootstrap.recorded", + {"receipt_id": "bootstrap-1", "orders": []}, + outbox_id="bootstrap:1", + ) + assert sqlite_source == jsonl_source + assert sqlite_journal.head() == ( + sqlite_source["sequence"], + sqlite_source["event_sha256"], + ) + assert jsonl_journal.head() == sqlite_journal.head() + + sqlite_ack = sqlite_journal.acknowledge_outbox( + "bootstrap:1", + acknowledgement={"delivered": True}, + ) + jsonl_ack = jsonl_journal.acknowledge_outbox( + "bootstrap:1", + acknowledgement={"delivered": True}, + ) + assert sqlite_ack == jsonl_ack + assert sqlite_journal.events() == jsonl_journal.events() + assert sqlite_journal.pending_outbox() == jsonl_journal.pending_outbox() + assert sqlite_journal.head() == ( + sqlite_ack["sequence"], + sqlite_ack["event_sha256"], + ) + assert jsonl_journal.head() == sqlite_journal.head() + sqlite_journal.close() + + +def test_jsonl_reference_supports_bounded_latest_kind_validation( + tmp_path: Path, +) -> None: + journal = AppendOnlyOperationalJournal( + tmp_path / "reference.jsonl", + now_fn=lambda: NOW, + ) + journal.append("state", {"cursor": 1}) + latest = journal.append("state", {"cursor": 2}) + journal.append("unrelated", {"value": 3}) + observed: list[tuple[dict[str, Any], ...]] = [] + + journal.append( + "state", + {"cursor": 3}, + validate_existing=observed.append, + validate_existing_latest_kinds=("state",), + ) + assert observed == [(latest,)] + + +def test_two_live_instances_incrementally_observe_committed_tail( + tmp_path: Path, +) -> None: + path = tmp_path / "two-live-instances.db" + first = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + second = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + assert first.healthy and second.healthy + + first_event = first.append( + "command.applied", + {"command_id": "command-1"}, + outbox_id="command:1", + ) + assert second.events(after_sequence=0, limit=1) == (first_event,) + + second_event = second.append( + "command.applied", + {"command_id": "command-2"}, + outbox_id="command:2", + ) + assert first.events(after_sequence=first_event["sequence"]) == ( + second_event, + ) + assert first.healthy and second.healthy + first.close() + second.close() + + +def test_serialized_operation_rolls_back_all_appends_and_cached_head( + tmp_path: Path, +) -> None: + path = tmp_path / "transaction-rollback.db" + journal = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + + with pytest.raises(RuntimeError, match="handler failed"): + with journal.serialized_operation(): + journal.append("command.received", {"command_id": "one"}) + journal.append("command.applied", {"command_id": "one"}) + raise RuntimeError("handler failed after second append") + + assert journal.healthy, journal.error + assert journal.events() == () + replacement = journal.append( + "command.failed", + {"command_id": "one"}, + ) + assert replacement["sequence"] == 1 + assert replacement["previous_sha256"] == "0" * 64 + journal.close() + + +def test_latest_kind_validation_view_is_bounded_and_atomic( + tmp_path: Path, +) -> None: + path = tmp_path / "latest-kind-cas.db" + journal = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + with journal.serialized_operation(): + for index in range(100): + journal.append("irrelevant.telemetry", {"index": index}) + prior = journal.append( + "command-feed.state.persisted", + {"cursor": 10}, + ) + for index in range(100, 200): + journal.append("irrelevant.telemetry", {"index": index}) + + observed: list[tuple[dict[str, Any], ...]] = [] + + def validate_latest(rows: tuple[dict[str, Any], ...]) -> None: + observed.append(rows) + assert rows == (prior,) + + appended = journal.append( + "command-feed.state.persisted", + {"cursor": 11}, + validate_existing=validate_latest, + validate_existing_latest_kinds=("command-feed.state.persisted",), + ) + assert appended["sequence"] == 202 + assert observed == [(prior,)] + journal.close() + + +def test_begin_immediate_makes_cross_process_validation_one_shot( + tmp_path: Path, +) -> None: + path = tmp_path / "claims.db" + initial = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + assert initial.healthy, initial.error + initial.close() + + context = multiprocessing.get_context("spawn") + start = context.Event() + results = context.Queue() + processes = [ + context.Process( + target=_claim_in_process, + args=(str(path), worker_id, start, results), + ) + for worker_id in range(4) + ] + for process in processes: + process.start() + start.set() + observed = [results.get(timeout=30) for _ in processes] + for process in processes: + process.join(timeout=30) + assert process.exitcode == 0 + + assert sum(result[0] == "committed" for result in observed) == 1 + assert sum(result[0] == "rejected" for result in observed) == 3 + + restarted = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + claims = restarted.events(kind="dispatch.claimed") + assert len(claims) == 1 + assert claims[0]["sequence"] == 1 + restarted.close() + + +def test_live_instance_detects_mid_chain_tamper_plus_valid_tail_append( + tmp_path: Path, +) -> None: + path = tmp_path / "live-tamper.db" + journal = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + journal.append("authority.granted", {"scope": "first"}) + journal.append("authority.granted", {"scope": "second"}) + + with sqlite3.connect(path) as connection: + trigger_sql = connection.execute( + "SELECT sql FROM sqlite_master " + "WHERE type = 'trigger' " + "AND name = 'operational_events_no_update'" + ).fetchone()[0] + event_json = connection.execute( + "SELECT event_json FROM operational_events WHERE sequence = 1" + ).fetchone()[0] + event = json.loads(event_json) + event["payload"]["scope"] = "tampered" + connection.execute("DROP TRIGGER operational_events_no_update") + connection.execute( + "UPDATE operational_events SET event_json = ? WHERE sequence = 1", + (canonical_json(event),), + ) + connection.execute(trigger_sql) + event_count, head = connection.execute( + "SELECT event_count, head_sha256 FROM journal_metadata " + "WHERE singleton = 1" + ).fetchone() + tail = { + "schema": OPERATIONAL_EVENT_SCHEMA, + "sequence": event_count + 1, + "recorded_at": "2026-07-26T12:31:00Z", + "kind": "authority.granted", + "payload": {"scope": "valid-tail"}, + "previous_sha256": head, + } + tail["event_sha256"] = sha256_json(tail) + connection.execute( + "INSERT INTO operational_events (" + "sequence, kind, outbox_id, acknowledges_outbox_id, " + "previous_sha256, event_sha256, event_json" + ") VALUES (?, ?, NULL, NULL, ?, ?, ?)", + ( + tail["sequence"], + tail["kind"], + tail["previous_sha256"], + tail["event_sha256"], + canonical_json(tail), + ), + ) + + assert not journal.healthy + assert journal.error is not None + assert "hash" in journal.error.casefold() + journal.close() + + +@pytest.mark.parametrize( + ("mutation_sql", "expected_error"), + [ + ( + "CREATE INDEX unauthorized_payload_idx " + "ON operational_events(event_json)", + "schema object set mismatch", + ), + ("PRAGMA user_version = 99", "schema version mismatch"), + ], +) +def test_live_instance_rejects_schema_or_version_mutation( + tmp_path: Path, + mutation_sql: str, + expected_error: str, +) -> None: + path = tmp_path / "schema-mutation.db" + journal = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + journal.append("authority.granted", {"scope": "one"}) + + with sqlite3.connect(path) as connection: + connection.execute(mutation_sql) + + assert not journal.healthy + assert journal.error is not None + assert expected_error in journal.error + journal.close() + + +@pytest.mark.parametrize("tamper", ["event", "head"]) +def test_restart_fails_closed_on_event_or_head_tamper( + tmp_path: Path, + tamper: str, +) -> None: + path = tmp_path / f"tamper-{tamper}.db" + journal = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + journal.append("authority.granted", {"scope": "single-order"}) + journal.close() + + with sqlite3.connect(path) as connection: + if tamper == "head": + connection.execute( + "UPDATE journal_metadata SET head_sha256 = ? " + "WHERE singleton = 1", + ("f" * 64,), + ) + else: + trigger_sql = connection.execute( + "SELECT sql FROM sqlite_master " + "WHERE type = 'trigger' " + "AND name = 'operational_events_no_update'" + ).fetchone()[0] + event_json = connection.execute( + "SELECT event_json FROM operational_events WHERE sequence = 1" + ).fetchone()[0] + event = json.loads(event_json) + event["payload"]["scope"] = "tampered-unbounded" + connection.execute("DROP TRIGGER operational_events_no_update") + connection.execute( + "UPDATE operational_events SET event_json = ? " + "WHERE sequence = 1", + (canonical_json(event),), + ) + connection.execute(trigger_sql) + + restarted = SQLiteOperationalJournal(path, now_fn=lambda: NOW) + assert not restarted.healthy + assert restarted.error is not None + assert any( + marker in restarted.error.casefold() + for marker in ("hash", "head") + ) + with pytest.raises(OperationalJournalError, match="unhealthy"): + restarted.events() + restarted.close() + + +def test_bounded_incremental_reads_use_kind_and_outbox_indexes( + tmp_path: Path, +) -> None: + path = tmp_path / "bounded.db" + journal = SQLiteOperationalJournal( + path, + now_fn=lambda: NOW, + scan_batch_size=7, + ) + with journal.serialized_operation(): + for index in range(2_000): + journal.append( + "kind.even" if index % 2 == 0 else "kind.odd", + {"index": index}, + outbox_id=f"event:{index}", + ) + + page = journal.events( + kind="kind.even", + after_sequence=40, + limit=6, + ) + assert len(page) == 6 + assert [row["sequence"] for row in page] == [41, 43, 45, 47, 49, 51] + + pending_page = journal.pending_outbox( + after_sequence=1_980, + limit=4, + ) + assert [row["sequence"] for row in pending_page] == [ + 1_981, + 1_982, + 1_983, + 1_984, + ] + journal.close() + + with sqlite3.connect(path) as connection: + metadata = connection.execute( + "SELECT schema, event_count FROM journal_metadata " + "WHERE singleton = 1" + ).fetchone() + assert metadata == (SQLITE_OPERATIONAL_JOURNAL_SCHEMA, 2_000) + + indexes = { + row[0] + for row in connection.execute( + "SELECT name FROM sqlite_master WHERE type = 'index'" + ) + } + assert "operational_events_kind_sequence_idx" in indexes + assert "operational_events_outbox_sequence_idx" in indexes + assert "operational_events_ack_target_idx" in indexes + + kind_plan = " ".join( + str(row[3]) + for row in connection.execute( + "EXPLAIN QUERY PLAN " + "SELECT event_json FROM operational_events " + "WHERE kind = ? AND sequence > ? " + "ORDER BY sequence LIMIT ?", + ("kind.even", 40, 6), + ) + ) + assert "operational_events_kind_sequence_idx" in kind_plan + + outbox_plan = " ".join( + str(row[3]) + for row in connection.execute( + "EXPLAIN QUERY PLAN " + "SELECT event_json FROM operational_events " + "WHERE outbox_id = ?", + ("event:1999",), + ) + ) + assert "INDEX" in outbox_plan + assert "SCAN operational_events" not in outbox_plan diff --git a/tests/test_taker_execution_truth.py b/tests/test_taker_execution_truth.py index cc79d5ac..d2910888 100644 --- a/tests/test_taker_execution_truth.py +++ b/tests/test_taker_execution_truth.py @@ -235,6 +235,53 @@ def test_live_reconciliation_records_weighted_price_role_cost_and_fee(tmp_path): ledger.close() +@pytest.mark.parametrize( + "broker_fill_count", + ["0.50", "-1.00", "NaN", "not-a-number"], +) +def test_nonintegral_or_invalid_terminal_fill_is_retained_without_truncation( + tmp_path, + broker_fill_count, +): + ledger = AutonomyLedger(tmp_path / "ledger.db") + try: + decision = _decision() + ledger.record_decision(decision) + ledger.record_outcome(TradeOutcome( + decision_id=decision.decision_id, + market_ticker=TICKER, + kind=OutcomeKind.ACCEPTED, + order_id="order-fractional", + fill_count=0, + fill_price_cents=50, + pnl_cents=None, + broker_contacted=True, + detail={ + "submitted_price_cents": 50, + "submitted_count": 2, + "submitted_notional_cents": 100, + "liquidity_role": "maker", + }, + )) + reconciler = Reconciler( + ledger, + order_status_fn=lambda _order_id: {"order": { + "status": "canceled", + "fill_count_fp": broker_fill_count, + "maker_fill_cost_dollars": "0.2500", + }}, + ) + + updates = reconciler.reconcile_open_orders() + + assert updates == [] + pending = ledger.open_decisions("live")[0] + assert pending["order_active"] == 1 + assert pending["filled_count"] == 0 + finally: + ledger.close() + + @pytest.mark.parametrize("liquidity_role", ["maker", "taker"]) def test_settlement_uses_witnessed_price_and_correct_role_fee(liquidity_role): class _Ledger: