多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

health-blockchain实战:链上存证与IPFS存储的完整Demo

health-blockchain实战:链上存证与IPFS存储的完整Demo 简介这份资源面向区块链初学者与医疗信息化方向的开发者展示区块链与IPFS集成的基础实现思路。项目基于以太坊、Truffle、Ganache、MetaMask与MyEtherWallet构建通过Solidity合约让医生从去中心化服务器检索健康记录的IPFS ID并支持患者下载对应记录适合作为去中心化健康记录追踪的入门实践参考。压缩包共30个文件约1.15MB包含16张png操作截图、4个js脚本、3个sol智能合约、3个json配置及license、md说明等覆盖合约编写、部署配置与前端交互各环节。目前已有176人学习浏览。读者可借此了解IPFS与链上合约的协作方式、Truffle项目目录结构及Ganache本地测试流程并参考截图完成环境搭建与合约部署为后续补充加密与安全机制打下基础。1. 从一份健康数据上链 Demo 说起health-blockchain 到底能跑通什么如果你正在找一个能把「链上存证 链下存储」讲清楚的练手项目health-blockchain 这个仓库值得花一个下午拆一遍。它做的事情很具体把一份健康记录文件的哈希写进以太坊合约文件本体丢给 IPFS前端用 MetaMask 签名发起交易再用 MyEtherWallet 验证合约调用结果。整套流程不依赖任何中心化后端合约、存储、钱包三层各司其职。它适合两类人一是刚学完 Solidity 语法、想找个完整链路练手的开发者二是需要给现有系统加「数据不可篡改」能力、但还没想清楚 IPFS 和链上到底怎么分工的工程师。仓库本身不复杂但麻雀虽小Truffle 编译部署、Ganache 本地链、IPFS 节点接入、前端合约实例化这几块都齐了。跑通一遍你对「什么该上链、什么不该上链」会有比看十篇科普更实在的判断。2. 环境搭建与合约部署Truffle Ganache 的最小闭环2.1 为什么选 Truffle 而不是 Hardhat这个仓库用的是 Truffle 工具链不是现在更流行的 Hardhat。原因很直接项目成型时间较早Truffle 的truffle migrate和truffle console对新手更友好配置文件truffle-config.js结构扁平改网络、改编译器版本一目了然。Hardhat 的插件生态确实更强但如果你只是想快速验证「合约能不能存哈希、能不能读回来」Truffle 的认知负担更低。常见做法是本地开发用 Ganache 起一条内存链它默认给你 10 个带 100 ETH 的测试账户私钥直接暴露在终端里方便导入 MetaMask。注意 Ganache 的 RPC 地址默认是http://127.0.0.1:7545而 Truffle 默认连的是8545这两个端口不一致是新手第一个翻车点。2.2 从零把合约跑起来先确认 Node.js 版本Truffle 对 Node 16 以上支持较好Node 18 也能跑但部分老版本 Truffle 在 Node 20 上会有ERR_OSSL_EVP_UNSUPPORTED报错。我一般会锁 Node 16 或 18。# 全局安装 Truffle 和 Ganache CLI npm install -g truffle npm install -g ganache # 启动本地链指定端口和网络ID ganache --port 7545 --networkId 5777 --deterministic--deterministic这个参数值得说一句它让每次启动生成的账户和私钥完全一致省得你每次重启链都要重新往 MetaMask 里导账户。--networkId 5777是 Ganache 的惯用网络 IDMetaMask 添加自定义网络时填这个值。// truffle-config.js 关键片段 module.exports { networks: { development: { host: 127.0.0.1, port: 7545, // 必须和 Ganache 启动端口一致 network_id: 5777, // 对应 Ganache 的 networkId }, }, compilers: { solc: { version: 0.8.19, // 按合约 pragma 声明调整 }, }, };配置里port和network_id是最容易写错的两个参数。端口写错truffle migrate会直接报连接超时network_id 写错MetaMask 会提示「无法连接到该网络」。改完配置后先跑truffle compile确认合约能编译通过再跑truffle migrate --reset部署。# 编译并部署到本地 Ganache truffle compile truffle migrate --reset --network development # 进入控制台验证合约方法 truffle console --network development进入 console 后可以手动调一下存哈希和读哈希的方法确认合约逻辑没问题。这一步很多人跳过结果前端调不通时不知道是合约问题还是前端问题。先在这里把set和get走一遍后面排错会省很多时间。2.3 合约里到底存了什么这个项目的合约核心就两个动作存一个字符串文件哈希按地址或 ID 读回来。它不会把健康记录原文上链因为链上存储成本极高而且一旦写入无法删除涉及隐私的数据绝不能直接上链。合约里通常是一个mapping结构key 是记录编号或用户地址value 是 IPFS 返回的 CID 或文件哈希。参数设置上哈希用string类型存即可长度固定的话也可以用bytes32省 gas。但bytes32对前端不友好需要额外做转换练手项目用string更直观。如果你要改成bytes32记得在合约里加require(bytes(_hash).length 32)做长度校验否则超长字符串会被截断这是血泪经验。3. IPFS 接入与文件上传CID 怎么和链上记录对上3.1 IPFS 节点的两种接法IPFS 接入有两种常见方式一是本地跑一个 IPFS 节点通过ipfs daemon启动API 默认在5001端口二是用公共网关或第三方 Pin 服务。本地节点的好处是数据完全自己掌控坏处是节点下线后文件可能被垃圾回收。练手阶段我建议本地节点因为你能看到完整的add和cat过程。启动本地节点# 初始化 IPFS 仓库只需一次 ipfs init # 启动守护进程开放 API 和网关 ipfs daemon启动后终端会显示 API 地址http://127.0.0.1:5001和网关地址http://127.0.0.1:8080。前端通过ipfs-http-client连接这个 API 端口注意不是网关端口。这两个端口搞混是第二个高频翻车点API 用于上传和查询网关用于浏览器直接访问文件。3.2 上传文件并拿到 CID// 使用 ipfs-http-client 上传文件 const { create } require(ipfs-http-client); // 连接本地 IPFS 节点的 API 端口 const ipfs create({ url: http://127.0.0.1:5001/api/v0 }); async function uploadToIPFS(fileBuffer) { // add 方法返回一个异步迭代器取第一个结果 const result await ipfs.add(fileBuffer); // result.path 就是 CID形如 QmXxx... console.log(CID:, result.path); return result.path; }ipfs.add接收 Buffer、字符串或文件流。返回的result.path是 CID v0 格式以Qm开头。如果你用ipfs.add的cidVersion: 1选项会得到以b开头的 CID v1两者在网关访问时路径格式略有不同。练手项目保持默认 v0 即可兼容性更好。拿到 CID 后把它传给合约的存储方法链上就留下了一条「某文件哈希对应某 CID」的记录。验证时用ipfs.cat(cid)能把文件内容读回来再算一次哈希和链上存的对比一致就说明整个链路没被篡改。3.3 前端怎么把 IPFS 和合约串起来前端通常用web3.js或ethers.js实例化合约。这个仓库用的是 web3.js配合 MetaMask 注入的window.ethereum。关键步骤是先请求账户授权再用账户实例化合约最后调方法发交易。// 前端连接 MetaMask 并调用合约 async function storeHash(cid) { // 请求账户授权 const accounts await window.ethereum.request({ method: eth_requestAccounts, }); const web3 new Web3(window.ethereum); const contract new web3.eth.Contract(abi, contractAddress); // 发交易from 必须是已授权账户 await contract.methods.setHash(cid).send({ from: accounts[0] }); }send({ from: accounts[0] })里的from不能省也不能填一个没授权的地址否则 MetaMask 会弹窗报错。交易发出后MetaMask 会弹出确认框确认后等几秒链上打包receipt里能看到交易哈希。如果一直 pending检查 Ganache 是否还在运行以及 MetaMask 网络是否切到了本地链。4. 避坑与排查五个让 Demo 跑不起来的典型问题4.1 MetaMask 连不上本地链现象是 MetaMask 添加自定义网络后一直转圈或者提示「无法获取账户」。原因通常是 RPC 地址填成了http://localhost:7545而 Ganache 只监听了127.0.0.1或者 chainId 和 networkId 填反了。解决方法是 RPC 填http://127.0.0.1:7545chainId 填1337Ganache 默认networkId 填5777。如果还不行重启 Ganache 和浏览器。4.2 合约部署后地址对不上现象是前端调合约报「返回地址没有合约代码」。原因是truffle migrate每次--reset都会重新部署合约地址变了但前端里写死的地址没更新。解决办法是不要在前端硬编码地址而是从build/contracts/YourContract.json里读networks字段或者每次部署后手动同步一次。我一般会在部署脚本里把地址写到一个config.js前端引这个文件。4.3 IPFS 上传成功但网关访问 404现象是ipfs.add返回了 CID但浏览器打开http://127.0.0.1:8080/ipfs/QmXxx显示 404。原因是本地节点默认不自动 Pin 新文件垃圾回收可能已经把它清了或者网关端口被占用。解决方法是上传后显式调ipfs.pin.add(cid)并确认ipfs daemon终端没有报错。如果端口冲突用ipfs config Addresses.Gateway改端口。4.4 交易一直 pending 不打包现象是 MetaMask 显示交易已提交但 Ganache 终端没有新块。原因是 Ganache 默认是即时出块但如果之前手动改过blockTime或者用了--miner.blockTime参数就会变成定时出块。解决方法是重启 Ganache 不加额外参数或者用ganache --miner.blockTime 0恢复即时出块。另外检查 MetaMask 的 gas 费是否设得太低本地链一般用默认值即可。4.5 合约编译报 solc 版本不匹配现象是truffle compile报「Source file requires different compiler version」。原因是合约头部的pragma solidity ^0.8.0和truffle-config.js里指定的solc.version不一致。解决方法是把配置里的版本改成合约 pragma 允许的范围比如0.8.19。如果合约用了^0.8.0配置写0.8.19没问题如果合约写死了0.6.0配置也得跟着改。改完记得删掉build目录重新编译。5. 进阶技巧用脚本批量验证链上记录与 IPFS 文件的一致性跑通单条记录后真正有价值的是批量校验。我一般会写一个 Node 脚本遍历合约里存过的所有 CID逐个从 IPFS 拉回文件、算哈希、和链上记录比对。这个脚本能当回归测试用每次改完合约或前端跑一遍确认没有破坏已有记录。// verify.js 批量校验链上哈希与 IPFS 文件 const Web3 require(web3); const { create } require(ipfs-http-client); const crypto require(crypto); const ipfs create({ url: http://127.0.0.1:5001/api/v0 }); const web3 new Web3(http://127.0.0.1:7545); async function verifyAll(contractAddress, abi, totalCount) { const contract new web3.eth.Contract(abi, contractAddress); for (let i 0; i totalCount; i) { // 假设合约有 getHash(index) 方法 const onChainHash await contract.methods.getHash(i).call(); // 从 IPFS 拉回文件内容 const chunks []; for await (const chunk of ipfs.cat(onChainHash)) { chunks.push(chunk); } const fileBuffer Buffer.concat(chunks); // 重新计算哈希 const localHash crypto .createHash(sha256) .update(fileBuffer) .digest(hex); console.log(记录 ${i}: 链上 ${onChainHash} | 本地 ${localHash}); } } verifyAll(0xYourContractAddress, abi, 10);脚本里totalCount需要你根据实际存了多少条记录来传合约如果没提供计数方法可以加一个recordCount状态变量每次setHash时自增。ipfs.cat返回的是异步迭代器必须用for await收集直接await会拿到迭代器对象而不是内容这是第三个容易翻车的地方。参数上crypto.createHash(sha256)的算法要和上传时保持一致。如果上传时用的是sha256校验也用sha256如果用的是keccak256Node 原生 crypto 不支持得用ethers.utils.keccak256。这个细节不注意校验永远对不上但你又找不到原因属于典型的玄学问题。还有一个实用技巧把校验结果写进一个 CSV方便对比。我习惯在脚本里加fs.appendFileSync(verify-log.csv, ...)每次跑完看一眼哪些记录不一致。不一致的记录优先查 IPFS 节点是否被清理过其次查合约是否被重新部署过导致索引错位。从那以后我每次改完合约或前端都强制走一遍这个校验脚本确认链上链下数据还对得上。希望帮到你。本文还有配套的精品资源点击获取
返回列表