智能合约
含AI生成内容
第6章 区块链智能合约开发
一、考点速览
| 序号 | 考点 | 重要程度 | 所在节 |
|---|---|---|---|
| 1 | 智能合约定义与三大特性(尼克·萨博 1996) | ★★★★★ | 6.1 |
| 2 | 中心化智能合约的弊端与区块链智能合约的优势 | ★★★★★ | 6.1 |
| 3 | Solidity 语言定位、设计特点与学习资源 | ★★★★ | 6.2.1 |
| 4 | Remix IDE 四大功能模块与开发五步流程 | ★★★★★ | 6.2.2 |
| 5 | SPDX 许可标识与版本编译指示(version pragma) | ★★★★ | 6.2.2 |
| 6 | 常用类型(bool/int/uint/address/bytes/string/array/mapping/struct) | ★★★★★ | 6.2.3 |
| 7 | address 类型成员函数(balance/transfer/send/call) | ★★★★★ | 6.2.3 |
| 8 | mapping 底层存储原理(keccak256 哈希) | ★★★★ | 6.2.3 |
| 9 | storage vs memory 存储位置 | ★★★★ | 6.2.3 |
| 10 | 函数可见性(public/private/internal/external) | ★★★★★ | 6.2.3 |
| 11 | 函数行为修饰符(pure/view/constant/payable) | ★★★★★ | 6.2.3 |
| 12 | 构造函数 constructor(部署时运行一次,字节码不上链) | ★★★★★ | 6.2.3 |
| 13 | 回退函数 fallback 与接收函数 receive | ★★★★★ | 6.2.3 |
| 14 | 函数修改器 modifier | ★★★★ | 6.2.3 |
| 15 | 特殊变量与函数(msg/block/tx/this/selfdestruct/keccak256) | ★★★★★ | 6.2.3 |
| 16 | transfer/send/call 三种转账方式对比 | ★★★★★ | 6.2.3 |
| 17 | 事件日志 event/emit — 与状态树的区别、布隆过滤器机制 | ★★★★ | 6.2.3 |
| 18 | 错误处理(require/revert/assert/try…catch) | ★★★★★ | 6.2.3 |
| 19 | SolidityOJ 在线代码评测系统 | ★★ | 6.2.4 |
| 20 | 私有链搭建(geth init → geth console → 挖矿) | ★★★★ | 6.3.1 |
| 21 | 智能合约部署到私有链(Remix + External Http Provider) | ★★★★ | 6.3.2 |
| 22 | Web3.js 调用智能合约(ABI + 合约地址 → call/send) | ★★★★ | 6.3.3 |
| 23 | DApp 架构:“轻钱包 + 第三方节点”模式 | ★★★ | 6.3.3 |
| 24 | 课后选择题(10道)核心考点 | ★★★★★ | 6.4 |
二、详细笔记
6.1 智能合约及其背景
6.1.1 智能合约的定义
- 提出者:尼克·萨博(Nick Szabo),1996年
- 定义:能够自动执行合约条款的计算机程序
- 三大特性:
| 特性 | 说明 | 售货机类比 |
|---|---|---|
| 事件驱动 | 以外部输入作为触发条件,驱动合约动作执行 | 投币触发出货 |
| 价值转移 | 外部输入价值,合约输出商品/资产,完成价值交换或转移 | 钱币→饮料/食品 |
| 自动执行 | 履约行为完全自动化,除初始输入动作外不需要人为干预 | 投币后自动出货 |
6.1.2 中心化执行环境的弊端(以自动售货机为例)
| 弊端 | 具体表现 |
|---|---|
| 容易被篡改 | 可以人为修改售货机程序,使其免费出售商品 |
| 出错后难以追溯恢复 | 恶意篡改人已掌握整个机器的控制权,篡改源头无法追溯 |
| 依赖执行环境可靠性 | 一旦断电或机器故障,投币后无法获得商品 |
核心结论:从计算机的角度,这是由智能合约执行所依赖的”中心化”环境导致的弊端:容易被篡改、出错后难以追溯恢复。
6.1.3 区块链智能合约的优势
以以太坊为例:
- 在多节点组成的 P2P 网络中,节点维护共同的区块链数据
- 通过区块链上的交易进行智能合约的创建、调用、结束等操作
- 多节点维护的区块链状态一致 → 多节点上运行的智能合约的过程和结果也一致
| 对比维度 | 中心化智能合约 | 区块链智能合约 |
|---|---|---|
| 执行环境 | 单一中心化服务器 | 分布式多节点 |
| 可篡改性 | 容易被篡改 | 难以篡改 |
| 验证机制 | 无多方验证 | 多方验证 |
| 追溯性 | 难以追溯 | 不可否认、可追溯 |
| 可靠性 | 依赖单一环境 | 去中心化、高可靠 |
核心结论:区块链智能合约的优点(去中心化、多方验证、难以篡改)恰好解决了中心化智能合约的痛点。
6.1.4 智能合约的范畴
- 比特币脚本:每笔交易所附带的脚本,某种程度上也可视为智能合约,但不是图灵完备的
- 常见区块链智能合约:一般指以太坊、Fabric等平台中运行的图灵完备的智能合约
- 区块链 1.0 与 2.0 的分界:是否支持图灵完备的智能合约
6.2 Solidity 语言
6.2.1 背景
- 定义:用于实现智能合约的面向对象的高级语言
- 语法:接近 JavaScript,便于开发者快速上手
- 设计目标:编写以太坊上的智能合约,部分语法与以太坊工作原理高度耦合
- 其他智能合约语言:Serpent、Vyper、Mutan(以太坊);Golang(Hyperledger Fabric)
Solidity 特有之处:
- 特殊类型:
address、event - 特殊关键字:
payable、now - 需结合以太坊特性对变量进行存储位置定义(storage/memory)
学习资源:
| 资源 | 地址 |
|---|---|
| GitHub | https://github.com/ethereum/solidity |
| 在线编译器(Remix) | http://remix.ethereum.org/ |
| 英文文档 | https://docs.soliditylang.org/ |
| 中文文档 | https://solidity-cn.readthedocs.io |
推荐使用英文文档,因 Solidity 版本更新频繁。
6.2.2 入门示例:Remix IDE 完整开发流程
Remix(http://remix.ethereum.org/)是以太坊官方支持的 Solidity 和 Vyper 语言在线编译器。
左侧工具栏四大功能模块:
| 图标序号 | 功能模块 | 英文名称 |
|---|---|---|
| 1 | 工作空间(文件管理) | Workspace |
| 2 | 文件搜索 | Search in Files |
| 3 | 合约编译 | Solidity Compiler |
| 4 | 部署与交易运行 | Deploy & Run Transactions |
Step 1 — 打开 Remix:访问 http://remix.ethereum.org/
Step 2 — Solidity 编程:在 contracts 文件夹下创建 .sol 源文件
(A) SPDX 许可标识(第1行注释,必写)
| 许可标识 | 含义 |
|---|---|
// SPDX-License-Identifier: MIT | 合约开源 |
// SPDX-License-Identifier: UNLICENSED | 私有或者无授权 |
(B) 版本编译指示(version pragma)
| 写法 | 含义 |
|---|---|
pragma solidity 0.8.7; | 只能运行在 0.8.7 版本编译器 |
pragma solidity >=0.7.0 <0.9.0; | 运行在 >=0.7.0 且 <0.9.0 的所有版本 |
pragma solidity ^0.8.7; | 运行在 >=0.8.7 的所有 0.8.x 版本 |
本书推荐使用固定版本写法(如
0.8.7):Solidity 语法规则变动频繁,早期版本的合约代码可能无法被较新的编译器编译。本书所有示例采用 Solidity v0.8.7。
(C) 合约基本结构(Example1 示例)
// SPDX-License-Identifier: MITpragma solidity 0.8.7;
contract Example1 { uint public num; // 存储变量,public 类型可被内外访问
function change(uint _num) public { num = _num; // 修改 num 变量的值 }}contract Example1:类似于面向对象语言中的类uint public num:public 变量,编译器自动生成同名的只读 getter 函数function change:修改状态变量的函数
Step 3 — 编译
- 确认编译器版本和合约版本一致
- 点击 “Compile Example1.sol”
- 编译结果:
- 生成可部署到以太坊区块链的 EVM 字节码(Bytecode)
- 生成对应的应用二进制接口(ABI)
- 编译通过后左侧编译图标上方出现绿色对勾
- 点击左下角 “Compilation Details” 可查看详细信息
Step 4 — 部署
- 进入部署界面(左侧第4个图标)
- 环境选择:
| 环境选项 | 说明 |
|---|---|
| Remix VM(默认) | 模拟环境,所有测试在浏览器本地进行 |
| Injected Provider - MetaMask | 借助 MetaMask 钱包将合约部署至以太坊测试网络 |
- 点击 “Deploy” 部署合约,成功后出现 “Deployed Contracts”
Step 5 — 测试
- 展开 Deployed Contracts 中合约左侧的箭头符号
- 点击 “num”:查看当前状态(初始值为 0)——这是一个只读调用
- 调用 “change” 函数并传入参数(如 1024),发送交易
- 再次点击 “num” 查看:值已变为 1024
- 控制台显示交易详情:发起地址、合约地址、Gas 消耗值、合约函数及参数等
6.2.3 基础语法
1. 常用类型
Solidity 是静态类型语言,每个变量都需被定义成能被编译器识别的特定类型。
(1) 布尔类型
bool声明,取值true或false
(2) 整型
int:有符号整型;uint:无符号整型- 可加位数:
int8,int16, …,int256(步长8) - 未声明位数默认取最长位数:
int→int256 - 早期版本需注意整数溢出漏洞(Solidity 0.8+ 已内置溢出检查)
(3) 地址类型(address)
- 声明:
address,存储为 20 字节的值(与以太坊账户地址长度相同) - 自带成员变量/函数:
| 成员 | 语法 | 说明 |
|---|---|---|
balance | <address>.balance | 获取该地址的余额(返回 uint256) |
transfer | <address>.transfer(uint256 amount) | 转账,失败则回滚整个交易;gas 上限 2300 单位 |
send | <address>.send(uint256 amount) | 转账,失败返回 false;gas 上限 2300 单位 |
call | <address>.call{...}() | 底层合约调用消息 |
callcode | <address>.callcode{...}() | 底层调用 |
delegatecall | <address>.delegatecall{...}() | 底层委托调用 |
关键区别:2300 单位 gas 不足以支持内存读写等基本操作 → transfer 和 send 无法发送以太币到合约地址;call 可以发送到合约地址,但容易出现**重入(reentrancy)**安全问题。
(4) 定长字节数组
bytes1,bytes2, …,bytes32
(5) 字符串(变长字节数组)
bytes或string声明
(6) 数组
类型[]声明,通过push(添加元素)和length(获取长度)操作
(7) 映射(mapping)
- 声明:
mapping(键类型 => 值类型) - 键类型不可以是:映射、变长数组、合约、枚举、结构体
- 底层存储原理:不存储键的值,而是存储键的 keccak256 哈希值,便于减小存储体积并查询实际值
- 映射没有长度(与数组的重要区别)
(8) 结构体(struct)
struct声明,将多个基本变量组合为一个结构
变量存储位置:
| 存储位置 | 说明 | 持久性 | Gas 成本 | 典型场景 |
|---|---|---|---|---|
storage | 保存在状态树中(永久存储) | 持久化 | 高 | 状态变量 |
memory | 保存在内存中(临时存储) | 函数执行完毕释放 | 低 | 函数参数、局部变量 |
每个变量有默认存储位置;编译器无法默认识别时,会提示开发者修改。
变量可见性:
public:可被合约外部调用获取查询private:仅合约内部可见
2. 函数类型
完整声明形式:
function (参数) {public|private|internal|external} [pure|constant|view|payable] [returns (返回值)]可见性修饰符:
| 类型 | 合约内部 | 子合约(继承) | 合约外部 | 说明 |
|---|---|---|---|---|
public | Y | Y | Y | 对合约内外均可见,可被外部人或合约调用 |
private | Y | N | N | 仅对合约内可见 |
internal | Y | Y | N | 仅对内部可见,对继承了该合约的合约也可见 |
external | N | N | Y | 仅对外部可见,仅能通过 合约.函数 的形式调用 |
行为修饰符:
| 类型 | 读状态 | 写状态 | 接收以太币 | 说明 |
|---|---|---|---|---|
pure | N | N | N | 不允许函数读取或写入状态 |
view / constant | Y | N | N | 不允许函数写入状态(但可读取) |
| 无修饰 | Y | Y | N | 默认无限制(但不能收币) |
payable | Y | Y | Y | 允许函数接收以太币 |
注意:调用合约时,对不带有 payable 的函数进行带有以太币的转账将导致调用失败。
3. 特殊函数
(1) 构造函数(constructor)
- 由
constructor关键字声明的可选函数 - 只在合约部署时运行一次
- 构造函数的字节码不会被保存在区块链上(节省空间)
- 典型用法:将部署合约的地址(
msg.sender)赋值给owner变量- 利用
owner变量控制合约函数执行权限 → 相当于合约的管理员
- 利用
- 历史变化:早期版本构造函数是与合约同名的函数(如
Example3(){}),易出现笔误(大小写错误)导致任何人都可修改 owner,该写法已逐渐被废弃
(2) 回退函数(fallback)
- 一个合约最多只有一个回退函数
- 声明方式:
fallback() external [payable]fallback(bytes calldata input) external [payable] returns (bytes memory output)
- 触发条件一:函数发生错误调用时自动执行(如大小写拼写错误)
- 触发条件二:合约收到以太币时自动执行,需同时满足:
- 回退函数有
payable修饰词 - 合约不存在
receive()函数
- 回退函数有
- 早期版本:
function() external [payable],是合约中唯一一个没有函数名的函数
(3) 接收函数(receive)
- 一个合约最多只有一个接收函数
- 声明方式:
receive() external payable - 在接收到以太币转账时自动执行
- 早期 Solidity 版本中不存在
receive(),fallback 是唯一能在收到以太币时自动执行的函数
(4) 函数修改器(modifier)
- 由
modifier关键字声明 - 用于限制函数的行为:只有通过修改器检查才能继续执行
- 典型示例:
modifier onlyOwner {require(msg.sender == owner);_; // 占位符,代表被修饰的函数体}
- 在函数声明中加入修改器(如
function kill() public onlyOwner)即可限制使用权限
4. 特殊变量与函数
全局可用变量详细列表:
| 变量/函数 | 含义/用途 |
|---|---|
block.blockhash(区块号) | 近 256 个区块哈希 |
block.coinbase | 合约交易所处区块的矿工地址 |
block.difficulty | 合约交易所处区块的挖矿难度 |
block.gaslimit | 合约交易所处区块的 gasLimit |
block.number | 合约交易所处区块的高度 |
block.timestamp | 合约交易所处区块的时间戳 |
gasleft() | 合约交易剩余可用 gas(= gasLimit - 当前 gasUsed) |
msg.data | 合约消息的 calldata(一笔交易可发生多次调用/多条消息) |
msg.gas | 合约消息剩余可用 gas |
msg.sender | 合约消息发送者(当前调用的直接发送方) |
msg.value | 合约消息所带的以太币,单位为 Wei |
now | 同 block.timestamp |
tx.gasprice | 合约交易的 gasPrice |
tx.origin | 合约交易的 from(完整调用链的最初发起者) |
this | 当前合约,可明确转换为地址类型 |
selfdestruct(地址) | 删除合约代码,并把余额返回到特定地址 |
msg.sender vs tx.origin:
| 变量 | 含义 | 安全建议 |
|---|---|---|
msg.sender | 当前消息的直接发送者 | 用于权限验证 |
tx.origin | 完整调用链的最初发起者 | 不应用于权限验证(易受钓鱼攻击) |
selfdestruct 详解:
- 调用后,保存在区块链上的字节码被
0x代替 - 代码已消失,但该地址同样可以接受以太币
- 所有发送到该地址的以太币将永久丢失
随机数生成:
- 问题:区块链挖矿机制要求所有节点执行合约的结果相同,因此智能合约上生成真随机数困难
- 简单做法:利用
block.difficulty+block.timestamp+msg.sender,经abi.encodePacked()打包后用keccak256()哈希取模 - 不安全:矿工能一定程度影响交易发送时间(约 900 秒),因此矿工能一定程度上控制随机数
三种以太币发送方式对比:
| 方式 | 失败处理 | Gas 限制 | 能否发到合约地址 | 安全风险 |
|---|---|---|---|---|
transfer | 回滚整个交易 | 2300 | 否 | 低(推荐用于普通转账) |
send | 返回 false | 2300 | 否 | 低(需检查返回值) |
call | 返回 bool | 可自定义 | 能 | 高(重入攻击等) |
为什么 transfer/send 不能发到合约地址:2300 单位 gas 不足以支持内存读写等基本操作。
address(this).balance:获取当前合约所有的余额。
5. 事件日志(Event Log)
为什么需要事件日志:
- 合约状态数据存储于状态树中,运行结果也写入状态
- 如果只依赖状态树记录,上层应用获取合约运行过程中的某些状态时,必须比对前后状态改变或开辟状态增量数组进行记录,成本较高
- 事件日志提供简洁且消耗较低的方式观察合约运行状态
事件日志与状态树的对比:
| 对比维度 | 状态树(State) | 事件日志(Event) |
|---|---|---|
| 读写权限 | 可读可写 | 单向输出,不可被合约读取 |
| Gas 定价 | 较高 | 更便宜 |
| 存储位置 | 状态树 | 收据树(Receipt Tree),收据树根记录到区块 |
| 写入方式 | 变量赋值 | emit 关键字 |
| 合约可访问 | 是 | 否(不可读取) |
事件的定义与使用:
// 定义事件(含参数类型)event ETHReceipt(address indexed sender, uint256 amount);
// 触发事件(在函数中使用 emit)function receive() external payable { emit ETHReceipt(msg.sender, msg.value);}底层机制:
- 事件名及参数类型被哈希编码为 topic(与函数签名类似,同样的事件名具有同样的 topic 值)
- 每个区块中,所有交易收据经计算生成布隆过滤器(Bloom Filter),记录为区块中
logsBloom的值 - 布隆过滤器特点:
- 不可以判断出某个值一定存在某个集合中
- 可以判断出某个值一定不在某个集合中
- 上层应用查询流程:
- 请求区块的
logsBloom值 - 判断某个区块中某个合约的事件是否一定没有发生
- 若确定没发生 → 等待下一个区块
- 若不确定(可能发生) → 请求详细的事件收据确认
- 请求区块的
测试事件(Remix):
- 进入部署界面,确认发送账户
- 在 Value 中输入金额(如 1000 Wei)
- 点击 Transact 发送以太币给合约
- 控制台接收到事件,包含 sender 和 amount 参数的具体值
6. 错误处理
三种错误处理方式:
| 方式 | 语法 | 行为描述 |
|---|---|---|
require | require(条件参数, [消息参数]) | 条件为 false → 撤销状态更改 + 提供错误消息(可选) |
revert | revert([消息参数]) | 终止运行并撤销状态更改 + 提供错误消息(可选) |
assert | assert(条件参数) | 条件为 false → 使当前交易没有效果,消耗交易剩余的 gas |
三种方式对比:
| 对比维度 | require | revert | assert |
|---|---|---|---|
| 返还剩余 Gas | 是 | 是 | 否(全部消耗) |
| 可附带消息 | 是 | 是 | 否 |
| 使用场景 | 判断条件是否满足(输入验证、权限检查) | 复杂条件判断 + 自定义错误(配合 error) | 判断内部错误(除 0、溢出等) |
| 自定义 error | 不支持 | 支持(Solidity 提供错误汇报器) | 不支持 |
使用原则:
require:用于判断某一条件是否满足assert:用于判断某种错误是否发生(如除 0 错误)revert:配合自定义 error 传递错误信息
try…catch 机制:
- 由于合约的原子性,一旦异常发生,交易将被回滚
- 在某些情况下需要即使错误发生也不回滚交易,可使用
try...catch - 用法:调用外部函数时,若异常则执行 catch 部分代码,否则正常返回
6.2.4 Solidity 在线代码评测系统(SolidityOJ)
- 网址:https://SolidityOJ.com/
- 配套的在线代码评测系统(Online Judge, OJ)
- 默认编译器版本:Solidity v0.8+,题库列表定期更新
- 点击网页上方 Problem 按钮选择习题,OJ 在线评测代码正确性并返回结果
OJ 六道典型例题:
| 题号 | 题目 | 核心要求 |
|---|---|---|
| 例1 | A + B | 编写合约 Contract,调用 main(a,b) 返回 a+b |
| 例2 | 存证合约 | set(a) 存储整数,get() 获取上次存证的 a 值 |
| 例3 | A 乘 B 事件 | main(a,b) 输出事件 result(uint, uint) |
| 例4 | 数组操作 | push() 往数组存入整数,last() 返回最后一个存入的整数 |
| 例5 | 回退函数 | 调用任意函数均返回 "Hello, World!" 的 abi 编码 |
| 例6 | 投票合约 | vote(候选人编号) 投票,result() 返回票数最高的候选人(编号范围 0 ~ 2^256-1) |
6.3 DApp 开发示例
概述
- DApp:去中心化应用(Decentralized Application),基于区块链智能合约
- 本章介绍一种较典型的、基于区块链智能合约的 DApp 开发方式
- 实验资源包:http://inpluslab.com/filedbook.zip
- Go-ethereum 客户端(geth.exe)
- 创世区块配置文件(genesis.json)
- Web3.js 库(web3.min.js)
- Linux/macOS 用户:自行下载 1.9.9 版本的 Go-ethereum 二进制文件
6.3.1 私有链搭建
完整搭建步骤:
步骤一:初始化创世区块
geth --datadir mydata init genesis.json--datadir mydata:指定区块链数据存储目录为 mydata 文件夹genesis.json:创世区块配置文件
步骤二:启动以太坊节点
geth --datadir mydata --networkid 666 --rpc --rpccorsdomain "*" --allow-insecure-unlock console| 配置项 | 作用 |
|---|---|
--datadir mydata | 数据存储目录 |
--networkid 666 | 区块链网络 ID 为 666(避免与互联网中其他节点干扰) |
--rpc | 打开 RPC 接口,默认端口 8545 |
--rpccorsdomain "*" | 允许浏览器在任意地址进行跨域调用 |
--allow-insecure-unlock | 允许通过 HTTP 解锁账户 |
console | 打开 JavaScript 控制台(自带 web3.js 库) |
控制台是连接了节点的 JavaScript 控制台,自带 web3.js 库,可对以太坊节点信息进行查询。
步骤三:查看区块链状态
eth.blockNumber // 获取最新区块高度eth.getBlock(0) // 获取区块内容步骤四:新建账户
personal.newAccount("")- 参数为空字符串,方便后续实验
- 私钥生成并保存于数据目录下
- 控制台输出新建账号的公钥地址
步骤五:确认账户
eth.accounts // 获取节点中保存的账号地址列表eth.getBalance(eth.accounts[0]) // 查看首个账号余额- 新建账号以太币余额为零
步骤六:挖矿获取以太币
miner.start(); admin.sleepBlocks(1); miner.stop()- 开启挖矿 → 等待 1 个区块的产生 → 结束挖矿
- Coinbase 地址默认为本地账号中的第一个(即上文新建的账号)
- 首次挖矿时 geth 客户端将生成挖矿所需的 DAG,时间视机器配置而定
- 极少数情况下可能出现挖不到块的情况 → 删除数据目录(mydata),重新从 init 命令开始
- 多核 CPU 下因挖矿难度较低,可能出现不止挖到一个块的情况
- 挖矿完成后,区块高度与账号以太币余额(单位为 Wei)增加
建议不使用
--dev开发者模式,手动挖矿更利于理解交易确认、挖矿等过程。
多节点互联(选做):重复以上步骤新建数据目录和新节点客户端,通过 admin.nodeInfo 获取 enode 身份地址进行互联。
6.3.2 智能合约部署与测试
将合约从 Remix 部署到私有链:
Share.sol 示例合约:
// SPDX-License-Identifier: MITpragma solidity 0.8.7;
contract Share { struct ShareInfo { address sender; string text; } ShareInfo[] public allShare; // 结构体数组记录用户地址和分享信息 event Added(uint indexed id); // 事件输出分享信息的下标
function NewShare(string memory something) public { allShare.push(ShareInfo(msg.sender, something)); emit Added(allShare.length - 1); }
function getLength() public view returns (uint) { return allShare.length; // 返回数组长度 }}部署步骤:
- 在 Remix 中编写 Share.sol,编译通过
- 进入 “Deploy & run transactions”,Environment 选择 “External Http Provider”
- 默认 8545 端口即为上一节开放的 RPC 端口
- 在节点控制台解锁账号:
personal.unlockAccount(eth.accounts[0], "", 3600)
- 以空字符串解锁账号 3600 秒
- 在 Remix 中点击 Deploy,发送部署交易
- 在节点控制台查看待打包交易:
txpool.status
- 挖矿打包交易:
miner.start(); admin.sleepBlocks(1); miner.stop()
- 挖矿完成后合约部署到区块链上
合约函数测试:
- 调用
NewShare("Hello World!")发送交易 - 关键观察:交易发送后、区块打包前,调用
getLength()查看 —— 合约状态并未改变 - 挖矿打包后,合约状态才改变
- Remix 与节点控制台均输出相关交易信息
6.3.3 使用 Web3.js 调用智能合约
Web3.js:以太坊官方支持开发的 JavaScript 接口库,用于浏览器与以太坊节点的通信。
开发步骤:
1. 引入 Web3.js 库
<script src="web3.min.js"></script>2. 新建 Web3 Provider(网页加载完毕时)
var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));3. 根据合约 ABI 及地址新建合约对象
- 合约地址从 Remix 控制台交易信息中获取,或在节点控制台中查询交易收据得到
4. 调用只读函数(call)
Share.methods.getLength().call(function(err, data) { var length = data; for (var id = 0; id < length; id++) { Share.methods.allShare(id).call(function(err, data) { // 将结果显示到网页中 }); }});- Web3.js 自动将调用编码为 RPC 请求,并将结果返回到回调函数中
5. 发送交易(send)
- 点击网页按钮时触发 send 函数
- Web3.js 获取当前节点账号列表,获取输入框的值,调用合约函数
- 交易发送后,在节点控制台再次开启挖矿打包
- 打包完成后刷新网页即可读取最新合约状态
6. 事件监听(进阶)
- 开启 geth 的 WebSocket 端口
- 在网页中监听该端口订阅合约事件
- 合约更新时收到事件通知,不必时时刻刻进行链上查询,节省成本
DApp 当前主流使用模式:
| 模式 | 说明 |
|---|---|
| ”轻钱包 + 第三方节点” | 用户不需要运行自己的以太坊节点,只需保管好私钥 |
| 代表工具 | MetaMask(浏览器轻钱包插件) |
| 优点 | 大大降低区块链应用的使用门槛 |
| 缺点 | 用户本身不运行节点,带来一定的中心化问题 |
三、名词解释(高频考题)
| 名词 | 解释 |
|---|---|
| 智能合约(Smart Contract) | 尼克·萨博 1996 年提出,能自动执行合约条款的计算机程序,具有事件驱动、价值转移、自动执行三大特性 |
| Solidity | 用于实现智能合约的面向对象高级语言,语法接近 JavaScript,是目前较成熟流行的智能合约语言 |
| EVM(以太坊虚拟机) | Ethereum Virtual Machine,以太坊智能合约的运行环境 |
| ABI(应用二进制接口) | Application Binary Interface,编译时自动生成的合约外部交互接口描述 |
| Bytecode(字节码) | Solidity 源代码编译后生成的 EVM 可识别执行的字节码 |
| Remix | 以太坊官方支持的 Solidity/Vyper 语言在线编译器(http://remix.ethereum.org/) |
| SPDX 许可标识 | 合约开头必写的许可声明,如 MIT(开源)、UNLICENSED(私有) |
| version pragma | 版本编译指示,声明合约适用的编译器版本范围 |
| storage | 变量永久保存在区块链状态树中,Gas 消耗高 |
| memory | 变量临时保存在内存中(函数执行完毕释放),Gas 消耗低 |
| mapping(映射) | 键值对存储结构,底层存储键的 keccak256 哈希值,没有长度属性 |
| constructor | 构造函数,仅在合约部署时运行一次,字节码不保存在区块链上 |
| fallback | 回退函数,在函数错误调用时或合约收到以太币时(无 receive 时)自动执行 |
| receive | 接收函数,在接收到以太币转账时自动执行 |
| modifier | 函数修改器,用于限制函数行为(如 onlyOwner 权限控制) |
| event | 事件,通过 emit 输出到交易收据中,不可被合约读取 |
| 布隆过滤器(Bloom Filter) | 空间效率高的概率数据结构,可判断某值一定不在集合中(不能确认一定在),用于高效查询事件日志 |
| logsBloom | 区块中记录布隆过滤器的字段,上层应用通过它高效判断事件是否发生 |
| selfdestruct | 删除合约代码(字节码变为 0x),余额返回到指定地址;此后发送到该地址的以太币永久丢失 |
| Web3.js | 以太坊官方支持的 JavaScript 接口库,用于浏览器与以太坊节点通信 |
| Go-ethereum(geth) | Golang 版本的以太坊客户端实现 |
| genesis.json | 创世区块配置文件,定义私有链初始参数 |
| DApp | Decentralized Application,基于区块链智能合约的去中心化应用 |
| MetaMask | 浏览器轻钱包插件,可配合 Remix 将合约部署至以太坊测试网络 |
| Wei | 以太币最小单位,msg.value 的单位,1 ETH = 10^18 Wei |
| DAG | 有向无环图,以太坊挖矿所需的数据结构 |
| Gas | 以太坊上计算资源计量单位,衡量执行操作所需费用 |
| SolidityOJ | 本书配套的 Solidity 在线代码评测系统(https://SolidityOJ.com/),默认编译器版本 v0.8+ |
四、重点对比表格
4.1 函数可见性对比
| 可见性 | 合约内部 | 子合约(继承) | 合约外部 | 调用说明 |
|---|---|---|---|---|
public | Y | Y | Y | 内外均可直接调用 |
private | Y | N | N | 仅本合约内 |
internal | Y | Y | N | 内部 + 派生合约 |
external | N(不可直接) | N | Y | 仅 合约.函数 形式调用 |
4.2 函数行为修饰符对比
| 修饰符 | 读状态 | 写状态 | 接收以太币 | Gas 消耗 |
|---|---|---|---|---|
pure | N | N | N | 最低 |
view / constant | Y | N | N | 低 |
| 无修饰符 | Y | Y | N | 正常 |
payable | Y | Y | Y | 正常 |
4.3 三种错误处理方式对比
| 方式 | 撤销状态 | 退还剩余 Gas | 可附带消息 | 支持自定义 error | 典型使用场景 |
|---|---|---|---|---|---|
require() | 是 | 是 | 是 | 否 | 验证输入条件/权限 |
revert() | 是 | 是 | 是 | 是 | 复杂条件 + 自定义错误 |
assert() | 是 | 否 | 否 | 否 | 检查内部不变量(如除0) |
4.4 三种特殊函数对比
| 函数 | 数量限制 | 执行时机 | 当前声明方式 | 早期版本 |
|---|---|---|---|---|
| constructor | 最多 1 个 | 合约部署时运行一次(字节码不上链) | constructor() { ... } | 与合约同名函数(已废弃) |
| fallback | 最多 1 个 | 错误调用时 / 收到 ETH 时(无 receive 时) | fallback() external [payable] | function() external [payable](无函数名) |
| receive | 最多 1 个 | 收到 ETH 转账时 | receive() external payable | 不存在(早期由 fallback 代理) |
4.5 三种以太币转账方式对比
| 方式 | 失败处理 | Gas 限制 | 可发到合约地址 | 安全风险 | 推荐场景 |
|---|---|---|---|---|---|
transfer | 回滚整个交易 | 2300(固定) | 否 | 低 | 普通地址间转账 |
send | 返回 false | 2300(固定) | 否 | 低(需检查返回值) | 需自行处理失败的场景 |
call | 返回 bool | 可自定义 | 能 | 高(重入攻击) | 需发送到合约地址时 |
transfer/send 限制 2300 gas:不足以支持内存读写等基本操作,无法发送以太币到合约地址。
4.6 事件日志 vs 状态树
| 对比维度 | 状态树(Storage) | 事件日志(Event) |
|---|---|---|
| 读写权限 | 可读可写 | 单向输出,不可被合约读取 |
| Gas 成本 | 高 | 便宜 |
| 存储位置 | 状态树 | 交易收据 → 收据树 |
| 记录到区块 | 状态树根(stateRoot) | 布隆过滤器(logsBloom) |
| 写入方式 | 变量赋值 | emit 关键字 |
| 上层应用查询 | 直接读取状态 | 布隆过滤器 + 事件收据(高效) |
4.7 msg.sender vs tx.origin
| 变量 | 含义 | 安全建议 |
|---|---|---|
msg.sender | 当前消息的直接发送者地址 | 用于权限验证 |
tx.origin | 完整调用链的最初发起者地址 | 不应用于权限验证(易受钓鱼攻击) |
4.8 编译版本声明写法对比
| 写法 | 含义 | 推荐程度 |
|---|---|---|
pragma solidity 0.8.7; | 仅 0.8.7 版本 | ★★★★★ 推荐 |
pragma solidity >=0.7.0 <0.9.0; | >=0.7.0 且 <0.9.0 的所有版本 | ★★★ 常用 |
pragma solidity ^0.8.7; | >=0.8.7 的所有 0.8.x 版本 | ★★★ 常用 |
本书推荐固定版本写法:因 Solidity 语法规则变动频繁,能确保代码可被稳定编译复用。
五、可能考题
5.1 简答题预测
1. 简述智能合约的定义及其三大特性,并分析中心化智能合约的不足以及区块链如何解决这些问题。
- 定义:尼克·萨博 1996 年提出,能自动执行合约条款的计算机程序
- 三大特性:事件驱动、价值转移、自动执行
- 中心化不足:容易被篡改、出错后难以追溯恢复(根源:依赖中心化执行环境)
- 区块链解决:去中心化、多方验证、难以篡改。多节点执行结果一致,使智能合约具备更高可靠性
2. 对比说明 Solidity 中 require、revert、assert 三种错误处理方式的区别和使用场景。
require(条件, [消息]):条件为 false 时撤销状态更改,退还剩余 gas,可附带错误消息 → 用于验证外部输入和权限检查revert([消息]):终止运行并撤销状态更改,退还剩余 gas,可配合自定义 error 传递复杂错误信息 → 用于复杂条件判断assert(条件):条件为 false 时使交易无效果,消耗剩余 gas(不退还)→ 用于检查内部不变量(如除0、溢出)- 共性:由于合约原子性,异常发生则交易回滚
3. 简述以太坊事件日志(Event)的作用,并说明事件日志与状态树的主要区别。
- 作用:提供简洁且消耗较低的方式让上层应用观察合约运行状态
- 区别:状态树可读可写,Gas 高;事件日志单向输出不可被合约读取,Gas 便宜
- 存储:状态树根记录到区块,事件日志通过布隆过滤器(logsBloom)记录到区块
- 查询效率:事件日志利用布隆过滤器实现高效判断(判断事件一定未发生的区块可跳过)
4. 说明 Solidity 中 transfer、send、call 三种转账方式的区别。
transfer:失败回滚整个交易,固定 2300 gas 上限,不能发送到合约地址send:失败返回 false,固定 2300 gas 上限,不能发送到合约地址call:失败返回 bool,可自定义 gas,可以发送到合约地址,但需注意重入攻击安全问题- 原因:2300 gas 不足以支持内存读写等基本操作,因此 transfer/send 无法发送以太币到合约地址
5. 简述 DApp 开发的基本流程及 Web3.js 的作用。
- 流程:搭建私有链环境(geth+genesis.json)→ 编写智能合约(Remix)→ 编译生成 ABI 和字节码 → 部署合约到链上 → 使用 Web3.js 构建前端交互界面 → 可选使用 MetaMask 降低使用门槛
- Web3.js 作用:以太坊官方支持的 JavaScript 接口库,用于浏览器与以太坊节点通过 RPC 端口通信,自动编码 RPC 请求
- DApp 主流模式:“轻钱包 + 第三方节点”
6. 简述 Solidity 中 constructor、fallback、receive 三种特殊函数的区别。
| 函数 | 执行时机 | 执行次数 | 备注 |
|---|---|---|---|
| constructor | 合约部署时 | 1次 | 字节码不上链,用于初始化(如设置 owner) |
| fallback | 错误调用时;收到 ETH 时(无 receive 时) | 按需 | 历史上最高 1 个,无函数名 |
| receive | 收到 ETH 转账时 | 按需 | 最高 1 个,比 fallback 更明确专用于收 ETH |
7. mapping 类型的底层存储特点是什么?与数组有何区别?
- mapping 底层不存储键的值,而是存储键的 keccak256 哈希值,便于减小存储体积并查询
- 键类型不可为:映射、变长数组、合约、枚举、结构体
- 与数组的关键区别:mapping 没有长度(数组有 length 属性)
5.2 选择题(课后原题 + 解析)
1. Solidity 中获取完整调用链交易发起者的变量是( D. tx.origin )
- A. msg.data / B. msg.sender / C. tx.gasprice / D. tx.origin
- 解析:
msg.sender是当前调用者,tx.origin是完整调用链的最初发起者
2. Solidity 的 pure 类型表示的是( C. 不允许函数读取或者写入状态 )
- A. 允许函数读取或写入状态 / B. 允许函数读取状态 / C. 不允许函数读取或者写入状态 / D. 允许函数写入状态
- 解析:pure 既不能读也不能写;view 可读不可写
3. Solidity 的日志关键词是( D. emit )
- A. internal / B. pure / C. public / D. emit
- 解析:事件由 event 定义,由 emit 输出
4. Solidity 中被标记为 external 的函数( A. 可以被合约外任何函数直接调用 )
- 解析:external 仅对外部可见,仅能通过
合约.函数形式调用
5. 以太坊 *.sol 文件里,( A. 可以 )定义多个合约。
6. 编译部署到以太坊的智能合约是智能合约的( C. 字节码 )。
- 解析:源代码 → 编译 → EVM 字节码 → 部署
7. 消息调用的操作码不包括( D. dynamiccall )。
- 解析:只有 call、callcode、delegatecall 三种
8. Solidity 中获取当前调用消息发送者的变量是( B. msg.sender )。
9. Solidity 中获取当前区块 Gas 限制的变量是( B. block.gaslimit )。
10. Solidity 中,对合约内外可见的函数类型是( A. public )。
- 解析:public 对内外均可见;external 仅外部;internal 仅内部+子合约;private 仅本合约
5.3 实验题(课后原题,均可在 SolidityOJ 上测试)
| 题号 | 题目 | 核心要求 | 关键考点 |
|---|---|---|---|
| 1 | 字符串拼接 | main(string, string) 返回 a+b | string 类型操作、abi 编码 |
| 2 | 字符串比较 | main(string, string) 相同返回 1,不同返回 0 | keccak256 哈希比较 |
| 3 | 栈操作 | push(int256), pop(), top()(空栈时 top 返回 -1) | 数组模拟栈 |
| 4 | 队列操作 | push(int256), pop(), front(), back()(空时返回 -1) | 数组双指针模拟队列 |
5.4 代码填空题预测
1. 完整的合约文件头:
// SPDX-License-Identifier: MITpragma solidity 0.8.7;2. 声明一个公开的映射(地址 → uint256):
mapping(address => uint256) public balances;3. 定义一个只允许合约部署者调用的修改器:
modifier onlyOwner { require(msg.sender == owner); _;}4. 定义一个在收到以太币时触发的事件:
event Received(address indexed sender, uint256 amount);
receive() external payable { emit Received(msg.sender, msg.value);}5. 使用 require 进行输入验证:
function divide(uint a, uint b) public pure returns (uint) { require(b > 0, "Cannot dividing by zero"); return a / b;}6. 构造函数中保存部署者地址为管理员:
address public owner;
constructor() { owner = msg.sender;}重点记忆口诀:
- 智能合约三特性:事件驱动、价值转移、自动执行
- 四大可见性:public 内外、private 仅内、internal 内+子、external 仅外
- 函数修饰四兄弟:pure 不读不写、view 只读不写、默认可读可写、payable 另可收币
- 错误处理三件套:require 验条件(退 gas)、revert 终止撤(退 gas)、assert 查内部(耗 gas)
- 转账三兄弟:transfer 回滚限 2300、send 返 false 限 2300、call 无限制但防重入
- 三个特殊函数:constructor 部署一次不上链、fallback 错误调用兼收币、receive 专收币
- SPDX + 版本号:合约开头必备两行
- Remix 四图标:1 工作空间 → 2 文件搜索 → 3 编译 → 4 部署运行
- 事件日志优点:单向输出、Gas 便宜、布隆过滤器高效查询
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!










