把北大肖臻老师的《区块链技术与应用》公开课中跟BTC相关的内容重新刷了一遍,很多原本以为记住的名词,在逐字逐句回放时被我推倒重来。整理到第十三篇笔记时,我发现最值得写下来的其实不是某个协议细节,而是那些“我原以为懂了、但换个问法就露馅”的思考。
所以这篇文章大致可以分为三块:先聊我在听课过程中反复纠正的几个底层概念,比如数字签名和地址的关系、UTXO和“账户余额”的差别;再回到BTC的共识与脚本设计,聊一聊为什么整个系统要这样取舍;最后把我课后自己做的一个双花验证实验完整记录下来。整个过程不涉及投资分析,也不评价币价,纯粹从工程和技术原理出发。适合刚看完公开课、但还没有把BTC整体逻辑串起来的学习者,也适合准备做区块链开发、想建立正确心智模型的从业者。
1. 反复“回炉”之后,我才意识到BTC的门槛不是代码,而是概念
1.1 从“数字货币”到“记账系统”的转折
我第一遍看肖老师的课时,注意力全放在“比特币怎么支付、怎么挖矿”这些操作层,笔记里记满了各种哈希算法和出块时间,觉得自己好像已经懂了。真正让我停下来的是肖老师反复用“账本”打比方的一个说法:区块链不会记录“张三拥有多少比特币”,而是记录每一笔钱的来源和流向。
这句话当时让我愣了一下。因为我过去一直在用银行账户的心智模型去理解比特币,总以为每个地址背后有一张余额表,交易就是余额表的加减。但BTC不是这么运作的,它维护的是一套“谁付给谁、付了多少、钱从哪一笔来”的交易链。所有地址里到底有多少钱,其实是根据未花费交易输出临时算出来的。
这个转折非常重要。后面的UTXO、脚本、双花验证,全部建立在这个“面向交易”而不是“面向账户”的模型上。如果不先把这里掰过来,看后面的代码和协议就会一直觉得别扭。
1.2 整理笔记前,我一直没有回答上来的三个问题
为了检验自己是不是真的理解了,我在重新听课之前先给自己提了几个问题,结果回答得很勉强。
第一个问题是:转账时,你拿私钥签名的到底是什么?我第一反应是“签一个付款指令”,但更准确地说,你签名的是“对某个未花费交易输出的处置权”。这个输出指向你的地址,你有权决定它下一步去哪里。第二个问题是:节点的“全节点”到底在验证什么?不是简单验证签名,而是要验证交易引用的UTXO是否真实存在、是否没有被花过、锁定脚本能否被解锁。第三个问题是:为什么BTC都说要用六个区块确认,而不是像以太坊那样用十几个?这些问题在公开课里其实都有对应答案,只是我第一遍没有把概念串起来。这篇文章,就是我把这些概念重新排好序之后的笔记。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数字签名和地址是两回事:我走了很久才绕出来的弯路
2.1 BTC里的“加密”其实不是加密,而是防伪
很多人第一次听到“比特币是基于密码学的”,会顺理成章认为交易过程是“加密传输”。但肖老师提醒过一个非常重要的区分:BTC的大多数数据本来就是公开的,区块、交易、地址全部在链上,根本不需要保密。它真正需要的是“防伪造”和“防篡改”,也就是让任何人都能验证一笔钱确实被合法拥有者转走,且没人能冒名顶替。
所以BTC用的核心密码学工具不是对称加密或非对称加密,而是哈希函数搭配数字签名。哈希函数在这里承担的是“指纹”功能:给任意长度的数据生成一个定长结果,很难从结果反推原文,也很难找到两个不同输入得到同一个输出。交易里的“一笔钱从哪里来、到哪里去”会被哈希后固定在区块中,一旦有人改动哪怕一个字节,后续所有引用它的结构都会暴露问题。
2.2 地址不是公钥,而是公钥的哈希指纹
这是我过去最容易混淆的一点。我最早做开发时,潜意识里觉得地址就是公钥,或者至少是公钥的某种缩写。直到自己手动构造交易时才发现,BTC地址的生成过程是多步哈希的结果:私钥通过椭圆曲线算法secp256k1生成公钥,公钥先做一次SHA256,再做一次RIPEMD160,得到20字节的HASH160,最后加上版本前缀和Base58Check校验,才变成我们常见的以1开头的地址。
换句话说,公开在链上的地址本身并不包含完整的公钥信息。这就造成一个很直接的开发后果:如果你只知道对方的地址,你是没法在交易脚本里直接使用对方公钥的。ECDSA签名验证必须拿到公钥,所以P2PKH交易里,花费方需要先提供公钥和签名,节点会把公钥哈希化后与锁定脚本中的哈希比对。我们经常看到“解锁脚本里要放公钥”,但公钥不是地址,也不是地址的编码版本。
2.3 验证一个BTC交易,其实要过两道关
如果说地址和公钥的关系是第一道坎,那“验证签名和验证公钥哈希是两步”就是紧接着的第二道坎。以最常见的P2PKH支付为例,锁定脚本里放的是对方公钥的哈希,而不是公钥本身。花费这笔UTXO时,解锁脚本要提供两个东西:一个是签名,另一个是原始公钥。
节点执行脚本时,会先用公钥验证签名是否有效,确认这笔交易确实由持有对应私钥的人发出;接着再把这个公钥做哈希,看它和锁定脚本里的哈希是否相等,确认公钥确实属于“地址”对应的那个人。两道检查都通过,这笔钱才能被花掉。有时候我听到有人问“为什么我看不到地址对应的私钥也能构造交易”,答案就在这里:你必须同时拿出私钥生成的合法签名,以及一个哈希后等于地址的公钥,二者缺一不可。这个设计比“直接给地址转账”多了一层安全缓冲。
3. UTXO和“没有余额”的账本:双花问题其实是被数据结构提前堵死的
3.1 余额模型和UTXO模型的差别
传统银行系统里,每个人有一个账户表,转账时在账户表里做加减就行。BTC没有这种中心化的账户表,它只有一串从创世区块一路延伸下来的交易记录。具体到“证明谁拥有多少钱”,依据的是UTXO集合:Unspent Transaction Output,也就是还没有被花掉的交易输出。
每一个UTXO都来自某笔交易的某个输出,内容包含金额和一个锁定脚本。锁定脚本决定了这笔钱以后可以由谁来花。钱包显示“你有1.5 BTC”,并不是因为系统里存了一个1.5,而是因为你的地址控制着若干笔UTXO,它们的金额加起来正好是1.5。当你转账给朋友1.2 BTC时,你需要把手里某个或某几个UTXO作为输入,这些输入会被整体消耗掉;再在输出里创建两个新的UTXO,一个是朋友的地址,另一个是找零给你的地址。如果输入金额比输出金额多,多余部分就是矿工费。
3.2 用一笔实际交易来解释“输入消耗”逻辑
举个例子。假设小明要支付给小红3 BTC,小明手里有一个2 BTC的UTXO和一个2 BTC的UTXO。他可以把这两个UTXO都作为输入,此时输入总额是4 BTC。然后创建两个输出,一个给小红3 BTC,另一个作为找零给自己1 BTC。到这一步,原来的两个UTXO就被标记为“已花费”,不再存在于UTXO集合中。
这个过程非常像现金交易:你手里有一张100元纸币,买完东西付了60元,店家找给你40元。那100元不会被“部分花掉”,而是被回收销毁,变成60元交易输出和40元找零输出。理解这一点对后面排查问题特别有用。我做本地实验时,经常看到初学者试图“只花一个UTXO的一部分”,结果构造的交易要么无法通过验证,要么手续费高得离谱,根本原因就是没有建立起UTXO不可分割的概念。
3.3 双花攻击为什么能被拒绝
双花攻击的想法很简单:同一笔UTXO,能不能在两条交易里各花一次?如果银行余额表没有及时扣款,这种问题可能出现;但BTC的机制里,每个UTXO只能被当作输入引用一次。全节点在验证新交易时,会检查引用的UTXO是否存在于自己的UTXO集合里,如果已经被花掉,就会直接拒绝。
有人会追问:如果攻击者把两个冲突交易分别广播给网络中的不同节点呢?这时候就轮到共识机制出场了。矿工在构造候选区块时,不会同时打包这两个冲突交易,因为同一个UTXO不能被同一区块引用两次。节点收到不同区块后,会按照累计工作量证明选择自己认为合法的主链。攻击者如果想确保自己那个写了冲突交易的区块被承认,必须有足够的算力让这条分支持续超过诚实链,这就是51%攻击的由来。
但是这里有一个非常容易被忽略的点:双花防护不是某一次验证就百分之百完成,而是“本地验证+网络广播+最重链选择”共同作用的结果。单看节点侧,它只是拒绝已经花掉的UTXO;真正让整个系统稳定运行的是PoW赋予“链的延续”以成本,让回滚历史变得极其昂贵。
4. 最重链原则不是节点投票:算力、难度和确认时间要放在一起看
4.1 为什么“最长链”其实是“累计工作量最大”
很多科普把BTC共识简单解释成“按照最长链原则,大家选最长的那条”。这句话听多了以后,容易产生一种错觉:好像网络里的节点像投票一样,哪条链支持的人多,哪条链就赢。肖老师课上特别强调,BTC里的最长链不是“节点数量最多的链”,而是“累计工作量证明最大的链”。
每个区块头里都有一个难度目标值,矿工要不断调整区块头里的随机数,让整个区块头做两次SHA256后的哈希值小于这个目标。这个“找随机数”的过程就是工作量证明。节点在比较两条分叉链时,会累加每条链上每个区块的难度总和。在难度一致的情况下,区块数量更多的那条链累计工作量更大;但如果两条链的难度不同,就不能只看高度。用代码习惯理解,节点不是简单比较height,而是比较chainwork。
4.2 难度调整公式背后的经济动机
BTC目标挖矿时间是10分钟一个区块,但网络总算力会不断变化。如果不调整难度,算力增长后出块速度会越来越快,安全性模型也会被破坏。所以BTC每2016个区块做一次难度调整,大概对应两周时间。调整的核心思路是:用过去2016个区块的实际耗时,和目标耗时做比较,再决定让后续难题变大还是变小。
公式可以粗略写成:新目标 = 旧目标 ×(实际耗时 / 目标耗时),其中目标耗时是2016 × 10分钟。如果全网算力暴涨,实际耗时远短于目标耗时,新目标值就会变小,也就是难度上升;如果算力下降,实际耗时过长,新目标值变大,难度降低。肖老师用“锚定时间窗”来解释这一点,让我意识到PoW不只是在消耗能源,它还在通过经济激励把出块时间稳定在一个区间,从而给网络确认时间提供一个可预期的基准。
4.3 六个确认的经验法则不是玄学
为什么比特币社区常用“六个区块确认”来判断交易最终性?这其实是一个概率问题,不是一个绝对规则。当一个新区块被广播并连接在主链上后,如果攻击者想回滚这笔交易,他必须从该区块之前的某个位置重新构造一条分叉链,并且要追上诚实链的高度,最终成为累计工作量最长的那一条。
你等的区块越多,攻击者要在后面追赶的区块数量就越多。在攻击者总算力小于50%的情况下,追赶概率会随着确认数增加呈指数下降。六个区块大约一个小时,对绝大多数场景已经把回滚概率压到了极低水平。这不是说永远不可能被回滚,而是说从工程角度,风险可以忽略不计。
我在自己跑节点时还遇到过一个相关现象:当网络中出现临时分叉,某个区块被挖出后没有被主链包含,它就变成所谓的“孤儿块”。很多新手以为孤儿块里的交易就消失了,但其实这些交易会被重新放回交易池,等到后续区块再打包。这也是“最终性不是即时完成”的直观体现。
5. 脚本系统、软分叉和“不图灵完备”:BTC的安全边界是主动设计的
5.1 脚本其实是一段可执行的小程序
交易输出里的锁定脚本,以及花费方提供的解锁脚本,并不是静态字符串,而是可以逐条压栈、执行的代码。BTC脚本是一套基于栈的语言,每条指令做什么都很明确。以P2PKH为例,锁定脚本是:
text复制OP_DUP OP_HASH160 <公钥哈希> OP_EQUALVERIFY OP_CHECKSIG
花费方提供的解锁脚本是:
text复制<签名> <公钥>
验证时,节点会把解锁脚本和锁定脚本拼接起来,然后让栈机从头开始执行。整个过程用到“复制顶部元素、计算哈希、比对、验签”这些指令。只要任何一步失败,交易就无效。这个设计让我意识到,BTC的“账户”不是人,而是脚本:任何人都可以构造一段锁定脚本,把资金锁定在一个可编程条件里,满足条件的解锁脚本才能花掉。
5.2 循环不是没本事加,而是故意不加
以太坊出来以后,很多人喜欢拿“可编程性”对比BTC和以太坊,说BTC脚本不是图灵完备,所以能力有限。肖老师课上给过一个很深入的角度:BTC不给脚本加循环语句,表面上是功能受限,本质上是为了保护验证节点的安全边界。
图灵完备系统里,程序可能无限循环下去。交易脚本是由网络里每个全节点去执行的,如果脚本能写出死循环,那就会变成一种拒绝服务攻击:攻击者只要广播一笔恶意交易,就能让所有节点陷入循环,整个网络都会瘫痪。BTC把脚本设计成非图灵完备,所有程序都保证会在有限步数内结束,这样全节点验证交易的成本是可预估的。对一条需要处理全球支付的系统来说,确定性比表达力更重要。
5.3 软分叉和硬分叉的分界点
BTC协议不是一成不变的,它也需要升级,但升级方式会分成软分叉和硬分叉。硬分叉指的是新规则产生的区块不会被旧节点接受。比如把区块大小上限从1MB提高到2MB,如果旧节点不认超过1MB的区块,这条链就会分裂:一部分节点继续走旧规则,另一部分节点走新规则。
软分叉则比较巧妙,它让新规则下产生的区块仍然被旧节点认为是合法的,只是旧节点不知道里面还有新语义。比如隔离见证SegWit就是一次典型软分叉升级,它改变了交易结构,但旧节点在验证时看到的仍然是一笔看起来合法的交易。软分叉的优势是不需要全网所有节点同时升级,减少协调成本;硬分叉则通常是因为双方已经无法在规则层面达成一致。
这个知识点对我理解区块链治理很有帮助。很多人以为分叉只是“代码版本分叉”,但在公链语境下,分叉更多是“社会共识的分裂”。技术协议能给出一套规则,但节点和算力愿不愿意跟,才是分叉能不能持续的根本原因。
6. 课后动手实验:在regtest里亲眼看一次“双花被拒绝”
6.1 为什么推荐自己跑一遍regtest
这部分不是肖老师课堂上的任务,而是我自己在整理第十二篇笔记之后加的一个动手验证。说实话,光看资料很容易把“UTXO不允许重复花费”背下来,但真正理解还是在命令行里构造交易、看到节点拒绝的那一刻。
Bitcoin Core提供了regtest模式,也叫本地回归测试网络。在这个模式下,你可以随时给自己生成区块,不用等真实出块时间,币也没有真实价值,用来造交易非常合适。我的建议是,桌面端钱包界面适合日常转账,但如果你想摸清交易结构,命令行才是最能让你看清全貌的入口。
6.2 我用的核心命令和观察到的结果
先启动一个regtest节点并创建钱包:
bash复制bitcoind -regtest -daemon
bitcoin-cli -regtest createwallet "test"
bitcoin-cli -regtest getnewaddress
然后用generatetoaddress给自己生成101个区块。为什么是101个?因为BTC里刚挖出来的币需要100个确认才能使用,多出一个区块是为了让第一笔区块奖励变得可花:
bash复制bitcoin-cli -regtest generatetoaddress 101 <你的地址>
接下来给另一个地址转一笔币,记录下返回的txid:
bash复制bitcoin-cli -regtest sendtoaddress <接收地址> 1.0
bitcoin-cli -regtest getrawtransaction <txid> true
在交易详情里,你可以清楚看到vin字段里的txid和vout,这就是这个交易消耗的UTXO来源。这个vin就是双花实验的核心对象。
最直观的实验方式是:拿出同一笔UTXO,尝试再构造一笔引用它的交易。如果节点已经有这个交易或这个UTXO已经被标记为已花费,广播新交易时通常会收到类似bad-txns-inputs-missingorspent的错误。我用这个报错截图做了笔记,因为“亲手被拒绝一次”之后,UTXO模型在我脑子里才真正变成一个硬约束,而不是一段抽象描述。
6.3 实验中的两个小坑
第一个坑是,regtest里默认的内存池规则和主网不完全一致,有些版本会对未确认交易的双花有更严格的限制。如果你要模拟“两个冲突交易广播到不同节点”的完整场景,最好开两个节点实例,分别连接不同端口,而不是在同一个节点里强行塞。第二个坑是,签名交易时很容易忘了指定输入对应的私钥或钱包,导致最后签名不完整。用signrawtransactionwithwallet可以省很多事,但前提是钱包里确实有对应地址的私钥。这个实验做完,你会发现“全节点验证”四个字变得无比具体:每一个输入能否花、每一段脚本能不能跑通,全部在节点本地严格检查。
7. 回到大框架:BTC给区块链留下的“坐标原点”
7.1 很多看似独立的特性,其实是互相耦合的
整理完这一轮笔记后,我最大的感受是,BTC这些特性不是一个个孤立的知识点,而是一套彼此咬合的设计。PoW保证了最重链难以被篡改,挖矿奖励吸引算力加入,算力反过来保护链的安全;UTXO模型让全节点能以相对无状态的方式验证交易,避免重复花费;脚本系统限制了可执行范围,让验证成本可控。任何一个环节改成“更灵活”的方案,都可能牵动其他环节的安全假设。
后来我再看其他区块链项目时,都会先问自己:它的共识机制和激励机制是否一致?它的状态模型是否支持它想做的业务?它的脚本语言是否让验证成本保持确定?这些追问的起点,其实都来自肖老师在讲BTC时的框架。BTC不是区块链的终点,但它很适合当作坐标系原点,其他项目往哪个方向走、走了多远,都能和这个原点对比出来。
7.2 我给自己留的个人检查清单
如果你也在整理学习笔记,我建议不要只停留在“看懂概念”。每次学完一个BTC知识点,可以问自己三个问题:第一,如果我运行一个全节点,收到包含冲突交易的区块会怎么做;第二,如果我用别人的API去重放一笔已经确认的交易,会发生什么,为什么;第三,把P2PKH改成P2SH之后,锁定脚本和解锁脚本的职责会怎么变化。
我自己用这个方式检查后,发现很多“我以为懂”的知识,其实只要换个问法就会露馅。能回答上第三题,说明你对脚本已经有了实操级的理解;能亲手跑通第二题,说明你对交易生命周期有了足够体感。肖老师的公开课适合反复刷,BTC部分尤其值得每隔一段时间回来看一遍,因为技术点虽然不变,但随着你写过的代码变多,你从同一句话里能读出的层次是完全不一样的。
最后再分享一个经验:课堂笔记记得再多,也不如自己手动构造一笔原始交易来得实在。从私钥生成公钥、地址,到用UTXO组装交易,再到签名、广播,如果能不看库函数把这些链路走通一次,BTC这台“去中心化记账机器”在你眼里就不再是黑板上的概念,而是一套可以被拆开、被理解、被调试的系统。
