banner
leaf

leaf

It is better to manage the army than to manage the people. And the enemy.
follow
substack
tg_channel

區塊鏈開發實戰

區塊鏈技術主要包括以下幾個部分。

1. 加密哈希函數

我們都知道,一個函數可以接收一個或若干個輸入值,然後經函數運算產生一個或若干個輸出值。哈希函數滿足所有下列條件:

・接收任意長度的字符串作為輸入。

・產生一個固定長度的輸出值。

・計算時間在合理範圍內。

只要滿足上述條件,一個函數就可以稱為哈希函數。舉個簡單的例子:取模運算,任意數字對 10 取模後得到的結果都是 0~9 之間的一個數字,那麼取模運算就可以認為是一個哈希函數。

目前用於比特幣等數字貨幣的哈希函數則是加密哈希函數。加密哈希函數除了擁有上述哈希函數的三個特點外,還有著更為獨特的特性:無碰撞性、隱藏性、結果隨機性,下面分別解釋。

(1)無碰撞性

無碰撞性分為強無碰撞性和弱無碰撞性。強無碰撞性的意思是,對於一個哈希函數 H,我們無法找到兩個不同的 x 和 y 值,使得 H(x)=H(y)。弱無碰撞性則是,對於一個哈希函數 H 以及輸入 x 值,無法找到另外一個 y 值,使得 H(x)=H(y)。

無碰撞性並不是真正的 “無” 碰撞,碰撞是肯定存在的,這裡強調的是尋找碰撞的難度。我們以比特幣中使用到的哈希函數 SHA256 為例,它的輸出值為 256 位,因此結果只有 2256 種可能,但是輸入值卻可以有無限種可能。現在就有一種必然能找到碰撞的辦法:我們首先找到 2256+1 個不同的值,分別計算出它們的哈希值,那結果集裡必然有重複的值,這樣就會發現一次碰撞了。但是這種方法的可行性怎樣呢?假設把全世界所有的計算設備集合起來,從宇宙誕生的時刻到現在一直不停地運算,能夠找到一次碰撞的概率和下一秒鐘地球被隕石撞擊而毀滅的概率一致。既然你讀到了這裡,那就說明地球毀滅沒有發生,也就是沒有碰撞發生。

現在還沒有一種加密哈希函數在數學上被證明是嚴格無碰撞性的,現在市面上提到的無碰撞性,一般認為是目前除了暴力破解之外沒有其他的途徑能夠更快地找到碰撞而已。以前也有曾經被認為是無碰撞性的哈希函數後來找到了破解方案的案例。比如 MD5 哈希算法。比特幣使用的 SHA256 哈希算法目前也被認為是無碰撞性的,但是不排除以後被破解的可能。

無碰撞性有什麼應用呢?一個比較常見的就是消息摘要(Message Digest)。消息摘要是指針對任意長度的輸入,通過加密哈希函數運算後得到的哈希值。以現在常用的哈希算法 MD5 為例,其運算示例如下:

image

可見輸入任何長度的字符串,得到的結果都是固定長度的隨機字符串。因為無碰撞性的存在,我們可以認為這個字符串能唯一地代表輸入值。

我們平時在互聯網上下載軟件時,如何確定我們下載的這個軟件和網站上的軟件就是同一個呢?這時消息摘要就可以發揮作用了。例如,有的網站在下載軟件時提供該軟件的 md5sum 值,那麼我們在下載完該軟件時,就可以手工計算一遍該軟件的 md5sum 值,然後和網站上的值進行對比,只要兩個數值一致,就可以說明我們下載的軟件是完整無誤的。

(2)隱藏性

給定 H(x),無法推測出 x 的值。不僅無法推測出 x 的值,也不能推測出關於 x 的任何特點,比如奇偶性等。

(3)結果隨機性

無論 x 值是否相近,經過哈希運算後得出的 H(x)都是完全隨機的。

這個特點是說,即使輸入值 x 的長度很長,同時另一個輸入值 x' 和 x 值只有一位不同,那它們經過哈希函數 H 運算後得到的結果沒有任何的相關性,就像輸入了兩個完全不同的 x 值一樣。

繼續以 md5sum 值為例:

liangpeili@LiangXiaoxin:~$ echo 'aschplatform' | md5sum
150fa3630db1d8f576d1266176f6e0f7  -
liangpeili@LiangXiaoxin:~$ echo 'aschplatform1' | md5sum
e915a617b2301631ec14d1ca2c093c63  -
liangpeili@LiangXiaoxin:~$ echo 'aschplatform2' | md5sum
bbb9d830f4a5d47051f9fd19cb0fc75e  -

從上面的程序中可以看出,即使只改變一個很小的值,經過哈希運算後的結果也會有很大的不同。

這個特性有什麼作用呢?如果我們針對特定的結果值 H(x),想找到一個符合 H(x),想找到一個符合條件的輸入值 x,那麼除了暴力嘗試之外沒有其他辦法。繼續以 SHA256 為例,它的輸出結果長度為 256 位,如果我們想找到這樣的一個 x 值,使得它經過 SHA256 運算後,結果的第一位是 0,求解這樣的 x 值的期望次數為 2,那如果想要得到連續 10 位為 0 的哈希值呢?期望計算次數就是 210 了。通過調整結果範圍,我們就可以對計算次數(也可以認為是結果難度)進行調整,這也是比特幣調整難度值的原理。

使用 Python 實現的挖礦算法如下:

#!/usr/bin/env python
#coding:utf-8
example of proof-of-work algorithm

import hashlib
import time

max_nonce = 2 ** 32 # 4 billion

def proof_of_work(header, difficulty_bits):

calculate the difficulty target

target = 2 ** (256-difficulty_bits)

for nonce in xrange(max_nonce):
hash_result = hashlib.sha256(str(header)+str(nonce)).hexdigest()
# check if this is a valid result, below the target
  if long(hash_result, 16) < target:
     print("Success with nonce %d" % nonce)
     print("Hash is %s" % hash_result)
     return (hash_result, nonce)
