L03 Smart Contract and Wallet:一台不能退货的自动售货机
合约的边界、两种代币标准,以及 12 个单词背后的整套推导。
一句话版
合约是一台部署后谁也改不了的自动售货机,而钱包管的只是开它的那把钥匙。
一个类比:一台不能退货的自动售货机
售货机的逻辑在出厂时就写死:投够钱、按对按钮,货就出来。合约也一样——require(msg.value == REGISTRATION_COST) 就是投币口那道卡尺,用的是严格相等,多付一个 wei 也照样回滚。机器不会通融,也没有店员可以叫来评理,争议解决那一栏填的是”平台”,代码怎么写就怎么执行。
机器里还能装自己的游戏币:一种面额统一、随便换,你手上那枚和我手上那枚没区别(ERC-20);一种是娃娃机里编了号的奖品,每个都不一样(ERC-721)。而开机门的钥匙不在机器里,在你的钥匙串上——钱包就是这串钥匙,助记词是配钥匙的母模,有母模就能把整串重配一遍。
类比在哪里失效:真售货机的老板还能开门补货、手动退款、贴张”故障停用”;合约部署后连作者都动不了,课件那个 Namespace 合约就吃了这个亏——没写取款函数,收进来的手续费永远锁在里面。还有一处更反直觉:售货机的货架不透明,链上存储却是玻璃的,人人可读。所以”先出拳再比较”这种在真机器上天经地义的玩法,搬到链上就是后手必胜。
概念卡
1. 智能合约的边界(Smart Contract)
人话定义:把多方协议翻译成代码部署上链,它自己强制执行,已部署字节码通常不可直接改写。补充:代理升级、暂停等预设机制仍可改变系统行为。“smart” 指的是自动执行,不是聪明,本身不自动决定法律效力(p.2)。
例子:传统合约与智能合约的六行对照(p.8):
| 维度 | 传统合约 | 智能合约 |
|---|---|---|
| Specification | 自然语言 | 代码 |
| Identity & Consent | 手写签名 | 数字签名 |
| Dispute Resolution | 法官、仲裁员 | 去中心化平台 |
| Cancel | 由法官裁定 | N/A |
| Payment | 另行进行 | 内置 |
| Escrow | 可信第三方 | 内置 |
最后两行能内置,是因为合约本身就是一个能持币的账户,钱可以直接锁在里面按代码条件放出。
常见误解
以为部署到链上的是 Solidity 源码,节点读源码执行 → 编译在链下由 solc 完成,部署交易里装的已经是 EVM 字节码,节点只负责执行(p.5、p.6)。而且每个全节点都处理每一笔交易、存全部状态,同一份字节码在各节点各跑一遍、结果必须一致——这就是合约必须确定性、不能发网络请求的原因(p.7)。
2. 承诺-揭示(Commit-Reveal)
人话定义:先在链上提交自己选择的哈希,过了截止时间再公开原值,合约重算一遍哈希比对。它把”同时出手”这件事拆成了两个阶段。
例子:猜拳的朴素版和修好的版本摆在一起看(p.11、p.14):
player 这个 mapping 即使没写 public,合约存储本来就是公开的,任何节点都能读。
常见误解
以为直接提交 hash("Rock") 就够了 → 出拳只有三种,对手把三个值各算一遍对比就知道你出了什么。hiding 只在输入空间大到无法穷举时才成立,所以必须拼一个随机 nonce(p.13、p.14)。第二个洞是后揭示的人可以耍赖:一算发现自己要输就干脆不揭示,因此链上实现一定要配押金和揭示截止时间,到期不揭示视为输(p.13)。
3. 两种代币标准(ERC-20 / ERC-721)
人话定义:以太坊上的代币不是链的原生资产,就是某个合约里的一张余额表。ERC-20 管同质化的,每个单位可互换;ERC-721 管非同质化的,每个 token 有唯一编号。
例子:ERC-20 规定 6 个必须函数(totalSupply、balanceOf、transfer、transferFrom、approve、allowance)、3 个可选(name、symbol、decimals)、2 个事件(Transfer、Approval)(p.20、p.28)。ERC-721 那边多出 ownerOf(tokenId) 和 setApprovalForAll,Etherscan 上的表现差别很直观:稳定币页显示单一价格,CryptoKitties 页显示 Min / Max 价格区间,因为每只猫价格不同(p.25、p.31、p.32)。
常见误解
以为只有 transfer 就够用 → 合约不能主动动你的余额,替你花钱要走 approve 给额度、transferFrom 取走两步,allowance 就是那张授权额度表(p.28)。另一个常踩的坑是小数位:USDC 和 USDT 都是 6 位小数,不是 ETH 的 18 位,链上原始余额 1000000 对 6 位小数的 USDC 是 1 USDC,不保证始终兑换 1 美元;错按 18 位会差 12 个数量级(10¹² 倍)(p.25)。
4. 三代钱包与 HD 推导(BIP32)
人话定义:钱包是私钥的容器,不是币的容器。币在链上,钱包里只有能动那些币的钥匙(p.41)。
例子:三代钱包的区别,看「要备份什么」最快(p.42–p.48):
JBOK 是 Just a Bunch of Keys 的缩写,生成多少把就得备份多少把,漏一把那笔币就永久损失。
常见误解
以为树状结构只是为了好看 → 父扩展公钥 xpub 可派生非硬化子公钥,所以收款服务器不必存私钥。但“仅凭子私钥难以反推父私钥”有条件:父 xpub + 非硬化子私钥一起泄露,会危及父私钥及该子树。硬化推导(hardened)不支持从父 xpub 派生子公钥(p.54–55;BIP32)。
把它们串起来
这一讲是两块拼起来的,但共用一条主线:合约的能力边界,和用户握住它的那个入口。
前半段从”合约能做什么”走到”你到底需不需要区块链”。顺序是有设计的:先立住两条硬性质(自动执行、不可修改),再用猜拳演示它们带来的新麻烦——链上没有隐私,所以要用哈希承诺把时序切开;接着用 ICO 和代币演示好处——发一种币只要部署一个合约,钱能锁在代码里而不需要中间人。最后 NIST 六问泼冷水:多方写入、不可变可审计、无可信中心,缺一个就该用数据库。
后半段转到钱包,核心是用可备份的种子管理密钥。助记词泄露非常危险,但若设置了额外 passphrase,恢复还需要它;跨钱包还需匹配派生路径。链上交易通常不可撤回,不代表泄露后毫无办法:尚未被盗的资产可迁至全新、安全生成的钱包。
课件里的坑
- [课件有误] p.6 写 EVM “compiles code from high-level language to bytecode” 不准确。编译在链下由 solc 完成,节点从来不见 Solidity 源码,收到的部署交易里已经是字节码,EVM 只负责执行(p.6)
- [历史与角色] p.24 的 Centre Consortium 涉及历史治理组织,不能直接等同于发行方。Circle 2023-08-21 公告说明 Circle 继续作为 USDC 发行方,并接收 Centre 的治理职责;不是到 2023 年才开始发行。
- [术语勘误] p.26 的 “once” 应为 “ounce”。按 Tether Gold 官方说明,1 XAUt 代表 1 金衡盎司纯金(fine troy ounce),不是普通盎司;这不保证代币市价始终等于现货金价。
- [标准与版本] p.28 的
increaseAllowance/decreaseAllowance/_burnFrom是实现扩展,不属于 ERC-20 标准。OpenZeppelin v5 已移除前两个 ERC20 函数;参考官方变更记录。不要与 SafeERC20 的辅助函数混淆。 - [条件补全] p.53 的 24 词示例使用 passphrase=
TREZOR时,完整种子与课件的3972e432…一致;空口令才得到3269bce2…。课件未注明口令,不能判成种子算错。BIP39 规范规定盐为mnemonic + passphrase。公开示例绝不可用于存放真实资产。
课后 10 分钟:考点复习
这 10 分钟怎么用:合上页面,先默写三条——commit-reveal 两阶段、ERC-20 与 ERC-721 的分界、BIP39 从熵到单词的位运算;再把下面的「变式题」做一遍;最后回查两个最容易错的地方——以为直接提交 hash("Rock") 就够、以为链上存的是 Solidity 源码。三步做完再往下看答案。
必背
- 智能合约把规则写成链上程序;代码不可直接改写不等于系统行为永远不变,代理升级或管理员权限须另查;自动执行不自动赋予法律效力。
- 源码用 Solidity 写、在链下编译成 EVM bytecode 再部署;节点不编译只执行,每个全节点处理每一笔交易、存全部状态。
- 朴素猜拳合约的致命问题是出拳明文进交易和合约存储,后手看完再出必胜;判胜公式 (p0 + 3 − p1) % 3,结果 1 则 P0 赢、2 则 P1 赢、0 则平分。
- commit-reveal 两阶段:先提交 H(nonce, choice),截止后再揭示;依赖哈希的 hiding 与 binding,nonce 防三选一被枚举,截止时间加押金防后揭示者耍赖。
- ERC-20 = 同质化代币标准,6 个必须函数(totalSupply / balanceOf / transfer / transferFrom / approve / allowance)+ 3 个可选(name / symbol / decimals)+ 2 个事件(Transfer / Approval)。
- ERC-721 = 非同质化代币标准,每个 token 有唯一 tokenId、不可分割、有 ownerOf;一个 ERC-721 合约代表一类资产,一个 ERC-20 合约代表一种资产。
- 三代钱包:Type-0 / JBOK 随机密钥集合 → 确定性钱包(一个种子推出全部密钥)→ HD 钱包(树状,BIP32);推导链是 Seed → HMAC-SHA512 → 主密钥 → CKD(父, 索引)。
- BIP39:128–256 位熵 → SHA256 取前 ENT/32 位作校验和接在末尾 → 切成 11 位一段 → 查 2048 词表;助记词经 PBKDF2-HMAC-SHA512、salt “mnemonic” + passphrase(默认空串)、2048 轮得到 512 位种子。
完整例题
题面(课件 p.51):用 256 位熵生成助记词,问会得到几个单词?再用 192 位 算一遍,并说明为什么总位数总能被 11 整除。
- 第一步,熵长度 ENT = 256 位。
- 第二步,对这 256 位做一次 SHA256,取哈希的前几位当校验和。位数规则是 CS = ENT / 32,代入得 256 / 32 = 8 位。
- 第三步,把 8 位校验和接在 256 位熵的末尾,总长 256 + 8 = 264 位。
- 第四步,按每段 11 位切开:264 / 11 = 24 段,正好整除。
- 第五步,每段 11 位是一个 0–2047 的索引,去 2048 词的词表里查词。课件的示例段
00001100000= 96,查出来是 army。 - 所以 256 位熵对应 24 个单词 ✓ 与课件表格一致。
- 换 192 位再走一遍:CS = 192 / 32 = 6,总长 192 + 6 = 198,198 / 11 = 18 个单词 ✓。
- 为什么总能整除:总位数 = ENT + ENT/32 = ENT × 33/32。要它能被 11 整除,只需 ENT 是 32 的倍数且 ENT/32 × 33 能被 11 整除——而 33 = 3 × 11,所以只要 ENT 取 32 的倍数就自动成立。128 / 160 / 192 / 224 / 256 五档分别对应 12 / 15 / 18 / 21 / 24 个词。
- 最后一步是单词到种子,和上面这套位运算无关:PBKDF2-HMAC-SHA512,salt
"mnemonic"加可选 passphrase,2048 轮,输出 512 位(p.52)。
整条流水线连起来是这样:
变式题(先自己做)
(1) 160 位熵会得到几个单词?(2) 反过来:看到一串 12 个单词的助记词,原始熵是多少位、校验和几位?
提示
第二问别急着套 ENT/32。先把 12 个词换算成总位数,那个数是熵和校验和加起来的结果。
参考答案与自检(非官方评分标准)
自检要点:① 正向三步不能跳——先算 CS,再加总,再除 11;② 反推必须先写 12 × 11 = 132 是总位数,把 132 直接当熵长是主要失分点;③ 反推完要回代验一次。
(1) CS = 160 / 32 = 5 位,总长 160 + 5 = 165 位,165 / 11 = 15 个单词 ✓ 与课件表格一致。
(2) 总位数 = 12 × 11 = 132。设熵为 ENT,则 ENT + ENT/32 = 132,即 ENT × 33/32 = 132,解得 ENT = 132 × 32 / 33 = 128 位,校验和 CS = 128 / 32 = 4 位。回代验算:128 + 4 = 132 = 12 × 11 ✓。12 词助记词对应 128 位熵,这也是最常见的那一档。
闪卡自测
1. 六行对照表里 Cancel 一栏为什么是 N/A?
因为合约部署后不可修改,也没有任何一方有权叫停。这是定义第四条的直接后果。传统合约那一栏填的是”由法官裁定”(p.2、p.8)。
2. Payment 和 Escrow 为什么能"内置"?
因为合约本身就是一个能持币的账户,有 balance 字段。钱可以直接锁在合约里、按代码写的条件放出,不需要银行或律师楼当托管中间人(p.8)。
3. 朴素猜拳合约里,第二个玩家从哪两个地方能读到对手的出拳?
一是 mempool——input 是普通交易,参数 choice 明文写在交易里;二是合约存储——上链之后写进 player 这个 mapping,即使没标 public,合约存储本来就是公开的,任何节点都能读(p.11)。
4. 除了出拳公开,朴素猜拳合约还有什么问题?
send 的返回值没检查,赢家若是收不了款的合约地址钱就卡住;check_winner 可被任何人反复调用且 reward 不清零,这个合约只能玩一局;start 在每次 add_player 时被覆盖,30 分钟从第二个人加入时才开始算,而且只有一人加入时 player[1] 是零地址、默认出 Rock,P0 出 Paper 就能拿走全部押金(p.11)。
5. commit-reveal 依赖哈希的哪两条性质?各防什么?
hiding——看到 H(a) 推不出 a,所以截止前对方不知道你的选择;binding——公布 H(a) 之后换不了 a,因为找不到另一个 a’ 使 H(a’) = H(a),所以揭示阶段你改不了主意。对应的正是抗原像和抗碰撞(p.12)。
6. 为什么"在以太坊上发币不需要新建一条区块链"?代币在存储里到底是什么?
写一个合约就是发了一种代币。代币不是链的原生资产,本质是合约里的一张 mapping(address => uint256) 余额表,“持有 100 个 X 币”就是 X 合约的表里你的地址对应 100,转账就是一行减、一行加(p.17、p.21)。
7. approve → transferFrom 两步模式解决的是什么问题?
合约替你花钱的场景。去交易所换币时交易所合约要从你账户扣币,但合约不能主动动你的余额,于是你先 approve(交易所, 100) 给额度,交易所再 transferFrom(你, 它, 100) 取走,allowance 查的就是还剩多少额度(p.28)。
8. ERC-721 有哪个 ERC-20 没有的授权函数?用在什么场景?
setApprovalForAll——ERC-20 的 approve 授权的是”多少数量”,ERC-721 的 approve 授权的是”哪一个 tokenId”,而 setApprovalForAll 是”授权我名下全部 token”,挂到交易市场时用(p.33)。
9. NIST 六问归纳成几个条件?绝大多数企业需求在哪一问出局?
三个条件加一个排除项:多方写入(问 1、2)、不可变且可审计(问 3、6)、没有可信中心(问 5),排除项是别把敏感标识符写上链(问 4,措辞是反的,答”不会写入”才继续)。多数需求在第 2 问或第 5 问出局——数据只有自己写,或者大家都信任某一方来管,那就是数据库的活。注意终点措辞是 “may have”,六问全过也只是可能(p.36、p.37)。
10. "钱包里存的是币"错在哪?Type-0 钱包的备份问题又是什么?
币在链上,钱包是私钥的容器。Type-0 叫 JBOK,是一堆互不相关的随机私钥,生成多少把就得备份多少把——早期客户端默认预生成 100 把、用完再生成,用户以为备份过一次就安全,其实新密钥不在旧备份里,这是当年真实的丢币原因(p.41、p.42、p.43)。
11. 为什么 HD 钱包能部署在不安全的服务器上只收款?
服务器上只放扩展公钥,不放扩展私钥。非硬化推导可以只用父公钥推子公钥,所以服务器能为每笔交易生成不同的收款地址,仅凭扩展公钥不能直接签名花币;但服务器被攻破仍可能被替换收款地址或泄露隐私,不能说绝不会丢钱。冷存储是同一招的另一面:扩展私钥离线存放,扩展公钥在线(p.55)。
下一讲
下一讲正式进 Solidity 语法,这一讲出现的两份合约代码就是预习材料——struct、mapping、payable、require、msg.sender、view 这些关键词会被逐个拆开讲。先把它们读通,下一讲会轻很多。
下一讲的通俗笔记上完课会补,先回 COMP5565 课程页。
个人整理的学习笔记,不是官方材料;数字与结论以课件和讲师为准。