From 70c379b882bdbc4821c59922db1f60b2643396b8 Mon Sep 17 00:00:00 2001 From: leafwithered Date: Fri, 21 Aug 2026 12:01:46 +0800 Subject: [PATCH 01/11] feat: add V3 pre-sign intent reasoning --- README.md | 41 +++++-- app/api/analyze/route.ts | 22 +++- app/globals.css | 22 +++- app/page.tsx | 32 ++++- docs/SECURITY_BENCHMARK.md | 70 +++++++++++ lib/ai/provider.ts | 42 +++++-- lib/consequence.ts | 196 ++++++++++++++++++++++++++++++ lib/intent.ts | 234 ++++++++++++++++++++++++++++++++++++ lib/presets.ts | 4 +- package.json | 3 + test/ai-provider.ts | 3 +- test/consequence.ts | 79 +++++++++++++ test/intent.ts | 100 ++++++++++++++++ test/security-benchmark.ts | 236 +++++++++++++++++++++++++++++++++++++ 14 files changed, 1049 insertions(+), 35 deletions(-) create mode 100644 docs/SECURITY_BENCHMARK.md create mode 100644 lib/consequence.ts create mode 100644 lib/intent.ts create mode 100644 test/consequence.ts create mode 100644 test/intent.ts create mode 100644 test/security-benchmark.ts diff --git a/README.md b/README.md index 4e14053..3a2f5f1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > **The explainable pre-sign security layer for X Layer.** -XGuard AI decodes transaction intent, inspects real X Layer on-chain context, runs a transaction preflight, and fuses deterministic security rules with AI explanation. AI may raise risk, but it cannot reduce known deterministic security signals. Users remain in control of every wallet action. +XGuard AI decodes transaction behavior, explains the deterministic consequences of signing, compares those consequences with the user's optional stated intent, inspects real X Layer on-chain context, and fuses deterministic security rules with AI explanation. AI may raise risk, but it cannot reduce known deterministic security signals. Users remain in control of every wallet action. **Live Demo:** https://xguard-ai-six.vercel.app · **GitHub:** https://github.com/leafwithered/xguard-ai · **Final Judge Demo Video:** https://github.com/leafwithered/xguard-ai/blob/main/demo/xguard-ai-build-x-demo.mp4 @@ -17,7 +17,7 @@ XGuard AI decodes transaction intent, inspects real X Layer on-chain context, ru 1. Open the Live Demo and select **⚡ Try Judge Demo**. 2. Load **Safe Transfer**, then explicitly click **Analyze risk** to see the LOW baseline. 3. Load **Unlimited Approval** to inspect `approve(address,uint256)`, the spender, and `Amount: Unlimited`. -4. Load **Suspicious Airdrop** to see the deterministic safety floor remain at `100 HIGH` through AI enrichment. +4. Load **Suspicious Airdrop** to see a claim intent conflict with an unlimited approval, producing `MISMATCH` while the deterministic safety floor remains `100 HIGH`. 5. Select **Load Verified X Layer Receipt** to inspect the existing confirmed RiskRegistry transaction from real X Layer RPC data. Judge Mode only loads examples, navigates, and explains. It never auto-analyzes, connects a wallet, signs, records an assessment, or broadcasts a transaction. @@ -25,6 +25,8 @@ Judge Mode only loads examples, navigates, and explains. It never auto-analyzes, ## Why XGuard Is Different - **Deterministic first:** known security rules and decoded permissions establish an inspectable floor. +- **Transaction consequences:** a deterministic engine answers “What happens if I sign this?” with source-labeled facts and no invented token metadata. +- **Intent vs Reality:** optional user intent is normalized, then compared against decoded behavior; deterministic mismatches may raise risk and cannot be downgraded by AI. - **Transparent AI fusion:** `Final Risk = max(Deterministic Floor, AI Assessment)` is shown in the product. - **Real X Layer intelligence:** `eth_getCode`, EIP-1967 inspection, `eth_call`, and `eth_estimateGas` provide live context with isolated timeouts. - **Explainable evidence:** risk signals are labeled `RULE`, `DECODER`, `ON-CHAIN`, or `AI`; unavailable data is never fabricated. @@ -32,7 +34,7 @@ Judge Mode only loads examples, navigates, and explains. It never auto-analyzes, ## Stable Production Baseline -The Final Judge Upgrade core application baseline is commit `409aa73c211a6b350af757d130c3f41ac8cfe962` and is live on the canonical Vercel Production URL. Production Hybrid Analysis is verified through the provider-neutral adapter; the upstream provider is selected only through server-side environment variables and is not asserted by the public client. The V1 RiskRegistry evidence and every public URL remain unchanged. +The verified Production baseline is commit `8f37ee568ee02cb7affa51069eac618c3adb9363` and remains live on the canonical Vercel Production URL. Production Hybrid Analysis is verified through the provider-neutral adapter; the upstream provider is selected only through server-side environment variables and is not asserted by the public client. The V1 RiskRegistry evidence and every public URL remain unchanged. - Safe Transfer: `8 LOW`, Hybrid Analysis - Unlimited Approval: `72 HIGH`, decoded ERC20 `approve`, spender and `Amount: Unlimited` visible @@ -40,6 +42,10 @@ The Final Judge Upgrade core application baseline is commit `409aa73c211a6b350af - Clear Analysis, wallet connection, X Layer Testnet switching, and explicit user confirmation are included - Contract V2 is documented as a proposal only; no new contract or chain transaction was introduced +### V3 Preview Candidate + +The `v3-competition` branch adds a deterministic Transaction Consequence Engine, optional Intent vs Reality comparison, and a reproducible 34-case security benchmark. It is developed and deployed as a Preview candidate only until explicit approval; it does not alter the existing Production deployment, RiskRegistry contract, or verified receipt. + ## Project Overview The application supports wallet connection, X Layer Testnet detection and switching, transaction decoding, real RPC intelligence, bounded preflight checks, provider-neutral AI analysis, deterministic fallback analysis, post-hoc transaction inspection, Judge Mode, user confirmation, and an optional on-chain risk receipt. @@ -70,21 +76,27 @@ The hybrid design combines a deterministic Risk Engine with a configurable OpenA 1. Connect an EVM wallet. 2. Detect or switch to X Layer Testnet (`1952`). -3. Enter `from`, `to`, value, calldata, and context. -4. Decode supported calldata and inspect the target through real X Layer RPC calls. -5. Run bounded `eth_call` and `eth_estimateGas` preflight checks—not a full state-diff simulation. -6. Run the deterministic Local Risk Engine and merge optional AI enrichment above its safety floor. -7. Review the score, source-labeled signals, intelligence, reasons, and recommendation before signing. -8. Optionally confirm explicitly and record the assessment hash and score through `RiskRegistry`. +3. Enter `from`, `to`, value, calldata, and an optional plain-language expectation. +4. Decode supported calldata and generate deterministic, provenance-labeled transaction consequences. +5. Compare optional stated intent with decoded reality. Deterministic mismatch may raise the safety floor. +6. Inspect the target through real X Layer RPC calls and run bounded `eth_call` / `eth_estimateGas` preflight checks—not a full state-diff simulation. +7. Run the deterministic Local Risk Engine and merge optional AI enrichment above its safety floor. +8. Review consequences, Intent vs Reality, score, source-labeled signals, intelligence, reasons, and recommendation before signing. +9. Optionally confirm explicitly and record the assessment hash and score through `RiskRegistry`. ## Architecture ```mermaid flowchart TD U[User Transaction] --> D[Transaction Decoder] - D --> C[On-chain Contract Intelligence] - C --> P[Transaction Preflight] + D --> C[Deterministic Consequences] + I[Optional User Intent] --> N[Intent Normalization] + C --> M[Intent vs Reality] + N --> M + D --> O[On-chain Contract Intelligence] + O --> P[Transaction Preflight] P --> L[Deterministic Risk Engine] + M --> L L --> S[Security Floor] L --> A[AI Enrichment] S --> F[Risk Fusion] @@ -99,7 +111,7 @@ flowchart TD `lib/ai/provider.ts` isolates provider-specific behavior. Configure an OpenAI-compatible provider using `AI_API_KEY`, `AI_BASE_URL`, and `AI_MODEL`. The adapter attempts `/v1/responses` first and then `/v1/chat/completions`. Output is validated before use. Production Hybrid Analysis is verified through this server-configured adapter; public artifacts do not assert the upstream provider identity. -The deterministic Local Risk Engine checks zero addresses, exact bigint native value thresholds, decoded ERC20/NFT approvals, unlimited permissions, transfer methods, malformed and unknown calldata, unknown-contract context, and common social-engineering signals. +The deterministic Local Risk Engine checks zero addresses, exact bigint native value thresholds, decoded ERC20/NFT approvals, unlimited permissions, transfer methods, malformed and unknown calldata, intent mismatches, unknown-contract context, and common social-engineering signals. AI may normalize ambiguous natural language, but deterministic code performs the consequence comparison wherever supported. The browser never receives `AI_API_KEY`. Missing configuration, timeouts, unsupported endpoints, and malformed output automatically use Local Analysis. @@ -201,8 +213,13 @@ pnpm run intelligence:test pnpm run transaction-analyzer:test pnpm run judge:test pnpm run analysis-state:test +pnpm run consequence:test +pnpm run intent:test +pnpm run security-benchmark:test ``` +The full 34-case V3 corpus and its invariants are documented in [docs/SECURITY_BENCHMARK.md](docs/SECURITY_BENCHMARK.md). + The browser smoke path is documented in [docs/DEMO.md](docs/DEMO.md). The API returns `400` for invalid transaction input and keeps Local Analysis available when the configured provider fails. `npm run contract:deploy` is intentionally not part of the automated test run because it requires a user-controlled deployer key and testnet OKB. diff --git a/app/api/analyze/route.ts b/app/api/analyze/route.ts index d6c9d42..2817991 100644 --- a/app/api/analyze/route.ts +++ b/app/api/analyze/route.ts @@ -4,6 +4,8 @@ import { localRiskAnalysis, validateRiskInput, type RiskInput } from "../../../l import { mergeRiskResults } from "../../../lib/risk-fusion"; import { consumeAnalyzeRateLimit } from "../../../lib/api-rate-limit"; import { inspectContract, signalsFromIntelligence } from "../../../lib/chain/intelligence"; +import { buildTransactionConsequences } from "../../../lib/consequence"; +import { applyIntentRisk, compareIntentToReality } from "../../../lib/intent"; export async function POST(request: Request) { const forwardedFor = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim(); @@ -22,11 +24,23 @@ export async function POST(request: Request) { } const errors = validateRiskInput(input); if (errors.length) return NextResponse.json({ error: errors.join(". "), code: "INVALID_INPUT" }, { status: 400 }); - const fallback = localRiskAnalysis(input); + const localResult = localRiskAnalysis(input); + const preliminaryConsequences = buildTransactionConsequences(input, { decodedAction: localResult.decodedAction }); + const preliminaryIntentComparison = compareIntentToReality(input, localResult.decodedAction, preliminaryConsequences); + const deterministicFallback = applyIntentRisk(localResult, preliminaryIntentComparison); const [aiResult, contractIntelligence] = await Promise.all([ - analyzeTransaction(input, fallback), + analyzeTransaction(input, deterministicFallback, preliminaryConsequences), inspectContract(input) ]); - const riskResult = aiResult ? mergeRiskResults(fallback, aiResult) : fallback; - return NextResponse.json({ ...riskResult, advisorySignals: [...riskResult.advisorySignals, ...signalsFromIntelligence(contractIntelligence)], contractIntelligence }); + const consequences = buildTransactionConsequences(input, { decodedAction: deterministicFallback.decodedAction, intelligence: contractIntelligence }); + const intentComparison = compareIntentToReality(input, deterministicFallback.decodedAction, consequences, aiResult?.normalizedIntent); + const fusedRisk = aiResult ? mergeRiskResults(deterministicFallback, aiResult) : deterministicFallback; + const riskResult = applyIntentRisk(fusedRisk, intentComparison); + return NextResponse.json({ + ...riskResult, + advisorySignals: [...riskResult.advisorySignals, ...signalsFromIntelligence(contractIntelligence)], + consequences, + intentComparison, + contractIntelligence + }); } diff --git a/app/globals.css b/app/globals.css index 5850b85..08a30ea 100644 --- a/app/globals.css +++ b/app/globals.css @@ -67,6 +67,7 @@ label { display: block; color: var(--muted); font-size: 13px; margin: 18px 0 8px textarea, input { width: 100%; background: var(--panel-soft); border: 1px solid var(--line); border-radius: 12px; color: var(--text); padding: 13px; outline: none; } textarea:focus, input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(119,242,194,.08); } textarea { min-height: 110px; resize: vertical; } +.field-note { margin-top: 7px; color: var(--muted); font-size: 11px; line-height: 1.4; } .row { display: grid; grid-template-columns: .7fr 1.3fr; gap: 12px; } .actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; } .primary, .secondary { border-radius: 12px; padding: 12px 16px; border: 1px solid transparent; font-weight: 700; } @@ -85,8 +86,8 @@ textarea { min-height: 110px; resize: vertical; } .score-copy strong { font-size: 20px; margin: 5px 0; } .score-copy small { color: var(--muted); } .analysis-mode { display: inline-block; border: 1px solid rgba(119,242,194,.3); border-radius: 999px; padding: 6px 10px; color: var(--accent); font-size: 12px; } -.decoded-card, .intelligence-card, .fusion-card, .explanation, .record-card { margin-top: 18px; background: var(--panel-soft); border: 1px solid var(--line); border-radius: 14px; padding: 16px; } -.decoded-card h3, .intelligence-card h3, .fusion-card h3, .signal-section h3, .explanation h3 { margin: 0 0 10px; font-size: 14px; } +.decoded-card, .intelligence-card, .fusion-card, .consequence-card, .intent-card, .explanation, .record-card { margin-top: 18px; background: var(--panel-soft); border: 1px solid var(--line); border-radius: 14px; padding: 16px; } +.decoded-card h3, .intelligence-card h3, .fusion-card h3, .consequence-card h3, .intent-card h3, .signal-section h3, .explanation h3 { margin: 0 0 10px; font-size: 14px; } .decoded-grid { display: grid; grid-template-columns: 96px minmax(0,1fr); gap: 8px 12px; font-size: 12px; } .decoded-grid span { color: var(--muted); } .decoded-grid strong { word-break: break-all; } @@ -104,6 +105,23 @@ textarea { min-height: 110px; resize: vertical; } .fusion-grid div { display: grid; gap: 5px; padding: 12px; background: rgba(8,11,18,.45); border-radius: 10px; } .fusion-grid span { color: var(--muted); font-size: 10px; } .fusion-grid strong { font-size: 21px; } +.consequence-card { border-color: rgba(119,242,194,.22); } +.consequence-list { display: grid; gap: 9px; margin: 0; padding: 0; list-style: none; } +.consequence-list li { display: grid; gap: 6px; padding: 12px; border-left: 3px solid var(--accent); border-radius: 10px; background: rgba(8,11,18,.45); } +.consequence-list li.consequence-caution { border-left-color: var(--warning); } +.consequence-list li.consequence-critical { border-left-color: var(--danger); } +.consequence-list li > div { display: flex; justify-content: space-between; color: var(--muted); font-size: 9px; letter-spacing: .06em; } +.consequence-list li > strong { font-size: 13px; } +.consequence-list li > p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.45; } +.intent-card { border-color: rgba(249,198,107,.3); } +.intent-card.intent-match { border-color: rgba(119,242,194,.4); } +.intent-card.intent-mismatch { border-color: rgba(255,124,139,.5); } +.intent-status { border: 1px solid currentColor; border-radius: 999px; padding: 5px 8px; color: var(--warning); font-size: 10px; font-weight: 800; } +.intent-match .intent-status { color: var(--accent); } +.intent-mismatch .intent-status { color: var(--danger); } +.intent-grid { display: grid; grid-template-columns: 118px minmax(0,1fr); gap: 9px 12px; font-size: 12px; } +.intent-grid span { color: var(--muted); } +.intent-grid strong { line-height: 1.45; word-break: break-word; } .signal-section { margin-top: 18px; } .risk-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; } .risk-list li { display: grid; gap: 3px; background: var(--panel-soft); border-left: 3px solid var(--warning); border-radius: 10px; padding: 10px 12px; color: #dbe5f3; } diff --git a/app/page.tsx b/app/page.tsx index 2a154ba..e4689b5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,6 +5,8 @@ import { createPublicClient, createWalletClient, custom, http, isAddress, keccak import { riskRegistryAbi, xLayerTestnet } from "../lib/xlayer"; import type { RiskInput, RiskResult } from "../lib/risk"; import type { ContractIntelligence } from "../lib/chain/intelligence"; +import type { TransactionConsequence } from "../lib/consequence"; +import type { IntentComparison } from "../lib/intent"; import type { XLayerTransaction } from "../lib/chain/transaction-analyzer"; import { judgePresets as presets } from "../lib/presets"; import { currentAnalysisResult, invalidateStaleAnalysis } from "../lib/analysis-state"; @@ -18,20 +20,30 @@ const contractUrl = `${explorerBase}/address/0xf4505A4e8dEca4659b8A2054555788Ddc const verifiedTxUrl = `${explorerBase}/tx/0x1492bc179e98fe5fe79add3528f8f1f26990ab37e189a98d4c4a052d6fd11bcb`; const verifiedTxHash = "0x1492bc179e98fe5fe79add3528f8f1f26990ab37e189a98d4c4a052d6fd11bcb"; type WalletOption = { info: { uuid: string; name: string; icon: string }; provider: WalletProvider }; -type AnalysisResult = RiskResult & { contractIntelligence?: ContractIntelligence }; +type AnalysisResult = RiskResult & { + contractIntelligence: ContractIntelligence; + consequences: TransactionConsequence[]; + intentComparison: IntentComparison; +}; const shortAddress = (value: string) => value ? `${value.slice(0, 6)}…${value.slice(-4)}` : ""; const signalSources = new Set(["RULE", "DECODER", "ON-CHAIN", "AI"]); +const consequenceSources = new Set(["DECODER", "VALUE", "ON_CHAIN"]); +const intentStatuses = new Set(["MATCH", "PARTIAL", "MISMATCH", "UNKNOWN"]); function isCurrentAnalysisResult(value: unknown): value is AnalysisResult { if (!value || typeof value !== "object") return false; const candidate = value as Partial; const validSignals = (signals: unknown) => Array.isArray(signals) && signals.every((item) => item && typeof item === "object" && signalSources.has(String((item as { source?: unknown }).source))); + const validConsequences = Array.isArray(candidate.consequences) && candidate.consequences.every((item) => item && typeof item === "object" && consequenceSources.has(String((item as { evidenceSource?: unknown }).evidenceSource))); + const validIntent = Boolean(candidate.intentComparison && intentStatuses.has(String(candidate.intentComparison.status))); return typeof candidate.finalScore === "number" && typeof candidate.deterministicScore === "number" && validSignals(candidate.criticalSignals) && validSignals(candidate.advisorySignals) - && Boolean(candidate.contractIntelligence); + && Boolean(candidate.contractIntelligence) + && validConsequences + && validIntent; } export default function Home() { @@ -43,7 +55,7 @@ export default function Home() { const [to, setTo] = useState(""); const [value, setValue] = useState("0"); const [data, setData] = useState("0x"); - const [context, setContext] = useState("Swap on a new token router"); + const [context, setContext] = useState(""); const [result, setResult] = useState(null); const [lastInput, setLastInput] = useState(null); const [analyzing, setAnalyzing] = useState(false); @@ -267,7 +279,7 @@ export default function Home() {
Explainable pre-sign intelligence

