Lab 2 Web3.js 与 Hardhat:把 Remix 那个按钮拆开
同一件事在命令行做两遍,学的是「源码 → 编译产物 → 交易 → 节点 → 链上状态」每一环归谁。
一句话版
Lab 1 是在浏览器里点按钮把合约部署上链,这份 Lab 把同一件事换成命令行做两遍——先 Web3.js 后 Hardhat,你要敲的命令不到十条,真正要学的是那条流水线上每一环归谁管。
题目地图
53 页分四段,命令都封在 Docker 镜像里,不用自己装 Node、solc 和 Hardhat。末页那句话划定了考试边界:Lab test 不要求写智能合约,要求知道怎么编译和执行合约。
| 段落 | 页码 | 学什么 | 主要失分点 |
|---|---|---|---|
| Lab 1 回顾 | p.4–13 | 合约的不可变性、交易原子性 | 把「不可变」理解成状态也不能改 |
| Web3.js 原理 | p.14–19 | 编译产物、节点、三步流程 | 说不清 ABI 和 bytecode 各去哪 |
| Web3.js 实操 | p.27–42 | 四条命令 + 浏览器对账 | 以为查 counter 也算一笔交易 |
| Hardhat | p.43–51 | 工程化工具链 + ERC-721 | 不知道 8 行合约的逻辑从哪来 |
前两段回答「编译」,后两段回答「执行」,正好覆盖考试边界的两个词。
概念卡
1. 编译产物:ABI 与 bytecode
人话定义:本 Lab 主要使用 solc 的 bytecode 与 ABI 两类输出;编译器还能输出其他信息。
例子:bytecode 是 EVM 真正执行的低级代码,随创建交易上链;ABI 是一份接口描述,告诉调用方这个合约有哪些函数、参数怎么编码、返回值怎么解码,它留在本地。Remix 把这一步藏在一个按钮后面,所以 Lab 1 做完你也不知道产物长什么样;命令行这一遍会把两样东西都摆到面前。
常见误解
以为 ABI 文件必须随合约上链 → ABI 描述供客户端编码、解码,不是部署时必须储存的文件。补充:掌握函数签名和参数类型也能手工编码,不必拥有完整 ABI JSON;Etherscan 源码验证是核对源码与字节码,不是协议强制的调用前提。
2. Web3.js 与 RPC 节点(JSON-RPC)
人话定义:Web3.js 是一组 JavaScript 库,通过 HTTP、IPC 或 WebSocket 连上一个以太坊节点,按通用 JSON-RPC 规范说话。
例子:链路是「你的脚本 → RPC 节点 → 以太坊 P2P 网络」。脚本从不直接接触 P2P 网络,它只跟节点说话,广播由节点代劳。节点有两种来源:自己搭一台(要同步链数据,硬盘和时间都不便宜)或者用 Infura 这类公共服务。Lab 1 里 MetaMask 扮演的就是这个角色,只是它内置了节点地址,你没感觉到。
常见误解
以为 Lab 1 没用过 Infura → MetaMask 默认走的就是它,这次只是要你自己注册一个 key。注册时网络类别选 Web3 API 而非 IPFS,拿到的端点形如 https://sepolia.infura.io/v3/<key>,末尾那 32 位就是配置命令要填的第一个参数。
3. view 函数与「什么才算一笔交易」
人话定义:要让状态变化持久写入链上,就提交 transaction(交易);只想模拟结果可用 call(调用)。view 不等于免费开关:它限制状态修改,不决定调用是否上链(补充)。
例子:Incrementer 合约里 increment 和 reset 会改 counter,各产生一笔交易和一个交易哈希;getCounter 标了 view,承诺不改状态,节点用 eth_call 本地执行,不上链、不花 gas。所以做完「一次 increment、两次查询、一次 reset」之后去浏览器看,交易列表里只有 2 笔。
常见误解
以为 failed 等于「没发生交易」 → 已上链但顶层失败的交易仍消耗 nonce、支付已用 gas,合约状态回滚。REVERT 保留未用 gas,out of gas 耗尽所供 gas;提交前就因 intrinsic gas 不足被拒绝,则根本未上链(补充)。
4. Hardhat 工程与继承(Inheritance)
人话定义:Hardhat 是命令行的合约开发环境,把生命周期管理、自动测试、脚本化部署、网络管理、交互控制台、外部脚本执行整合成一套脚手架。
例子:CompCoin 合约正文只有 8 行,却是一个完整的 ERC-721 非同质化代币——所有权、转账、授权、枚举的逻辑全部来自 import 进来的 OpenZeppelin ERC721Enumerable,本合约只做了两件事:给父类构造函数传名字 CompCoin 和符号 COMP,然后 _safeMint(msg.sender, 4142) 给部署者铸一枚 token。Remix 也能解析支持的 npm 导入;Hardhat 的价值在于可复现的依赖、测试和部署工程。
常见误解
把 _safeMint 和 _mint 当同一个 → _safeMint 多一步检查:接收方若是合约,必须通过 onERC721Received 接收检查,否则拒绝铸造;这降低误转风险,但不保证接收合约一定有正确的取出逻辑。
逐题拆解
回顾那十页里有两条比 Lab 1 讲得更精确的定义。第一条是「部署后不可变」后面括号里补的 (At the bytecode level)——字节码改不了,但合约存储的状态天天在变,本 Lab 的 counter 就是活例子。第二条是合约能读取执行上下文,也可调用其他合约;历史块哈希有可查询范围。它不能自己访问网页,block.timestamp 也不是任意精度的外部时钟,这就是真实项目需要预言机的原因。
Web3.js 那一段把 Remix 的一个按钮拆成三步:solc 在本地编译、Web3.js 把字节码和交易发给节点、公共节点服务广播到网络。三步各自的产物要能对上号——第一步出 ABI + bytecode,第二步部署时带创建字节码和构造参数,调用既有合约时才填它的地址和编码数据,第三步才轮到链。
实操那一段是考区。四条命令:deploy 返回一个链上地址,后面三条命令的第一个参数都是它;increment <地址> <值> 返回交易哈希;counter <地址> 只打印一个数、什么哈希都没有;reset <地址> <值> 又是一个哈希。课件的演示数是初值 4142,increment 1222 之后查得 5364,再 reset 成 5521。配置那一步要交两个东西:Infura 的 API key(32 位)和 MetaMask 导出的私钥(64 位十六进制,不带 0x)。为什么现在要私钥——部署和调用都要签名,Lab 1 里签名由 MetaMask 弹窗完成,现在脚本得自己签。
Hardhat 那一段是同一条流水线换一套工具再走一遍。npm run build 实际是 hardhat compile,输出会告诉你编译器版本 0.8.29 和 EVM 目标 cancun;npm run deploy 实际是 hardhat run script/deploy.js --network sepolia,那个 --network sepolia 说明 RPC 地址、Sepolia 的 chain ID 和签名私钥都写在配置文件里、由配置那步注入。部署完去浏览器看合约页,多出一栏 Token Tracker 显示 CompCoin (COMP)——浏览器检测到字节码实现了 ERC-721 接口,自动识别成代币合约;Incrementer 的页面没有这一栏。
课件里的坑
- [课件有误] 创建交易的
to字段为空,不是零地址0x000…000。答题应明确「contract creation: no recipient」,不要为了贴近课件而写成向零地址转账。 - [课件留白]
reset函数没有任何权限检查,链上任何人都能把 counter 改成任意值。教学合约不管这个,真实合约这里必须加onlyOwner之类的访问控制。 - [补充]
counter已经声明为public,编译器会自动生成一个同名 getter,严格说getCounter()是多余的。课件保留它是为了演示view修饰符。 - [补充]
--rm会在容器退出后删除容器及其可写层;挂载卷或宿主机文件不因此消失。若配置只存在容器内,重建容器后需重新配置;Docker 是否要 sudo 取决于本机权限配置。 - [补充] 只用全新、无真实资产的测试账户私钥,绝不提交仓库、截图或发给他人。新建账户不等于撤销旧密钥;泄漏后应迁移旧账户资产和权限。64 位 hex 是私钥字节的文本表示;是否带
0x按本 Lab 脚本要求,不是通用规则。 - [课件有误] Mac 安装页的标题错写成 “Install Docker Desktop on Windows”,正文把
Docker.dmg写成了 “Docer.img”;Windows 页的 “checkboxs”、重置脚本输出的 “Reseting” 也都少字母。都是笔误,按流程做即可,别以为自己下错了文件。 - [补充] p.38 之后发交易的账户是
0x4F54…E071,和 p.10 Remix 里连着的0x7223…FB1F1不是同一个:TA 换了一个账户导出私钥。这不影响流程,但对照 Etherscan 时别拿 p.10 的地址去找那两笔交易,找不到不是你做错了。
课后 10 分钟:考点复习
这 10 分钟怎么用:合上页面,先默写三样——solc 的两样输出各去哪、生成一笔调用交易需要哪三个输入、哪些操作会上链;再做下面的「变式题」,重点是数清交易笔数;最后回查两处最爱错的地方——私钥不带 0x、查询不产生交易。
必背
- 本 Lab 使用 solc 的两类输出:bytecode 随创建交易上链交给 EVM 执行,ABI 不上链、留在本地供调用方编码参数和解码返回值。
- Web3.js 只跟一个 RPC 节点说话,由节点替你广播到 P2P 网络;三种连法是 HTTP、IPC、WebSocket,实现的是通用 JSON-RPC 规范。
- 本 Lab 用合约地址、ABI、方法参数构造调用;签名交易还需 nonce、gas、chain ID 等字段,部分由工具补齐。
- 本 Lab 查询通过 eth_call 模拟执行,不产生链上交易费;view 只约束不修改状态,被链上交易调用仍消耗 gas。
- 顶层执行失败回滚合约执行状态,但 nonce 与已用 gas 费用不回滚;REVERT 保留未用 gas,执行耗尽 gas 则消耗所供 gas。
- 配置命令的两个参数是 Infura API key 和私钥,前者 32 位、后者 64 位十六进制且不带 0x 前缀;私钥的用途是给交易签名。
- Incrementer 的 counter 初值 4142,increment 1222 之后是 5364;一次 increment 加一次 reset 在浏览器上是两笔交易,中间的查询不留痕迹。
- CompCoin 只有 8 行是因为继承了 OpenZeppelin 的 ERC721Enumerable;构造函数里的 _safeMint(msg.sender, 4142) 给部署者铸一枚 token ID 为 4142 的 NFT。
完整例题
把课件演示的那一轮从头对账一遍:部署 → increment 1222 → 查 counter → reset 5521 → 再查 counter,最后去浏览器核对。
- 部署。
deploy打印出合约地址0x85bf…9ecd,构造函数收到 4142,所以 counter 初值是 4142。这一步是一笔创建交易,to为空。 - increment 1222。脚本用 ABI 把
increment(1222)编码进 data 字段,用私钥签名,经节点广播,等上链后打印交易哈希。counter 变成 4142 + 1222 = 5364。 - 查 counter。打印 “The current counter stored in the contract is: 5364”,没有交易哈希——
getCounter是 view 函数,节点本地算完就返回。 - reset 5521。又一笔交易,counter 直接被设成 5521。再查一次仍然没有哈希。
- 浏览器对账。合约页显示 “Latest 2 from a total of 2 transactions”:一笔 Reset 在区块 11708700,一笔 Increment 在区块 11708683。三处可验算——① 五步操作共三笔链上交易(部署、increment、reset),截图列表展示后两笔,两次查询无痕;② 两笔相隔 11708700 − 11708683 = 17 个区块,页面显示时间差约 4 分钟,17 × 12 s ≈ 3.4 min,与 12 秒 slot 的数量级相符;可能漏 slot,不能用区块差推精确耗时;③ 两笔的 Value 列都是 0 ETH,它们只带 data 不带钱。
部署的创建交易也存在链上。课件截图列表只显示两笔调用,可从 Contract Creator 链接另查创建交易;不要把页面展示范围当协议规则。
变式题(先自己做)
换一串操作自己数:合约部署完 counter 初值仍是 4142,然后依次执行 increment 379 → 查 counter → reset 900 → increment 121 → 查 counter。
(1) 两次查询各打印什么数?(2) 浏览器的交易列表里会出现几笔交易,Value 列分别是多少?(3) 假设那两笔 increment 和一笔 reset 分别落在区块 X、X+9、X+26,假设没有漏 slot,最后一笔约比第一笔晚多久?(4) 如果第二笔 increment 已上链、顶层执行时耗尽 gas,counter 停在多少、这笔 gas 退不退?
提示
第 (1) 问注意 reset 是直接赋值而不是加减。第 (2) 问数清哪些操作改了状态——创建交易不在这个列表里。第 (3) 问用 Sepolia 12 秒一个块去乘。第 (4) 问回想交易的原子性那一条。
参考答案与自检(非官方评分标准)
自检要点:① reset 是赋值;② 本 Lab 查询走 eth_call;③ Value 与 gas fee 不同,0 ETH 也可能有手续费;④ 区分执行回滚、nonce 和费用。
(1) 第一次打印 4142 + 379 = 4521;reset 900 把 counter 直接设成 900,再加 121,第二次打印 1021。
(2) 3 笔:两笔 increment、一笔 reset,两次查询走 eth_call、不上链。三笔的 Value 列都是 0 ETH——它们只在 data 字段里带方法调用,没转任何币。此处不含已经完成的部署;算上部署总共 4 笔链上交易。
(3) 区块号差 (X+26) − X = 26,无漏 slot 时,26 × 12 s = 312 s ≈ 5.2 分钟;实际应读两个区块时间戳。
(4) counter 停在 900;已上链的第二笔 increment 顶层耗尽 gas,执行状态回滚、所供 gas 被消耗,nonce 不回滚。它仍显示 Failed,所以部署后的调用交易仍共 3 笔。若提交前被拒绝,则不能这样计数(补充)。
闪卡自测
1. 「合约部署后不可变」在哪个层面成立?
字节码层面。合约存储的状态照样变——counter 每次 increment 都在变,并不违反这条。工程上还可以用代理模式把逻辑地址指向新合约,不过那超出课件范围。
2. 合约能读到哪些上下文?它能去查一个网页吗?
可读自己的状态与交易、区块上下文,并能调用其他合约;不能直接访问网页。外部数据需通过预言机等机制输入链上,不能把「不能联网」误解为「不能读别的合约」(补充)。
3. solc 输出的两样东西各给谁用?
bytecode 给 EVM,随创建交易上链;ABI 给调用方(Web3.js、前端),不上链,用来编码调用参数和解码返回值。
4. Web3.js 和 P2P 网络之间隔着什么?
一个 RPC 节点。脚本通过 HTTP / IPC / WebSocket 按 JSON-RPC 规范跟它说话,由它把交易广播到 P2P 网络。节点要么自建,要么用 Infura 这类公共服务。
5. 为什么命令行脚本需要私钥,而 Remix 里不需要手动提供?
部署和调用都要对交易签名。Remix 里签名由钱包弹窗完成,钱包握着私钥;换成脚本之后没人替你签,本 Lab 将私钥交给脚本签名;真实工程也能用外部签名器。只用测试账户,结束后停止使用它。
6. 四条命令各返回什么?
deploy 返回合约地址;increment 和 reset 各返回一个交易哈希;counter 只打印当前值,没有哈希,因为这个脚本用 eth_call 查询,而不是发送交易。
7. CompCoin 只有 8 行,ERC-721 的转账和授权逻辑从哪来?
从 import 的 OpenZeppelin ERC721Enumerable 继承而来。合约自己只做两件事:给父构造函数传名字和符号、给部署者铸一枚 token。
8. 为什么 CompCoin 的合约页有 Token Tracker,Incrementer 的没有?
浏览器会检测字节码是否实现了 ERC-721 等代币接口,命中就自动识别成代币合约并读出名字和符号显示为 Token Tracker。Incrementer 不是代币,所以没有这一栏。
下一步
第 4 周公众假期停课,接下来两次 Lab 是 Project 介绍。这两周把两条流水线各自完整跑一遍,另外做三件课件没要求的事。
第一,去末页给的两个 GitHub 仓库把 deploy.js、increment.js、get_counter.js、reset.js 读一遍,找出三件事:ABI 从哪个文件读进来、web3.eth.Contract 的构造参数是什么、签名用的是哪个方法。考试说的「知道怎么执行」,边界就是这四个脚本。
第二,给 CompCoin 再铸一枚 token(改构造函数或加一个 mint 函数),重新编译部署,对比编译输出和 Token Tracker 有没有变化。
第三,在浏览器上找到 Incrementer 那笔创建合约的交易,看它的 to 字段和 Input Data,跟课件那句「目标地址 0x0」当面对质一次——这是全份材料里最值得自己验一遍的地方。
Project 大概率要在 Hardhat 这套工程结构上做,现在把镜像里的 contracts/、script/ 和配置文件看熟,十月初听 Project 介绍时就不会一头雾水。
下一讲的通俗笔记上完课会补,先回 COMP5521 课程页。
个人整理的学习笔记,不是官方材料;数字与结论以课件和讲师为准。