智能合约

9071 字
45 分钟
智能合约
Warning

含AI生成内容

第6章 区块链智能合约开发#


一、考点速览#

序号考点重要程度所在节
1智能合约定义与三大特性(尼克·萨博 1996)★★★★★6.1
2中心化智能合约的弊端与区块链智能合约的优势★★★★★6.1
3Solidity 语言定位、设计特点与学习资源★★★★6.2.1
4Remix IDE 四大功能模块与开发五步流程★★★★★6.2.2
5SPDX 许可标识与版本编译指示(version pragma)★★★★6.2.2
6常用类型(bool/int/uint/address/bytes/string/array/mapping/struct)★★★★★6.2.3
7address 类型成员函数(balance/transfer/send/call)★★★★★6.2.3
8mapping 底层存储原理(keccak256 哈希)★★★★6.2.3
9storage 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
16transfer/send/call 三种转账方式对比★★★★★6.2.3
17事件日志 event/emit — 与状态树的区别、布隆过滤器机制★★★★6.2.3
18错误处理(require/revert/assert/try…catch)★★★★★6.2.3
19SolidityOJ 在线代码评测系统★★6.2.4
20私有链搭建(geth init → geth console → 挖矿)★★★★6.3.1
21智能合约部署到私有链(Remix + External Http Provider)★★★★6.3.2
22Web3.js 调用智能合约(ABI + 合约地址 → call/send)★★★★6.3.3
23DApp 架构:“轻钱包 + 第三方节点”模式★★★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 特有之处

  • 特殊类型:addressevent
  • 特殊关键字:payablenow
  • 需结合以太坊特性对变量进行存储位置定义(storage/memory)

学习资源

资源地址
GitHubhttps://github.com/ethereum/solidity
在线编译器(Remix)http://remix.ethereum.org/
英文文档https://docs.soliditylang.org/
中文文档https://solidity-cn.readthedocs.io

推荐使用英文文档,因 Solidity 版本更新频繁。

6.2.2 入门示例:Remix IDE 完整开发流程#

Remixhttp://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: MIT
pragma 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 声明,取值 truefalse

(2) 整型

  • int:有符号整型;uint:无符号整型
  • 可加位数:int8, int16, …, int256(步长8)
  • 未声明位数默认取最长位数intint256
  • 早期版本需注意整数溢出漏洞(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) 字符串(变长字节数组)

  • bytesstring 声明

(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 (返回值)]

可见性修饰符

类型合约内部子合约(继承)合约外部说明
publicYYY对合约内外均可见,可被外部人或合约调用
privateYNN仅对合约内可见
internalYYN仅对内部可见,对继承了该合约的合约也可见
externalNNY仅对外部可见,仅能通过 合约.函数 的形式调用

行为修饰符

类型读状态写状态接收以太币说明
pureNNN不允许函数读取或写入状态
view / constantYNN不允许函数写入状态(但可读取)
无修饰YYN默认无限制(但不能收币)
payableYYY允许函数接收以太币

注意:调用合约时,对不带有 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)
  • 触发条件一:函数发生错误调用时自动执行(如大小写拼写错误)
  • 触发条件二:合约收到以太币时自动执行,需同时满足:
    1. 回退函数有 payable 修饰词
    2. 合约不存在 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
nowblock.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返回 false2300低(需检查返回值)
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 的值
  • 布隆过滤器特点
    • 不可以判断出某个值一定存在某个集合中
    • 可以判断出某个值一定不在某个集合中
  • 上层应用查询流程
    1. 请求区块的 logsBloom
    2. 判断某个区块中某个合约的事件是否一定没有发生
    3. 若确定没发生 → 等待下一个区块
    4. 若不确定(可能发生) → 请求详细的事件收据确认

测试事件(Remix)

  1. 进入部署界面,确认发送账户
  2. 在 Value 中输入金额(如 1000 Wei)
  3. 点击 Transact 发送以太币给合约
  4. 控制台接收到事件,包含 sender 和 amount 参数的具体值

6. 错误处理#

三种错误处理方式

方式语法行为描述
requirerequire(条件参数, [消息参数])条件为 false → 撤销状态更改 + 提供错误消息(可选)
revertrevert([消息参数])终止运行并撤销状态更改 + 提供错误消息(可选)
assertassert(条件参数)条件为 false → 使当前交易没有效果,消耗交易剩余的 gas

三种方式对比

对比维度requirerevertassert
返还剩余 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 六道典型例题