print("Failed after %d (max_nonce) tries" % nonce)
return nonce
if name == "main":
nonce = 0
hash_result = ''

difficult from 0 to 15 bits

for difficulty_bits in xrange(16):

difficulty = 2 ** difficulty_bits
print("Difficulty: %ld (%d bits)" %(difficulty, difficulty_bits))

print("Starting search...")

# checkpoint the current time
start_time = time.time()

# make a new block which includes the hash from the previous block
# we fack a block of transactions - just a string
new_block = 'test block with transactions' + hash_result
# find a valid nonce for the new block
(hash_result, nonce) = proof_of_work(new_block, difficulty_bits)

# checkpoint how long it took to find a result 
end_time = time.time()

elapsed_time = end_time - start_time
print("Elapsed Time: %.4f seconds" % elapsed_time) 

if elapsed_time > 0:

  # estimate the hashes per second
  hash_power = float(long(nonce)/elapsed_time)
  print("Hashing Power: %ld hashes per second" % hash_power)  
2.數字簽名

在現實工作和生活中,我們使用簽名的方式表達了對一份文件的認可,其他人可以識別出你的簽名並且無法偽造你的簽名。數字簽名就是對現實簽名的一種電子實現,它不僅可以完全達到現實簽名的特點,甚至能做得更好。常用的數字簽名算法有RSA(Rivest-Shamir-Adleman Scheme)、DSS(Digital Signature Standard)等。比特幣使用ECDSA(椭圆曲线数字签名算法)來生成賬戶的公私鑰以及對交易和區塊進行驗證數字簽名的工作原理如下所示:

1)Alice生成一對密鑰,一個是sk(signing key),是非公開的;另一個是vk(verification key),是公開的。這一對密鑰同時生成,並且在數學上是相互關聯的,同時,根據vk無法推測出關於sk的任何信息。

2)數字簽名算法接收兩個輸入:信息M和sk,生成一個數字簽名Sm。

3)驗證函數接收信息M、Sm以及vk作為輸入,返回的結果是yes或者no。這一步的目的是為了驗證你看到的針對信息M的數字簽名確實是由Alice的sk來簽發的,用於確認信息與簽名是否相符。

與手寫簽名不同,手寫簽名基本都是相似的,但是數字簽名卻受輸入影響很大。對輸入的輕微改變都會產生一個完全不同的數字簽名。一般不會直接對信息進行數字簽名,而是對信息的哈希值進行簽名。由加密哈希函數的無碰撞性可知,這樣和對原信息進行簽名一樣安全。

3.共識機制

區塊鏈可以看做是一本記錄所有交易的分佈式公開賬簿,而區塊鏈中每個節點都是對等的。這就帶來一個問題:誰有權往這個賬本錄入數據?如果有好幾個節點同時對區塊鏈進行數據寫入,最終以誰的為準?這就是在分佈式網絡中如何保持數據一致性的问题。共識機制是指在一個分佈式的網絡中,讓各個參與網絡的節點達成數據上的一致性。在區塊鏈中,共識機制的作用還包括區塊生產、區塊驗證以及系統的經濟激勵等功能。

不同的共識機制適用於不同的應用場景,以下是常用的共識機制及其適用的應用場景介紹:

工作量證明(Proof of Work,POW)——比特幣使用的就是工作量證明的共識機制。在這種機制裡,任何擁有計算能力的設備都可以參與競爭區塊的生產,系統會根據當前全網的算力動態調整難度值,來保證平均每10分鐘網絡將根據後續區塊的態度來決定認可哪個區塊。一般來說,一筆交易在經過6次確認(約1個小時)後被認為是比較安全而且不可逆的。中本聰在設計比特幣時,使用工作量證明機制背後的核心思想是“one cpu one vote”,期望能夠把比特幣設計成一個完全去中心化的系統,任何人都可以使用電腦等終端參與進來。雖然後來由於礦池的出現,使得比特幣系統的算力比較集中,但目前工作量證明機制仍然被認為是最適合公鏈的共識機制。

·股權證明(Proof of Stake,POS)——股權證明機制於2013年被提出,最早應用於Peercoin中。在工作量證明機制中,生產區塊的概率和你擁有的算力成正比。相應的,在股權證明機制中,生產區塊的難度和你在該系統中占有的股權成正比。在股權證明機制中,一個區塊的生產過程為:節點通過保證金(代幣、資產、名聲等具備價值屬性的物品即可)來對賭一個合法的區塊會成為新的區塊,其收益為抵押資本的利息和交易服務費。提供的保證金越多,獲得記帳權的概率就越大。一旦生產了一個新的區塊,節點就可以獲得相應的收益。股權證明機制的目標是為了解決工作量證明機制裡大量能源被浪費的問題。惡意參與者存在保證金被罰沒的風險。

·授權股權證明(Delegated Proof of Stake,DPOS)——工作量證明和股權證明機制雖然都可以解決區塊鏈數據的一致性問題,但正如上面提到的工作量證明機制存在算力集中(礦池)的问题,而股權證明機制根據保證金的數量來調節生產區塊難度的方式則會導致“馬太效應”的出現,也就是擁有大量代幣的賬戶權利會越來越大,有可能支配記帳權。為了解決前兩者的問題,後來又有人提出了基於股權證明機制的改進算法——授權股權證明機制。在這種共識機制裡,系統中的每個持幣用戶都可以投票給某些代表,最終得票率在前101名的代表可以獲得系統的記帳權。這些代表按照既定時間來鍛造區塊,並且獲取鍛造區塊的收益。授權股權證明機制既可以提高共識的效率(相較於比特幣每10分鐘生產一個區塊,這種機制可以實現10秒以內生產一個區塊),又避免了能源的浪費和馬太效應,因此成為了很多新興公鏈(比如EOS)的選擇。

4.交易的區塊鏈

在比特幣網絡中,每筆交易完成後,這筆交易會廣播到比特幣的P2P網絡。礦工不僅能夠接收到這筆交易,而且還能接收到相同時間段內其他的所有未被記錄的交易。礦工的工作就是把這些所有交易打包成一個交易區塊。具體的過程是:

