上一篇咱們已經知道了什麼是區塊鏈,此篇說一下區塊鏈的第一個應用——比特幣。其實先有比特幣,後有的區塊鏈。比特幣是中本聰提出的一種P2P去中心化的支付系統,其底層原理爲:去中心化、數據不可篡改、可溯源;數據以區塊的形式保存,而且像鏈條同樣鏈接起來,很形象地稱爲區塊鏈。比特幣就是一條上文說過的公有鏈。web
上一節咱們說過,帳本以交易的形式記錄,那交易是如何存儲在區塊鏈上呢?算法
交易存儲在區塊之中。區塊分爲區塊頭以及區塊體,區塊頭包含區塊的概要信息,區塊體包含交易信息。安全
哈希算法就是將任意長度的數據轉變爲一個定長的數據(也叫哈希或者摘要)。常見的哈希算法有CRC3二、MD五、SHA一、SHA二、SHA三、Hmac等。網絡
哈希算法具備如下特色:數據結構
單向不可逆性:想要經過哈希值逆推出原來的數據,只有經過暴力破解的方法實現,但這幾乎沒法作到,可能量子計算機出來後就能夠破解了。app
肯定性:若是hash值不一樣, 能夠肯定原數據必定不一樣。若是相同,則還須要進一步判斷,由於可能hash碰撞了。編輯器
雪崩效應:原始數據任何微小的變更都會致使哈希值徹底不同分佈式
不變性:同一個數據,屢次hash結果相同。ide
非對稱加密有公鑰和私鑰兩個概念,私鑰本身擁有,不能給別人,公鑰公開。根據應用的不一樣,咱們能夠選擇使用不一樣的密鑰加密:post
私鑰簽名:使用私鑰簽名(加密),公鑰驗籤(解密)。用於讓公鑰全部者驗證私鑰全部者的身份而且用來防止私鑰全部者發佈的內容被篡改,可是不用來保證內容不被他人得到。(內容自己非加密的,別人能夠得到信息但沒法篡改)
公鑰加密:用公鑰加密,私鑰解密。公鑰全部者可用於加密發佈的信息,這個信息可能被他人篡改,可是沒法被他人得到。
舉例:好比小黑持有公鑰,將 「我愛你」 加密後的消息 「xxx」 發送給喜歡的人小花(小花持有私鑰),但小灰也喜歡小花而且也有公鑰,小灰劫持了小黑的消息(消息內容已加密,小灰也不知道內容是什麼),並將 「我恨你」 加密爲 「xxx」組裝爲小黑的請求發送給小花,小花收到小黑的請求後,用私鑰解密,獲得內容爲:「我恨你」。這下小黑就涼涼,心機boy小灰就成功了。
簽名與加密能夠混合使用,提升安全性與隱私性。
做用:保證信息傳輸的完整性、發送者的身份驗證(鑑權)
原理:使用非對稱加密技術及hash技術,非對稱加密生成一對公私鑰對,私鑰用於簽名,公鑰用於驗籤。簽名的對象爲內容的hash。
爲何簽名不直接簽署內容,而是簽署hash呢?
答:效率問題,若是你的原內容很大,直接對原內容簽名效率很低,並且簽名的結果也相對很大,傳輸也慢。
在比特幣系統中,公鑰(地址)用於接收比特幣,而私鑰則用於比特幣支付時的交易簽名。
在支付比特幣時,比特幣的全部者須要在交易中提交本身的公鑰和該交易的簽名。而比特幣網絡中全部節點能夠經過所提交的公鑰和簽名進行驗證,從而確認支付者對交易的比特幣的全部權。
Merkle樹是⼀種哈希⼆叉樹,它是⼀種⽤做快速概括和校驗⼤規模數據完整性的數據結構。這種⼆叉樹包含加密哈希值。
在⽐特幣⽹絡中,Merkle樹被⽤來概括⼀個區塊中的全部交易,同時⽣成整個交易集合的hash,且提供了⼀種校驗區塊是否存在某交易的⾼效途徑。
當N個數據元素通過加密後插⼊Merkle樹時,時間複雜度爲log(N)【由於Merkle樹可能爲徹底二叉樹或者滿二叉樹。而滿二叉樹的高度爲log(N)】就能檢查出任意某數據元素是否在該樹中,這使得該數據結構⾮常⾼效。
計算merkleRoot時,若是交易數量爲奇數,則複製最後一個交易hash進行計算。
交易數量 | 區塊的近似大小 | 路徑大小(Hash數量) | 路徑大小(字節) |
---|---|---|---|
16筆交易 | 4KB | 4個Hash | 128字節 |
512筆交易 | 128KB | 9個Hash | 288字節 |
2048筆交易 | 512KB | 11個Hash | 352字節 |
65535筆交易 | 16MB | 16個Hash | 512字節 |
注:單個Hash32字節
依表可得,當區塊⼤⼩由16筆交易(4KB)急劇增長⾄65,535筆交易(16MB)時,爲證實交易存在的Merkle路徑⻓度增⻓極其緩慢,僅僅從128字節到512字節。有了Merkle樹,⼀個節點可以僅下載區塊頭(80字節/區塊),而後經過從⼀個滿節點回溯⼀條⼩的Merkle路徑就能認證⼀筆交易的存在,⽽不須要存儲或者傳輸⼤量區塊鏈中⼤多數內容,這些內容可能有⼏個G的⼤⼩。這種不須要維護⼀條完整的區塊鏈的節點,⼜被稱做簡單⽀付驗證(SPV)節點,它不須要下載整個區塊⽽經過Merkle路徑去驗證交易的存在。
Merkle樹被SPV節點⼴泛使⽤。SPV節點不保存全部交易也不會下載整個區塊,僅僅保存區塊頭。它們使⽤認證路徑或者Merkle路徑來驗證交易存在於區塊中,⽽沒必要下載區塊中全部交易。
區塊頭只有80字節,而一個區塊大小爲1M(比特幣已擴容爲2M),因此SPV節點驗證交易是否存在,只須要比特幣區塊容量的千分之一的數據就能夠,大大節省容量。
例子:使用上面的merkelTree中的交易。假設SPV節點想驗證交易A是否存在與區塊內。
如圖所示,SPV節點只須要獲取該交易所在的區塊頭以及驗證路徑:即N一、N4便可。
注意:SPV只能驗證某交易是否存在與區塊中,而沒法驗證該交易的UTXO是否雙花,須要等待該區塊後是否累積了多個區塊,越多證實該交易被大多數人共識確認,越沒法篡改。比特幣中,6個區塊就能夠確認該交易基本沒法篡改,篡改的機率很低,而且成本昂貴。
爲何比特幣的區塊大小爲1M?
//For now it exists as an anti-DoS measure to avoid somebody creating a titanically huge but valid block and forcing everyone to download/store it forever.
public static final int MAX_BLOCK_SIZE = 1 * 1000 * 1000;
源碼裏有註釋:預防dos攻擊,防止有節點建立很大且有效的區塊發送到比特幣網絡,這樣你們都會去驗證並廣播,形成網絡擁堵。
UTXO:unspent transaction output 未花費的交易輸出。若是單看比特幣,其實就是UTXO,擁有多少比特幣,實質是你的UTXO集合有多少。一個交易輸出就是一個UTXO。
咱們再看一下交易的結構
txHash :交易的hash
Index : 交易的輸出列表中的下標,從0開始。
Value : 轉帳金額,比特幣最小單位爲聰,1Btc = 10^8聰,轉帳單位也爲聰
lockScpript:鎖定腳本,一般爲地址。(表示轉帳的比特幣存儲的形式)
腳本類型
public enum ScriptType {
P2PKH(1), // pay to pubkey hash (aka pay to address)一般爲此形式,支付到地址
P2PK(2), // pay to pubkey
P2SH(3), // pay to script hash
P2WPKH(4), // pay to witness pubkey hash
P2WSH(5); // pay to witness script hash
public final int id;
private ScriptType(int id) {
this.id = id;
}
}
交易輸出表述爲:轉帳給哪個地址的金額,位於交易輸出列表中的哪一個位置。
挖礦是增長⽐特幣貨幣供應的⼀個過程。挖礦同時還保護着⽐特幣系統的安全,防⽌欺詐交易,避免「雙花」 ,「雙花」是指屢次花費同⼀筆⽐特幣。
**挖礦:指比特幣網絡中的節點將網絡中收到的合法交易進行打包,生成區塊的過程。**而且,交易列表的第一筆交易礦工會生成一個coinbase交易,即爲創幣交易。(此交易沒有輸入,只有輸出,輸出到本身地址做爲挖礦獎勵)礦⼯經過創造⼀個新區塊獲得的⽐特幣數量⼤約每四年(或準確說是每210,000個塊)減小⼀半。開始時爲2009年1⽉每一個區塊獎勵50個⽐特幣,而後到2012年11⽉減半爲每一個區塊獎勵25個⽐特幣。以後將在2016年的某個時刻再次減半爲每一個新區塊獎勵12.5個⽐特幣。基於這個公式,⽐特幣挖礦獎勵以指數⽅式遞減,直到2140年。屆時全部的⽐特幣(20,999,999.98)所有發⾏完畢。換句話說在2140年以後,不會再有新的⽐特幣產⽣。如今(2020/7/02)爲6.25BTC。
靈魂拷問:
比特幣網絡中中的節點那麼多,你們均可以挖礦?而最終肯定的區塊只有一個或少數幾個,全網這麼多礦工,你們怎麼都認可是你的區塊被接受,個人不被接受呢?
比特幣中有一個挖礦難度,這個難度能夠轉換爲一個256bit的大數,比特幣的共識爲:全網的礦工都去打包區塊,但有一個條件,區塊的hash轉換的256bit的大數必定要不大於比難度轉換的才行。(也就是你們常說的計算的Hash前置的0多於目標值Hash的前置0一個意思)
protected boolean checkProofOfWork(boolean throwException) throws VerificationException {
//將難度轉換爲一個256bit的大數
BigInteger target = getDifficultyTargetAsInteger();
//區塊hash轉換爲一個256bit的大數
BigInteger h = getHash().toBigInteger();
//如何區塊的數大於目標的,則不合法
if (h.compareTo(target) > 0) {
// Proof of work check failed!
if (throwException)
throw new VerificationException("Hash is higher than target: " + getHashAsString() + " vs "
+ target.toString(16));
else
return false;
}
return true;
}
如何計算Hash?
void writeHeader(OutputStream stream) throws IOException {
//版本
Utils.uint32ToByteStreamLE(version, stream);
//父區塊Hash
stream.write(prevBlockHash.getReversedBytes());
//交易merkleTree hash
stream.write(getMerkleRoot().getReversedBytes());
//交易時間
Utils.uint32ToByteStreamLE(time, stream);
//區塊難度
Utils.uint32ToByteStreamLE(difficultyTarget, stream);
//挖礦的嘗試次數
Utils.uint32ToByteStreamLE(nonce, stream);
}
能夠看出,區塊的hash中,變化的量有交易的merkleRoot、區塊時間、區塊nonce。但若是挖礦時交易列表已經肯定,區塊時間也是肯定的,那就只有嘗試更改nonce來更改區塊hash,以達到在該難度下的合法區塊hash。
public void solve() {
while (true) {
try {
// 工做量證實
if (checkProofOfWork(false))
return;
// 增長nonce,從新計算
setNonce(getNonce() + 1);
} catch (VerificationException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
}
能夠看到比特幣挖礦的過程就是尋找一個合法nonce的過程。
咱們再來看一看,UTXO不是隻有惟一的一個嗎?爲何還會產生雙花?
其實雙花在比特幣中對應於分叉。由於比特幣網絡中的節點都是P2P的對等節點,每一個節點打包的交易可能不一樣,時間也不一樣,nonce也不一樣。最終計算的區塊hash也不一樣。這樣就會在同一高度產生多個合法區塊,這就是分叉。
可能你的同一筆交易被打進了高度爲101的兩個區塊中,分別對應於兩條鏈。只要被打進區塊中的交易,證實都是被礦工確認共識事後的合法交易,因此若是是一個做惡的用戶,則能夠利用比特幣的分叉進行雙花攻擊。
比特幣網絡爲了不雙花,引入了一個確認的機制。須要6個區塊才能確認一個區塊中的交易是否真正有效。(6個區塊是估算的)
區塊驗證的過程就是共識的過程:驗證區塊是否按照比特幣協議產生的,是否都打包的合法交易,是否找到了合法nonce等。
每一個全節點依據綜合標準對每一個交易進⾏獨⽴驗證
▷交易的語法和數據結構必須正確。 ▷輸⼊(coinbase交易除外)與輸出列表都不能爲空。
▷交易的字節⼤⼩是⼩於 MAX_BLOCK_SIZE(當前1M) 的。 ▷每⼀個輸出值,以及總量,必須在規定值的範圍內 (⼩於2,100萬個幣,⼤於0)。 ▷交易的字節⼤⼩是⼤於或等於100的。
▷交易的字節⼤⼩最大爲100kb。 ▷交易中的簽名數量應⼩於簽名操做數量上限。 ▷解鎖腳本( scriptSig )只可以將數字壓⼊棧中,而且鎖定腳本( scriptPubkey )必需要符合 isStandard 的格式 (該格式將會拒絕⾮標準交易)。 ▷池中或位於主分⽀區塊中的⼀個匹配交易必須是存在的。 ▷對於每⼀個輸⼊,若是引⽤的輸出存在於池中任何的交易,該交易將被拒絕。 ▷對於每⼀個輸⼊,在主分⽀和交易池中尋找引⽤的輸出交易。若是輸出交易缺乏任何⼀個輸⼊,該交易將成爲⼀個孤⽴的交易。若是與其匹配的交易尚未出如今池中,那麼將被加⼊到孤⽴交易池中。 ▷對於每⼀個輸⼊,若是引⽤的輸出交易是⼀個coinbase輸出,該輸⼊必須⾄少得到 COINBASE_MATURITY (100)個確認。 ▷對於每⼀個輸⼊,引⽤的輸出是必須存在的,而且沒有被花費。 ▷使⽤引⽤的輸出交易得到輸⼊值,並檢查每⼀個輸⼊值和總值是否在規定值的範圍內 (⼩於2100萬個幣,⼤於0)。 ▷若是輸⼊值的總和⼩於輸出值的總和,交易將被中⽌。 ▷若是交易費⽤過低以⾄於⽆法進⼊⼀個空的區塊,交易將被拒絕。 ▷每⼀個輸⼊的解鎖腳本必須依據相應輸出的鎖定腳原本驗證。
經過完成⼯做量證實算法的驗算。
**判斷區塊高度是否爲2016的倍數,由於每2016個區塊會調整難度。**若是沒達到2016的倍數,只須要驗證目標難度是否相等。(比特幣10分鐘產生一個區塊,2016差很少須要2周時間)
若是須要調整,則須要計算前2016個區塊的實際產生時間。
往前遍歷2016個區塊,找到前2016個區塊的開始區塊
計算前2016個區塊產生的實際時間:當前已確認的區塊時間 - 前2016個區塊的開始區塊的時間
限制實際時間在調整係數(4)以內
//獲取前2016個區塊的實際使用時間
int timespan = (int) (prev.getTimeSeconds() - blockIntervalAgo.getTimeSeconds());
// Limit the adjustment step.
final int targetTimespan = this.getTargetTimespan();
if (timespan < targetTimespan / 4)
timespan = targetTimespan / 4;
if (timespan > targetTimespan * 4)
timespan = targetTimespan * 4;
根據前2016個區塊的實際使用時間計算新的目標值(難度的另外一種表示方式)
newTarget = timespan * preDifficulty/targetTimespan
targetTimespan = 14 * 24 * 60 * 60 兩個星期
目標值壓縮爲4個字節的表達方式。能夠節約存儲。
//難度太大會限制爲創世塊的默認難度(大約10分鐘一個區塊)
if (newTarget.compareTo(this.getMaxTarget()) > 0) {
log.info("Difficulty hit proof of work limit: {}", newTarget.toString(16));
newTarget = this.getMaxTarget();
}
比較計算的難度與區塊中的目標難度是否相等。由於驗證的過程跟礦工挖礦的過程是同樣流程。
每一個節點對區塊鏈進⾏獨⽴選擇,在⼯做量證實機制下選擇累計⼯做量最⼤的區塊鏈
若是該區塊是接着主鏈打的,則處理交易。(處理時會驗證交易,驗證過程就是第一步)將交易輸入引用的UTXO刪掉;交易的輸出轉換爲UTXO存儲。
若是該區塊不是接着主鏈打的,會判斷區塊的工做量是否大於了主鏈的,若是沒有大於,則只是將區塊連接上。
什麼叫工做量呢?
工做量被定義爲:平均狀況下生成一個合法區塊所需的嘗試次數(nonce)
/** * Returns the work represented by this block.<p> * * Work is defined as the number of tries needed to solve a block in the * average case. Consider a difficulty target that covers 5% of all possible * hash values. Then the work of the block will be 20. As the target gets * lower, the amount of work goes up. */
public BigInteger getWork() throws VerificationException {
//將目標難度值轉換爲256bit的大數,這個數值表示該難度下的目標hash值
//(假設這個hash值覆蓋了全部hash範圍的5%,則工做量爲20)
BigInteger target = getDifficultyTargetAsInteger();
//LARGEST_HASH: 該數字比最大可表示的SHA-256哈希大1
return LARGEST_HASH.divide(target.add(BigInteger.ONE));
}
鏈累積的工做量計算:
/** * 構建區塊存儲的索引。包含了這條鏈從創創世塊到如今的全部工做量 */
public StoredBlock build(Block block) throws VerificationException {
//僅表明了該鏈的工做量總和(這裏的this指前一個區塊)每一個區塊都如此計算,就是一個累和
BigInteger chainWork = this.chainWork.add(block.getWork());
int height = this.height + 1;
return new StoredBlock(block, chainWork, height);
}
如何判斷是否產生新的主鏈?
假設如今產生了分叉,則只須要計算出分叉鏈的鏈累積工做量,與當前主鏈的對比,若是大於,則新產生的鏈爲主鏈,舊的主鏈則會做廢。
其實就是最長鏈原則,由於鏈越長,累積的工做量會越大。
//判斷當前鏈是否比主鏈的工做量大,若是大於,則爲新主鏈
public boolean moreWorkThan(StoredBlock other) {
return chainWork.compareTo(other.chainWork) > 0;
}
比特幣處理分叉:
上圖中,區塊高度爲103的區塊到來後,新的主鏈產生。這時候,會用當前主鏈的102,與新主鏈的103向前遍歷,找到分叉點101區塊。(注意:)
經過最長鏈原則,以6個區塊爲確認主鏈,確認主鏈後,分叉鏈須要回退交易。花掉的utxo進行增長,而新增的utxo進行刪除。
注意:6個區塊:判斷當前區塊時間是否大於等於最近11個區塊的區塊時間的中位數(6),因此只能回退最近6個區塊內的數據,反向意思爲:6個以前的沒法回退,即6個區塊確認主鏈。
若是區塊分叉,但尚未成爲最長鏈,只是保存了區塊,並無花費UTXO(由於可能已經花費過了)
對於新區塊102,不會處理交易。由於主鏈已經處理過了102區塊的交易(可能區塊中的交易不徹底同樣,但也沒有關係,由於可能新區塊102中的新交易已經被主鏈的103區塊打包並處理了)
⽐特幣將區塊間隔設計爲10分鐘,是在更快速的交易確認和更低的分叉機率間做出的妥協。更短的區塊產⽣間隔會讓交易清算更快地完成,也會致使更加頻繁地區塊鏈分叉。
什麼叫51%攻擊?
你們可能也常常聽到,比特幣51%的概念。看上面分叉圖。若是有人掌握了全網51%的算力,則就能夠比任何人都先計算出合法nonce,因此就能夠連續接着102的新區塊打區塊,當下面這條鏈的長度長於主鏈,那就是一條新的主鏈。畢竟你這麼厲害,都控制了51%的算力,那咱們就跟着你這條鏈玩吧,你就是主鏈。
分叉的總類?
什麼叫孤兒區塊?
好比如今來了一個105高度的區塊,使用此區塊的preBlockHash沒法找到父區塊,則此區塊就稱爲孤兒區塊。
當收到一個區塊爲孤兒區塊後,會將其存入孤兒區塊池中。並嘗試遍歷孤兒區塊池中的區塊,看是否能連接到主鏈上。由於處於分佈式網絡中,每一個節點的網絡狀況、同步等有差別,可能節點會先收到高度更高的區塊,而後再收到高度低一點的區塊,而後再與本地的鏈高度連接上。
爲何coinbase中的UTXO須要100個確認才能使用呢?
由於若是使用了coinbase中的UTXO的交易被打包進入了兩個分叉的區塊中。上面說過,從新切換主鏈時,舊主鏈的交易須要回退,產生的UTXO須要刪除,使用了的UTXO須要新增。因此,致使新主鏈中引用了coinbase中UTXO的交易無效,由於已經沒有這個UTXO了。這樣就會致使區塊驗證不經過,主鏈沒法從新切換。因此比特幣假設沒有不可能有這麼長遠的分叉,6個確認主鏈已經小於100個確認coinbase。
此篇略說了一些比特幣相關的技術知識,以及比特幣的一些原理。文中有錯誤之處,敬請各位指出。也歡迎你們評論區一塊兒交流學習。
參考資料
《精通比特幣》強烈推薦。
bitcoinJ源碼
本文使用 mdnice 排版