题号题目核心要求
例1A + B编写合约 Contract,调用 main(a,b) 返回 a+b
例2存证合约set(a) 存储整数,get() 获取上次存证的 a 值
例3A 乘 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 私有链搭建#

完整搭建步骤

步骤一:初始化创世区块

Terminal window
geth --datadir mydata init genesis.json
  • --datadir mydata:指定区块链数据存储目录为 mydata 文件夹
  • genesis.json:创世区块配置文件

步骤二:启动以太坊节点

Terminal window
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: MIT
pragma 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; // 返回数组长度
}
}

部署步骤

  1. 在 Remix 中编写 Share.sol,编译通过
  2. 进入 “Deploy & run transactions”,Environment 选择 “External Http Provider”
    • 默认 8545 端口即为上一节开放的 RPC 端口
  3. 在节点控制台解锁账号:
    personal.unlockAccount(eth.accounts[0], "", 3600)
    • 以空字符串解锁账号 3600 秒
  4. 在 Remix 中点击 Deploy,发送部署交易
  5. 在节点控制台查看待打包交易:
    txpool.status
  6. 挖矿打包交易:
    miner.start(); admin.sleepBlocks(1); miner.stop()
  7. 挖矿完成后合约部署到区块链上

合约函数测试

  • 调用 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创世区块配置文件,定义私有链初始参数
DAppDecentralized Application,基于区块链智能合约的去中心化应用
MetaMask浏览器轻钱包插件,可配合 Remix 将合约部署至以太坊测试网络
Wei以太币最小单位,msg.value 的单位,1 ETH = 10^18 Wei
DAG有向无环图,以太坊挖矿所需的数据结构
Gas以太坊上计算资源计量单位,衡量执行操作所需费用
SolidityOJ本书配套的 Solidity 在线代码评测系统(https://SolidityOJ.com/),默认编译器版本 v0.8+

四、重点对比表格#

4.1 函数可见性对比#

可见性合约内部子合约(继承)合约外部调用说明
publicYYY内外均可直接调用
privateYNN仅本合约内
internalYYN内部 + 派生合约
externalN(不可直接)NY合约.函数 形式调用

4.2 函数行为修饰符对比#

修饰符读状态写状态接收以太币Gas 消耗
pureNNN最低
view / constantYNN
无修饰符YYN正常
payableYYY正常

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返回 false2300(固定)低(需检查返回值)需自行处理失败的场景
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 中 requirerevertassert 三种错误处理方式的区别和使用场景。

  • require(条件, [消息]):条件为 false 时撤销状态更改,退还剩余 gas,可附带错误消息 → 用于验证外部输入和权限检查
  • revert([消息]):终止运行并撤销状态更改,退还剩余 gas,可配合自定义 error 传递复杂错误信息 → 用于复杂条件判断
  • assert(条件):条件为 false 时使交易无效果,消耗剩余 gas(不退还)→ 用于检查内部不变量(如除0、溢出)
  • 共性:由于合约原子性,异常发生则交易回滚

3. 简述以太坊事件日志(Event)的作用,并说明事件日志与状态树的主要区别。

  • 作用:提供简洁且消耗较低的方式让上层应用观察合约运行状态
  • 区别:状态树可读可写,Gas 高;事件日志单向输出不可被合约读取,Gas 便宜
  • 存储:状态树根记录到区块,事件日志通过布隆过滤器(logsBloom)记录到区块
  • 查询效率:事件日志利用布隆过滤器实现高效判断(判断事件一定未发生的区块可跳过)

4. 说明 Solidity 中 transfersendcall 三种转账方式的区别。

  • 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+bstring 类型操作、abi 编码
2字符串比较main(string, string) 相同返回 1,不同返回 0keccak256 哈希比较
3栈操作push(int256), pop(), top()(空栈时 top 返回 -1)数组模拟栈
4队列操作push(int256), pop(), front(), back()(空时返回 -1)数组双指针模拟队列

5.4 代码填空题预测#

1. 完整的合约文件头:

// SPDX-License-Identifier: MIT
pragma 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 便宜、布隆过滤器高效查询

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

智能合约
https://lingluoa.icu/posts/blockchain/第6章-智能合约/
作者
lingluoa
发布于
2026-07-13
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
lingluoa
Hello, I'm lingluoa.
公告
欢迎来到我的博客!不定期更新中。
文章目录
标签
站点统计
文章
53
分类
9
标签
75
总字数
175,771
运行时长
0
最后活动
0 天前
站点信息
构建平台
ESA Pages
博客版本
Firefly v6.15.6
文章许可
CC BY-NC-SA 4.0