1)礦工會把這些交易記錄兩兩配對,通過默克爾樹計算出根節點的值。

2)根節點和上一個區塊的哈希值結合,作為一個Challenge String,供礦工作為工作量證明的輸入值。

3)礦工完成工作量證明,並把proof公開出去供其他節點驗證。同時在第一條記錄(這條記錄也稱為coinbase transaction)裡給自己分配挖礦獎勵。

4)其他節點驗證通過,該區塊作為新區塊加入到區塊鏈中。

5)礦工也可以收集其他交易記錄裡的交易費分配給自己。

比特幣的誕生和區塊鏈技術的不斷發展給我們巨大的想像力。目前,互聯網完成了信息的傳遞,而區塊鏈技術或許可以為互聯網帶來價值的傳遞。區塊鏈技術的基礎設施、應用場景或許還需要一定的時間才可以發展到目前互聯網技術的水平,但是區塊鏈技術的潛力卻不容小覷。用300行代碼開發一個區塊鏈系統
本節使用Node.js來實現一個簡單的區塊鏈系統,只需300行代碼。
區塊和區塊鏈的創建
區塊鏈是把區塊用哈希指針連接起來的鏈條,區塊是其中的基本單位。這裡我們從設計一個區塊的數據結構開始。

1.創建區塊

區塊是構建區塊鏈的基本單位,一個區塊至少要包含以下信息。

·index:區塊在區塊鏈中的位置。

·timestamp:區塊產生的時間。

·transactions:區塊包含的交易。

·previousHash:前一個區塊的Hash值。

·hash:當前區塊的Hash值。

其中,最後兩個屬性previousHash和hash是區塊鏈的精華所在,區塊鏈的不可篡改特性正是由這兩個屬性來保證的。

根據上面的信息,我們來創建一個Block類
const SHA256 = require('crypto-js/sha256');

class Block {
  // 構造函數
  constructor(index, timestamp) {
    this.index = index;
    this.timestamp = timestamp;
     this.transactions = [];
    this.previousHash = '';
    this.hash = this.calculateHash();
  }
  // 計算區塊的哈希值
  calculateHash() {
    return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce).toString();    
  }
  // 添加新的交易到當前區塊
  addNewTransaction(sender, recipient, amount) {
    this.transactions.push({
      sender,
      recipient,
      amount
    })
  }
  // 查看當前區塊裡的交易信息
  getTransactions() {
    return this.transactions;
  }
}

  在上面的Block類的實現中,我們實用了crypto-js裡的SHA256來作為區塊的哈希算法,這也是比特幣中使用的哈希算法。transactions是一系列交易對象的列表,其中包含的每筆交易的格式為:

{
  sender: sender,
  recipient: recipient,
  amount: amount
}

另外我們給Block類添加了三個方法:calculateHash、addNewTransaction、get-Transactions,分別用來計算當前區塊哈希、增加新交易到當前區塊、獲取當前區塊所有交易。

區塊構建完成後,下一步就是考慮如何把區塊組裝成一個區塊鏈了。

2.創建區塊鏈

一個區塊鏈就是一個鏈表,鏈表中的每個元素都是一個區塊。區塊鏈需要一個創世區塊(Genesis Block)來進行初始化,這也是區塊鏈的第一個區塊,需要手工生成。在我們創建Blockchain類時,需要考慮到創世區塊的生成。以下是代碼示例:

class Blockchain {
  constructor() {
    this.chain = [this.createGenesisBlock()];
  }
  // 創建創始區塊
  createGenesisBlock() {
    const genesisBlock = new Block(0, "01/10/2017");
    genesisBlock.previousHash = '0';
    genesisBlock.addNewTransaction('Leo', 'Janice', 520);
    return genesisBlock;
  }
  // 獲取最新區塊
  getLatestBlock() {
    return this.chain[this.chain.length - 1];
  }
  // 添加區塊到區塊鏈
  addBlock(newBlock) {
    newBlock.previousHash = this.getLatestBlock().hash;
    newBlock.hash = newBlock.calculateHash();
    this.chain.push(newBlock);
  }
  // 驗證當前區塊鏈是否有效
  isChainValid() {
    for (let i = 1; i < this.chain.length; i++){
      const currentBlock = this.chain[i];
      const previousBlock = this.chain[i - 1];

      // 驗證當前區塊的 hash 是否正確

if(currentBlock.hash !== currentBlock.calculateHash()){
        return false;
      }

      // 驗證當前區塊的 previousHash 是否等於上個區塊的 hash
      if(currentBlock.previousHash !== previousBlock.hash){
        return false;
      }
    }
    return true;
  }
}

在Blockchain這個類中,我們實現了一個創建創世區塊的方法。由於創世區塊中並沒有前一個區塊,因此previousHash設置為0。另外假定這一天是Leo和Janice的結婚紀念日,Leo給Janice轉帳520個代幣,由此產生了一筆交易並記錄到創世區塊中。最後我們把這個創世區塊添加到構造函數中,這樣區塊鏈就包含一個創世區塊了。方法getLatestBlock和addBlock含義比較明顯,含義分別是獲取最新區塊和往區塊鏈中添加新的區塊。最後一個isChainValid方法是通過驗證區塊的哈希值來驗證整個區塊鏈是否有效,如果已經添加到區塊鏈的區塊數據被篡改,那麼該方法則返回為false。我們會在下一部分對此場景進行驗證。

3.對區塊鏈進行測試

到現在為止,我們已經實現了一個最簡單的區塊鏈了。在這一部分,我們會對創建的區塊鏈進行測試。方法是向區塊鏈中添加兩個完整的區塊,並且通過嘗試修改區塊內容來展示區塊鏈的不可篡改的特性。

我們先創建一個名字叫作testCoin的區塊鏈。使用Blockchain類新建一個對象,此時它應該只包含創世區塊:

const testCoin = new Blockchain();
console.log(JSON.stringify(testCoin.chain, undefined, 2));

運行該程序,結果為:

[
  {
    "index": 0,
    "timestamp": "01/10/2017",
    "transactions": [
      {
        "sender": "Leo",
        "recipient": "Janice",
        "amount": 520
      }
    ],
    "previousHash": "0",
    "hash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e"
  }
]

