AI 辅助 DApp 崩溃分析:交易回滚原因的自动化归因与修复建议生成

一、DApp 交易回滚的诊断困境

DApp 前端报出的交易回滚错误信息通常极其有限。以太坊的 EIP-838 规范了 revert reason 的返回格式,但许多合约使用自定义错误(Custom Error,Solidity 0.8.4+)只返回 4 字节的 selector,前端框架(ethers.js v6 / viem)如果不做特殊处理,用户看到的可能是一条冷冰冰的十六进制字符串。即使拿到了可读的 revert reason,它也只描述"发生了什么"(Insufficient liquidity),而不解释"为什么发生"(路由计算偏差导致预估价格与执行价格不一致)以及"如何修复"(调高滑点容忍度或拆分交易路径)。

以 Uniswap V3 的 swap 回滚为例。一笔在 1inch 聚合器上发起的 swap 交易,可能经历以下失败路径:1inch 聚合器调用 Uniswap V3 Router → Router 调用多个 Pool 的 swap 方法 → 某个 Pool 因为 tick 被 MEV bot 的三明治攻击提前移动导致 sqrtPriceX96 偏移 → swap 在 SafeMath 溢出或滑点校验处回滚。整个调用链深度可能达到 5-7 层,跨 3-4 个合约地址。开发者在 Etherscan 上展开 Internal Txns 树逐个检查需要 10-20 分钟。而 DApp 需要的是在 3 秒内向用户展示:交易失败原因、下次成功的操作建议、以及是否需要调整 DApp 参数。

这正是 AI 辅助诊断的切入点。LLM 擅长处理结构化的调用链信息和合约 ABI 文档,将其翻译为人类可读的因果解释和操作建议。

二、自动化归因系统的处理管线

整个归因系统分为四个阶段:数据提取、上下文丰富、LLM 推理和用户输出。每个阶段有明确的输入输出合约。

graph LR
    subgraph 输入层
        A[交易Hash] --> B[tx收据+Internal Txns]
        A --> C[Event Logs]
        A --> D[Raw Calldata]
    end

    subgraph 上下文丰富层
        B --> E[调用树构建]
        C --> F[状态变更摘要]
        D --> G[calldata解码]
        E --> H[合约ABI匹配]
        G --> H
        H --> I[已知错误签名库]
        F --> I
    end

    subgraph LLM推理层
        I --> J[构建Prompt]
        J --> K[LLM推理]
        K --> L{置信度评估}
        L -->|高| M[输出诊断+修复建议]
        L -->|低| N[标记人工Review]
    end

    subgraph 输出层
        M --> O[用户侧: 失败原因+重试建议]
        M --> P[开发者侧: 诊断报告+代码定位]
        N --> Q[Sentry/Linear自动创建Triage工单]
    end

    style K fill:#4a9eff,stroke:#333
    style L fill:#f96,stroke:#333

数据提取层通过 ethers.js 的 getTransactionReceipt 配合 debug_traceTransaction(或 Tenderly API)获取完整的内部调用树和每条子调用的 calldata/returndata。

上下文丰富层的核心是将 EVM 层面的字节码交互还原为合约层面的语义信息。这需要两样东西:合约的已验证 ABI(从 Etherscan 拉取)和已知错误 selector 的映射库。4byte.directory 和 OpenChain 的 Signature Database 提供了 selector → 函数签名的映射。对于 custom error selector,可以通过 cast 4byte <selector> 查询或维护自有的常见协议错误库。

LLM 推理层将结构化数据(调用树、事件日志、错误签名)打包为 LLM prompt,要求模型输出三个维度的分析:根本原因(Root Cause)、用户操作建议(User Actionable Advice)、以及针对 DApp 开发者的代码修正提示(Dev Fix Hint)。

三、诊断管线核心实现

交易追踪与错误提取(TypeScript):

import { ethers } from 'ethers';

interface TraceFrame {
  type: string;
  from: string;
  to: string;
  value: string;
  gas: string;
  gasUsed: string;
  input: string;
  output: string;
  error?: string;
  calls?: TraceFrame[];
}

interface DiagnosticReport {
  rootCause: string;
  userAdvice: string;
  devFix: string;
  confidence: number;
  callDepth: number;
  gasUsed: string;
}

class TransactionDiagnostic {
  private provider: ethers.JsonRpcProvider;
  private knownErrors: Map<string, string>;

