{
  "schema_version": "1.0.0",
  "bot_id": "5.7",
  "bot_name": "ChainStateVerifier",
  "slug": "chainstateverifier",
  "layer": "Security",
  "layer_key": "sec",
  "bot_class": "Guardrail",
  "authority": [
    "Reject",
    "Pause"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Security",
    "bot_class": "Guardrail",
    "authority": "Reject, Pause",
    "runs_before": "Order signing",
    "runs_after": "RPCFailoverManager elects primary provider",
    "applies_to": "Every order before signing \u2014 verifies chain-derived fields against multiple RPC sources",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core"
  },
  "purpose": "Cross-check every order\u2019s chain-derived inputs (nonce, balance, allowance) against multiple sources before signing.",
  "why_it_matters": [
    {
      "failure": "Single RPC source used for nonce/balance without cross-checking",
      "consequence": "A stale or compromised RPC causes double-spend attempts or orders built on wrong nonce."
    },
    {
      "failure": "Reorg not detected before signing",
      "consequence": "An order signed on top of a reorged block may reference a state that no longer exists, causing failed settlement."
    },
    {
      "failure": "Balance check skipped",
      "consequence": "Orders submitted without sufficient pUSD collateral fail at settlement, wasting gas and degrading UX."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "pUSD balance of trading wallet",
      "source": "onchain",
      "required": true,
      "use": "Verify wallet has sufficient collateral before signing."
    },
    {
      "input": "Latest block number and block hash",
      "source": "onchain",
      "required": true,
      "use": "Cross-check across providers to detect reorg or fork."
    }
  ],
  "internal_inputs": [
    {
      "input": "Elected primary RPC provider from RPCFailoverManager",
      "source": "RPCFailoverManager",
      "required": true,
      "use": "Determine which provider to use as primary; verify against secondary."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Block all chain reads during global pause."
    }
  ],
  "raw_params": [
    "require_quorum \u00b7 int",
    "halt_on_mismatch \u00b7 bool",
    "reorg_depth_alert \u00b7 int",
    "publish_to \u00b7 list"
  ],
  "parameters": [
    {
      "name": "require_quorum",
      "default": 2,
      "warning": "Only 2 of 3 providers agree on state",
      "hard": "Fewer than require_quorum providers agree on block hash",
      "controls": "Minimum number of RPC providers that must agree on block state before signing.",
      "why_default_matters": "Quorum of 2 prevents a single compromised provider from poisoning the chain state view.",
      "threshold_logic": [
        {
          "condition": "agreeing_count >= require_quorum",
          "action": "APPROVE \u2014 proceed to signing"
        },
        {
          "condition": "agreeing_count < require_quorum",
          "action": "REJECT \u2014 CHAIN_STATE_MISMATCH"
        }
      ],
      "dev_check": "if (agreeing.count < p.require_quorum) return reject('CHAIN_STATE_MISMATCH');",
      "user_facing": "Chain state could not be verified. Orders are paused."
    },
    {
      "name": "halt_on_mismatch",
      "default": true,
      "warning": "Block hash mismatch detected between providers",
      "hard": "Block hash mismatch with halt_on_mismatch=true",
      "controls": "If true, halt all signing until providers agree on block state.",
      "why_default_matters": "Halting on mismatch prevents signing on a potentially reorged state.",
      "threshold_logic": [
        {
          "condition": "block hashes agree across require_quorum providers",
          "action": "APPROVE"
        },
        {
          "condition": "block hash mismatch AND halt_on_mismatch=true",
          "action": "REJECT \u2014 CHAIN_STATE_MISMATCH"
        }
      ],
      "dev_check": "if (p.halt_on_mismatch && !quorumAgreesOnBlockHash()) return reject('CHAIN_STATE_MISMATCH');",
      "user_facing": "A network inconsistency was detected. Orders are paused until it resolves."
    }
  ],
  "default_config": {
    "bot_id": "sec.chain_state_verifier",
    "version": "0.1.0",
    "mode": "hard_guard",
    "defaults": {
      "require_quorum": 2,
      "halt_on_mismatch": true,
      "reorg_depth_alert": 2,
      "publish_to": [
        "governance_audit",
        "risk_compliance"
      ]
    }
  },
  "implementation_flow": [
    "Receive order before signing.",
    "Check KillSwitch; if active, REJECT(KILL_SWITCH_ACTIVE).",
    "FETCH primary provider from RPCFailoverManager.",
    "FETCH block_number and block_hash from primary and at least one secondary provider.",
    "Compare block hashes at same block_number across providers.",
    "If mismatch detected and halt_on_mismatch=true: REJECT(CHAIN_STATE_MISMATCH).",
    "Check reorg depth; if > reorg_depth_alert blocks, emit WARN.",
    "FETCH pUSD balance of trading wallet from primary provider.",
    "If balance < order.size_usd: REJECT(CHAIN_STATE_MISMATCH) \u2014 insufficient collateral.",
    "Emit RiskVote(APPROVE) with block_hash, block_number, and balance snapshot."
  ],
  "decision_logic": {
    "approve": "Block state agrees across require_quorum providers, no reorg detected, and wallet balance sufficient.",
    "reshape_required": "Not applicable \u2014 verifier approves or rejects.",
    "reject": "Block hash mismatch across providers, reorg detected, or insufficient balance.",
    "warning_only": "Warn when reorg depth approaches reorg_depth_alert."
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "vote_id": "sec.chain_state_verifier.20260509T180000Z",
    "decision": "APPROVE",
    "reason_code": null,
    "evidence": {
      "block_number": 58420100,
      "block_hash": "0xabc123def456",
      "quorum_count": 3,
      "balance_pusd": 1200,
      "order_size_pusd": 400
    },
    "checked_at": "2026-05-09T18:00:00Z"
  },
  "developer_log": {
    "bot_id": "sec.chain_state_verifier",
    "decision": "APPROVE",
    "inputs_used": [
      "onchain.block_hash",
      "onchain.pusd_balance",
      "rpc_failover.primary"
    ],
    "checked_at": "2026-05-09T18:00:00Z"
  },
  "user_explanations": [
    {
      "situation": "Orders paused \u2014 chain state mismatch",
      "message": "A network inconsistency was detected. Orders are paused until the network is consistent."
    },
    {
      "situation": "Orders paused \u2014 insufficient balance",
      "message": "Your pUSD balance is below the order size. Please top up your wallet."
    },
    {
      "situation": "Reorg warning",
      "message": "A chain reorganisation was detected. Your orders have been paused briefly as a precaution."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "All providers agree on a stale or forked state, causing the quorum check to pass while chain data is wrong.",
    "false_positive_risk": "Brief network partition causes providers to temporarily disagree, triggering halt_on_mismatch for legitimate orders.",
    "false_negative_risk": "Quorum of 2 compromised providers pass while a third honest provider disagrees but is below quorum.",
    "safe_fallback": "If fewer than require_quorum providers respond, fail-closed: REJECT until quorum is restored.",
    "required_dependencies": [
      "RPCFailoverManager for provider health",
      "Polygon RPC pool",
      "KillSwitch"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Approve when block hashes agree across require_quorum providers",
        "setup": "2 providers return same block hash at block 58420100",
        "expected": "APPROVE with block evidence"
      },
      {
        "test": "Reject when block hashes diverge and halt_on_mismatch=true",
        "setup": "provider A hash != provider B hash at same block",
        "expected": "DENY(CHAIN_STATE_MISMATCH)"
      },
      {
        "test": "Reject when wallet balance < order size",
        "setup": "balance=100 pUSD, order size=400 pUSD",
        "expected": "DENY(CHAIN_STATE_MISMATCH)"
      }
    ],
    "integration": [
      {
        "test": "Reorg detected \u2014 halt fires and alert emitted",
        "expected": "DENY(CHAIN_STATE_MISMATCH) and alert; auto-resume when chain stabilises"
      },
      {
        "test": "RPCFailoverManager quorum lost propagates to ChainStateVerifier",
        "expected": "DENY(RPC_QUORUM_LOST) before any balance check"
      }
    ],
    "property": [
      {
        "property": "Block hash disagreement always produces DENY when halt_on_mismatch=true",
        "required": "Always true"
      },
      {
        "property": "Insufficient balance always produces DENY regardless of chain state",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Cross-check every order\u2019s chain-derived inputs (nonce, balance, allowance) against multiple sources before signing.",
  "legacy_pm_signals": [
    "Nonce and balance disagreement across RPCs",
    "Pending tx pool divergence",
    "Reorg detection and orphaned-tx recovery"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "risk_compliance",
    "governance_audit"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "onchain",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q3-2026"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "n/a",
      "to": "v2-spec",
      "reason": "Spec drafted post-CLOB-V2 cutover; bot not yet implemented",
      "action_taken": "Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain)"
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": false,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Verifies pUSD balance and Polygon block state before CTFExchangeV2 order signing; uses quorum across V2 RPC providers."
  },
  "reference_implementation": {
    "pseudocode": "// ChainStateVerifier\nFUNCTION verifyChainState(pendingOrder, providers):\n  IF FETCH(internal.killswitch).active:\n    EMIT RiskVote(DENY, KILL_SWITCH_ACTIVE); RETURN\n  // Fetch block state from multiple providers\n  blockStates = []\n  FOR provider IN providers:\n    state = FETCH(provider.eth_getBlockByNumber('latest'))\n    IF state != null: blockStates.append({provider, block_number: state.number, hash: state.hash})\n  // Quorum check on block hash\n  hashCounts = COUNT_BY(blockStates, x => x.hash)\n  topHash = hashCounts.max_count_hash\n  agreeing = blockStates.filter(x => x.hash == topHash)\n  IF agreeing.count < params.require_quorum:\n    EMIT RiskVote(DENY, CHAIN_STATE_MISMATCH); RETURN\n  // Balance check\n  balance = FETCH(primary.erc20.balanceOf(pUSD, pendingOrder.wallet))\n  IF balance < pendingOrder.size_usd:\n    EMIT RiskVote(DENY, CHAIN_STATE_MISMATCH); RETURN\n  EMIT RiskVote(APPROVE, {block_number: topHash.block_number, balance})\n  LOG(governance.audit, {block_number, balance, quorum_count: agreeing.count})",
    "sdk_calls": [
      "provider.eth_getBlockByNumber('latest')",
      "provider.erc20.balanceOf(pUSD_ADDRESS, wallet)",
      "internal.killswitch.status()"
    ],
    "complexity": "O(p) where p = provider count; single block per provider"
  },
  "wire_examples": {
    "input": [
      {
        "label": "Pending order requiring chain state check",
        "source": "internal",
        "payload": {
          "intent_id": "int_6f7a8b9c0d1e2f3a",
          "wallet": "0xUserWallet123",
          "size_usd": 400,
          "contract_address": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",
          "timestamp_ms": 1746768672000
        }
      }
    ],
    "output": [
      {
        "label": "RiskVote \u2014 APPROVE",
        "payload": {
          "vote_id": "sec.chain_state_verifier.20260509T180000Z",
          "decision": "APPROVE",
          "reason_code": null,
          "evidence": {
            "block_number": 58420100,
            "quorum_count": 3,
            "balance_pusd": 1200
          },
          "checked_at": "2026-05-09T18:00:00Z"
        }
      }
    ]
  },
  "reason_codes": [
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch is active.",
      "action": "Immediately return DENY.",
      "user_message": "Trading is currently paused."
    },
    {
      "code": "CHAIN_STATE_MISMATCH",
      "severity": "HARD_REJECT",
      "meaning": "Block hash disagreement across providers, reorg detected, or insufficient wallet balance.",
      "action": "Return DENY; emit alert.",
      "user_message": "A chain inconsistency was detected. Orders are paused."
    },
    {
      "code": "RPC_QUORUM_LOST",
      "severity": "HARD_REJECT",
      "meaning": "Fewer than require_quorum providers responded.",
      "action": "Return DENY; defer to RPCFailoverManager.",
      "user_message": "Not enough network providers available."
    },
    {
      "code": "CHAIN_STATE_REORG_WARN",
      "severity": "WARN",
      "meaning": "Reorg depth approaching reorg_depth_alert threshold.",
      "action": "Emit warn; continue checking.",
      "user_message": "A minor chain reorganisation is in progress."
    },
    {
      "code": "CHAIN_STATE_OK",
      "severity": "INFO",
      "meaning": "All checks passed; chain state verified.",
      "action": "Emit RiskVote(APPROVE).",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_sec_chainstateverifier_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "decision"
        ],
        "meaning": "Total chain state verification decisions."
      },
      {
        "name": "polytraders_sec_chainstateverifier_mismatches_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "reason"
        ],
        "meaning": "Block hash mismatches or balance failures detected."
      },
      {
        "name": "polytraders_sec_chainstateverifier_quorum_count",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Current number of providers in quorum agreement."
      },
      {
        "name": "polytraders_sec_chainstateverifier_verify_latency_ms",
        "type": "histogram",
        "unit": "ms",
        "labels": [],
        "meaning": "Total latency of chain state verification."
      }
    ],
    "alerts": [
      {
        "name": "ChainStateMismatch",
        "condition": "rate(polytraders_sec_chainstateverifier_mismatches_total[5m]) > 0",
        "severity": "P0",
        "runbook": "#runbook-chainstate-mismatch"
      },
      {
        "name": "ChainStateQuorumLow",
        "condition": "polytraders_sec_chainstateverifier_quorum_count < require_quorum",
        "severity": "P1",
        "runbook": "#runbook-chainstate-quorum"
      }
    ]
  },
  "state": {
    "store": "in-process; last-known block state per provider",
    "shape": "{provider_id -> {block_number, block_hash, checked_at}}",
    "ttl": "refreshed per order or every 2s in background",
    "recovery": "Re-fetch from all providers on restart; fail-closed if quorum unavailable.",
    "size_estimate": "< 1 KB"
  },
  "concurrency": {
    "execution_model": "parallel provider fetches per order",
    "max_in_flight": 200,
    "idempotency_key": "intent_id",
    "timeout_ms": 500,
    "backpressure": "drop newest above queue depth",
    "locking": "read-write lock on block state cache"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "KillSwitch gate.",
        "contract": "DENY(KILL_SWITCH_ACTIVE) short-circuits."
      },
      {
        "bot_id": "sec.rpc_failover_manager",
        "why": "Provides healthy primary RPC provider.",
        "contract": "Primary provider from RPCFailoverManager used for block state read."
      }
    ],
    "emits_to": [
      {
        "bot_id": "gov.builder_attribution",
        "why": "Log block state verification results.",
        "contract": "GovernanceLog entry on each APPROVE or DENY."
      }
    ],
    "sibling": [
      "sec.rpc_failover_manager"
    ],
    "external": [
      {
        "service": "Polygon RPC pool",
        "endpoint": "Configured provider endpoints",
        "sla": "best-effort",
        "failure_mode": "DENY if quorum cannot be established."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Colluding RPC providers presenting a forged fork to pass quorum check",
      "Stale balance read causing order submission despite insufficient pUSD"
    ],
    "mitigations": [
      "Quorum requires agreement across independently configured providers",
      "Balance checked immediately before signing; no caching of positive balance"
    ]
  },
  "failure_injection": [
    {
      "scenario": "BLOCK_HASH_MISMATCH",
      "how_to_inject": "Return different block hashes from two providers at same block number",
      "expected_behaviour": "DENY(CHAIN_STATE_MISMATCH); ChainStateMismatch alert fires",
      "recovery": "Automatic when providers re-sync."
    },
    {
      "scenario": "INSUFFICIENT_BALANCE",
      "how_to_inject": "Set wallet pUSD balance to 0",
      "expected_behaviour": "DENY(CHAIN_STATE_MISMATCH); user shown balance error",
      "recovery": "User tops up wallet."
    },
    {
      "scenario": "REORG_DETECTION",
      "how_to_inject": "Present chain tip that reorgs 3 blocks",
      "expected_behaviour": "WARN emitted; if reorg > reorg_depth_alert: DENY",
      "recovery": "Automatic when chain stabilises."
    }
  ],
  "runbook": {
    "summary": "ChainStateMismatch is a P0 event. Investigate provider disagreement immediately \u2014 could indicate network attack or RPC failure.",
    "oncall_actions": [
      {
        "alert": "ChainStateMismatch",
        "first_action": "Compare block hashes from each provider in logs; identify the outlier.",
        "escalate_to": "Security pod lead and infrastructure team."
      },
      {
        "alert": "ChainStateQuorumLow",
        "first_action": "Check RPCFailoverManager health; add provider if quorum at minimum.",
        "escalate_to": "Infrastructure team for provider recovery."
      }
    ],
    "manual_overrides": [
      {
        "name": "Override quorum for emergency",
        "how": "polytraders admin set-quorum sec.chain_state_verifier --require 1",
        "when": "Single trusted provider available during incident; use only with explicit security approval."
      }
    ],
    "healthcheck": "GET /internal/health/chainstateverifier \u2192 green if Quorum met; last successful verification within 10s.; red if Quorum < require_quorum or no successful verification in 60s."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for block hash quorum and balance check",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Block hash mismatch injection fires DENY and alert correctly",
        "how_measured": "Failure injection test",
        "threshold": "Pass"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero ChainStateMismatch alerts in 48h shadow",
        "how_measured": "Grafana alert history",
        "threshold": "0 alerts"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "RiskVote"
    ],
    "topics": [
      "polytraders.reports.risk"
    ],
    "retention_class": "2y",
    "cadence": "every-event",
    "sampling_rule": "emit-every",
    "bus_failure_action": "fail-closed",
    "user_visible": "summary-only",
    "consumes_kinds": [
      "ObservationReport"
    ]
  },
  "capital_impact": "Direct",
  "mode_support": [
    "quarantine"
  ],
  "v3_status": {
    "phase": 5,
    "phase_name": "Execution rails",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}