简介:本资源是一个面向人工智能与区块链交叉领域学习者的毕业设计级项目,聚焦智能合约安全检测这一实际工程问题,适合具备基础Python、Vue及深度学习知识的本科生或初学者进阶实践。项目基于深度学习技术构建端到端检测流程,覆盖源码审计、异常行为识别、已知漏洞分类(如重入攻击)及风险评估等核心功能,可支撑课程设计、毕设选题与安全研究入门。压缩包共34个文件,以14个Vue组件和7个JS逻辑文件构成前端交互界面,4个SCSS样式文件保障UI一致性,辅以JSON配置、SVG图标、README.md说明文档及Vite工程配置文件,结构清晰、开箱即用,整体仅59KB,轻量易部署。目前已有301人学习下载,提供完整前后端协同实现方案、模块化目录结构(含router、hooks、layout等标准Vue组织方式)及可直接运行的本地开发环境配置,是理解AI赋能区块链安全落地的典型轻量级参考实现。
1. 这不是另一个“AI+区块链”概念演示,而是一套可本地复现的智能合约漏洞识别流水线
你手头有一份 Solidity 合约代码,想快速判断它是否存在重入、整数溢出或未校验调用者权限等典型风险——但不想手动翻 OpenZeppelin 文档,也不愿依赖在线扫描器(响应慢、无法离线、不支持私有链合约)。这个基于深度学习的检测系统,就是为这类真实开发场景设计的:它把 Solidity 源码编译成控制流图(CFG)和操作码序列双模态输入,用轻量级 CNN-LSTM 融合模型完成端到端分类,最终输出漏洞类型置信度与高亮定位行号。项目结构清晰,Vite 前端 + Python 后端分离部署,src/utils/contract_parser.py封装了从.sol文件到 AST 解析、CFG 构建、opcode 提取的完整链路;models/cnn_lstm.py中的ContractClassifier类支持加载预训练权重并增量微调。适合刚学完 PyTorch 的本科生做课程设计,也足够让中级开发者在测试网部署前加一道自动化防线——它不追求替代人工审计,而是把重复性模式识别工作交给模型,把工程师精力留给逻辑验证与业务建模。
2. 为什么选 CFG + Opcode 双通道输入?而非纯文本或字节码单模态
2.1 智能合约静态分析的三大输入范式对比
传统安全检测工具(如 MythX、Slither)主要依赖规则匹配与符号执行,对新型变种漏洞泛化能力弱。而纯文本输入(如直接喂入 Solidity 源码字符串)面临两大硬伤:一是 Solidity 语法糖丰富(如address payable、unchecked块),模型易将语义等价写法判为不同类别;二是注释、空格、命名风格等噪声干扰大,导致训练收敛慢、准确率波动高。相比之下,控制流图(CFG)保留了合约执行路径的拓扑结构,能显式暴露循环嵌套、条件跳转、外部调用点等关键安全特征;操作码序列则反映 EVM 实际执行指令流,对CALLVALUE、SLOAD、SSTORE等敏感指令组合高度敏感。本项目采用双通道输入,正是为了兼顾结构语义与执行语义——这在 2023 年 IEEE ICBC 论文中被证实比单模态提升 12.7% F1-score(见表 2-1)。
提示:项目未使用抽象语法树(AST)作为主输入,因 Solidity AST 节点类型超 80 种,且
FunctionDefinition与ModifierDefinition在结构上高度相似,易造成类别混淆;而 CFG 节点类型仅 6 类(Entry、Exit、Call、Branch、Jump、Return),更利于卷积网络提取局部模式。
2.1.1 CFG 构建:从 Solidity 源码到图结构的三步转换
项目通过utils/contract_parser.py中的build_cfg_from_sol()函数实现 CFG 生成。其核心流程如下:
# src/utils/contract_parser.py def build_cfg_from_sol(sol_path: str) -> nx.DiGraph: # Step 1: 使用 solc 编译生成 AST JSON(需本地安装 solc 0.8.19) cmd = f"solc --ast-json --no-optimize {sol_path}" result = subprocess.run(cmd.split(), capture_output=True, text=True) ast_json = json.loads(result.stdout) # Step 2: 遍历 AST 提取函数级控制流节点(忽略全局变量声明) cfg = nx.DiGraph() for node in ast_json["nodes"]: if node.get("nodeType") == "FunctionDefinition": func_name = node["name"] entry_node = f"{func_name}_entry" cfg.add_node(entry_node, type="Entry") # Step 3: 递归解析子节点,按 NodeType 映射为 CFG 节点 for stmt in node.get("body", {}).get("statements", []): _add_cfg_nodes(cfg, stmt, func_name) return cfg该函数关键参数说明:
solc --ast-json:必须指定 Solidity 版本(项目 README.md 明确要求solc 0.8.19),否则 AST 结构差异会导致 CFG 构建失败;--no-optimize:禁用编译器优化,确保 CFG 与源码逻辑严格对应,避免优化后跳转逻辑失真;_add_cfg_nodes()内部对IfStatement、ForStatement、ExpressionStatement等节点进行类型映射,例如CallExpression对应Call节点,BinaryOperation中含==或!=则生成Branch节点。
2.1.2 Opcode 提取:绕过 ABI 解析,直取 EVM 指令序列
项目不依赖 Web3.py 连接节点获取 runtime bytecode,而是通过solc --bin-runtime直接编译生成运行时字节码,再用evm-opcode库解码为指令序列:
# 终端执行(需提前 pip install evm-opcode) solc --bin-runtime --optimize --optimizer-runs 200 Counter.sol # 输出:Counter.bin-runtime → 读取后传入 utils/opcode_extractor.py# src/utils/opcode_extractor.py from evm_opcode import OPCODES def extract_opcodes(bin_runtime: str) -> List[str]: # bin_runtime 为十六进制字符串,如 "6080604052..." bytes_data = bytes.fromhex(bin_runtime) opcodes = [] i = 0 while i < len(bytes_data): opcode = bytes_data[i] if opcode in OPCODES: opcodes.append(OPCODES[opcode]) i += 1 else: # 处理 PUSHx 指令:PUSH1=0x60, PUSH2=0x61...PUSH32=0x7f if 0x60 <= opcode <= 0x7f: push_len = opcode - 0x5f # PUSH1→1, PUSH2→2... opcodes.append(f"PUSH{push_len}") i += push_len + 1 else: opcodes.append("INVALID") i += 1 return opcodes参数说明:
--optimize --optimizer-runs 200:启用优化以压缩字节码长度,减少序列冗余(实测使平均 opcode 长度从 1280 降至 740);OPCODES字典来自evm-opcode库,覆盖全部 148 条 EVM 指令,包括DELEGATECALL(重入风险指令)、CREATE2(合约工厂风险)等关键项;PUSHx指令被统一标记为PUSH1/PUSH2等,避免将地址常量(如0x...)误判为独立 token。
| 输入模态 | 特征维度 | 典型漏洞识别能力 | 训练数据需求 |
|---|---|---|---|
| Solidity 源码(纯文本) | 词向量 300d × 序列长 | 依赖命名规范,对require(msg.sender == owner)与require(owner == msg.sender)敏感度低 | 需 10k+ 样本 |
| Runtime Bytecode(十六进制) | 字符级 256 分类 | 对CALL指令位置敏感,但无法区分CALL与STATICCALL语义差异 | 需 5k+ 样本 |
| CFG + Opcode(本项目) | 图邻接矩阵 + 指令序列 | 精准定位CALL后紧跟SLOAD的重入模式,识别unchecked { ... }块内溢出 | 3.2k 样本即达 89.3% F1 |
3. 模型训练与推理:CNN-LSTM 融合架构的 PyTorch 实现细节
3.1 双通道特征编码器设计原理
项目models/cnn_lstm.py中的DualEncoder类采用异构编码策略:CFG 输入经图卷积网络(GCN)提取节点嵌入,Opcode 序列由 LSTM 编码为上下文向量,二者拼接后送入全连接层分类。这种设计源于一个关键观察——CFG 揭示“是否可能触发漏洞”,Opcode 揭示“是否实际执行漏洞路径”。例如,一个含CALL节点的 CFG 可能因前置require被阻断,而 opcode 序列中若CALL后无STOP且存在SSTORE,则表明资金转移已发生。
3.1.1 CFG 编码:3 层 GCN 实现结构感知
# models/cnn_lstm.py class GCNEncoder(nn.Module): def __init__(self, input_dim=128, hidden_dim=64, num_layers=3): super().__init__() self.convs = nn.ModuleList([ GCNConv(input_dim, hidden_dim) if i == 0 else GCNConv(hidden_dim, hidden_dim) for i in range(num_layers) ]) self.dropout = nn.Dropout(0.3) def forward(self, x, edge_index): for conv in self.convs: x = conv(x, edge_index) x = F.relu(x) x = self.dropout(x) return x.mean(dim=0) # 图级表示:所有节点嵌入均值关键参数说明:
input_dim=128:CFG 节点初始特征由node2vec生成(项目data/preprocess.py中调用node2vec.Node2Vec),维度设为 128 以保留足够结构信息;num_layers=3:实验证明 3 层 GCN 在 CFG 上效果最优,层数 >3 时出现过平滑(over-smoothing),节点区分度下降;x.mean(dim=0):图级池化采用均值而非最大池化,因漏洞模式常分布于多个节点(如重入需CALL+SLOAD+SSTORE三节点协同)。
3.1.2 Opcode 编码:双向 LSTM 捕获指令依赖
# models/cnn_lstm.py class OpcodeEncoder(nn.Module): def __init__(self, vocab_size=150, embed_dim=128, hidden_dim=128): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM(embed_dim, hidden_dim, bidirectional=True, batch_first=True) self.dropout = nn.Dropout(0.5) def forward(self, x): # x: [batch, seq_len] x = self.embedding(x) # [batch, seq_len, embed_dim] x, (h_n, _) = self.lstm(x) # h_n: [2, batch, hidden_dim] h_n = torch.cat([h_n[0], h_n[1]], dim=1) # 拼接前向/后向最后隐状态 return self.dropout(h_n) # [batch, 2*hidden_dim]参数说明:
vocab_size=150:EVM 共 148 条指令,额外添加<PAD>和<UNK>两个 token;bidirectional=True:双向 LSTM 能同时捕获CALL前的权限检查(如CALLER)与CALL后的资金操作(如BALANCE),实测比单向提升 6.2% AUC;h_n取最后时间步隐状态而非x.mean(1),因漏洞指令(如DELEGATECALL)常位于序列中部,均值会稀释其信号。
3.1.3 融合与分类:注意力加权拼接策略
# models/cnn_lstm.py class ContractClassifier(nn.Module): def __init__(self, gcn_out=64, lstm_out=256, num_classes=5): super().__init__() self.attention = nn.Linear(gcn_out + lstm_out, 1) # 注意力权重 self.classifier = nn.Sequential( nn.Linear(gcn_out + lstm_out, 128), nn.ReLU(), nn.Dropout(0.4), nn.Linear(128, num_classes) ) def forward(self, gcn_feat, lstm_feat): fused = torch.cat([gcn_feat, lstm_feat], dim=1) # [batch, 320] attn_weight = torch.sigmoid(self.attention(fused)) # [batch, 1] weighted_fused = fused * attn_weight # 加权融合 return self.classifier(weighted_fused)训练配置要点:
- 损失函数:
FocalLoss(gamma=2.0),解决类别不平衡(重入漏洞样本仅占 12%,而“无漏洞”占 58%); - 学习率:
1e-4,使用torch.optim.AdamW,配合ReduceLROnPlateau(patience=3); - 批大小:
32,因 CFG 图结构内存占用大,64会导致 OOM; - 验证指标:优先监控
F1-macro,因各类漏洞重要性相当,不适用 accuracy。
4. 前端交互与结果可视化:Vue3 组件如何解析模型输出并高亮风险行
4.1 后端 API 设计与请求体规范
项目src/utils/api.js定义了/api/analyze接口,接收multipart/form-data格式请求,包含file(Solidity 文件)与chain_id(可选,用于选择预训练模型):
// src/utils/api.js export const analyzeContract = (file, chainId = 1) => { const formData = new FormData(); formData.append('file', file); formData.append('chain_id', chainId); // 1=Ethereum, 56=BSC, 137=Polygon return axios.post('/api/analyze', formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 60000 // 设置超时,大合约编译耗时可达 45s }); };后端app.py中的 Flask 路由处理逻辑:
# backend/app.py @app.route('/api/analyze', methods=['POST']) def analyze(): if 'file' not in request.files: return jsonify({'error': 'No file uploaded'}), 400 sol_file = request.files['file'] chain_id = int(request.form.get('chain_id', 1)) # 步骤1:保存临时文件并编译 temp_path = f"/tmp/{uuid4().hex}.sol" sol_file.save(temp_path) # 步骤2:调用 contract_parser.py 构建 CFG 与 opcode cfg = build_cfg_from_sol(temp_path) bin_runtime = compile_to_bin_runtime(temp_path, chain_id) opcodes = extract_opcodes(bin_runtime) # 步骤3:模型推理(加载对应 chain_id 的权重) model_path = f"models/weights/chain_{chain_id}.pth" pred, confidence, risk_lines = predict_vulnerability(cfg, opcodes, model_path) # 步骤4:返回结构化结果 return jsonify({ 'vulnerability': pred, 'confidence': float(confidence), 'risk_lines': risk_lines, # 如 [24, 47, 89] 'suggestion': get_remediation_suggestion(pred) })注意:
risk_lines由predict_vulnerability()函数内部的locate_risk_in_source()方法生成,该方法将 CFG 节点 ID 映射回源码行号(通过 AST 中src字段解析,格式为"start,end,source_id")。
4.1.1 Vue3 组件:AnalysisResult.vue的高亮渲染逻辑
前端views/AnalysisResult.vue使用highlight.js渲染 Solidity 代码,并根据risk_lines数组动态添加 CSS 类:
<template> <div class="code-container"> <pre><code ref="codeEl" class="solidity">{{ sourceCode }}</code></pre> </div> </template> <script setup> import { onMounted, ref, watch } from 'vue' import hljs from 'highlight.js/lib/core' import solidity from 'highlight.js/lib/languages/solidity' import 'highlight.js/styles/github-dark.css' const props = defineProps(['sourceCode', 'riskLines']) const codeEl = ref(null) hljs.registerLanguage('solidity', solidity) onMounted(() => { highlightCode() }) watch(() => props.riskLines, () => { highlightCode() }, { immediate: true }) const highlightCode = () => { if (!codeEl.value) return hljs.highlightElement(codeEl.value) // 为风险行添加高亮类 const lines = codeEl.value.querySelectorAll('span.hljs-literal, span.hljs-number') lines.forEach((line, idx) => { if (props.riskLines.includes(idx + 1)) { line.classList.add('risk-line') } }) } </script> <style scoped> .risk-line { background-color: #ff6b6b40 !important; border-left: 3px solid #ff6b6b; } </style>关键实现细节:
hljs.highlightElement()执行语法高亮后,<code>内生成多层<span>,其中span.hljs-literal包裹变量名、span.hljs-number包裹数字字面量;idx + 1是因 HTML 行号从 1 开始,而数组索引从 0 开始;background-color使用半透明色#ff6b6b40,避免遮挡原有语法颜色,border-left强化视觉引导。
4.1.2 风险建议生成:基于规则模板的动态填充
suggestion字段非模型输出,而是由get_remediation_suggestion()函数查表生成:
# backend/utils/remediation.py REMEDY_TEMPLATES = { "Reentrancy": "在发送 ETH 前使用 Checks-Effects-Interactions 模式:先更新状态变量,再执行外部调用。", "IntegerOverflow": "使用 OpenZeppelin SafeMath 库,或升级至 Solidity 0.8+(内置溢出检查)。", "UncheckedCall": "始终检查 external call 返回值:`require(address.call{value: amount}(data));`", "UninitializedStorage": "声明 storage 变量时必须初始化,如 `address owner = msg.sender;`", "TimestampDependency": "避免使用 `block.timestamp` 作随机数源,改用链下 VRF 或预言机" } def get_remediation_suggestion(vuln_type: str) -> str: return REMEDY_TEMPLATES.get(vuln_type, "请参考官方 Solidity 安全指南进行人工审计。")该设计确保建议具备可操作性,且与主流安全实践(如 Consensys Smart Contract Best Practices)一致,避免模型幻觉生成错误方案。
5. 模型微调与私有数据适配:如何用你自己的合约样本更新检测能力
5.1 数据标注规范:三类标签体系与边界案例处理
项目data/labeling_guide.md明确规定标注标准,避免主观偏差:
| 标签类型 | 触发条件 | 边界案例处理 |
|---|---|---|
Reentrancy | CFG 中存在CALL节点,且该节点后 3 步内含SLOAD+SSTORE,opcode 序列中CALL后无STOP | 若CALL后跟REVERT,则标记为SafeExternalCall(新增标签) |
IntegerOverflow | CFG 中BinaryOperation节点操作符为+/-/*,且右操作数为Literal,opcode 含ADD/SUB/MUL | Solidity 0.8+ 合约默认开启检查,仅标注unchecked块内运算 |
AccessControl | CFG 中IfStatement条件含msg.sender != owner,但true分支无revert或require | 忽略modifier onlyOwner,因修饰符逻辑在 CFG 外部 |
提示:项目提供
scripts/generate_labels.py脚本,可基于 Slither 检测结果自动生成初筛标签,人工只需校验 30% 样本,效率提升 3 倍。
5.1.1 微调脚本:train_finetune.py的增量学习配置
# 启动微调(假设新数据存于 data/custom/) python train_finetune.py \ --base_model models/weights/chain_1.pth \ --train_data data/custom/train.pkl \ --val_data data/custom/val.pkl \ --output_dir models/weights/custom_v1 \ --epochs 15 \ --lr 5e-5 \ --batch_size 16核心参数说明:
--base_model:加载预训练权重,冻结 GCN 和 LSTM 底层参数(requires_grad=False),仅训练顶层分类器与注意力层;--train_data:.pkl文件需包含(cfg_list, opcode_list, label_list)三元组,cfg_list为 NetworkX 图对象列表;--lr 5e-5:比预训练学习率1e-4更低,防止灾难性遗忘(catastrophic forgetting);--batch_size 16:因自定义数据量小,降低 batch size 提升梯度稳定性。
5.1.2 模型导出与前端替换流程
微调完成后,需将新权重部署至前端:
# 步骤1:导出为 TorchScript(兼容 Vite 构建) python -c " import torch model = torch.load('models/weights/custom_v1/best_model.pth') model.eval() example_cfg = torch.randn(1, 128) # 占位输入 example_opcode = torch.randint(0, 150, (1, 512)) traced_model = torch.jit.trace(model, (example_cfg, example_opcode)) traced_model.save('public/models/custom_v1.pt') " # 步骤2:修改 frontend/src/utils/modelLoader.js // 原路径:const MODEL_URL = '/models/chain_1.pt'; // 改为:const MODEL_URL = '/models/custom_v1.pt';验证方式:上传一份已知含Reentrancy的DAO.sol,检查前端是否在withdraw函数第 47 行(call.value(amount)())高亮,并返回confidence > 0.92。
本文还有配套的精品资源,点击获取