  constructor(rpcUrl: string) {
    this.provider = new ethers.JsonRpcProvider(rpcUrl);
    this.knownErrors = new Map([
      ['Insufficient liquidity', '流动性池余额不足以完成当前兑换量'],
      ['TransferHelper: TRANSFER_FROM_FAILED', '代币授权额度不足或用户取消了授权'],
      ['UniswapV2Router: EXPIRED', '交易截止时间已过,需要重新发起'],
      ['Too little received', '滑点保护触发:实际获得代币低于预期最小值'],
      ['STF', '滑点超限:链上价格变动超出你设定的允许范围'],
    ]);
  }

  async diagnose(txHash: string): Promise<DiagnosticReport> {
    const receipt = await this.provider.getTransactionReceipt(txHash);
    if (!receipt) throw new Error('Transaction not found');

    // 获取调用追踪
    let trace: TraceFrame | null = null;
    try {
      trace = await this.provider.send('debug_traceTransaction', [
        txHash,
        { tracer: 'callTracer', tracerConfig: { onlyTopCall: false } }
      ]);
    } catch {
      // 如果节点不支持 debug_traceTransaction,回退到 Tenderly API
      trace = await this.fetchFromTenderly(txHash);
    }

    // 提取错误信息
    const errorInfo = this.extractErrorInfo(trace, receipt);

    // 构建调用链路摘要
    const callChain = this.buildCallChainSummary(trace);

    // 构建 prompt 并调用 LLM
    const prompt = this.buildDiagnosticPrompt(errorInfo, callChain, receipt);
    const llmResult = await this.callLLM(prompt);

    return {
      rootCause: llmResult.rootCause,
      userAdvice: llmResult.userAdvice,
      devFix: llmResult.devFix,
      confidence: llmResult.confidence,
      callDepth: callChain.depth,
      gasUsed: receipt.gasUsed.toString()
    };
  }

  private extractErrorInfo(
    trace: TraceFrame | null,
    receipt: ethers.TransactionReceipt
  ): { revertReason: string; errorLocation: string; decodedError: string } {
    // 优先从 receipt 的 revert 信息中获取
    if (receipt.status === 0) {
      // 寻找调用树最深处的 error
      const deepestError = this.findDeepestError(trace);
      if (deepestError) {
        return {
          revertReason: deepestError.reason,
          errorLocation: `${deepestError.contract}.${deepestError.method}`,
          decodedError: this.decodeErrorSelector(deepestError.rawData)
        };
      }
    }
    return { revertReason: 'UNKNOWN', errorLocation: 'UNKNOWN', decodedError: 'UNKNOWN' };
  }

  private findDeepestError(frame: TraceFrame | null): {
    reason: string;
    contract: string;
    method: string;
    rawData: string;
  } | null {
    if (!frame) return null;

    // 递归搜索最深层的 error
    if (frame.error) {
      return {
        reason: frame.error,
        contract: frame.to,
        method: this.getMethodName(frame.input),
        rawData: frame.output || ''
      };
    }

    if (frame.calls) {
      for (const subCall of frame.calls) {
        const result = this.findDeepestError(subCall);
        if (result) return result;
      }
    }

    return null;
  }

  private buildDiagnosticPrompt(
    errorInfo: any,
    callChain: any,
    receipt: ethers.TransactionReceipt
  ): string {
    return `你是一个 Ethereum 智能合约安全分析专家。请分析以下交易回滚的原因并提供修复建议。

## 交易信息
- TxHash: ${receipt.hash}
- From: ${receipt.from}
- To: ${receipt.to}
- Gas Used: ${receipt.gasUsed.toString()}
- Gas Price: ${receipt.gasPrice?.toString()}

## 错误信息
- Revert Reason: ${errorInfo.revertReason}
- 错误位置: ${errorInfo.errorLocation}
- 解码错误: ${errorInfo.decodedError}

## 调用链路
${callChain.summary}

请按以下格式输出分析结果:
1. 根本原因(中文,简洁解释为什么回滚)
2. 用户操作建议(中文,用户端能做的具体操作)
3. 开发者修复建议(中文,DApp 代码层面的修正建议)
4. 置信度(0-1之间的数值)

输出 JSON 格式:
{
  "rootCause": "...",
  "userAdvice": "...",
  "devFix": "...",
  "confidence": 0.xx
}`;
  }

