前一篇文章: 搭建你的第一個區塊鏈網路(一)
共識與本地化
POW共識
共識機制也是區塊鏈系統中不可缺少的一部分,在比特幣網路中,使用的是POW共識,概念相對比較簡單,所以我們在該專案中使用POW共識機制(後期如果可以的話修改為可插拔的共識機制)。
POW原理
POW原理是通過解決一個數學難題,其實就是通過計算一個雜湊值,如果計算出來的雜湊值的字首有足夠多個"0",就說明成功解決了該數學難題。通常雜湊值中"0"的個數越多難度越大。難度值是通過之前生成的區塊所消耗的時間動態調整的。而生成雜湊值的原資料實際上就是區塊資訊,另外再加一個nonce
屬性,用於調整難度值。
在比特幣中,平均每10分鐘產出一個區塊,如果新區塊的產出只消耗了9分鐘,那麼難度值將會增加。如果算力不發生變化的話,下一次產出區塊將會消耗更多的時間。同理,如果新區塊的產出消耗了11分鐘,那麼難度值則會相應地降低。動態調整難度值維持區塊產出時間平均為10分鐘。實際上比特幣中的POW更加複雜,難度值的調整是通過過去的2016個區塊產出的時間與20160分鐘進行比較的。
在這裡,不設定那麼麻煩,難度值不再動態調整,暫時將雜湊值中"0"的數量固定保證每次生成區塊的難度是相同的。同時也要設定一個最大難度值,防止無限迴圈計算。
#Pow.java
public class Pow {
//固定的難度值
private static final String DIFFICULT = "0000";
//最大難度值 防止計算難度值變為無限迴圈
private static final int MAX_VALUE = Integer.MAX_VALUE;
public static int calc(Block block){
//nonce從0開始
int nonce = 0;
//如果nonce小於最大難度值
while(nonce<MAX_VALUE){
//計算雜湊值
if(Util.getSHA256(block.toString()+nonce)
//如果計算出的雜湊值字首滿足條件,退出迴圈
.startsWith(DIFFICULT))
break;
//不滿足條件,nonce+1,重新計算雜湊值
nonce++;
}
return nonce;
}
}
更新屬性
一個簡單的POW共識完成了,接下來需要更新一下區塊的屬性,新增nonce
屬性:
#Block.java
//產出該區塊的難度
public int nonce;
還要修改生成區塊的方法,每次生成區塊時需要進行POW共識計算:
public Block CrtGenesisBlock(){
Block block = new Block(1,"Genesis Block","00000000000000000");
block.setNonce(
Pow.calc(block));
//計算區塊雜湊值
String hash = Util.getSHA256(block.getBlkNum()+block.getData()+block.getPrevBlockHash()+block.getPrevBlockHash()+block.getNonce());
...
}
public Block addBlock(String data){
...
Block block = new Block(
num+1,data, this.block.curBlockHash);
//每次將區塊新增進區塊鏈之前需要計算難度值
block.setNonce(
Pow.calc(block));
//計算區塊雜湊值
String hash = Util.getSHA256(block.getBlkNum()+block.getData()+block.getPrevBlockHash()+block.getPrevBlockHash()+block.getNonce());
...
}
測試POW共識
OK了,還是之前的測試方法,測試一下:
#Test.java
public class Test {
public static void main(String[] args){
System.out.println(Blockchain.getInstance().CrtGenesisBlock().toString());
System.out.println(Blockchain.getInstance().addBlock("Block 2").toString());
}
}
可以看到區塊號為2的區塊nonce
屬性有了具體的值,並且每次測試curBlockHash
的值字首都是以"0000"開頭的。
{"blkNum":1,"curBlockHash":"000002278a13f6caefda04c77d35e14128aafbc287578b86e1f2079c0e6747b1","data":"Genesis Block","nonce":37846,"prevBlockHash":"00000000000000000","timeStamp":"2020-05-17 10:49:48"}
{"blkNum":2,"curBlockHash":"00002654109d8eb6092da686d66e70cdb1e26cf4a87e453e3d8e2ff7508f11f9","data":"Block 2","nonce":15318,"prevBlockHash":"000002278a13f6caefda04c77d35e14128aafbc287578b86e1f2079c0e6747b1","timeStamp":"2020-05-17 10:49:48"}
本地化
此外,每次重新啟動程式都需要從創世區塊重新開始生成,所以需要將區塊資訊序列化到本地。保證每次啟動程式都可以從本地讀取資料不再重新生成創世區塊。
方便起見,暫時不使用資料庫儲存區塊資訊,只簡單序列化到本地檔案中來。
首先需要修改區塊的資訊,繼承Serializable
介面才能進行序列化。
#Block.java
public class Block implements Serializable{
private static final long serialVersionUID = 1L;
...
}
序列化與反序列化
接下來是序列化與反序列化的方法,在這裡我們將每一個區塊都儲存為一個名字為區塊號,字尾為.block
的檔案,同樣從本地反序列化到程式中也只需要通過區塊號來取。
#Storage.java
public final class Storage {
//序列化區塊資訊
public static void Serialize(Block block) throws IOException {
File file = new File("src/main/resources/blocks/"+block.getBlkNum()+".block");
if(!file.exists()) file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(block);
oos.close();
fos.close();
}
/**
* 反序列化區塊
*/
public static Block Deserialize(int num) throws FileNotFoundException, IOException, ClassNotFoundException {
File file = new File("src/main/resources/blocks/"+num+".block");
if(!file.exists()) return null;
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Block block = (Block)ois.readObject();
ois.close();
return block;
}
}
然後是區塊鏈的屬性,之前我們使用ArrayList
儲存區塊資訊,而現在我們直接將區塊序列化到本地,需要哪一個區塊直接到本地來取,因此不再需要ArrayList
儲存區塊資料。對於區塊鏈來講,僅僅需要記錄最新區塊資料即可。
public final class Blockchain {
...
//Arraylist<Block> block修改為 Block block;
public Block block;
...
public static Blockchain getInstance() {
if (BC == null) {
synchronized (Blockchain.class) {
if (BC == null) {
BC = new Blockchain();
//刪除建立ArrayList
}
}
}
return BC;
}
public Block CrtGenesisBlock() throws IOException {
...
block.setCurBlockHash(hash);
//序列化
Storage.Serialize(block);
this.block=block;
return this.block;
}
public Block addBlock(String data) throws IOException {
int num = this.block.getBlkNum();
...
block.setCurBlockHash(hash);
//序列化
Storage.Serialize(block);
this.block = block;
return this.block;
}
}
測試一下:
public class Test {
public static void main(String[] args) throws IOException {
System.out.println(Blockchain.getInstance().CrtGenesisBlock().toString());
System.out.println(Blockchain.getInstance().addBlock("Block 2").toString());
}
}
儲存是沒有問題的,在resources/blocks/
檔案下成功生成了1.block,2.block
兩個檔案。
反序列化
但是還沒有完成從本地取資料的操作,接下來的流程是這樣子的:
啟動程式後,首先例項化Blockchain
的例項,然後從本地讀取資料,如果本地存在區塊資料,直接反序列化區塊號最大的區塊,如果本地沒有資料,則進行創始區塊的建立。
#Blockchain.java
public Block getLastBlock() throws FileNotFoundException, ClassNotFoundException, IOException {
File file = new File("src/main/resources/blocks");
String[] files = file.list();
if(files.length!=0){
int MaxFileNum = 1;
//遍歷儲存區塊資料的資料夾,查詢區塊號最大的區塊
for(String s:files){
int num = Integer.valueOf(s.substring(0, 1));
if(num>=MaxFileNum)
MaxFileNum = num;
}
//反序列化最大區塊號的區塊
return Storage.Deserialize(MaxFileNum);
}
return null;
}
然後是Blockchain
的例項方法,在獲取例項時候判斷是否需要建立創世區塊:
#Blockchain.java
public static Blockchain getInstance() throws FileNotFoundException, ClassNotFoundException, IOException {
if (BC == null) {
synchronized (Blockchain.class) {
if (BC == null) {
BC = new Blockchain();
}
}
}
//獲取到Blockchain例項後,判斷是否存在區塊
if(BC.block==null){
//如果不存在則嘗試獲取本地區塊號最大的區塊
//如果存在則直接賦值到Blockchain的屬性然後返回
Block block = BC.getLastBlock();
BC.block = block;
if(block==null){
//如果不存在則生成創世區塊
BC.CrtGenesisBlock();
}
}
return BC;
}
//因此建立創世區塊的方法可以修改為私有的
private Block CrtGenesisBlock() throws IOException {
...
}
接下來可以測試了:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println(Blockchain.getInstance().block.toString());
System.out.println(Blockchain.getInstance().addBlock("Block 2").toString());
}
}
測試多次可以發現區塊並沒有重新從創世區塊開始生成,而是根據先前生成的區塊號繼續增長。
{"blkNum":1,"curBlockHash":"000002278a13f6caefda04c77d35e14128aafbc287578b86e1f2079c0e6747b1","data":"Genesis Block","nonce":37846,"prevBlockHash":"00000000000000000","timeStamp":"2020-05-17 11:51:37"}
{"blkNum":2,"curBlockHash":"00002654109d8eb6092da686d66e70cdb1e26cf4a87e453e3d8e2ff7508f11f9","data":"Block 2","nonce":15318,"prevBlockHash":"000002278a13f6caefda04c77d35e14128aafbc287578b86e1f2079c0e6747b1","timeStamp":"2020-05-17 11:51:37"}
Current Last Block num is:2
{"blkNum":2,"curBlockHash":"00002654109d8eb6092da686d66e70cdb1e26cf4a87e453e3d8e2ff7508f11f9","data":"Block 2","nonce":15318,"prevBlockHash":"000002278a13f6caefda04c77d35e14128aafbc287578b86e1f2079c0e6747b1","timeStamp":"2020-05-17 11:51:37"}
{"blkNum":3,"curBlockHash":"0000d350c1199eb51c2d43194653f5b44444665e40373d5883edd3567c60cd68","data":"Block 2","nonce":23695,"prevBlockHash":"00002654109d8eb6092da686d66e70cdb1e26cf4a87e453e3d8e2ff7508f11f9","timeStamp":"2020-05-17 11:51:44"}
大致工作已完成,接下來新增幾個額外的方法:
#Block.java
/**
* 是否存在前一個區塊
*/
public boolean hasPrevBlock(){
if(this.getBlkNum()!=1){
return true;
}
return false;
}
@Transient
@JsonIgnore
/**
* 獲取前一個區塊
*/
public Block getPrevBlock() throws FileNotFoundException, ClassNotFoundException, IOException {
if(this.hasPrevBlock())
return Storage.Deserialize(this.getBlkNum()-1);
return null;
}
後一篇文章: 搭建你的第一個區塊鏈網路(三)
Github倉庫地址在這裡,隨時保持更新中.....
Github地址:Jchain