每一种加密货币都会采用某种共识机制使得整个分布式网络能够保持同步。比特币从诞生之初就采用了工作量证明(Proof of Work,以下简称PoW)共识机制,通过不断进行数字加密哈希运算实现工作量证明。由于哈希算法具有单向性,所以即使输入数据有极小的改变都会使得输出的哈希值完全不同。如果计算出的哈希值满足一定条件(称为“挖矿难度”),则比特币网络的参与者认定该工作量证明。挖矿难度是一个不断变化的哈希值目标,当网络生成区块的速度变快时,难度会自动提高,从而保持整个网络每10分钟一个区块的平均值。
定义
对于不太熟悉区块链的人,这里罗列一些基本的定义,帮助后文的理解:
* UTXO — 未花费的输出
* vin — 在一笔交易中vin表示被用于支付所输入的的UTXO
* vout — 在一笔交易中,vout表示支付后新创造出的UTXO。vouts是交易的所有输出
* 哈希运算(hashing) — 以任意数据作为输入,输出固定长度的信息摘要,并且运算是单向的。还有一点特性:即使是非常微小的输入变化都将导致输出值完全不同
* 哈希值(hash) — 哈希运算的输出
* 比特币脚本 — 决定如何花费UTXO的计算机语言
* pay-to-pubkeyhash脚本 — 在比特币中最常用的脚本,用于支付数字货币,作为支付者,你只需要知道支付对象的公钥的哈希值(通常用base58地址表示),而作为接收者想要花费这笔钱,只需要提供这个公钥对应的签名以及公钥本身
* pay-to-pubkey脚本 — 和pay-to-pubkeyhash非常相似,但支付到公钥本身而不是其哈希地址,接收者要花费这笔钱则需要提供加密签名来表明拥有该公钥
* prevout — 在交易中作为vin的前一笔交易的vout
* OP_RETURN脚本 — 使得某个output不可再被花费,一般用于在区块链中保存一些信息,同时不影响UTXO模型
PoW和区块链共识系统
通过比特币8年的发展,PoW机制的安全性已经得到了证实。然而PoW也有如下一些问题:
PoW浪费了大量的电力资源,对环境不友好;
PoW在经济方面只对拥有强大算力的大户比较有利(一般用户基本不可能挖到矿);
PoW缺乏对用户持币或用币的激励机制;
PoW有一定中心化风险,因为矿工们倾向于加入大矿池,这就使得大矿池对网络的影响力较大;
权益证明机制(Proof of Stake,以下简称PoS)可以解决这当中的很多问题,因为它使得任何在钱包中拥有代币的用户都可以有机会挖到矿(当然也会获得挖矿奖励)。PoS最初是在Peercoin中由Sunny King提出。其后在多种加密货币中被改进并采用。这当中包括Pavel Vasin提出的PoS2.0,Larry Ren提出的PoS Velocity,还有最近由Vlad Zamfir提出的CASPER,以及很多其它比较不知名的项目中的各种尝试。
Qtum采用的共识机制基于PoS3.0。PoS3.0是PoS2.0的升级版,也是由Pavel Vasin提出并实现。本文会重点阐述这个版本的PoS实现。Qtum基于PoS3.0做了一些修改,但核心的共识机制基本相同。
对于一般社区成员甚至是一些开发者来说,PoS并不是特别好理解,因为目前比较少文献详细描述在仅采用代币所有权实现共识的网络中如何确保网络安全。本文将会详细阐述PoS3.0中区块如何生成、验证、以及PoS区块链如何保证安全性等。文中可能会涉及到一些技术知识,但我会尽量结合本文提供的一些基本定义将其描述清楚。但至少读者需要对基于UTXO的区块链有一个基本的概念。
在介绍PoS之前,我先简单介绍一下PoW的工作机制,这可以帮助后文对PoS的理解。PoW的挖矿过程可以用如下伪代码表示:
while(blockhash > difficulty) {
block.nonce = block.nonce + 1
blockhash = sha256(sha256(block))
}
这里用到的哈希运算我之前已经解释过,即以任意长度数据作为输入,经过一系列运算,得到固定长度的信息摘要作为输出,但只知道信息摘要却不可能反推得到对应的输入数据。整个过程有点像彩票的中奖机制,你可以通过对数据进行哈希运算创造出一张“奖券”并和目标哈希范围比对,判断自己是否“中奖”。如果没有中奖,只要稍微改变部分数据,你就可以再创造出一张新的“奖券”。比特币中的随机数nonce就是用来调整输入数据的。一旦找到符合要求的哈希值,则区块就是合法的,可以被广播到分布式网络中。网络中其他矿工一旦接收到这个新区块消息并验证通过,就会将区块加到链上并在新区块之后继续建立区块。
PoS的协议结构和规则
现在我们开始介绍PoS。PoS有如下目标:
无法伪造区块;
“大户”不会获得过于不成比例的大部分奖励;
拥有很强的算力对于创造区块没有帮助;
网络中没有一个或几个成员能够控制整个区块链;
PoS的基本概念和PoW非常相似,都像是彩票。唯一的不同在于,PoS不能仅仅通过微调输入数据就获得新的“奖券”,PoW采用“block hash”当做彩票的奖券,而PoS引入了“kernel hash”的概念。
Kernel hash将当前区块中多个不可修改的数据作为输入。因此,由于矿工无法找到修改kernal hash的简单方法,他们也就不能通过大量遍历可能的hash来获得合法的新区块。
为了实现这一目标,PoS增加了许多额外的共识规则。首先,与PoW的不同是,PoS的coinbase交易(也即区块的第一笔交易)的输出为0。同时,为了奖励staker,引入了一个staking交易作为区块的第二笔交易。staking交易有如下特点:
有至少1个合法的vin
第一个vout必须为空脚本
第二个vout必须不为空
此外,staking交易还必须服从如下规则:
第二个vout必须为一个pubkey脚本(注意不是pubkeyhash)或是不可花出的OP_RETURN脚本用于在链上保存数据;
交易中的timestamp必须和区块timestamp保持一致;
staking交易的总输出值必须小于或等于所有输入值、PoS区块奖励以及交易手续费的总和(即output <= (input + block_reward + tx_fees));
第一个vin对应的output必须经过至少500个区块的确认(也就是说被花费的币至少需要500个区块的确认);
虽然staking交易可以有多个输入vins,但只有第一个vin被用于共识机制;
这些规则使得staking交易很容易被识别,从而保证了它能够提供足够多的信息用于验证区块。这里需要注意的是,第一个vout为空并不是用于辨别staking交易的唯一方法,但由于PoS3.0的设计者Sunny King一开始采用了这种方法,并且在长期实践中证明了其可靠性,所以我们也采用了这一方法来辨别staking交易。
现在我们知道了staking交易的定义,并且了解了其必须服从的规则,接下来我们介绍PoS区块的规则:
* 有且仅有一笔staking交易;
* staking交易必须是区块的第二笔交易;
* coinbase交易必须包含一个价值为0的输出和一个空的vout;
* 区块的timestamp的最后4位设为0,这就意味着blocktime只能表示16s单位时间,相当于增大了时间粒度;
* 区块版本必须为7;
* 区块的kernal hash必须符合PoS的加权挖矿难度要求;
* block hash必须用staking交易第二个vout对应的public key签名,签名数据保存在区块上(但不包含在正式的block hash中);
* 在区块上保存的签名必须为“LowS”,也就是说只能由单个数据块组成并且被充分压缩(不能增加一些0作为前缀,也不能包含其他的opcodes);
* 其余大部分适用于标准PoW的规则仍适用(如合法的merkle hash,合法的交易,timestamp必须小于最大允许延迟时间等);
这些规则中对PoS来说最重要的莫过于“kernal hash”。kernel hash的作用和PoW中block hash的作用类似,即hash值符合条件则认为区块合法。然而kernal hash并不能通过直接修改当前区块的部分内容来获得。接下来我会先介绍kernal hash的组成结构和运行机制,然后进一步解释这样设计的目的,以及如果改变此设计将带来的不可预见的后果。
PoS中的Kernel Hash
kernal hash由如下数据按顺序作为输入组成:
* 前一区块的“skate modifier”(稍后会进一步解释);
* prevout交易的timestamp (被用于staking交易第一个vin所对应的前一笔交易的output);
* prevout 的output number (也就是表明被staking交易使用的是哪一个输出);
* 当前区块时间(block time),但时间的最后4位设为0。这是在staking过程中唯一改变的值;
区块的“skate modifier”指的是如下数据的hash值:
* PoS区块中的prevout交易的hash,或是PoW区块的block hash;
* 前一区块的stake modifier (创世块的stake modifier为0);
能够改变当前kernel hash的方法(为了挖矿)只有2个,要么改变“prevout”,要么改变当前block time。
一般来说一个钱包会包含多个UTXO。钱包的余额实际上就是当前钱包中所有可用的UTXO的总和。这在PoS钱包中也适用,而且更为重要,因为任意的output都可能用于staking。这些outputs中的一个将可以成为staking交易中的prevout,用于生成合法的区块。
此外,PoS区块挖矿过程还有一点重要的变化(相较于PoW),那就是挖矿难度与拥有的币的数量成反比(而不是UTXO的数量)。比如有2个币的钱包挖矿难度是只拥有1个币的一半。如果不这么设计的话,就会鼓励用户生成许多小微额度的UTXO,这将导致区块大小不断变大,并且有可能导致一些安全性问题。
kernal hash的计算过程可以用伪代码表示为:
while(true){
foreach(utxo in wallet){
blockTime = currentTime - currentTime % 16
posDifficulty = difficulty * utxo.value
hash = hash(previousStakeModifier << utxo.time << utxo.hash << utxo.n << blockTime)
if(hash < posDifficulty){
done
}
}
wait 16s -- wait 16 seconds, until the block time can be changed
}
通过以上过程,我们找到其中一个UTXO可用于生成staking交易。这个staking交易有1个vin,即我们找到的那个UTXO。同时这个staking交易有至少两个vout,第一个为空,用于区块链对其进行识别,第二个vout为只包含一个public key的OP_RETURN数据交易,或包含pay-to-pub-key脚本。后者的作用比较纯粹(支付),而数据交易则可以在不破坏原来UTXO模型的前提下有更多的用途(比如独立的区块签名机)。
最后,交易池(mempool)中的全部交易会被加到区块中,接下来我们需要做的就是生成签名。这个签名必须采用staking交易的第二个vout对应的public key。实际的交易数据则通过block hash进行计算。签名之后,我们就可以将这个区块广播到网络中。网络中的其他节点会对区块进行验证,如果区块合法,则节点会接受这个区块并连到自己的区块链上,同时向它连接的其他节点广播这个新区块。
通过以上步骤,我们就可以得到一条完整并且安全的PoS3.0区块链了。PoS3.0被认为是在完全去中心化共识系统中对抗恶意攻击最好的共识机制。为什么会有这一结论?我们可以结合PoS的发展历史进行理解。
PoS的发展史
PoS由来已久,这里只做简要描述:
PoS1.0 — 在Peercoin中应用,严重依赖币龄(coin age,即自UTXO被花费出去后所经历的时间),币龄越大则挖矿难度越低。这产生了一个副作用,就是用户会选择间隔很久的时间(比如一个月或更长)开一次钱包,这样钱包中的UTXO的币龄都会比较大,用户可以很快的挖到新区块。这将导致更容易发生双花(double-spend)攻击。Peercoin本身并没有受此影响,原因在于它采用了PoW和PoS混合机制,PoW可以减少这种负面影响。
PoS2.0 — 在共识机制中移除了币龄,并采用了和PoS1.0完全不同的stake modifier。修改的内容比较多,但基本都是围绕如何移除币龄并且在不采用PoW/PoS混合模式的前提下实现安全的共识机制。
PoS3.0 — PoS3.0实际上可以说是PoS2.0的升级版。在PoS2.0中,stake modifier还包含了前一区块的block time,这在3.0中被移除了,主要是为了防止所谓的“short-range”攻击,即有可能通过遍历改变前一区块时间来遍历挖矿。PoS2.0采用区块时间和交易时间来决定UTXO的年龄,这和之前说的币龄稍有不同,它表示一个UTXO可用于staking需要的最少确认数。在PoS3.0中的UTXO年龄变得更加简单,它由区块高度决定。这可以避免在区块链中引入不太准确的timestamp,并且可以有效免疫“timewarp”攻击。PoS3.0还增加了对staking交易的OP_RETURN的支持,从而使得vout可以只包含public key,而不一定需要完整的pay-to-pubkey脚本。
参考文献
https://peercoin.net/assets/paper/peercoin-paper.pdfhttps://blackcoin.co/blackcoin-pos-protocol-v2-whitepaper.pdfhttps://www.reddcoin.com/papers/PoSV.pdfhttps://blog.ethereum.org/2015/08/01/introducing-casper-friendly-ghost/https://github.com/JohnDolittle/blackcoin-old/blob/master/src/kernel.h#L11https://github.com/JohnDolittle/blackcoin-old/blob/master/src/main.cpp#L2032https://github.com/JohnDolittle/blackcoin-old/blob/master/src/main.h#L279http://earlz.net/view/2017/07/27/1820/what-is-a-utxo-and-how-does-ithttps://en.bitcoin.it/wiki/Script#Obsolete_pay-to-pubkey_transactionhttps://en.bitcoin.it/wiki/Script#Standard_Transaction_to_Bitcoin_address_.28pay-to-pubkey-hash.29https://en.bitcoin.it/wiki/Script#Provably_Unspendable.2FPrunable_Outputs