區塊鏈這么火的技術,大java怎能落后,所以有了本文,主要代碼參考自 Learn Blockchains by Building One , 中文翻譯:用Python從零開始創建區塊鏈 。
一、區塊鏈對象模型的基礎屬性(BlockChain)
區塊鏈的基本數據模型參考:最基本的區塊鏈hello world(python3實現) 。主要屬性如下:
@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 表示,本節點的區塊鏈已經被集群其它節點中最長的那個替換掉了。
最后,文中演示的所有代碼,已經托管在github上,地址:https://github.com/yjmyzz/springboot-blockchain-helloworld 歡迎大家Fork.
