它住在一个地址上
链上执行的不是 Solidity 文件,而是合约账户关联的 EVM 运行时代码;需要跨交易保存的数据进入该地址的 storage。
从0到深入理解以太坊
第六篇 · 第九章 · 第一课
Ethereum · 09.01 · Smart Contracts
智能合约不是会自己思考的“智能合同”,而是位于以太坊地址上的程序与持久状态。它平时静止;当一笔交易直接或间接抵达,EVM 才按共同规则执行它。
同一前置状态 + 同一交易 + 同一执行上下文 → 同一结果状态
链上执行的不是 Solidity 文件,而是合约账户关联的 EVM 运行时代码;需要跨交易保存的数据进入该地址的 storage。
普通合约不能签名发起顶层交易。用户交易是执行树的根;合约可以在同一笔交易里继续 message call 其他合约。
处理区块的执行节点按同一 EVM 规则重放。成功则提交新状态;未被处理的失败会回滚相应状态写入。
01 · Orientation
“智能合约”这个名字容易让人同时想到法律合同、人工智能和自动化机器人。先把这些联想放下。
Working definition
智能合约是以太坊状态中,与某个地址关联的 EVM 运行时代码及其持久存储;当调用抵达时,代码依据输入和当前状态执行。
ethereum.org 的入门定义是:智能合约是在以太坊区块链上运行的程序,由代码(函数)和数据(状态)构成,并位于一个特定地址。[1] 这个定义里有四个不能丢的词:
代码可以表达和执行规则,但是否构成法律合同,取决于链下司法、当事人和具体事实。
“合约”描述可编程规则的用途;协议真正处理的是字节码、账户状态和调用。
合约不会理解意图,也不会判断规则是否公平。写错的条件,它也会确定性地写错。
一旦被调用,它不需要平台员工逐笔批准,而是按公开的程序规则计算。
时间经过、价格变化或比赛结束都不会直接唤醒合约;必须有人或服务提交调用。
条件只是代码中的判断。只有执行抵达那一行时,它才会被读取和验证。
本课只建立“是什么、存在哪里、如何被触发、怎样产生状态结果”的心智模型。编写、编译、部署、调用与升级的完整生命周期属于下一课;Solidity 数据类型、函数、Modifier、Event、Mapping 与 Struct 属于本章第三课。
02 · Metaphor
售货机比喻能说明“输入触发规则,规则产生输出”;真正的以太坊合约多了共享状态、公开验证和原子调用。
这个比喻抓住了核心:给定输入,程序检查条件并改变自己的状态。ethereum.org 也用售货机帮助初学者理解智能合约。[1] 但如果停在这里,会错过以太坊最重要的差异。
把合约想成一台“有记忆的规则机器”:它有当前状态 S,收到一份调用输入 I,在环境 E 中运行代码 C,得到下一状态 S' 与输出。只要前置状态、输入和相关执行上下文相同,合规执行节点就应得到同样结果。
03 · Anatomy
链上不是把一个项目文件夹塞进区块。协议状态只关心账户、字节码、余额、nonce 与持久存储等可执行信息。
以太坊状态中的账户包含四个核心字段:nonce、balance、storageRoot 与 codeHash。对于普通合约账户,codeHash 指向收到 message call 时应执行的 EVM 代码,storageRoot 承诺该账户的持久 storage 内容。[2][3]
选择字段,观察它在协议状态中的职责。
Interactive · Account
因此,“源码永久保存在链上”并不精确。协议保证可执行的 runtime bytecode 与当前 storage 属于链上状态;完整源码和 ABI 通常通过代码验证、编译器元数据或项目仓库另行公开。验证源码的价值,是证明某份人类可读代码可以重新编译为链上字节码,而不是让 EVM 直接执行 Solidity。[4]
Storage 会跨交易保留,但合约代码可以在后续获准调用中覆盖或清零某个槽位。历史区块仍留下可验证的旧状态承诺;合约在当前执行里读取的,是选定区块状态下的当前值,不是一个自动可枚举的历史数据库。
| 维度 | 传统后端服务 | 以太坊智能合约 |
|---|---|---|
| 运行位置 | 某家公司或云服务控制的服务器 | 处理链状态的执行客户端按协议规则重放 |
| 启动方式 | 常驻进程、定时任务、请求或队列 | 交易锚定执行;内部继续形成 message-call 树 |
| 持久数据 | 运营者控制的数据库 | 合约 storage 进入共享状态承诺 |
| 升级控制 | 运营者可替换服务代码 | 普通地址的 runtime code 通常固定;应用可显式设计代理 |
| 外部网络 | 可直接请求任意 API | 执行中不能随意访问链外互联网 |
| 结果核验 | 通常信任服务器响应或审计日志 | 节点可从前置状态和交易重算结果 |
04 · Trigger
一句“由交易触发”还不够。要把顶层交易、合约间 message call 与本地 eth_call 分成三个层级。
普通协议交易由可签名账户发起。若交易的 to 指向有代码的地址,执行会形成顶层 message call;合约 A 在运行中又可以同步调用合约 B,B 再调用 C。后两层是同一笔交易内部的 message call,没有独立签名、交易哈希或区块位置。Solidity 文档明确说明:每笔调用合约的交易包含一个顶层 message call,而它又可以创建更多 message call。[5]
切换入口,比较它们是否签名、是否入块、能否提交状态。
Interactive · Trigger
顶层锚点:钱包签名的是一笔交易;它被纳入区块后,EVM 才执行目标合约并可能提交状态。
区块浏览器常把调用 trace 中的价值转移或合约调用展示为 “Internal Transactions”。这对浏览很方便,但协议层并没有第二笔被签名、广播和排队的交易。更精确的语言是:外层交易中的内部 message call / CREATE 操作。
CALL 下,B 看到的 msg.sender 是 A,而不是最初的钱包。try/catch 处理它。eth_call 为什么也能“运行”合约,却不是链上交易?eth_call 让一个节点立即在指定区块状态上模拟 message call,并返回结果;它不创建链上交易,不进入区块,也不提交模拟期间的状态写入。它常用于读取 view 函数,但并非只能模拟 view:即使目标代码尝试写 storage,临时改动也会在模拟结束后丢弃。[6]
链上状态效果以交易为顶层锚点;交易可以直接抵达合约,也可以在执行中形成一棵 message-call 树。eth_call 是节点本地模拟,不是第四种会落链的触发器。
05 · Call request
地址回答“找谁”,calldata 回答“做什么、带什么参数”。钱包界面最终会把人类动作编码成字节。
调用已有合约的交易至少需要目标地址 to 与输入数据 data(也称 input / calldata);还可能携带 value,并包含 nonce、Gas 限额、费用字段和签名等交易信息。ethereum.org 的交易文档把“执行已部署合约”列为交易类型之一,并说明 data 字段通常按 ABI 解释。[7]
ABI-ENCODED CALL · transfer(address,uint256)
前 4 字节通常是函数签名 Keccak-256 哈希的前四字节,用来选择入口函数。
后续字节按 ABI 对参数编码。编码不自描述:只看字节,未必知道每段应该解释成什么类型。
Solidity ABI 规范把一次函数调用表示为 function_selector(f) + enc(arguments)。[8] 这不是说所有 EVM 合约都必须使用 Solidity ABI;EVM 看到的只是字节,合约运行时代码决定如何解释它。Solidity 常见的 receive / fallback 入口也可能处理空 calldata 或没有匹配选择器的输入。
所以“相同参数一定得到相同结果”仍然太粗。一个访问控制函数还会看 msg.sender,一个支付函数还会看 msg.value,一个读取价格的函数还会看 storage。确定性要求比较的是完整前置状态、输入与协议允许的执行上下文。
06 · State transition
EVM 不是一台藏在云端的神秘服务器,而是一套状态转换规则:旧状态与有效交易经过确定性执行,产生新状态。
ethereum.org 用 Y(S, T) = S' 表示以太坊状态转换:给定旧的有效状态 S 和一组有效交易 T,EVM 产生新的有效状态 S'。[9] 对一笔合约调用,可以沿下面七步理解。
更稳妥的表述是:区块生产者的执行客户端先计算候选结果;处理、验证或同步这个区块的执行节点能够依据同一规则重放并核对。轻节点、离线节点或只提供特定数据的服务不应被粗略算作“每一台网络设备都同时执行”。
EVM 允许代码读取区块时间戳、调用者、value 等受协议约束的环境值。关键不是“程序只能做纯数学”,而是这些值都来自可验证的执行上下文,节点无需各自访问一个可能返回不同结果的外部 API。
07 · Persistent memory
代码描述允许的转换;storage 保存当前状态。每次成功交易都可能把旧状态推进到一个新版本。
以一个最小计数器为例:运行时代码规定 increment 和 reset 的规则,storage 保存当前 count 与 owner。调用结束后,stack 与 memory 消失;只有成功提交到 storage 的值会成为下一笔交易的起点。ethereum.org 将 storage 描述为持久数据,而 memory 只存在于一次函数执行期间。[10]
同一份规则,不同 caller 与参数会得到提交、回滚或仅本地读取。
Interactive · State
尚未执行:当前链上示意状态为 count = 2。
revert 会让本次 EVM 状态写入不成为新状态。eth_call 可执行同一逻辑:节点会模拟,但把临时状态丢弃,链上没有新交易。Solidity 的 private 只限制其他合约通过自动生成接口访问,不会让链上数据变成秘密。节点需要状态数据来验证执行,外部观察者也可以直接读取 storage 或历史交易输入。
08 · Outcomes
“返回值、日志、状态和交易状态码”处在不同层。把它们混在一起,就会误读区块浏览器。
成功执行后的 storage、余额、nonce 等变化进入新的世界状态。
事件通过 LOG opcode 进入收据,便于前端与索引器搜索;不是合约 storage。
当前调用把字节返回给调用者;顶层交易的返回数据不会像 storage 一样长期可读。
收据状态标明顶层成功或失败;revert data 可描述失败原因,但不保证始终存在。
不是。若未处理异常到达顶层,EVM 执行产生的状态写入会回滚,收据状态为失败;但节点已经消耗计算资源,因此已用 Gas 仍然收费,已纳入区块的发送者 nonce 也已经前进。Solidity 文档还指出,内部 message call 的异常可以由调用者处理;低级调用会返回失败值,外层代码可能选择继续。[11]
| 结果 | 状态写入 | 已用 Gas | 日志 |
|---|---|---|---|
| 顶层成功 | 提交 | 收费 | 成功路径产生的日志保留 |
| 顶层未处理 revert / 异常 | 本次 EVM 作用域回滚 | 已消耗部分仍收费 | 被回滚作用域内的日志不保留 |
| 子调用失败但被捕获 | 子调用自身回滚;外层可继续 | 消耗仍计入外层交易 | 最终只保留未被回滚路径的日志 |
eth_call |
模拟后丢弃 | 不支付链上费用 | 只作为模拟结果,不产生链上收据 |
事件适合告诉链下应用“刚才发生了什么”;storage 适合让未来合约执行知道“现在是什么”。Solidity 文档说明日志映射到区块层级的索引结构,合约在创建之后不能读取这些历史日志。[5]
09 · Capability & limits
智能合约擅长处理链上可验证的状态与资产;一旦规则依赖链外世界,就需要新的输入与信任边界。
按公开条件转移资产、记录所有权、撮合交换、执行治理、组合调用其他公开合约。
直接请求天气 API、自己在午夜醒来、知道现实比赛结果、保证输入数据真实或隐藏公开状态。
合约没有后台线程,也不会轮询时钟。所谓“自动清算”“定时发放”,通常意味着某个用户、机器人或去中心化自动化网络观察条件,并在合适时刻提交交易;合约只负责验证交易到来时条件是否成立。
合约执行不能直接向任意互联网 API 发请求,否则不同节点可能在不同时间得到不同答案,破坏共识。预言机的做法,是让链外组件获取并验证数据,再通过交易把结果写到链上,供合约读取。它扩展了能力,也引入数据正确性、可用性与激励的新问题。[12]
10 · Modern precision
入门模型仍然有用,但 Pectra、Cancun 与代理模式让“账户、不可变、失败回滚”需要更精确的注脚。
经典合约地址上的 runtime bytecode 通常不能直接被替换;storage 却可以改变。代理模式让一个稳定入口通过 DELEGATECALL 执行另一个实现地址的代码,因此用户面对的“应用逻辑”可能由管理员或治理升级。这不是修改入口地址的 bytecode,而是入口代码本来就写了“去哪里找实现”。
传统模型把 EOA 说成私钥控制、无代码、可发起交易,把合约账户说成代码控制、不能发起顶层交易。Pectra 激活的 EIP-7702 允许 EOA 授权写入一个持久的 delegation indicator 0xef0100 || address。调用这个账户时,执行会跟随指针取得实现代码,但使用该 EOA 的上下文;私钥仍保留最终控制,委托可被后续授权替换或清除。[14]
因此,本课主线仍用经典账户模型帮助理解普通已部署合约,但不要把“有代码”和“能发交易”当作 2026 年永远互斥的分类测试。EIP-7702 的授权处理发生在交易 EVM 正文前;已经处理的 delegation indicator 不会因后续执行 revert 而自动回滚,这也是“失败会回滚所有变化”的一个协议层例外。
SELFDESTRUCT 通常不再删除既有合约Cancun 之后,既有合约执行 SELFDESTRUCT 时会终止当前执行并转移余额,但通常不会删除账户、代码或 storage。只有合约在同一笔交易中刚被创建又执行该操作,才保留旧式删除效果。该 opcode 已被弃用,不应作为新系统的普通升级或清理机制。[15]
某个普通合约地址关联的 runtime code 通常固定。
Storage 设计出来就是让获准的调用改变。
代理可把调用委托给可更换的实现地址。
升级密钥、时间锁、投票和预言机决定谁能改变什么。
应说:调用到来并执行相应代码时,条件才被检查。
应说:外层交易内发生同步 message call。
应说:协议状态保存可执行代码与当前 storage;源码通常另行验证。
应说:通过 eth_call 不付链上费用;在真实交易内执行仍消耗 Gas。
应说:状态写入可回滚,但计算已发生、Gas 已消耗、nonce 已前进。
应说:安全还取决于代码、权限、代理、依赖、预言机与经济条件。
11 · Recap
如果你能从“地址”一路推到“新状态”,就已经真正理解了本课,而不只是记住一句宣传语。
目标地址把调用指向余额、nonce、代码与 storage。
签名交易进入区块,形成最外层执行。
选择器与参数告诉运行时代码走哪条路径。
代码读取当前状态、上下文与临时数据空间。
合约间 message call 仍属于同一外层交易。
成功提交 S′;失败按作用域回滚,节点可重算核验。
选择一个答案。
选择一个答案。
选择一个答案。
eth_call,哪句话正确?选择一个答案。
选择一个答案。
选择一个答案。
完成任意一题后,这里会显示进度。
12 · Terms & primary sources
目录提供学习边界;技术语义以 ethereum.org、Solidity 文档、Yellow Paper 与已激活 EIP 为基线。
Y(S,T)=S′ 状态转换、世界状态、message call 与合约创建。
资料核对:2026-07-25 · 现代语义以已激活主网升级与对应 Final EIP 为准。
本课严格对应目录 PDF 第 4 页“第 9 章:智能合约”的第一项:智能合约是什么:保存在链上、由交易触发执行的程序。生命周期与 Solidity 基础分别留给本章后两课。
合约不是自己行动的机器人,
而是被共同执行的链上规则。