用spring boot 2從零開始建立區塊鏈
一、區塊鏈物件模型的基礎屬性(BlockChain)
@ApiModelProperty(value = "當前交易列表", dataType = "List<Transaction>") @JSONField(serialize = false) @JsonIgnore private List<Transaction> currentTransactions; @ApiModelProperty(value = "所有交易列表", dataType = "List<Transaction>") private List<Transaction> transactions; @ApiModelProperty(value = "區塊列表", dataType = "List<BlockChain>") @JSONField(serialize = false) @JsonIgnore private List<BlockChain> chain; @ApiModelProperty(value = "叢集的節點列表", dataType = "Set<String>") @JSONField(serialize = false) @JsonIgnore private Set<String> nodes; @ApiModelProperty(value = "上一個區塊的雜湊值", dataType = "String", example = "f461ac428043f328309da7cac33803206cea9912f0d4e8d8cf2786d21e5ff403") private String previousHash = ""; @ApiModelProperty(value = "工作量證明", dataType = "Integer", example = "100") private Integer proof = 0; @ApiModelProperty(value = "當前區塊的索引序號", dataType = "Long", example = "2") private Long index = 0L; @ApiModelProperty(value = "當前區塊的時間戳", dataType = "Long", example = "1526458171000") private Long timestamp = 0L; @ApiModelProperty(value = "當前區塊的雜湊值", dataType = "String", example = "g451ac428043f328309da7cac33803206cea9912f0d4e8d8cf2786d21e5ff401") private String hash;
注:上面有些註解來自swagger,主要為了方便生成線上文件以及直接除錯rest介面。相對之前最基本的區塊鏈hello world(python3實現)一文,每個區塊中的data,在這裡細分為transactions、currentTransactions。另外區塊“鏈”本質上可以理解為連結串列,所以得有一個List<?> chain;此外這裡引入了所謂“工作量證明”,用於驗證每個區域的hash值不是隨便來的,而是要達到一定規則的運算量才能獲取,可以理解為控制挖礦速度的難度係數。
二、BlockChain的常規操作
2.1 生成新塊newBlock
public BlockChain newBlock(Integer proof, String previousHash) { BlockChain block = new BlockChain(); block.index = chain.size() + 1L; block.timestamp = System.currentTimeMillis(); block.transactions.addAll(currentTransactions); block.proof = proof; block.previousHash = previousHash; currentTransactions.clear(); chain.add(block); return block; }
2.2 生成第1個"創世"塊
連結串列總歸要有一個Head節點,區塊鏈也不例外
public void newSeedBlock() { newBlock(100, "1"); }
約定previousHash=1的,即為所謂的"創世"塊
2.3 生成hash值
public String getHash() { String json = jsonUtil.toJson(this.getCurrentTransactions()) + jsonUtil.toJson(this.getTransactions()) + jsonUtil.toJson(this.getChain()) + this.getPreviousHash() + this.getProof() + this.getIndex() + this.getTimestamp(); hash = SHAUtils.getSHA256Str(json); return hash; }
這裡把區塊的主要屬性:交易資料、連結串列中所有元素、工作量證明、區塊索引號、時間戳 拼在一起,然後計算sha256。總之,這些主要屬性中的任何一個屬性發生變化,整個hash值就變了。
2.4 工作量證明
相信對區塊鏈有了解的同學,都知道“挖礦”。為了控制挖礦的難度,得有一個規則來約束下,所以就有了這個工作量證明,這裡我們模擬一個簡單的策略:
public Boolean validProof(Integer lastProof, Integer proof) { System.out.println("validProof==>lastProof:" + lastProof + ",proof:" + proof); String guessHash = SHAUtils.getSHA256Str(String.format("{%d}{%d}", lastProof, proof)); return guessHash.startsWith("00"); }
把上一塊的proof值與本區塊的proof在一起,算sha256值,如果正好前2位是00,表示證明通過。(注:0的個數越多,挖礦難度越大,有興趣的同學可以自己調整試下)
2.5 區塊鏈驗證資料是否正確
為了防止區塊鏈的節點中混入非法髒資料(或被篡改),需要一個檢測資料完整性的方法
public boolean validChain(List<BlockChain> chain) { if (CollectionUtils.isEmpty(chain)) { return false; } BlockChain previousBlock = chain.get(0); int currentIndex = 1; while (currentIndex < chain.size()) { BlockChain block = chain.get(currentIndex); if (!block.getPreviousHash().equals(previousBlock.getHash())) { return false; } if (!validProof(previousBlock.getProof(), block.getProof())) { return false; } previousBlock = block; currentIndex += 1; } return true; }
規則很簡單:
a)每個區塊的previousHash值,必須等於前一個塊的hash值
b) 驗證每個塊上的proof值是否有效
2.6 叢集中的分叉校驗
區塊鏈是一個去中心化的分散式體系,每個節點都能挖礦,挖出來的“新區塊”都能加入鏈中,如果出現節點之間的區塊鏈資料不一致,需要一個策略來做仲裁,可以定一個簡單的規則:鏈最長的節點認定為有效的,其它節點都以此為準。
為了模擬這種情況,在BlockChain類的屬性中,特地留了一個nodes節點列表,用於登記叢集中的其它節點資訊。
public void registerNode(String address) { nodes.add(address); }
上面的方法,將把其它節點的例項(類似http://localhost:8081/),登記到節點列表中。知道了叢集中所有其它節點,就可以一一檢查誰的鏈條最長,程式碼如下:
public boolean resolveConflicts() { int maxLength = getChain().size(); List<BlockChain> newChain = new ArrayList<>(); for (String node : getNodes()) { RestTemplate template = new RestTemplate(); Map map = template.getForObject(node + "chain", Map.class); int length = MapUtils.getInteger(map, "length"); String json = jsonUtil.toJson(MapUtils.getObject(map, "chain")); List<BlockChain> chain = jsonUtil.fromJson(json, new TypeReference<List<BlockChain>>() { }); if (length > maxLength && validChain(chain)) { maxLength = length; newChain = chain; } } if (!CollectionUtils.isEmpty(newChain)) { this.chain = newChain; return true; } return false; }
大意是遍歷整個節點,逐一請求其它節點的rest介面,獲取其完整的連結串列,然後跟自己對比,如果比自己長的,就把自己給換掉。這樣輪一圈後,自身的連結串列,就被替換為整個叢集中最長的那個。
三、除錯執行
為了方便除錯,本文引入了swagger(不熟悉的同學可以參考spring cloud 學習(10) - 利用springfox整合swagger一文),然後加一堆rest api,跑起來,就可以直接測了:
3.1 呼叫/chain檢視下初始值:
{ "chain": [ { "transactions": [], "previousHash": "1", "proof": 100, "index": 1, "timestamp": 1527427873298, "hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479" } ], "length": 1 }
可以看到就只有一個“創世”塊,其previousHash為特定值1
3.2 呼叫/mine挖一塊礦
{ "previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479", "index": 2, "proof": 172, "message": "New Block Forged", "transactions": [ { "sender": "0", "recepient": "50130c5283e640779b4e5e7a5afd2e6b", "amount": 1 } ] }
挖到礦(即:產生一個新的區塊block),系統自動獎勵本節點1個幣(從transaction可以看出這一點),同時這筆獎勵的交易被寫入新塊中。這時再來看下/chain
{ "chain": [ { "transactions": [], "previousHash": "1", "proof": 100, "index": 1, "timestamp": 1527427873298, "hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479" }, { "transactions": [ { "sender": "0", "recepient": "50130c5283e640779b4e5e7a5afd2e6b", "amount": 1 } ], "previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479", "proof": 172, "index": 2, "timestamp": 1527427956435, "hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93" } ], "length": 2 }
可以看到,有二個區塊加入"連結串列"中了,可以繼續再挖一塊,最終/chain可能長成這樣:
{ "chain": [ { "transactions": [], "previousHash": "1", "proof": 100, "index": 1, "timestamp": 1527427873298, "hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479" }, { "transactions": [ { "sender": "0", "recepient": "50130c5283e640779b4e5e7a5afd2e6b", "amount": 1 } ], "previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479", "proof": 172, "index": 2, "timestamp": 1527427956435, "hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93" }, { "transactions": [ { "sender": "0", "recepient": "50130c5283e640779b4e5e7a5afd2e6b", "amount": 1 } ], "previousHash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93", "proof": 153, "index": 3, "timestamp": 1527428128077, "hash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1" } ], "length": 3 }
3.3 呼叫/transactions/new 發起一筆新交易
引數如下:
{ "amount": 1.0, "recepient": "block-on-other-node", "sender": "50130c5283e640779b4e5e7a5afd2e6b" }
注:sender一般取為當前礦機的標識,即本節點的nodeId,接收方一般指其它節點(這裡我們隨便輸入點內容,當作演示),然後交易的金額為“1”個幣,成功後,將返回
{ "message": "Transaction will be added to Block 4" }
但這時,如果呼叫/chain檢視整個鏈的資料,會發現沒有變化,因為這筆交易資料,只是放在本區塊的currentTransactions列表中(注:該屬性並未json序列化輸出,忘記的同學,可以拉到本文最開頭,複習下幾個重要的屬性)。只有下一個可用區塊產生時,這筆交易才會寫入新的區塊中,so,我們再繼續挖一塊新礦,呼叫/mine,然後再檢視/chain
{ "chain": [ { "transactions": [], "previousHash": "1", "proof": 100, "index": 1, "timestamp": 1527427873298, "hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479" }, { "transactions": [ { "sender": "0", "recepient": "50130c5283e640779b4e5e7a5afd2e6b", "amount": 1 } ], "previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479", "proof": 172, "index": 2, "timestamp": 1527427956435, "hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93" }, { "transactions": [ { "sender": "0", "recepient": "50130c5283e640779b4e5e7a5afd2e6b", "amount": 1 } ], "previousHash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93", "proof": 153, "index": 3, "timestamp": 1527428128077, "hash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1" }, { "transactions": [ { "sender": "50130c5283e640779b4e5e7a5afd2e6b", "recepient": "block-on-other-node", "amount": 1 }, { "sender": "0", "recepient": "50130c5283e640779b4e5e7a5afd2e6b", "amount": 1 } ], "previousHash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1", "proof": 86, "index": 4, "timestamp": 1527428477991, "hash": "4b3c261d2f878cebbdc1ade1a09809bb647abbd990353e529f630220a53d60ed" } ], "length": 4 }
剛才的交易,已經被寫入最後一個剛挖出的Block中。
3.4 模擬多節點資料不一致,使用/resolve仲裁解決
a) 再啟動一個新埠的執行例項
方法一:參考下圖,idea中設定執行時的環境變數,填上server.port=8081,就可以在另一個埠上啟動
方法二:在build.gradle里加一個task
task 8081 << { bootRun.systemProperty 'server.port', '8081' }
然後就可以命令列下,直接gradle 8081 bootRun
方法三:java -jar xxx.jar --name="Spring" --server.port=8081 直接在執行jar的時候指定埠
b) 呼叫/register 將新節點例項(即:8081埠的節點),註冊到8080的節點上
引數如下:
{ "nodes": [ "http://localhost:8081/" ] }
反過來,把8080老節點也註冊到新節點上(即:相當於兩兩相互註冊)。註冊成功後,這時呼叫8081新節點上的/chain ,因為這是個新節點,裡面只有一個創世塊,顯然跟8080老節點上的資料不一致
c) 新8081節點上呼叫/resolve
輸出如下:
{ "newChain": [ { "transactions": [], "previousHash": "1", "proof": 100, "index": 1, "timestamp": 1527427873298, "hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479" }, { "transactions": [ { "sender": "0", "recepient": "50130c5283e640779b4e5e7a5afd2e6b", "amount": 1 } ], "previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479", "proof": 172, "index": 2, "timestamp": 1527427956435, "hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93" }, { "transactions": [ { "sender": "0", "recepient": "50130c5283e640779b4e5e7a5afd2e6b", "amount": 1 } ], "previousHash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93", "proof": 153, "index": 3, "timestamp": 1527428128077, "hash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1" }, { "transactions": [ { "sender": "50130c5283e640779b4e5e7a5afd2e6b", "recepient": "block-on-other-node", "amount": 1 }, { "sender": "0", "recepient": "50130c5283e640779b4e5e7a5afd2e6b", "amount": 1 } ], "previousHash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1", "proof": 86, "index": 4, "timestamp": 1527428477991, "hash": "4b3c261d2f878cebbdc1ade1a09809bb647abbd990353e529f630220a53d60ed" } ], "message": "Our chain was replaced" }
最後一行的message: Our chain was replaced 表示,本節點的區塊鏈已經被叢集其它節點中最長的那個替換掉了。