然後,我們新建兩個區塊,每個區塊裡都包含一筆交易。然後把這兩個區塊依次添加到testCoin這個區塊鏈上:

lock1 = new Block('1', '02/10/2017');
block1.addNewTransaction('Alice', 'Bob', 500);
testCoin.addBlock(block1);

block2 = new Block('2', '03/10/2017');
block2.addNewTransaction('Jack', 'David', 1000);
testCoin.addBlock(block2);
console.log(JSON.stringify(testCoin.chain, undefined, 2));

可以得到以下結果:


  {
    "index": 0,
    "timestamp": "01/10/2017",
    "transactions": [
      {
        "sender": "Leo",
        "recipient": "Janice",
        "amount": 520
      }
    ],
    "previousHash": "0",
    "hash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e"
  },
  {
    "index": "1",
    "timestamp": "02/10/2017",
    "transactions": [
      {
        "sender": "Alice",
        "recipient": "Bob",
        "amount": 500
      }
    ],
    "previousHash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e",
    "hash": "32b96fa0bba9a7353e67498d822fb0c1f89c307098295c288459cb44dbc5d0f1"
  },
  {
    "index": "2",
    "timestamp": "03/10/2017",
    "transactions": [
      {
        "sender": "Jack",
        "recipient": "David",
        "amount": 1000
      }
    ],
    "previousHash": "32b96fa0bba9a7353e67498d822fb0c1f89c307098295c288459cb44dbc5d0f1",
    "hash": "3a0b9a0471bb474f7560968f2f05ff93306cfc26be7f854a36dc4fea92018db2"
  }
]

testCoin現在包含三個區塊,除了創世區塊以外,剩下的兩個區塊是我們剛剛添加的。注意每一個區塊的previousHash屬性是否正確地指向了前一個區塊的哈希值。

此時我們使用isChainValid方法可以驗證該區塊鏈的有效性。console.log(testCoin.isChainValid())的返回結果為true。

區塊鏈的防篡改性體現在哪裡呢?我們先來修改第一個區塊的交易。在第一個區塊中,Alice向Bob轉帳500元,假設Alice後悔了,她只想付100元給Bob,於是修改交易信息如下:

block1.transactions[0].amount = 100;
console.log(block1.getTransactions())

Alice查看區塊鏈的交易信息,發現已經改成了100元,放心地走了。Bob看到後,發現交易遭到了篡改。於是Bob開始收集證據,他怎麼證明block1的那筆交易是被人為篡改後的交易呢?Bob可以調用isChainValid方法來證明目前的testCoin是無效的。因為testCoin.isChainValid()返回值為false。但是testCoin.isChainValid()為什麼會返回false呢?我們來看一下背後的邏輯:首先Alice修改了交易的內容,這個時候block1的哈希值肯定和通過之前交易計算出的哈希值是不同的。這兩個值的不同會觸發isChainValid返回為false,也就是如下代碼實現的功能:

if(currentBlock.hash !== currentBlock.calculateHash())
{
  return false;
}

既然如此,Alice在修改交易內容的同時修改block1的hash不就可以了嗎?Alice可以繼續篡改其他的區塊內容:

block1.transactions[0].amount = 100;
block1.hash = block1.calculateHash();
console.log(testCoin.isChainValid())
這樣的話,最後的結果依然是false。為什麼呢?是因為下面這段代碼:

if(currentBlock.previousHash !== previousBlock.hash){
  return false;
}
每一個區塊都存儲了上個區塊的哈希值,只修改一個區塊是不夠的,還需要修改下一個區塊存儲的previousHash。如果我們已經安全地存儲了block2的哈希值,那無論如何Alice都是不可能在不被發現的情況下篡改已有數據的。在真實的區塊鏈項目中,修改一個區塊必須修改接下來該區塊之後的所有區塊,這也是無法辦到的事情。區塊鏈的這個“哈希指針”的特性,保證了區塊鏈數據的不可篡改性。




工作量證明#

實現的區塊鏈系統還比較簡單,並且沒有解決電子貨幣系統中需要解決的 “双重支付” 問題。要想維持整個系統健康運轉,需要在系統中設計一定的經濟激勵機制。在比特幣體系中,中本聰就設計了一個 “工作量證明” 的機制,解決了系統裡的經濟激勵問題以及雙重支付問題。下面我們介紹工作量證明算法的原理和實現。

1. 工作量證明算法

一個健康運行的區塊鏈系統隨時會產生交易,我們需要有伺服器進行以下工作:定時把一個時間段(比特幣是 10 分鐘,Asch 是 10 秒)的交易打包到一個區塊,並且添加到現有的區塊鏈中。但是一個區塊鏈系統中可能有很多台伺服器,究竟是以哪台伺服器打包的區塊為準呢?為了解決這個問題,比特幣中採用了一種叫做工作量證明的算法來決定採用哪一台伺服器打包的區塊並且給予相應的獎勵。

工作量證明算法可以簡單地描述為:在一個時間段同時有多台伺服器對這一段時間的交易進行打包,打包完成後連帶區塊 Header 信息一起經過 SHA256 算法進行運算。在區塊頭以及獎勵交易 coinbase 裡各有一個變量 nonce,如果運算的結果不符合難度值(稍後會解釋這個概念)要求,那麼就調整 nonce 的值繼續運算。如果有某台伺服器率先計算出了符合難度值的區塊,那麼它可以廣播這個區塊。其他伺服器驗證沒問題後就可以添加到現有區塊鏈上,然後大家再一起競爭下一個區塊。這個過程也稱為 “挖礦”。

工作量證明算法採用了哈希算法 SHA256,這種算法的特點是難以通過運算得到特定的結果,但是一旦計算出來合適的結果後則很容易驗證。在比特幣系統裡,找到一個符合難度要求的區塊需要耗費 10 分鐘左右,但是驗證它是否有效卻是瞬間的事。在下一節的代碼實現裡我們會看到這一點。

