实操实验室·基础卷
本卷共 17 个实验(实验 1–17),覆盖从"网页请求"到"钱包分层与事故响应"的完整入门链路。
默认技术栈:本地 EVM 链(Anvil / Hardhat 本地节点)、Solidity、viem 或 ethers.js、Node.js 18+,少量 Python / HTML。 公共测试网仅作可选增强项,本地链是默认环境。 所有实验都能在断网状态下完成主线部分。
通用安全红线:本卷所有涉及私钥、助记词、签名的实验,一律使用本地链自动生成的测试账户。 永远不要在任何实验中输入你的真实助记词或私钥。测试助记词(如 Anvil 默认助记词)是公开的, 任何人都能算出对应私钥,绝不可向这些地址转入真实资产。
环境准备(所有实验共用)
# 1. 安装 Node.js 18+(已装可跳过)
node -v # 期望输出 v18.x 或更高
# 2. 安装 Foundry(提供 anvil / cast)
curl -L https://foundry.paradigm.xyz | bash
foundryup
anvil --version # 期望输出 anvil 版本号
# 3. 建立实验工作区
mkdir -p ~/web3-labs && cd ~/web3-labs
npm init -y
npm i viem ethers
失败排查:foundryup 报 command not found 说明 shell 没重载 PATH,执行 source ~/.zshrc 后重试;
公司网络下载失败时,改用 Docker 镜像 docker run -p 8545:8545 ghcr.io/foundry-rs/foundry anvil --host 0.0.0.0。
实验1:网页请求流程
等级:入门 适合角色:所有人(尤其非技术背景) 风险等级:无风险(纯本地/只读) 预计时间:40 分钟 前置知识:会用浏览器、能打开终端 学习目标:亲手观察一次 HTTP 请求的完整生命周期,理解"客户端—服务器"这一中心化模型的默认形态,为后面对比区块链的"客户端—网络"模型建立参照系。
场景:你打开一个网页,看到的内容其实是某台服务器"愿意给你看"的内容。同一个 URL,服务器可以对不同人返回不同结果,也可以随时下线。这是理解"为什么需要去中心化"的第一块砖。
环境要求:任意现代浏览器 + 终端(curl)。无需联网也可用本地静态服务器完成。
初始化命令:
mkdir -p ~/web3-labs/lab01 && cd ~/web3-labs/lab01
printf '<h1>Hello Web2</h1>' > index.html
python3 -m http.server 8080 # 做什么:起一个本地 Web 服务器
# 正常输出:Serving HTTP on :: port 8080 ...
# 失败排查:Address already in use → 换端口 8081,或 lsof -i :8080 找到占用进程
操作步骤 / 每步预期结果:
- 浏览器打开
http://localhost:8080→ 看到 "Hello Web2"。 - 打开开发者工具 Network 面板,刷新页面 → 看到一条
index.html请求,状态码 200,含 Request Headers / Response Headers。 - 终端执行
curl -v http://localhost:8080→ 完整看到 TCP 连接、请求行、响应头、响应体。 - 把
index.html内容改成<h1>内容被服务器改了</h1>,刷新 → 浏览器显示新内容,你没有任何办法阻止或验证这次修改。 - 按 Ctrl+C 停掉服务器,刷新 → 页面变成"无法访问"。
验证命令:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080 # 服务运行中输出 200,停掉后输出 000
完成标准:能用自己的话说清三件事——响应内容由谁决定、你如何验证内容没被篡改(答案是:在 Web2 里你无法验证)、服务器下线后数据去了哪。
常见错误与排查:python3 不存在 → 用 npx serve .;端口被占 → 换端口;curl 显示 Connection refused → 服务器进程已退出。
安全提示:本实验完全在 localhost 运行,不涉及任何真实凭证。不要把本地服务器绑定到 0.0.0.0 并暴露在公网。
重置方法:rm -rf ~/web3-labs/lab01,Ctrl+C 停掉进程即可,无残留状态。
进阶挑战:用 curl -H "User-Agent: Googlebot" 请求某个真实新闻站,观察返回内容是否与浏览器不同——体会"同一 URL 不同结果"。
思考题:如果这台服务器明天关停,你保存的链接还有价值吗?"数据所有权"在这个模型里实际上属于谁?
对应章节:第 1 章 互联网的信任模型
实验2:中心化账本模拟
等级:入门 适合角色:所有人 风险等级:无风险(纯本地) 预计时间:50 分钟 前置知识:实验 1;能读懂基础 JS 学习目标:用 100 行代码实现一个中心化账本,然后亲手"作弊",理解为什么中心化记账的安全性完全依赖于"记账人是否可信"。
场景:你是一家小银行的唯一记账员。所有余额存在你的一个 JSON 文件里。客户无法查看原始账本,只能问你"我还有多少钱"。
环境要求:Node.js 18+。
初始化命令:
mkdir -p ~/web3-labs/lab02 && cd ~/web3-labs/lab02
npm init -y
操作步骤 / 每步预期结果:
- 新建
ledger.js:
const fs = require('fs');
const FILE = './ledger.json';
const load = () => JSON.parse(fs.readFileSync(FILE, 'utf8'));
const save = (d) => fs.writeFileSync(FILE, JSON.stringify(d, null, 2));
function transfer(from, to, amount) {
const d = load();
if (d[from] < amount) throw new Error('余额不足');
d[from] -= amount; d[to] += amount;
save(d); // 只有"我"能写这个文件
console.log(`${from} -> ${to} : ${amount}`);
}
module.exports = { load, save, transfer };
- 写入初始账本
{"Alice":100,"Bob":50}→ 文件创建成功。 - 执行
node -e "require('./ledger').transfer('Alice','Bob',30)"→ 输出转账日志,Alice 70 / Bob 80。 - 作弊环节:直接用编辑器把
Alice改成 999999,保存 → 账本"合法"地变了,没有任何报错、没有任何痕迹。 - 删掉
ledger.json→ 全部资产归零,客户无从申诉。
验证命令:
cat ledger.json # 观察篡改前后差异
node -e "console.log(require('./ledger').load())"
完成标准:你能指出中心化账本的三个失效点——单点篡改、单点故障、无法独立审计。
常见错误与排查:Unexpected token in JSON → 手改文件时逗号/引号写错,用 node -e "JSON.parse(...)" 定位;ENOENT → 忘了先创建 ledger.json。
安全提示:这只是教学模拟,请勿把这种"无校验直接写文件"的模式用于任何真实记账系统。
重置方法:rm -rf ~/web3-labs/lab02 后重新初始化。
进阶挑战:加一个 history 数组记录每笔转账,然后思考——你作为记账员同样可以改历史,追加日志真的解决问题了吗?
思考题:审计公司来查账时,它验证的是"账本"还是"记账人"?这两者的信任成本差多少?
对应章节:第 2 章 记账的历史
实验3:分布式账本模拟
等级:入门 适合角色:所有人 / 开发者 风险等级:无风险(纯本地) 预计时间:60 分钟 前置知识:实验 2 学习目标:把实验 2 的单份账本复制成 5 份,引入"多数一致"规则,亲眼看到单点篡改如何被网络自动拒绝。
场景:现在有 5 个记账员各持一份完全相同的账本。任何一笔转账要被接受,必须超过半数节点认可。你扮演其中一个作恶的记账员。
环境要求:Node.js 18+。
初始化命令:
mkdir -p ~/web3-labs/lab03 && cd ~/web3-labs/lab03 && npm init -y
操作步骤 / 每步预期结果:
- 写
network.js,用数组模拟 5 个节点,每个节点持有独立账本副本。 - 实现
broadcast(tx):把交易发给所有节点,各自校验余额后写入 → 5 份账本保持一致。 - 实现
consensus():对 5 份账本做哈希,取出现次数最多的哈希作为"真实状态" → 正常情况 5/5 一致。 - 作恶环节:手动把节点 0 的账本余额改大 → 运行
consensus(),输出显示 4/5 一致,节点 0 被标记为分叉。 - 把 3 个节点都改成同一个假余额 →
consensus()输出显示假账本变成"多数派"。这就是 51% 攻击的本质。
验证命令:
node -e "require('./network').consensus()"
# 正常输出:majorityHash=0x..., agree=5/5
# 作恶后:agree=4/5, forked=[node0]
完成标准:能解释"多数一致"保护了什么、又假设了什么(假设作恶者不超过半数)。
常见错误与排查:哈希不一致但内容看着一样 → JSON key 顺序不同,序列化前先排序键;节点数为偶数导致平票 → 使用奇数节点。
安全提示:本实验的"共识"是极度简化的教学模型,不含网络延迟、女巫攻击、激励机制,不要据此评估真实链的安全性。
重置方法:删除目录重跑初始化。
进阶挑战:把节点数改成 3,观察只需控制 2 个节点就能改写历史——理解为什么节点数量与分布本身就是安全参数。
思考题:如果 5 个节点都由同一家公司运营,"分布式"还有意义吗?
对应章节:第 3 章 从分布式到去中心化
实验4:哈希实验
等级:入门 适合角色:所有人 风险等级:无风险 预计时间:40 分钟 前置知识:会用终端 学习目标:亲手验证哈希的四个关键性质——确定性、雪崩效应、定长输出、不可逆,并理解它为什么是区块链"防篡改"的物理基础。
场景:你要证明一份合同在某个时间点存在且未被修改,但又不想公开合同内容。
环境要求:终端(自带 shasum)+ Node.js。
初始化命令:
mkdir -p ~/web3-labs/lab04 && cd ~/web3-labs/lab04
echo "甲方向乙方支付 100 万元" > contract.txt
操作步骤 / 每步预期结果:
shasum -a 256 contract.txt→ 得到 64 位十六进制串,多次运行结果完全相同(确定性)。- 把文件里的"100"改成"1OO"(字母 O),再算哈希 → 输出与之前完全不同(雪崩效应)。
echo -n "a" | shasum -a 256与cat 100MB大文件 | shasum -a 256对比 → 输出长度都是 64 字符(定长)。- 用 Node 做单向性验证:
const { createHash } = require('crypto');
const h = (s) => createHash('sha256').update(s).digest('hex');
console.log(h('hello')); // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
// 反向:给定 hash 求原文,只能穷举 —— 对 256 位空间不可行
- 做一个迷你"工作量证明":循环找 nonce 使
h('block'+nonce)以0000开头 → 大约几万次尝试后成功,体感理解挖矿。
验证命令:
node -e "const{createHash}=require('crypto');let n=0;while(!createHash('sha256').update('block'+n).digest('hex').startsWith('0000'))n++;console.log('nonce=',n)"
完成标准:能说出为什么"改一个字哈希就全变"能让区块链的链式结构防篡改。
常见错误与排查:Linux 上无 shasum → 用 sha256sum;echo 默认带换行导致哈希对不上 → 必须用 echo -n。
安全提示:哈希不是加密,不能用来隐藏低熵信息(比如手机号、6 位密码),因为可被彩虹表穷举。存密码必须用 bcrypt/argon2 加盐。
重置方法:rm -rf ~/web3-labs/lab04。
进阶挑战:把难度从 0000 提到 00000,测量耗时变化,估算难度每加一位耗时约乘以 16。
思考题:如果有人找到 SHA-256 的碰撞,区块链会发生什么?现有系统有迁移预案吗?
对应章节:第 4 章 密码学基础
实验5:数字签名实验
等级:入门 适合角色:所有人 / 开发者 风险等级:低(使用一次性测试密钥) 预计时间:50 分钟 前置知识:实验 4 学习目标:亲手生成密钥对、签名、验签、篡改后验签失败,理解"地址即公钥的哈希"和"私钥即所有权"。
场景:你要向全世界证明"这条消息是我发的",但不能把证明自己身份的秘密交出去。
环境要求:Node.js + viem。
初始化命令:
mkdir -p ~/web3-labs/lab05 && cd ~/web3-labs/lab05 && npm init -y && npm i viem
操作步骤 / 每步预期结果:
- 生成一次性测试密钥(文件名
sign.mjs):
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { verifyMessage } from 'viem';
const pk = generatePrivateKey(); // 仅本实验使用,用完即弃
const account = privateKeyToAccount(pk);
console.log('地址:', account.address); // 0x 开头 40 位十六进制
const msg = '我同意转让编号 A001 的资产';
const sig = await account.signMessage({ message: msg });
console.log('签名:', sig); // 0x 开头 130 位
console.log('验签:', await verifyMessage({ address: account.address, message: msg, signature: sig })); // true
console.log('篡改后:', await verifyMessage({ address: account.address, message: msg + '!', signature: sig })); // false
- 运行 → 依次看到地址、签名、
true、false。 - 把签名的最后一个字符改掉再验 → 抛错或返回 false。
- 用别人的地址去验你的签名 → false,说明签名绑定身份。
验证命令:
node sign.mjs # 期望最后两行分别是 true / false
完成标准:能解释"为什么验签不需要私钥",以及"为什么私钥泄露等于资产全部丢失"。
常见错误与排查:Cannot use import statement → 文件改成 .mjs 或 package.json 加 "type":"module";签名长度不对 → 检查是否误把 hash 当签名。
安全提示:私钥一旦泄露不可挽回,没有客服、没有冻结、没有找回。 本实验用 generatePrivateKey() 现场生成,永远不要把真实钱包私钥粘贴进任何脚本、聊天框或 AI 对话。
重置方法:删除目录即可,测试密钥无任何价值。
进阶挑战:实现 recoverMessageAddress,从"消息+签名"直接反推地址,理解链上合约如何用 ecrecover 校验签名。
思考题:签名证明了"谁签的",但能证明"什么时候签的"吗?为什么区块链需要额外的时间戳与 nonce?
对应章节:第 4 章 密码学基础
实验6:本地测试钱包
等级:入门 适合角色:所有人 风险等级:低(隔离环境) 预计时间:45 分钟 前置知识:实验 5 学习目标:建立一个只用于测试的浏览器钱包,掌握助记词、派生路径、账户切换、网络切换,形成"测试钱包与主钱包物理隔离"的肌肉记忆。
场景:你要开始动手做链上实验,第一件事不是转账,而是把"实验用具"和"真实资产"彻底分开。
环境要求:Chrome/Firefox 浏览器 + MetaMask(或 Rabby)扩展。建议使用独立浏览器 Profile。
初始化命令:
# 建议在独立浏览器配置文件中安装扩展,命令示例(macOS Chrome)
open -na "Google Chrome" --args --user-data-dir=/tmp/web3-lab-profile
操作步骤 / 每步预期结果:
- 在新 Profile 中安装 MetaMask → 出现欢迎页。
- 选择"创建新钱包"(不要导入你的真实助记词)→ 生成 12 词助记词。
- 把助记词抄在纸上并标注"TEST ONLY 测试专用 勿转真钱" → 完成备份验证。
- 记录第一个账户地址;点击"添加账户"生成第二、第三个 → 观察三个地址不同,但都由同一助记词派生(路径 m/44'/60'/0'/0/0、/1、/2)。
- 设置里查看"账户详情 → 显示私钥",确认它与助记词的关系 → 理解一份助记词可派生无限私钥。
- 网络下拉框中确认能看到"添加自定义网络"入口,为实验 7 做准备。
验证命令:
# 稍后配合本地链验证:地址存在于本地链账户列表或可接收本地测试币
cast balance <你的测试地址> --rpc-url http://127.0.0.1:8545 # 需先启动 anvil
完成标准:你拥有一个明确标记为"测试用"的钱包,且能说出助记词、私钥、地址三者的派生关系。
常见错误与排查:扩展无法安装 → 检查浏览器版本;助记词抄错 → 重新创建钱包,测试钱包重建零成本。
安全提示(重要): - 本实验创建的钱包永久禁止接收真实资产。 - 助记词绝不截图、绝不存云盘、绝不输入任何网页表单(真钱包同理)。 - 任何网站/客服/"官方"要求你输入助记词,100% 是诈骗。
重置方法:删除 /tmp/web3-lab-profile 目录,钱包连同数据一并清除。
进阶挑战:用 ethers.js 的 HDNodeWallet.fromPhrase 在本地复现同一助记词的前 5 个派生地址,与钱包显示的对比一致。
思考题:如果助记词能派生无限地址,为什么很多人仍会"以为换个地址就更安全"?隐私和安全在这里是同一件事吗?
对应章节:第 5 章 钱包与密钥管理
实验7:启动本地区块链
等级:入门 适合角色:开发者 / 想动手的非技术人 风险等级:无风险(本地链,代币无价值) 预计时间:40 分钟 前置知识:实验 6 学习目标:在自己电脑上跑起一条完整的 EVM 链,理解区块、出块、RPC、chainId 这些概念对应的实体。
场景:你需要一个可以随便折腾、随便重置、不花一分钱的链上沙盒。
环境要求:已安装 Foundry(anvil)。
初始化命令:
anvil
# 做什么:启动一条本地 EVM 链,默认 chainId=31337,RPC 在 http://127.0.0.1:8545
# 正常输出:列出 10 个测试账户地址 + 对应私钥 + 助记词 + "Listening on 127.0.0.1:8545"
# 失败排查:端口占用 → anvil --port 8546;命令不存在 → 重跑 foundryup
操作步骤 / 每步预期结果:
- 启动 anvil,保持终端不关 → 看到 10 个账户各有 10000 ETH(测试币)。
- 新开终端:
cast chain-id --rpc-url http://127.0.0.1:8545→ 输出31337。 cast block-number --rpc-url http://127.0.0.1:8545→ 输出0(还没出块)。cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:8545→ 输出一个 19 位大数(wei)。- 在 MetaMask 里添加自定义网络:RPC
http://127.0.0.1:8545、chainId31337、货币符号ETH→ 切换后余额显示 0(因为你的测试地址还没收到币)。 - 用
anvil --block-time 5重启 → 观察即使无交易也每 5 秒出一个块。
验证命令:
cast rpc eth_syncing --rpc-url http://127.0.0.1:8545 # 期望 false
cast block latest --rpc-url http://127.0.0.1:8545 # 输出区块头字段
完成标准:本地链跑通,MetaMask 能连上并显示 31337 网络。
常见错误与排查:MetaMask 报 "could not fetch chain ID" → RPC 写成了 localhost 而某些环境解析到 IPv6,改用 127.0.0.1;nonce 混乱 → 重启 anvil 并在 MetaMask 中"清除活动数据"。
安全提示:anvil 打印的助记词与私钥是全世界公开的固定值,任何人都能取走这些地址在任意链上的资产。绝不可向它们转入真实代币。同时不要用 --host 0.0.0.0 把本地链暴露到公网。
重置方法:Ctrl+C 停止 anvil,重启即全新状态;或用 anvil --state ./state.json 做持久化后删除该文件。
进阶挑战:用 anvil --fork-url <某公共RPC> 分叉主网状态到本地,体验"在主网副本上随便试"的开发模式。
思考题:本地链只有一个"矿工"(你),它还具备去中心化的安全性吗?开发环境与生产环境的信任假设差在哪?
对应章节:第 6 章 区块链是怎么运转的
实验8:第一笔链上交易
等级:入门 适合角色:所有人 风险等级:无风险(本地链) 预计时间:50 分钟 前置知识:实验 7 学习目标:完整走一遍"构造交易 → 签名 → 广播 → 打包 → 确认",并读懂交易回执里的每个字段。
场景:你第一次把价值从一个地址转到另一个地址,全程没有任何银行参与。
环境要求:anvil 运行中;已装 viem。
初始化命令:
cd ~/web3-labs && mkdir -p lab08 && cd lab08 && npm init -y && npm i viem
anvil # 另一个终端保持运行
操作步骤 / 每步预期结果:
- 用
cast发一笔最简交易:
cast send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
--value 1ether \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--rpc-url http://127.0.0.1:8545
# 做什么:从 anvil 账户0 向账户1 转 1 ETH(测试币)
# 正常输出:blockNumber、transactionHash、gasUsed、status 1 (success)
# 失败排查:insufficient funds → 账户选错;nonce too low → 重启 anvil
- 记下
transactionHash→ 用cast tx <hash>查看原始交易字段(from/to/value/nonce/gasPrice/input)。 cast receipt <hash>→ 查看回执(status、gasUsed、logs、blockHash)。cast balance分别查两个地址 → 发送方少了 1 ETH + gas,接收方多了正好 1 ETH。- 用 viem 重做一遍,观察
sendTransaction返回 hash 后需waitForTransactionReceipt才算确认。 - 故意发一笔超过余额的交易 → 节点直接拒绝,交易根本不上链。
验证命令:
cast balance 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --rpc-url http://127.0.0.1:8545
# 期望:初始 10000 ETH + 1 ETH
完成标准:能逐字段解释一份交易回执,并说清 gas 由谁支付、扣给谁。
常见错误与排查:nonce too high → MetaMask 缓存,清除活动数据;replacement transaction underpriced → 同 nonce 重发需提高 gas price。
安全提示:这里粘贴的私钥是 anvil 公开测试私钥。在真实环境中,任何要求你把私钥写进命令行的操作都极度危险(会留在 shell history),生产环境请用硬件钱包或 keystore。
重置方法:重启 anvil,状态归零。
进阶挑战:手工构造一笔 EIP-1559 交易,分别设置 maxFeePerGas 与 maxPriorityFeePerGas,观察实际扣费与 baseFee 的关系。
思考题:交易一旦确认就不可撤销。这个"特性"在什么场景是优点,在什么场景是灾难?
对应章节:第 6 章 区块链是怎么运转的
实验9:本地区块浏览器
等级:进阶入门 适合角色:开发者 / 分析师 风险等级:无风险 预计时间:60 分钟 前置知识:实验 8 学习目标:自己写一个 100 行的区块浏览器,理解区块浏览器本质上只是"RPC 查询 + 渲染",从而学会不迷信单一浏览器的展示结果。
场景:你想知道链上到底发生了什么,而不是只看某个网站告诉你的版本。
环境要求:anvil 运行中;Node.js + viem;浏览器。
初始化命令:
mkdir -p ~/web3-labs/lab09 && cd ~/web3-labs/lab09 && npm init -y && npm i viem
操作步骤 / 每步预期结果:
- 写
explorer.mjs,用 viem 的createPublicClient连本地链。 - 实现"列出最近 10 个区块":
import { createPublicClient, http } from 'viem';
const client = createPublicClient({ transport: http('http://127.0.0.1:8545') });
const latest = await client.getBlockNumber();
for (let i = 0n; i < 10n && latest - i >= 0n; i++) {
const b = await client.getBlock({ blockNumber: latest - i, includeTransactions: true });
console.log(`#${b.number} 交易数=${b.transactions.length} 时间=${new Date(Number(b.timestamp)*1000).toISOString()}`);
}
- 运行 → 打印区块列表,与实验 8 的交易对得上。
- 扩展:输入交易 hash 打印 from/to/value/gasUsed,输入地址打印余额与 nonce。
- 用一个极简 HTML 页面(fetch 调用本地 Node 接口)渲染成表格 → 你有了自己的浏览器。
- 对比:把同一笔交易在 anvil 日志里的信息与你的浏览器输出对照 → 完全一致。
验证命令:
node explorer.mjs | head -20 # 期望输出与 cast block-number 一致的最新块号
完成标准:能通过自己的工具查到任意区块/交易/地址信息,并解释浏览器与节点的关系。
常见错误与排查:BigInt 无法 JSON 序列化 → 用 (k,v)=>typeof v==='bigint'?v.toString():v 作为 replacer;空链无区块 → 先发几笔交易。
安全提示:区块浏览器展示的"代币名称""合约已验证"等标签是人为标注,可被伪造。判断一个合约是否可信,永远以合约代码与地址为准,不以浏览器显示的名字为准。
重置方法:删除目录;重启 anvil 清空链数据。
进阶挑战:加上"按地址过滤交易"功能,体会为什么真实浏览器需要索引数据库(直接遍历全链不可行)。
思考题:如果你常用的区块浏览器明天关站,你还能查到链上数据吗?需要什么条件?
对应章节:第 7 章 链上数据与可验证性
实验10:Gas 模拟器
等级:进阶入门 适合角色:所有人 / 开发者 风险等级:无风险 预计时间:50 分钟 前置知识:实验 8 学习目标:搞清 gasLimit / gasUsed / baseFee / priorityFee / 最终花费 的计算关系,学会估算成本和排查"交易卡住"。
场景:你发了一笔交易,钱包提示"手续费 3 美元",但你不知道这个数字从哪来,也不知道为什么有时候交易迟迟不确认。
环境要求:anvil 运行中;cast。
初始化命令:
anvil --block-base-fee-per-gas 1000000000 # 固定 baseFee = 1 gwei,便于观察
操作步骤 / 每步预期结果:
cast estimate <收款地址> --value 1ether --rpc-url http://127.0.0.1:8545→ 输出21000,这是最简转账的固定 gas。- 发一笔转账后
cast receipt <hash>→gasUsed=21000,effectiveGasPrice=?。 - 计算:
费用 = gasUsed × effectiveGasPrice,用cast --from-wei换算成 ETH → 与余额变化对得上。 - 部署一个含循环的合约方法,分别以循环 10 次 / 1000 次调用 → gasUsed 显著增长,理解"计算越多越贵"。
- 把 gasLimit 故意设成 20000(低于 21000)→ 交易失败并仍然扣掉 gas,理解"失败也付费"。
- 用
cast send --gas-price 1发一笔极低价交易 → 在有竞争的链上会长期 pending(本地链会立即打包,可用anvil --order fifo与手动 mine 模拟排队)。
验证命令:
cast receipt <hash> --rpc-url http://127.0.0.1:8545 | grep -E "gasUsed|effectiveGasPrice"
完成标准:给定 gasUsed 和 gasPrice,你能手算出花费,并解释交易 pending 的两种常见原因(gas 价过低 / nonce 有空洞)。
常见错误与排查:out of gas → 提高 gasLimit(注意:gasLimit 是上限不是花费);交易一直 pending → 用相同 nonce + 更高 gasPrice 发一笔"加速"或"取消"(转 0 给自己)。
安全提示:不要为了省 gas 而使用来路不明的"gas 代付"服务或签署可疑请求,部分钓鱼会伪装成"低 gas 加速工具"。
重置方法:重启 anvil。
进阶挑战:写脚本采集 100 个区块的 baseFee 变化,画出 EIP-1559 的调整曲线(目标 50% 区块占用率)。
思考题:Gas 机制本质是给"公共计算资源"定价。如果没有 gas,网络会发生什么?
对应章节:第 6 章 区块链是怎么运转的
实验11:UTXO 与账户模型
等级:进阶入门 适合角色:所有人 / 开发者 风险等级:无风险(纯本地模拟) 预计时间:60 分钟 前置知识:实验 3、实验 8 学习目标:用代码实现两种记账模型,理解比特币 UTXO 与以太坊账户模型在隐私、并发、状态体积上的根本差异。
场景:同样是"转账 3 元",比特币的做法像"找零的现金",以太坊的做法像"改银行卡余额数字"。
环境要求:Node.js。
初始化命令:
mkdir -p ~/web3-labs/lab11 && cd ~/web3-labs/lab11 && npm init -y
操作步骤 / 每步预期结果:
- 实现 UTXO 模型:
// 每个 UTXO = { id, owner, amount },花费必须整笔消耗并产生找零
function spend(utxos, owner, to, amount) {
const mine = utxos.filter(u => u.owner === owner);
let picked = [], sum = 0;
for (const u of mine) { picked.push(u); sum += u.amount; if (sum >= amount) break; }
if (sum < amount) throw new Error('余额不足');
const rest = utxos.filter(u => !picked.includes(u));
rest.push({ id: crypto.randomUUID(), owner: to, amount });
if (sum > amount) rest.push({ id: crypto.randomUUID(), owner, amount: sum - amount }); // 找零
return rest;
}
- 给 Alice 三个面额 5/3/2 的 UTXO,转 6 给 Bob → 结果:消耗 5+3,产生 Bob 6 和 Alice 找零 2。
- 实现账户模型(沿用实验 2 的余额加减)→ 同样转 6,只是两个数字变化。
- 对比状态体积:UTXO 集合随交易增长,账户表大小只与用户数相关。
- 并发实验:让 Alice 同时发两笔各花同一个 UTXO 的交易 → 第二笔必然失败(双花被结构性阻止);账户模型下则需要 nonce 来排序。
- 隐私实验:账户模型下所有转账挂在同一地址上一目了然;UTXO 下找零地址可每次更换。
验证命令:
node utxo.js # 期望打印转账后 UTXO 集合,总量守恒
完成标准:能列出两种模型各自的三个优势与三个代价。
常见错误与排查:金额精度问题 → 全部用整数(最小单位)计算,禁用浮点。
安全提示:本实验不涉及真实网络。请注意"UTXO 隐私更好"只是相对的,链上分析有成熟的聚类启发式方法,不要把它当作匿名保证。
重置方法:删除目录。
进阶挑战:给 UTXO 模型加"脚本锁"(如需两个签名才能花费),体会比特币的 Script 与多签的关系。
思考题:以太坊为什么选择账户模型?智能合约的存在如何改变了这个权衡?
对应章节:第 8 章 两条主线:比特币与以太坊
实验12:公共测试网或本地替代
等级:进阶入门 适合角色:所有人 风险等级:低(测试网资产无价值,但仍需防钓鱼) 预计时间:60 分钟 前置知识:实验 6、实验 8 学习目标:体验一次"真实网络"的完整流程(水龙头 → 交易 → 浏览器确认),同时掌握在没有测试网的条件下如何用本地链完整替代。
场景:本地链永远只有你一个人。你需要感受一次多方参与、有排队、有延迟的公开网络。
默认路径是本地链(路线 B)。路线 A 为可选增强项,需联网。
环境要求:路线 A:浏览器 + 测试钱包 + 网络。路线 B:anvil。
初始化命令:
# 路线 B(默认,离线可完成):模拟多用户与出块延迟
anvil --block-time 12 --accounts 5
操作步骤 / 每步预期结果:
路线 B(默认) 1. 启动带 12 秒出块间隔的 anvil → 观察交易发出后需等待下一个块才确认,产生"真实网络等待感"。 2. 用 5 个账户互相转账,同时在两个终端并发发送 → 观察交易按 nonce 与到达顺序排队。 3. 用实验 9 的浏览器查看每个区块包含多少笔交易。
路线 A(可选增强,需联网) 1. 在测试钱包中切换到某个公共测试网。 2. 通过项目官方文档链接进入水龙头领取测试币 → 余额出现(可能需等待数分钟)。 3. 发一笔转账,在公共区块浏览器上查到该交易 → 状态从 Pending 变 Success。
验证命令:
cast block latest --rpc-url http://127.0.0.1:8545 | grep timestamp # 相邻块相差约 12 秒
完成标准:理解"确认时间""待打包队列""区块容量"三个概念的实际体感。
常见错误与排查:测试币迟迟不到 → 水龙头限流,换时间再试或直接走路线 B;测试网 RPC 不稳 → 更换公共 RPC 端点。
安全提示(重要): - 领水龙头只从项目官方文档跳转,搜索引擎广告位的"faucet"大量是钓鱼站。 - 任何要求你先转入 ETH 才能领测试币的水龙头都是诈骗。 - 测试网操作也务必使用实验 6 的测试钱包,绝不用主钱包连接陌生站点。
重置方法:本地链重启即可;测试网无需重置(资产无价值)。
进阶挑战:用 anvil --fork-url 分叉一条公链的最新状态,在本地"重放"一个真实地址的余额查询,体会分叉测试的威力。
思考题:测试网的代币没有价值,它的共识安全性也就无从谈起。在测试网上"跑通了"能证明主网也安全吗?
对应章节:第 9 章 从测试到主网
实验13:签名请求阅读器
等级:进阶
适合角色:所有人(安全关键)
风险等级:无风险(本地解析,不广播)
预计时间:60 分钟
前置知识:实验 5、实验 8
学习目标:学会把钱包弹出的一串十六进制"翻译"成人话,识别 personal_sign、eth_signTypedData_v4(EIP-712)、eth_sendTransaction 三类请求的风险差异。
场景:一个网站弹窗让你"签名登录",另一个让你签一段看不懂的结构化数据。前者可能只是登录,后者可能是把你全部 NFT 挂单卖出 0 元。
环境要求:Node.js + viem;anvil(可选)。
初始化命令:
mkdir -p ~/web3-labs/lab13 && cd ~/web3-labs/lab13 && npm init -y && npm i viem
操作步骤 / 每步预期结果:
- 解析
personal_sign明文:把十六进制转 UTF-8。
import { hexToString, decodeFunctionData, parseAbi } from 'viem';
console.log(hexToString('0x48656c6c6f')); // Hello —— 能读懂就是低风险的登录类签名
- 解析一段 EIP-712 typed data JSON → 逐字段读
domain.verifyingContract(谁会用这个签名)、primaryType、message里的金额与接收方。 - 关键练习:拿一份教学用的"无限授权"typed data 样本,找出其中
spender是陌生地址、value是2^256-1(无限)→ 判定为高风险。 - 解析交易
data字段:
const abi = parseAbi(['function approve(address,uint256)']);
console.log(decodeFunctionData({ abi, data: '0x095ea7b3...' }));
// 输出 functionName: 'approve', args: [spender, amount]
- 建一张自查表:签名前必答四问——谁在请求(域名)、签给哪个合约、我授权了什么、金额上限是多少。
- 用 anvil 部署一个教学合约,签名后本地验签通过 → 但不要把练习中的签名发到任何真实站点。
验证命令:
node parse.mjs '0x095ea7b3...' # 期望输出可读的函数名与参数
完成标准:拿到任意一段 data,能在 2 分钟内说出它调用了什么函数、参数是什么、风险在哪。
常见错误与排查:selector 未知 → 用 4 字节签名数据库或让对方提供 ABI,无法解析就不要签;解析出的地址与网站宣称不符 → 立即拒签。
安全边界(重要):本实验全部在本地解析已有样本,不构造、不广播任何针对真实协议的签名。目的只有一个——学会识别,从而拒签。
重置方法:删除目录。
进阶挑战:写一个 CLI,输入 typed data JSON 自动输出"风险评分"(无限额度 +3 分、陌生 spender +2 分、域名与 verifyingContract 不匹配 +3 分)。
思考题:为什么"签名"比"转账"更危险?(提示:转账你能看到金额,签名可能是一张空白支票。)
对应章节:第 10 章 交互安全
实验14:Token Approval 与撤销
等级:进阶 适合角色:所有人(安全关键) 风险等级:低(本地链上完成全流程) 预计时间:70 分钟 前置知识:实验 13 学习目标:在本地链完整体验 approve → transferFrom → revoke 的机制,理解"无限授权"为何是长期风险,并掌握定期审计与撤销的操作习惯。
场景:三年前你用过一个已经跑路的 DEX。你早忘了,但你给它的无限授权还挂在链上,合约权限还在别人手里。
环境要求:anvil + Foundry(forge)。
初始化命令:
mkdir -p ~/web3-labs/lab14 && cd ~/web3-labs/lab14
forge init --no-git .
anvil # 另开终端
操作步骤 / 每步预期结果:
- 部署一个最小 ERC-20(可用 OpenZeppelin 或手写 60 行)→ 得到 token 地址,账户 0 持有全部供应。
- 授权:
cast send <token> "approve(address,uint256)" <spenderEOA> 115792089237316195423570985008687907853269984665640564039457584007913129639935 --private-key <anvil私钥0>→ 授出无限额度。 - 查询:
cast call <token> "allowance(address,address)(uint256)" <owner> <spender>→ 返回巨大数字。 - 用 spender 账户执行
transferFrom(owner, spender, 1000)→ 成功。此时 owner 并未再次签名,钱就走了。 - 撤销:
approve(spender, 0)→ 再查 allowance 为 0,再次transferFrom失败并 revert。 - 复现"授权竞态"教学点:把额度从 100 改成 50 之前,spender 抢先花掉 100 → 理解为什么建议先置 0 再设新值。
验证命令:
cast call <token> "allowance(address,address)(uint256)" <owner> <spender> --rpc-url http://127.0.0.1:8545
# 撤销前:巨大数字;撤销后:0
完成标准:能说清"授权额度 ≠ 余额",并能独立完成一次撤销操作。
常见错误与排查:transferFrom revert 且额度足够 → 检查是余额不足还是 allowance 不足,两者报错不同;地址写错导致授权给了错的合约 → 立即置 0。
安全边界(重要):本实验只在本地链和自己部署的教学 ERC-20 上进行,不涉及任何真实协议、不提供任何盗取真实资产的路径。练习目标是养成"定期审计授权"的习惯。
安全提示:真实环境中请定期用官方授权管理工具检查并撤销不再使用的授权;撤销本身需要付 gas,这是必要成本。对已跑路项目的授权应立即撤销。
重置方法:重启 anvil,所有授权与代币消失。
进阶挑战:实现 ERC-2612 permit(离线签名授权),体会它如何省一笔交易,同时思考它为什么让钓鱼签名更危险。
思考题:如果一个 DApp 只需要花你 100 USDC,它为什么默认要无限授权?这个"用户体验优化"把风险转嫁给了谁?
对应章节:第 10 章 交互安全
实验15:钓鱼网站识别
等级:进阶 适合角色:所有人(安全关键) 风险等级:无风险(本地静态页面,不访问真实钓鱼站) 预计时间:60 分钟 前置知识:实验 13、实验 14 学习目标:在自建的本地"仿冒页面"上训练识别能力,建立一套可复用的核查流程。
安全边界(务必先读):本实验不访问、不收集、不传播任何真实钓鱼网址。 所有样本都是你自己在
localhost上写的静态 HTML,不连接任何真实钱包、不发起任何真实请求。 目的是训练"识别",绝不是制作可用于欺骗他人的工具。请勿把练习页面部署到公网。
场景:一个页面长得和官网一模一样,域名只差一个字母,弹窗要求你"验证钱包"。
环境要求:浏览器 + 任意静态服务器。
初始化命令:
mkdir -p ~/web3-labs/lab15 && cd ~/web3-labs/lab15
python3 -m http.server 8080
操作步骤 / 每步预期结果:
- 写
real.html(模拟正规站)与fake.html(教学仿冒样本,仅本地)。仿冒样本刻意包含四类特征: - 域名/标题中的同形字符(如rn冒充m、西里尔字母а) - 弹窗文案:"验证钱包 / 同步钱包 / 解锁资产" - 请求内容为setApprovalForAll或无限额度approve- 页面无任何官方社交账号可交叉验证 - 逐条对照,写出 5 条判别依据 → 每条能定位到页面上的具体元素。
- 用浏览器 DevTools 查看仿冒页的 JS,找到它准备发起的方法名(教学样本中只是
console.log,不真正连接钱包)→ 理解"页面文案"与"实际请求"可以完全不一致。 - 建立核查清单:域名逐字符核对、从收藏夹进入而非搜索/私信链接、签名内容必须能被你读懂(实验 13)、大额操作先小额试、任何"验证/同步/解锁"话术一律视为诈骗。
- 做一次同形字符测验:把
а(U+0430) 与a(U+0061) 混排,肉眼分辨 → 大概率失败,从而认可"必须靠收藏夹而非肉眼"。
验证命令:
node -e "console.log([...'аpple'].map(c=>c.codePointAt(0).toString(16)))"
# 输出首字符是 430 而非 61 —— 证明肉眼不可靠
完成标准:产出一份属于你自己的、可打印的《签名前核查清单》。
常见错误与排查:只看"有没有 HTTPS 锁"就判定安全 → 钓鱼站同样能申请证书,锁只代表传输加密,不代表站点可信。
安全提示:永远不要为了"验证"这个练习去访问真实的可疑链接。 遇到疑似钓鱼,正确做法是不点击、不连接、向官方渠道举报。若已连接过可疑站点,立刻用实验 14 的方法检查并撤销授权。
重置方法:删除目录,停掉本地服务器。
进阶挑战:写一个小脚本,输入域名后输出其中所有非 ASCII 字符及码点,作为你自己的"同形域名检测器"。
思考题:为什么钓鱼的核心手段是"制造紧迫感"?把"我需要立刻处理"改成"我明天再看",能挡掉多少攻击?
对应章节:第 11 章 常见骗局与防御
实验16:钱包分层方案
等级:进阶 适合角色:所有人 风险等级:低(本地链演练) 预计时间:70 分钟 前置知识:实验 6、实验 14 学习目标:设计并落地一套"冷 / 温 / 热"三层钱包结构,用本地链完整演练资金流转与权限边界。
场景:把全部资产放在一个连过几十个 DApp 的地址里,等于把家里所有现金放在一个经常带出门的钱包里。
环境要求:anvil + 测试钱包(实验 6)。
初始化命令:
anvil --accounts 6 # 用不同账户扮演不同层级
操作步骤 / 每步预期结果:
- 定义三层: - 冷层(Vault):长期存放,永不连接任何 DApp,理想情况用硬件钱包/离线签名。 - 温层(Ops):中转与批量操作,只连接少数已长期验证的协议。 - 热层(Burner):交互测试、领空投、连接新协议,只放"丢了不心疼"的额度。
- 在本地链上用账户 0/1/2 分别代表冷/温/热,各转入 1000 / 100 / 5 测试 ETH → 余额符合分层比例。
- 演练一次完整流程:冷 → 温(大额,一次)→ 热(小额,多次)→ 热层去和教学合约交互 → 收益回温层。
- 在热层对教学合约做一次无限授权(实验 14)→ 确认即使热层被完全清空,冷层与温层零影响。
- 制定并写下规则表:每层的连接白名单、单笔上限、签名审批要求、审计周期(如每月撤销一次热层授权)。
- 演练"热层疑似泄露"应急:立即从热层把剩余资产转出,撤销授权,废弃该地址,不再复用。
验证命令:
for a in <cold> <ops> <burner>; do cast balance $a --rpc-url http://127.0.0.1:8545; done
# 期望:冷层余额始终未变
完成标准:写出一份自己的分层方案文档,包含各层地址用途、额度上限、操作规则。
常见错误与排查:把三层都放在同一助记词下 → 助记词泄露即三层同时失守,冷层应使用独立助记词与独立设备;转账时选错地址 → 使用地址簿并每次核对首尾各 6 位(注意:仅核对首尾可被"靓号碰撞"绕过,重要转账应全量核对)。
安全提示:分层降低的是"单点失误的爆炸半径",不能替代基本功。冷层助记词的物理备份(金属板/多地存放)与继承方案同样重要。永远不要把冷层助记词录入任何联网设备。
重置方法:重启 anvil。
进阶挑战:引入一个 2/3 多签作为温层,演练"单个私钥泄露也无法转出资产"。
思考题:分层带来了操作摩擦。摩擦本身是不是一种安全特性?你愿意为多少资产承担多少摩擦?
对应章节:第 12 章 资产管理与自托管
实验17:资产误转与事故响应
等级:进阶 适合角色:所有人 风险等级:低(本地链复现,不涉及真实资产) 预计时间:70 分钟 前置知识:实验 8、实验 14、实验 16 学习目标:在本地链复现四类典型误转事故,建立一套"发生了什么 → 能不能挽回 → 下一步做什么"的冷静响应流程。
场景:你手一抖,把代币转到了合约地址;或者转错了链;或者把 NFT 发给了自己的另一个地址但那个地址私钥丢了。
环境要求:anvil + 教学 ERC-20 / ERC-721 合约。
初始化命令:
mkdir -p ~/web3-labs/lab17 && cd ~/web3-labs/lab17 && forge init --no-git . && anvil
操作步骤 / 每步预期结果:
- 事故 A:转到合约地址。把教学 ERC-20 转到一个不含提取逻辑的合约 → 交易成功,但代币永久锁死。用
balanceOf(合约)确认代币确实在那里但无人能动。 - 事故 B:转到黑洞地址。转到
0x000...000→ 成功且不可逆,链上余额永久增加。 - 事故 C:链选错。在本地链 A(chainId 31337)与链 B(另起
anvil --chain-id 31338 --port 8546)上部署同名代币,向"另一条链上才有的地址"转账 → 理解为什么"地址相同不等于资产互通",以及为什么跨链转错需要对方私钥持有者协助。 - 事故 D:转给可救援的合约。部署一个含
rescueERC20(onlyOwner)的教学合约 → 误转后由 owner 取回,成功。对比 A,理解"能否挽回完全取决于接收方合约是否预留了救援函数"。 - 写下响应流程:停止一切进一步操作 → 记录交易 hash 与时间 → 判断接收方类型(EOA / 合约 / 黑洞 / 交易所)→ 若是交易所立即联系客服并提供 hash → 若是合约查看是否开源、有无救援函数 → 若不可挽回,记录并复盘。
- 演练一次"防呆"改进:给自己的转账脚本加上"地址白名单 + 二次确认 + 小额试转"。
验证命令:
cast call <token> "balanceOf(address)(uint256)" <合约地址> --rpc-url http://127.0.0.1:8545
# 事故 A 中:余额>0 且任何人无法转出
完成标准:能对任意一笔误转迅速判断"可挽回 / 不可挽回",并写出对应动作清单。
常见错误与排查:慌乱中继续操作导致二次损失(最常见)→ 第一原则是停手;轻信"链上资产找回服务"→ 这类服务绝大多数是二次诈骗,任何要求你先付款或提供助记词的"找回"都是骗局。
安全提示(重要):本实验全部在本地链上复现,不涉及任何真实资产、不提供从他人地址取回资产的方法。真实事故中,除交易所充值转错、合约留有救援函数这两种情况外,链上转账基本不可逆——这是设计使然,不是 bug。
重置方法:重启 anvil,两条本地链均重置。
进阶挑战:给你的教学代币加上"转入合约需实现回调"的检查,让误转在交易层面直接 revert,体会协议级防呆的价值。
思考题:不可逆性是区块链的核心价值之一,也是最大的用户体验代价。如果可以引入"可撤销转账",你愿意为此让渡多少去中心化?
对应章节:第 12 章 资产管理与自托管
本卷小结
| 主题 | 实验 |
|---|---|
| 信任模型对照 | 1、2、3 |
| 密码学基础 | 4、5 |
| 环境与首次上链 | 6、7、8 |
| 链上数据与成本 | 9、10、11 |
| 网络与交互安全 | 12、13、14、15 |
| 资产管理与事故 | 16、17 |
本卷贯穿始终的三条铁律:
- 实验用钱包与真实资产钱包物理隔离,永不混用。
- 任何本地链/测试网的公开私钥与助记词绝不接收真实资产。
- 看不懂的签名与授权一律拒绝——拒签的成本永远低于被盗的成本。
下一卷(labs-defi-dev.md,实验 18–38)进入合约开发与 DeFi 机制模拟。