Hardhat 本地链卡住:交易 pending、区块不出块的排查与解决
Hardhat 本地链卡住通常表现为:交易发出后一直 pending、await tx.wait() 不返回、区块号停止增长。 快速诊断:区块是否在出块 # 用 cast(foundry 工具) cast block-number --rpc-url http://127.0.0.1:8545# 用 curl curl -s -X POST http://127.0.0.1:8545 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'重复执行,如果区块号不增长说明链已卡死。 原因一:开启了手动挖矿模式 hardhat.config.js 中配置了 auto: false: networks: { hardhat: { mining: { auto: false, interval: 0 } } }此时交易发出后不会自动打包,需要手动触发出块: // Hardhat 测试脚本里 await network.provider.send("evm_mine");// 一次挖多个块 await network.provider.send("hardhat_mine", ["0xa"]); // 10 个块用 curl 触发: curl -X POST http://127.0.0.1:8545 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"evm_mine","params":[],"id":1}'原因二:nonce 冲突导致交易队列堵塞 某笔 gas 过低的交易占住 nonce,后续交易全部等待。查当前 nonce: cast nonce 0x你的地址 --rpc-url http://127.0.0.1:8545最简单的解法是直接重置链: await network.provider.request({ method: "hardhat_reset", params: [] });或者重置到 fork 某个区块高度: await network.provider.request({ method: "hardhat_reset", params: [{ forking: { jsonRpcUrl: "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY", blockNumber: 20000000 } }] });原因三:fork 主网时 RPC 超时 npx hardhat node --fork https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY如果 Alchemy/Infura 限速或网络超时,Hardhat 会表现得像卡住。开启调试日志: DEBUG=hardhat* npx hardhat node --fork ...看是否有 timeout 或 rate limit 报错。 原因四:缓存/artifacts 脏数据 有时前端或脚本缓存了旧的合约地址/ABI,链本身没问题但调用失败。清理后重新编译部署: rm -rf cache artifacts npx hardhat compile npx hardhat run scripts/deploy.js --network localhost最直接的解法:重启 如果不需要保留链状态,直接重启即可: Ctrl+C npx hardhat nodeHardhat 本地链的状态不持久化,重启后从创世块重新开始。如果需要持久化,使用 hardhat_reset + snapshot(evm_snapshot / evm_revert)来管理状态。
让 Nginx 直接返回固定 JSON:不走后端的假接口
前端联调时想要一个"始终返回 VIP=1"的假接口,去写后端太重,Nginx 直接顶上就行。 最简写法 location /is_vip { default_type application/json; return 200 '{"code":200,"msg":"操作成功","data":1}'; }三行做完三件事:default_type application/json;——告诉浏览器返回的是 JSON,不是 text/plain return 200 '...'——直接给 HTTP 200 + 固定 body,不用 upstream 外层用单引号包住整个 JSON,内层用双引号,省得转义防止被前端缓存 浏览器可能把这个 200 响应缓存下来,联调时容易被误导。加个 no-store: location /is_vip { default_type application/json; add_header Cache-Control no-store; return 200 '{"code":200,"msg":"操作成功","data":1}'; }跟根据参数返回不同内容 要根据 query string 分支返回,可以用 if + map: map $arg_uid $vip_result { default '{"code":200,"msg":"操作成功","data":0}'; "1" '{"code":200,"msg":"操作成功","data":1}'; "2" '{"code":200,"msg":"操作成功","data":1}'; }location /is_vip { default_type application/json; return 200 $vip_result; }访问 /is_vip?uid=1 返回 VIP=1,其它返回 VIP=0。 需要更复杂的 mock 逻辑就该上 OpenResty 或者写真后端了——Nginx 原生更适合"固定返回"这类死数据。 一句话总结 default_type application/json + return 200 '{...}'。联调阶段最快的 mock 接口,比启 Node/Python 都省事。
Nginx 直接返回固定 JSON:return 指令与 default_type 配置
Nginx 可以通过 return 指令直接返回固定响应,无需经过后端服务,适合 mock 接口、健康检查、维护模式等场景。 基本写法 location /api/status { default_type application/json; return 200 '{"code":200,"msg":"ok","data":null}'; }default_type application/json 设置响应的 Content-Type,不设置则默认为 text/plain return 200 '...' 返回 HTTP 200 和固定响应体 响应体用单引号包裹,内部 JSON 用双引号禁用缓存 接口类响应通常不应该被浏览器缓存: location /api/health { default_type application/json; add_header Cache-Control "no-store, no-cache"; return 200 '{"status":"healthy","timestamp":"dynamic"}'; }返回不同 HTTP 状态码 # 404 错误响应 location /api/v1/ { default_type application/json; return 404 '{"code":404,"msg":"接口不存在"}'; }# 维护模式 503 location / { default_type application/json; return 503 '{"code":503,"msg":"系统维护中,请稍后再试"}'; }多行 JSON:用变量 return 本身不支持换行,多行 JSON 用变量存储: location /api/config { default_type application/json; set $json_body '{"version":"1.0","debug":false,"features":{"pay":true,"upload":true}}'; return 200 $json_body; }根据请求方法区分 location /api/mock { default_type application/json; add_header Access-Control-Allow-Origin *; if ($request_method = OPTIONS) { add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type"; return 204; } return 200 '{"code":200,"data":[]}'; }与 echo 模块对比 Nginx 标准版不支持 echo 指令,return 是不需要额外模块的内置方案。OpenResty(含 ngx_http_echo_module)可以用: location /api/test { default_type application/json; echo '{"msg":"hello"}'; }标准 Nginx 统一使用 return 即可满足固定响应需求。
Solidity 合约安全优化:SimpleDeposit 代码审查
一份简单的 EVM 充值合约,在上主网前需要做几项安全优化。 原始合约 // SPDX-License-Identifier: MIT pragma solidity ^0.8.28;contract SimpleDeposit { address public owner; bool public paused; event Deposit( address indexed user, uint256 amount, uint256 indexed orderId, uint256 timestamp ); modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } constructor() { owner = msg.sender; } function deposit(uint256 orderId) public payable { require(!paused, "Paused"); require(msg.value > 0, "Amount zero"); emit Deposit(msg.sender, msg.value, orderId, block.timestamp); } function withdraw(uint256 amount) external onlyOwner { (bool success, ) = payable(owner).call{value: amount}(""); require(success, "Transfer failed."); } function setPaused(bool _paused) external onlyOwner { paused = _paused; } receive() external payable { emit Deposit(msg.sender, msg.value, 0, block.timestamp); } }优化 1:withdraw 前检查余额 余额不足时 call 会失败,但报错信息不明确: function withdraw(uint256 amount) external onlyOwner { require(address(this).balance >= amount, "Insufficient balance"); (bool success, ) = payable(owner).call{value: amount}(""); require(success, "Transfer failed"); }优化 2:补充 Withdraw 事件 充值有事件,提现没有事件,后端无法追踪提现记录: event Withdraw( address indexed to, uint256 amount, uint256 timestamp );function withdraw(uint256 amount) external onlyOwner { require(address(this).balance >= amount, "Insufficient balance"); (bool success, ) = payable(owner).call{value: amount}(""); require(success, "Transfer failed"); emit Withdraw(owner, amount, block.timestamp); }优化 3:自定义 error 替代 require 字符串 require("string") 会把字符串编码进 calldata,消耗更多 gas。用 custom error 替代: error NotOwner(); error ContractPaused(); error ZeroAmount(); error InsufficientBalance(); error TransferFailed();modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; }function deposit(uint256 orderId) public payable { if (paused) revert ContractPaused(); if (msg.value == 0) revert ZeroAmount(); emit Deposit(msg.sender, msg.value, orderId, block.timestamp); }自定义 error 平均节省 30~50% gas。 优化 4:owner 转移功能 现在 owner 一旦设置就无法更改,新增两步转移以防误操作: address public pendingOwner;event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);function transferOwnership(address newOwner) external onlyOwner { require(newOwner != address(0), "Zero address"); pendingOwner = newOwner; }function acceptOwnership() external { require(msg.sender == pendingOwner, "Not pending owner"); emit OwnershipTransferred(owner, pendingOwner); owner = pendingOwner; pendingOwner = address(0); }优化 5:ReentrancyGuard 防重入 withdraw 使用 call,存在重入风险(尽管对当前合约逻辑影响有限): bool private _locked;modifier nonReentrant() { require(!_locked, "Reentrant call"); _locked = true; _; _locked = false; }function withdraw(uint256 amount) external onlyOwner nonReentrant { require(address(this).balance >= amount, "Insufficient balance"); (bool success, ) = payable(owner).call{value: amount}(""); require(success, "Transfer failed"); emit Withdraw(owner, amount, block.timestamp); }或者直接引入 OpenZeppelin: import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";contract SimpleDeposit is ReentrancyGuard { // ... function withdraw(uint256 amount) external onlyOwner nonReentrant { ... } }可升级代理(按需) 如果合约逻辑以后可能更新,考虑 UUPS 代理模式。但对于简单充值合约,升级能力也意味着合约不再是纯粹的"不可更改",用户信任模型会变化,需要权衡。 最终版本概览优化点 作用withdraw 余额检查 明确报错,节省 gasWithdraw 事件 链上可追踪提现记录Custom error 减少 gas 30~50%两步 owner 转移 防止误转到无效地址ReentrancyGuard 防重入攻击
数据库版本范围查询:SemVer 比较与漏洞影响版本存储设计
版本号不能用字符串比较 -- 错误:字符串排序,结果是 2.10.0 < 2.9.0 SELECT '2.10.0' < '2.9.0' -- 返回 1(true)-- 正确:语义化版本(SemVer)比较 -- 2.10.0 > 2.9.0(major.minor.patch 各位独立比较)版本范围字符串如 >= 2.0-beta9 < 2.15.0 也无法直接用 SQL BETWEEN 或 LIKE 处理。 方案一:数据库存原始版本,程序用 SemVer 库比较 表结构简单: -- 资产表 asset(id, name, product_version)-- 漏洞规则表 vuln_rule(id, cve, affected_version_raw)Java 使用 semver4j 比较: <dependency> <groupId>org.semver4j</groupId> <artifactId>semver4j</artifactId> <version>5.3.0</version> </dependency>List<Asset> assets = assetMapper.selectAll(); List<VulnRule> rules = vulnMapper.selectAll();for (Asset asset : assets) { for (VulnRule rule : rules) { if (VersionMatcher.match(asset.getVersion(), rule.getAffectedVersion())) { // 命中漏洞 } } }优点:存储简单,逻辑清晰。 缺点:全表扫描,百万资产时性能差。 方案二:拆成上下界存数据库(企业推荐) 把 >= 2.0-beta9 < 2.15.0 拆成字段: CREATE TABLE vulnerability_affected_version ( id BIGINT PRIMARY KEY, vuln_id BIGINT, min_version VARCHAR(50), max_version VARCHAR(50), min_include TINYINT, -- 1=>=, 0=> max_include TINYINT -- 1=<=, 0=< );Spring4Shell 的两个受影响区间存成两条记录: INSERT INTO vulnerability_affected_version VALUES (1, 'CVE-2022-22965', '5.2.0', '5.2.20', 1, 0), -- 5.2.0 <= ver < 5.2.20 (2, 'CVE-2022-22965', '5.3.0', '5.3.18', 1, 0); -- 5.3.0 <= ver < 5.3.18查询时取出所有规则,在程序里用 SemVer 库比较,兼容 beta、rc、snapshot 等预发布版本。 方案三:版本号数字化后 SQL 直接查 把 2.15.0 转成整数: // 2 * 1_000_000 + 15 * 1_000 + 0 = 2_015_000 int versionNum = major * 1_000_000 + minor * 1_000 + patch;数据库存两列: ALTER TABLE asset ADD COLUMN version_num BIGINT; ALTER TABLE vuln_rule ADD COLUMN min_num BIGINT, ADD COLUMN max_num BIGINT;-- 建索引 CREATE INDEX idx_version_num ON asset(version_num);直接 SQL 查询: SELECT a.*, v.cve FROM asset a JOIN vuln_rule v ON a.version_num >= v.min_num AND a.version_num < v.max_num;优点:可建索引,百万级数据也快。 缺点:不支持 beta、rc 等预发布后缀。 方案对比方案 速度 支持预发布 复杂度程序 SemVer 比较 全表扫 是 低拆字段 + SemVer 库 取出再比 是 中版本数字化 + SQL 索引查询 否 中推荐设计(漏洞管理平台) 参考 NVD、OSV、Snyk 等平台的数据结构: -- 漏洞主表 vulnerability(cve, severity, description, published_date)-- 受影响版本(一个漏洞可能有多个区间) vulnerability_affected_version( vuln_id, product, -- 组件名 min_version, max_version, min_inclusive, max_inclusive )接入外部数据源(NVD API、OSV API)时,它们的 JSON 结构也是这样分字段的,迁入成本最低。