舉一個簡單的例子:假設有一群人玩一個扔硬幣遊戲,每個人有十枚硬幣,依次扔完十枚硬幣,最後看十枚中的正面和反面的排序結果。由於最後的結果是有順序的,結果總有 210 種可能。現在有個規定,在一輪遊戲中,誰先扔出了前 4 枚硬幣都是正面的結果,誰就可以得到獎勵。於是大家都開始扔十枚硬幣並統計結果。前四枚都是正面的可能性有 26 種,因此一個人能獲取該結果的期望嘗試次數為 24。如果規定正面的個數越多,那麼每個人的嘗試次數就會越多,而這裡的個數就是難度。如果玩遊戲的人逐漸增多,那我們就可以要求結果的前 6 個、前 8 個是正面,這樣每輪遊戲的時間依然差不多。這也是為什麼比特幣的算力大增,而依然能保持平均每 10 分鐘產生一個區塊的原因。

上面闡述了什麼是工作量證明,那如何把它添加到我們的區塊鏈應用中呢?

任何一個數據經過 SHA256 運算後都會得到長度為 256 位的二進制數值,我們可以通過調整最開始的部分連續 0 的個數作為 “難度值”。比如我們要求最後的區塊經過 SHA256 運算後第一位為 0,那麼平均每兩次運算就會得到一個這樣的結果。但是如果我們要求連續 10 位都是 0,那就需要平均計算 210 次才能得到一次這樣的結果了。系統可以通過調整計算結果裡連續 0 的個數來達成調整難度的目標。

我們在區塊的頭信息中添加一個變量 nonce。通過不停地調節 nonce 的值來重新計算整個區塊的哈希值,直到計算的結果滿足難度要求。

2. 工作量證明的代碼實現

基於上節的概念,我們開始改造現在的區塊鏈應用。首先在 Block 類添加一個 nonce 變量:

class Block {
  constructor(index, timestamp) {
    this.index = index;
    this.timestamp = timestamp;
    this.transactions = [];
    this.previousHash = '';
    this.hash = this.calculateHash();
    this.nonce = 0;
  }
  ...
}

然後在 Block 類中添加一個 mineBlock 方法:

mineBlock(difficulty) {
    console.log(`Mining block ${this.index}`);
    while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
        this.nonce++;
        this.hash = this.calculateHash();
    }
    console.log("BLOCK MINED: " + this.hash);
  }

方法 mineBlock 就是根據難度值來尋找 nonce,只有找到合適的 nonce 之後才可以提交區塊。這裡的 difficulty 指的是結果裡從開頭連續為 0 的個數。如果計算出來的哈希值不符合要求,那麼 nonce 加 1,然後重新計算區塊的哈希值。

於是,我們在 Blockchain 類裡定義一個難度值:

constructor() {
    this.chain = [this.createGenesisBlock()];
    this.difficulty = 2;
  }

把挖礦的過程應用到添加區塊到區塊鏈的過程中:

addBlock(newBlock) {
    newBlock.previousHash = this.getLatestBlock().hash;
    newBlock.mineBlock(this.difficulty);
    this.chain.push(newBlock);
  }

到此為止,我們對應用的改造就完成了。下面對這部分添加後的代碼進行測試。

我們先只添加一個區塊


block1 = new Block('1', '02/10/2017');
block1.addNewTransaction('Alice', 'Bob', 500);
testCoin.addBlock(block1);

console.log(block1)

運算結果為:

Mining block 1
BLOCK MINED: 005fed00324fcbe1f0ab1703afe94e45a99e197a7df142e669444687f9513e57
Block {
  index: '1',
  timestamp: '02/10/2017',transactions: [ { sender: 'Alice', recipient: 'Bob', amount: 500 } ],
  previousHash: '31b15cc32d6772f237dcf298d5b7a2417f298f40ce6d8d5fbe07958141df7a4c',
  hash: '005fed00324fcbe1f0ab1703afe94e45a99e197a7df142e669444687f9513e57',
  nonce: 419 
      }

注意那個 nonce 值以及 hash 值。nonce 值表明了計算次數,hash 值是最後得到的結果。這次我們設置的難度值為 2,期望計算次數是 28 次(hash 裡一個字符代表 4 位)。如果把難度值改成 3 呢?運算結果為:

Mining block 1
BLOCK MINED: 000b7f17beaf58bc8fea996a9fed11103ed27ad6d63818b87d89a440cd9757b5
Block {
  index: '1',
  timestamp: '02/10/2017',
  transactions: [ { sender: 'Alice', recipient: 'Bob', amount: 500 } ],
  previousHash: '31b15cc32d6772f237dcf298d5b7a2417f298f40ce6d8d5fbe07958141df7a4c',
  hash: '000b7f17beaf58bc8fea996a9fed11103ed27ad6d63818b87d89a440cd9757b5',
  nonce: 4848
     }

可以看到,計算的次數增加了。隨著難度值增大,CPU 計算的次數也會呈指數級增加,相應耗費的時間也就越長。

提供和區塊鏈進行交互的 API#

1. 挖礦獎勵

在實現相關 API 之前,我們首先來看一下什麼是挖礦獎勵。

上面介紹了挖礦的原理並且實現了工作量證明算法,可是伺服器為什麼願意貢獻自己的 CPU 資源去打包區塊呢?答案就是挖礦時有一個獎勵機制。礦工在打包一個時間段的交易後,會在區塊的第一筆交易的位置創建一筆新的交易。這筆交易沒有發送人,接收人可以設為任何人(一般設置為自己的地址),獎勵的數額是多少呢?目前比特幣礦工每打包一個區塊的獎勵是 12.5 個 BTC。這筆獎勵交易是由系統保證的,並且可以通過任何一個其他節點的驗證。

這裡面有幾個問題。首先,獎勵金額的問題。比特幣剛開始發行時,每個區塊的獎勵是 50BTC,其後每隔四年時間減半,2018 年 7 月已經是 12.5 個 BTC 了。其次,礦工能否創建多筆獎勵交易或者加大奖勵金額?礦工當然可以這麼幹,但是這麼做以後廣播出去的區塊是無法通過其他節點驗證的。其他節點收到區塊後會進行合法性驗證,如果不符合系統的規則就會丟棄該區塊,而該區塊最終也不會被添加到區塊鏈中。