The pre-sign security layer for X Layer.

Decode transactions, inspect on-chain context, apply deterministic safety rules, and use AI without allowing AI to override known security signals.

-

Built for X Layer

Network{networkName}
Analysis{modeLabel}
Safety floorDeterministic
SigningAlways user-confirmed
+

What happens if I sign this?

Network{networkName}
Analysis{modeLabel}
Safety floorDeterministic
SigningAlways user-confirmed
Transaction DecoderOn-chain IntelligenceDeterministic Safety FloorAI Risk Analysis
{judgeModeOpen &&
@@ -275,7 +287,7 @@ export default function Home() {
01Safe TransferExpected: LOW

Baseline deterministic analysis plus optional AI enrichment.

02Unlimited ApprovalExpected: HIGH

Decoded approve(), visible spender, and Unlimited amount.

-
03Suspicious AirdropExpected: HIGH

Demonstrates that AI cannot lower the deterministic floor.

+
03Suspicious AirdropExpected: HIGH + MISMATCH

Claim intent contradicts decoded unlimited permission; the safety floor remains deterministic.

04Verified X Layer EvidenceReceipt: Confirmed

Real user-signed RiskRegistry receipt on Chain 1952.

✓ Human-readable calldata✓ Deterministic safety floor✓ AI enrichment✓ X Layer intelligence✓ User-controlled signing✓ Verified on-chain receipt
@@ -287,7 +299,7 @@ export default function Home() { setFrom(event.target.value)} /> setTo(event.target.value)} />
setValue(event.target.value)} />
setData(event.target.value)} />
-