  private async callLLM(prompt: string): Promise<any> {
    // 调用 OpenAI API 或其他 LLM 端点
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
      },
      body: JSON.stringify({
        model: 'gpt-4o',
        messages: [
          { role: 'system', content: 'You are an Ethereum smart contract diagnostic expert.' },
          { role: 'user', content: prompt }
        ],
        response_format: { type: 'json_object' },
        temperature: 0.1
      })
    });

    const data = await response.json();
    return JSON.parse(data.choices[0].message.content);
  }

  private getMethodName(input: string): string {
    if (!input || input === '0x') return 'ETH_TRANSFER';
    return input.slice(0, 10); // 返回方法 selector
  }

  private decodeErrorSelector(rawData: string): string {
    if (!rawData || rawData === '0x') return 'NO_DATA';
    const selector = rawData.slice(0, 10);
    return this.knownErrors.get(rawData) || `Custom Revert: ${selector}`;
  }

  private buildCallChainSummary(
    trace: TraceFrame | null
  ): { summary: string; depth: number } {
    if (!trace) return { summary: 'No trace data', depth: 0 };

    const lines: string[] = [];
    let maxDepth = 0;

    const traverse = (frame: TraceFrame, depth: number) => {
      const indent = '  '.repeat(depth);
      const method = this.getMethodName(frame.input);
      const status = frame.error ? 'FAILED' : 'OK';
      const gasStr = frame.gasUsed ? `[gas: ${frame.gasUsed}]` : '';
      lines.push(`${indent}${frame.to}::${method} ${status} ${gasStr}`);
      maxDepth = Math.max(maxDepth, depth);

      if (frame.calls) {
        frame.calls.forEach((c) => traverse(c, depth + 1));
      }
    };

    traverse(trace, 0);
    return { summary: lines.join('\n'), depth: maxDepth };
  }

  private async fetchFromTenderly(
    txHash: string
  ): Promise<TraceFrame | null> {
    try {
      const response = await fetch(
        `https://api.tenderly.co/api/v1/public-contract/${1}/trace/${txHash}`
      );
      return await response.json();
    } catch {
      return null;
    }
  }
}

四、AI 诊断方案的边界与现实限制

幻觉风险的量化。LLM 在分析不常见协议的错误时可能生成看似合理但实际错误的根因解释。降低幻觉的策略包括:要求 LLM 引述具体的调用栈行号作为证据、在 prompt 中优先匹配已知错误签名库、对置信度低于 0.7 的诊断结果自动标记为需要人工复核。另一个有效做法是使用结构化输出(JSON mode + Pydantic 校验),确保 LLM 返回的诊断结果符合预定义的 schema,非结构化文本中的幻觉更容易被模型"自由发挥"。

错误签名库的维护成本。自定义错误 selector 的本质是 keccak256("ErrorName(type1,type2)") 的前 4 字节。新协议上线时,其自定义错误签名未收录在任何公共数据库中。维护一个覆盖主流 DApp(Uniswap、Aave、Compound、GMX)的错误签名库是合理的投入,但面向长尾协议的覆盖只能依赖 LLM 根据源码或 ABI 的反向推断。

LLM 推理延迟与用户预期的矛盾。一次完整的诊断管线(trace 获取 + LLM 推理)耗时在 5-15 秒之间,对于用户等待的容忍度来说偏长。优化策略是将诊断分为两阶段:第一阶段在 1 秒内返回基于规则匹配的快速诊断(已知错误签名的即时映射),第二阶段异步执行 LLM 深度分析并通过消息推送(或页面通知)返回完整报告。

隐私与数据安全。将交易的完整 calldata 和调用追踪发送给第三方 LLM 服务(OpenAI、Anthropic)存在潜在的数据泄露风险。私有化部署方案(自建 Llama 3.1 / DeepSeek / Qwen 实例)能规避这个问题,但推理延迟和推理质量的权衡需要在 GPU 资源和模型选型上做额外考量。

五、总结

AI 辅助 DApp 崩溃分析的核心价值不在于替代开发者阅读调用栈,而在于将 EVM 原生的字节码级错误信息翻译为用户和开发者都能理解的多层次诊断结果。生产实践中,最优方案是"规则引擎快速分类 + LLM 深度分析"的两阶段架构:规则引擎负责已知错误的零延迟匹配,LLM 负责未知错误的语义理解和修复建议生成。置信度阈值机制确保低质量诊断不会直接呈现给用户,而是流转到人工审核队列,形成持续优化的数据飞轮。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