2. 代碼重構

為了把我們當前的代碼改造成適合通過 API 對外提供的形式,需要做以下幾個處理:

1)在 Blockchain 類中添加屬性 currentTransactions,用於收集最新交易,並且準備打包到下一個區塊中:

constructor() {
    this.chain = [this.createGenesisBlock()];
    this.difficulty = 3;
    this.currentTransactions = [];
  }

2)把 Block 類中的 addNewTransaction 方法移到 Blockchain 類裡。

3)把 Block 類和 Blockchain 類輸出(export),將 app.js 重命名為 blockchain.js。

最後的 blockchain.js 內容應該為:

const SHA256 = require('crypto-js/sha256');

// 區塊類
class Block {
  constructor(index, timestamp) {
    this.index = index;
    this.timestamp = timestamp;
    this.transactions = [];
    this.previousHash = '';
    this.hash = this.calculateHash();
    this.nonce = 0;
  }

  calculateHash() {
    return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce).toString();    
  }

  mineBlock(difficulty) {
    console.log(`Mining block ${this.index}`);
    while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
        this.nonce++;
        this.hash = this.calculateHash();
    }
    console.log("BLOCK MINED: " + this.hash);
}

  getTransactions() {
    return this.transactions;
  }
}

// 區塊鏈類
class Blockchain {
  constructor() {
    this.chain = [this.createGenesisBlock()];
    this.difficulty = 3;
    this.currentTransactions = [];
  }

  addNewTransaction(sender, recipient, amount) {
    this.currentTransactions.push({
      sender,
      recipient,
      amount
    });
  }

  createGenesisBlock() {
    const genesisBlock = new Block(0, "01/10/2017");
    genesisBlock.previousHash = '0';
    genesisBlock.transactions.push({
      sender: 'Leo',
      recipient: 'Janice',
      amount: 520
    });
    return genesisBlock;
  }

  getLatestBlock() {
    return this.chain[this.chain.length - 1];
  }

  addBlock(newBlock) {
    newBlock.previousHash = this.getLatestBlock().hash;
    newBlock.mineBlock(this.difficulty);
    this.chain.push(newBlock);
}

  isChainValid() {
    for (let i = 1; i < this.chain.length; i++){
      const currentBlock = this.chain[i];
      const previousBlock = this.chain[i - 1];

      if(currentBlock.hash !== currentBlock.calculateHash()){
        return false;
      }

      if(currentBlock.previousHash !== previousBlock.hash){
        return false;
      }
    }
    return true;
  }
}

module.exports = {
  Block,
  Blockchain
}

注意,上面順便修改了 Blockchain 裡的方法 createGenesisBlock 的代碼。

3. 使用 Express 提供 API 服務

為了能夠提供 API 服務,這裡我們採用 Node.js 中最流行的 Express 框架。區塊鏈對外提供以下三個接口:

・POST/transactions/new:添加新的交易,格式為 JSON。

・GET/mine:將目前的交易打包到新的區塊。

・GET/chain:返回當前的區塊鏈。

基礎代碼如下:

const express = require('express');
const uuidv4 = require('uuid/v4');
const Blockchain = require('./blockchain').Blockchain;

const port = process.env.PORT || 3000;
const app = express();
const nodeIdentifier = uuidv4();
const testCoin = new Blockchain();

// 接口實現
app.get('/mine', (req, res) => {
  res.send("We'll mine a new block.");
});

app.post('/transactions/new', (req, res) => {
  res.send("We'll add a new transaction.");
});

app.get('/chain', (req, res) => {
  const response = {
    chain: testCoin.chain,
    length: testCoin.chain.length
  }
  res.send(response);
})

app.listen(port, () => {
  console.log(`Server is up on port ${port}`);
});

下面我們完善路由 /mine 以及 /transactions/new,並添加一些日誌功能(非必需)。

先來看路由 /transactions/new,在這個接口中,我們接收一個 JSON 格式的交易,內容如下:

{
  "sender": "my address",
  "recipient": "someone else's address",
  "amount": 5
}

然後,把該交易添加到當前區塊鏈的 currentTransactions 中。這裡會用到 body-parser 模塊,最後的代碼為:

const bodyParser = require("body-parser");
const jsonParser = bodyParser.json();
app.post('/transactions/new', jsonParser, (req, res) => {
  const newTransaction = req.body;
  testCoin.addNewTransaction(newTransaction);
  res.send(`The transaction ${JSON.stringify(newTransaction)} is successfully added to the blockchain.`);
});

接下來是路由 /mine。該接口實現的功能是收集目前未被打包的交易,將其打包到一個新的區塊中;添加獎勵交易(這裡設置為 50,接收地址為 uuid);進行符合難度要求的挖礦,返回新區塊信息。代碼實現如下:

app.get('/mine', (req, res) => {
  const latestBlockIndex = testCoin.chain.length;
  const newBlock = new Block(latestBlockIndex, new Date().toString());
  newBlock.transactions = testCoin.currentTransactions;
  // Get a reward for mining the new block
  newBlock.transactions.unshift({
    sender: '0',
    recipient: nodeIdentifier,
    amount: 50
  });
  testCoin.addBlock(newBlock);
  testCoin.currentTransactions = [];
res.send(`Mined new block ${JSON.stringify(newBlock, undefined, 2)}`);
});

至此,代碼基本完成,最後我們添加一個記錄日誌的中間件:

app.use((req, res, next) => {
  var now = new Date().toString();
  var log = `${now}: ${req.method} ${req.url}`;
  console.log(log);
  fs.appendFile('server.log', log + '\n', (err) => {
    if (err) console.error(err);
  });
  next();  
})
  res.send(`The transaction 
${JSON.stringify(newTransaction)} is successfully added to the blockchain.`);

測試 API

使用 Node Server.js 啟動應用,我們使用 Postman 來對當前的 API 進行測試。

在啟動應用後,當前區塊鏈應該只有一個創世區塊,我們使用 /chain 來獲取當前區塊鏈信息

可以看到,當前區塊鏈只有一個區塊。那怎麼添加新的交易呢?

參考資料:

·Implementing proof-of-work

https://www.savjee.be/2017/09/Implementing-proof-of-work-javascript-blockchain/

·Learn Blockchains by Building One

https://hackernoon.com/learn-blockchains-by-building-one-117428612f46

·Building Blockchain in Go

https://jeiwan.cc/posts/building-blockchain-in-go-part-2/

·Bitcoin whitepaper

https://bitcoin.org/bitcoin.pdf

智能合約#

通過前面兩章介紹我們初步認識了智能合約及以太坊的核心概念,從這一章開始將逐步介紹用 Solidity 編寫智能合約。本章將介紹一個合約通常包含哪些內容。我們將分兩個角度來討論,一是從 Solidity 合約文件結構的角度:二是從合約內容的角度。 Solidity 文件結構 Solidity 合約源文件使用的擴展名為 “sol”。從文件結構上看,一個合約文件通常包含以下幾個部分:合約版本聲明、引入其他源文件、定義一個合約及註釋等。 合約版本聲明 Solidity 的源文件需要進行版本聲明,告知編譯器此源文件所支持的編譯器版本,當出現不兼容的新的編譯器版本時,它會拒絕編譯舊的源文件。經常閱讀版本更新日誌是一個好習慣,尤其是當大版本發布時。

版本聲明方式如下: pragma solidity ^0.4.0: 這樣一個源文件不兼容 Solidity0.4.0 之前的版本和 Solidity0.5.0 之後的版本 (“” 符號用來控制版本號的第 2 部分)。通常版本號第 3 部分的升級僅僅是一些小變化(不會有任何兼容性問題),所以通常使用這種方式,而不是指定特定的版本。這樣當編譯器有 bug 要修復時,不需要更改代碼。 如果要使用更複雜的版本聲明,那麼其聲明表達式和 npm 要保持一致,可以參考:https:/docs.npmjs.com/misc/semver. 引入其他源文件 Solidity 支持 import 語句,類似於 JavaScript (ES6),但 Solidity 沒有 “缺省導出” 的概念。 全局引入,引入形式如下:

import "filename";

從 “filename” 引入所有的全局符號 (包括 filename 從其他文件引入的) 到當前的全局作用域。
自定義命名空間引入,引入形式如下:

import as symbolName from "filename";

創建一個全局的命名空間 symbolName,. 成員來自 filename 的全局符號。
有一種非 ES6 兼容的簡寫語法與其等價:

import {symbol1 as alias,symbol2}from "filename";

image

Solidity 數據類型#

Solidity 是一種靜態類型語言,這一章我們將深入介紹 Solidity 的數據類型。
主要內容包括:
・類型概述及分類
・布爾類型
・整型
・定長浮點型
定長字節數組
・有理數和整型常量
・字符串常量
十六進制常量
枚舉

函數類型

地址類型
・地址常量
數據位置
數組
結構體
映射
・類型轉換

類型推導 運算符 類型概述及分類 Solidity 是一種靜態類型語言,常見的靜態類型語言有 C、C++、Java 等,靜態類型意味著在編譯時需要為每個變量(本地或狀態變量)都指定類型(或至少可以推導出類型)。 Solidity 數據類型看起來使用很簡單,但卻是最容易產生漏洞的地方(如出現溢出等問題)。

有一點大家也需要關注,就是 Solidity 的類型非常在意所占空間的大小。另外,Solidity 的一些基本類型可以組合成複雜類型。 Solidity 類型分為兩類:值類型 (Value Type) 和引用類型 (Reference Type).

另外,不同類型還可以與不同的運算符組合,支持表達式運算,並按照表達式的執行順序 (Order of Evaluation of Expression) 來執行。 值類型 值類型所占空間在 32 個字節以內,值類型變量在賦值或傳參時總是進行值拷貝。

值類型包括:・布爾類型 (Boolean)・整型 (Integer)・定長浮點型 (Fixed Point Number) 定長字節數組 (Fixed-size Byte Array) 有理數和整型常量 (Rational and Integer Literal) 字符串常量 (String Literal) 十六進制常量 (Hexadecimal Literal) 枚舉 (Enum)・函數類型 (Function Type) 地址類型 (Addres) 地址常量 (Address Literal) 引用類型 引用類型主要包括:數組 (Array)、結構體 (Struct) 和映射 (Mapping)。

布爾類型 (Boolean) 布爾類型使用 bool 關鍵字聲明,聲明方式如下: bool isActive; boo1is0k=false;/ 帶默認值 布爾類型可能的取值為常量值 tnue 和 false.

布爾類型支持的運算符如下。・!, 邏輯非。・&&, 邏輯與。・, 邏輯或。・=, 等於。・=, 不等於。 注意:運算符 “&&” 和 “川” 是短路運算符,如 fx) 川 gy), 當 fx) 為真時, 則不會繼續執行 gy))&&gy), 當 fx) 為假時,則不會繼續執行 gy) 整型 (Integer) 和 Java 等語言用 short、.int、long 來表示整型有些不一樣,Solidity 的整型 用 it 加一個類型所占位數的數字一起表示,這種方式和 Go 語言一致。

整型的關鍵字有 int8、imt16 到 int256, 數字以 8 步進。

對應的無符號整型 有 uint8 到 uint256,uint 和 int 默認對應的是 uint256 和 int256. 聲明方式如下: 1nt8×=-1; 1nt16y=2; int32 z 整型支持的運算符如下。

・比較運算符:<=、<、=、I=、>=、>(返回布爾值 tue 或 false).

位運算符:&、、A(異或)、~(位取反)。

算術運算符:+、一、一元運算符 “-”、一元運算符 “+”、*、人、%(取 余數)、(幂)、<<(左移位)、>(右移位)。

说明:

・整數除法總是截斷的,但如果運算的是常量(常量稍後講),則不會截斷。・整數除 0 會拋異常,即 x0 為非法的。・右移位和除是等價的,如 x>y 和 x/2**y 是相等的。左移位和乘等價,x<y 和 x2y 是相等的。移位運算的結果的正負取決於運算符左邊的數。右移位一個負數,向下取整時會為 0.・不能進行負移位,即運算符右邊的數不可以為負數,否則會拋出運行時異常,如 3>-1 為非法的。 我門多手看下而一個別子

image

運算符簡寫

和 C、C++、Java 類似,對於一些運算符運算賦值支持以下簡寫。a+=e 等價於 a=a+e, 類似的操作符有:-=、*=、仁、%=、=、&=、=。

a + 和 a 一等價於 a+=1 和 a=1, 但表達式仍為 a 的值。而一 a 和 ++a 則返回更改之後的值。 整型溢出問題 在使用整型時,要特別注意整型的大小及所能容納的最大值和最小值,很 多合約就是因為溢出問題導致了漏洞,比如美鏈的漏洞,大家可以參考博客文 章 (https:/learnblockchain.cn/2018/04/25bec-overflow/) 了解。

下面列舉幾個關於溢出問題的例子。

image

避免溢出的一个方法是在運算之後對結果值進行一次檢查,比如對上面的
k 做一個檢查,如使用 assert (k>=i)。也推薦大家在進行加、減、乘、除運算時
使用 OpenZeppelin 的 SafeMath 庫,代碼的 GitHub 地址為 https:/github.coml
OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol.

定長浮點型 (Fixed Point Number)#

定長浮點型的功能和其他語言的浮點型 float 和 double 差不多,都是用來表 示浮點數的。但是定長浮點型又有不一樣的地方,它需要在聲明時指定類型所占的大小,以及小數點的位數,而傳統的浮點型 float 和 double 通常值會不一樣,即所占用的空間不一樣。 Solidity 還不完全支持定長浮點型,它可以用來聲明變量,但不可以用來賦值,因此當前還沒法使用定長浮點型。 定長浮點型的聲明方式如下:

ufixed32x1 f;
ufixed32x1 fi=0.1;//UnimplementedFeatureError:Not yet implemented

fixed/ufixed 表示有符號和無符號的固定位浮點數。關鍵字為 ufixedMxN 和 ufixedMxN, 其中 M 表示這種類型要占用的位數,以 8 步進,可以為 8~256 位:N 表示小數點的個數,可以為 0~80. fixed/ufixed 分別代表 fixed128x18 和 ufixed1:28xl8。

其支持的運算符如下。

・比較運算符:<=、<、=、!=、>=、>(返回布爾值 tnue 或 false).

算術運算符:+、一、一元運算符 “_”、一元運算符 “+”、*、人、%(取 余數)。 注意:它和大多數語言的 float 和 double 不一樣,這裡的 M 表示整個數占 用的固定位數,包含整數部分和小數部分。因此,當用一個小位數 (M 較小) 來表示一個浮點數時,小數部分幾乎會占用整個空間。

定長字節數組 (Fixed-size Byte Array)#

定長字節數組是指一個所占空間固定的數組,每個元素都是一個字節。由於變長數組不是值類型,因此我們會用單獨一節來介紹。
定長字節數組的聲明方式如下:

byte bte;
bytes1 bt1 0x01;
bytes2 bt2 "ab";
bytes3 bt3 "abc";

關鍵字有 bytes1,bytes?2,bytes3.,…,bytes32, 以步長 1 遞增,byte 代表 bytes1. 在實際使用中,定長字節數組經常被用來代替字符串(如聲明中的 b2 及 bt3). 定長字節數組支持的運算符如下。

  • ・比較運算符:<=、<、一、!=、>=、>(返回布爾值 tue 或 falsa).

  • ・位運算符:&、、(按位異或)、~(按位取反)、<<(左移位)、>> (右移位)。

  • 索引(下標)訪問:如果 x 是 bytesI, 當 O≤k<I 時,則 xk] 返回第 k 個字節(只讀)。 移位運算和整型類似,移位運算結果的正負取決於運算符左邊的數,且不 能進行負移位。例如可以 - 5<<1,不可以 5<<-1. 定長字節數組有成員變量:。length, 表示這個字節數組的長度(不能修改)。

  • 例如獲取 bt2 和 bt3 的長度:
    bt2.1 ength;/ 返回 2
    bt3.length;/ 返回 3
    有理數和整型常量 (Rational and Integer Literal)
    有理數和整型常量是表達式中直接出現的數字常量,不過也有人把它稱為 字面量。

  • 整型常量是由一系列 09 的數字組成的,以十進制數表示。比如:八進制數是不存在的,前置 0 在 Solidity 中是無效的。十進制小數常量 (Decimal Fraction Literal) 帶了一个 “。”,在 “。” 的兩邊至少有一個數字,有效的表示如 1、1 和 13 等。 它也支持科學符號,基數可以是小數,指數必須是整數,有效的表示如 210、 -2e10、2e-10、2.5e1 等。 數字常量表達式本身支持任意精度,也就是不會發生運算溢出或除法截斷。 但是當它被轉換成對應的非常量類型,或者將它與非常量進行運算時,就不能 保證精度了。比如 (2800+1)-2**800 的結果為 1 (uint8 整型,儘管中間結果己經超過計算機字長):58 的結果是 4(儘管有非整型數參與了運算):5/2+

  • 52 的結果是 5 (會轉為有理數,不會被截斷,不過在早期的版本中會被截斷)。只要操作數是整型,整型支持的運算符就適用於整型常量表達式。如果兩 個操作數都是小數,則不允許進行位運算,指數也不能是小數。 示例代碼如下

: pragma solidity ^0.4.18; 
         contract testType2{
        function interLiteral ()
        public returns (uint,uint){ return(2800+1)-2880,0.5*8);

       }
   }
  • 注意:Solidity 的每一個數字常量都有對應的數字常量類型,整型常量和有 理數常量屬於數字常量類型。所有的數字常量表達式的結果都是數字常量。
    在數字常量表達式中一旦含有非常量表達式,它就會被轉換為非常量類型。
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。