使用Java從零開(kāi)始創(chuàng)建區(qū)塊鏈
目前網(wǎng)絡(luò)上關(guān)于區(qū)塊鏈入門(mén)、科普的文章不少砂代,本文就不再贅述區(qū)塊鏈的基本概念了,如果對(duì)區(qū)塊鏈不是很了解的話率挣,可以看一下我之前收集的一些入門(mén)學(xué)習(xí)資源:
對(duì)區(qū)塊鏈技術(shù)感到新奇的我們刻伊,都想知道區(qū)塊鏈在代碼上是怎么實(shí)現(xiàn)的,所以本文是實(shí)戰(zhàn)向的椒功,畢竟理論我們都看了不少捶箱,但是對(duì)于區(qū)塊鏈具體的實(shí)現(xiàn)還不是很清楚,本文就使用Java語(yǔ)言來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的區(qū)塊鏈动漾。
但是要完全搞懂區(qū)塊鏈并非易事丁屎,對(duì)于一門(mén)較為陌生的技術(shù),我們需要在理論+實(shí)踐中學(xué)習(xí)旱眯,通過(guò)寫(xiě)代碼來(lái)學(xué)習(xí)技術(shù)會(huì)掌握得更牢固晨川,構(gòu)建一個(gè)區(qū)塊鏈可以加深對(duì)區(qū)塊鏈的理解。
準(zhǔn)備工作
掌握基本的JavaSE以及JavaWeb開(kāi)發(fā)删豺,能夠使用Java開(kāi)發(fā)簡(jiǎn)單的項(xiàng)目共虑,并且需要了解HTTP協(xié)議。
我們知道區(qū)塊鏈?zhǔn)怯蓞^(qū)塊的記錄構(gòu)成的不可變呀页、有序的鏈結(jié)構(gòu)妈拌,記錄可以是交易、文件或任何你想要的數(shù)據(jù)赔桌,重要的是它們是通過(guò)哈希值(hashes)鏈接起來(lái)的供炎。
如果你還不是很了解哈希是什么,可以查看這篇文章
環(huán)境描述
- JDK1.8
- Tomcat 9.0
- Maven 3.5
- JSON 20160810
- javaee-api 7.0
pom.xml文件配置內(nèi)容:
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20160810</version>
</dependency>
</dependencies>
然后還需要一個(gè)HTTP客戶端疾党,比如Postman音诫,Linux命令行下的curl或其它客戶端,我這里使用的是Postman雪位。
Blockchain類(lèi)
首先創(chuàng)建一個(gè)Blockchain類(lèi)竭钝,在構(gòu)造器中創(chuàng)建了兩個(gè)主要的集合,一個(gè)用于儲(chǔ)存區(qū)塊鏈雹洗,一個(gè)用于儲(chǔ)存交易列表香罐,本文中所有核心的主要代碼都寫(xiě)在這個(gè)類(lèi)里,方便隨時(shí)查看时肿,在實(shí)際開(kāi)發(fā)則不宜這么做庇茫,應(yīng)該把代碼拆分仔細(xì)降低耦合度。
以下是Blockchain類(lèi)的框架代碼:
package org.zero01.dao;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class BlockChain {
// 存儲(chǔ)區(qū)塊鏈
private List<Object> chain;
// 該實(shí)例變量用于當(dāng)前的交易信息列表
private List<Object> currentTransactions;
public BlockChain() {
// 初始化區(qū)塊鏈以及當(dāng)前的交易信息列表
this.chain = new ArrayList<Object>();
this.currentTransactions= new ArrayList<Object>();
}
public List<Object> getChain() {
return chain;
}
public void setChain(List<Object> chain) {
this.chain = chain;
}
public List<Object> getCurrentTransactions() {
return currentTransactions;
}
public void setCurrentTransactions(List<Object> currentTransactions) {
this.currentTransactions = currentTransactions;
}
public Object lastBlock() {
return null;
}
public HashMap<String, Object> newBlock() {
return null;
}
public int newTransactions() {
return 0;
}
public static Object hash(HashMap<String, Object> block) {
return null;
}
}
Blockchain類(lèi)用來(lái)管理區(qū)塊鏈螃成,它能存儲(chǔ)交易旦签,加入新塊等查坪,下面我們來(lái)進(jìn)一步完善這些方法。
區(qū)塊的結(jié)構(gòu)
首先需要說(shuō)明一下區(qū)塊的結(jié)構(gòu)宁炫,每個(gè)區(qū)塊包含屬性:索引(index)偿曙,時(shí)間戳(timestamp),交易列表(transactions)羔巢,工作量證明(稍后解釋?zhuān)┮约扒耙粋€(gè)區(qū)塊的Hash值望忆。
以下是一個(gè)區(qū)塊的結(jié)構(gòu):
block = {
'index': 1,
'timestamp': 1506057125.900785,
'transactions': [
{
'sender': "8527147fe1f5426f9dd545de4b27ee00",
'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
'amount': 5,
}
],
'proof': 324984774000,
'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}
到這里,區(qū)塊鏈的概念就清楚了竿秆,每個(gè)新的區(qū)塊都包含上一個(gè)區(qū)塊的Hash启摄,這是關(guān)鍵的一點(diǎn),它保障了區(qū)塊鏈不可變性袍辞。如果攻擊者破壞了前面的某個(gè)區(qū)塊鞋仍,那么后面所有區(qū)塊的Hash都會(huì)變得不正確。不理解的話搅吁,慢慢消化威创,可以參考區(qū)塊鏈記賬原理。
由于需要計(jì)算區(qū)塊的hash谎懦,所以我們得先編寫(xiě)一個(gè)用于計(jì)算hash值的工具類(lèi):
package org.zero01.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Encrypt {
/**
* 傳入字符串肚豺,返回 SHA-256 加密字符串
*
* @param strText
* @return
*/
public String getSHA256(final String strText) {
return SHA(strText, "SHA-256");
}
/**
* 傳入字符串,返回 SHA-512 加密字符串
*
* @param strText
* @return
*/
public String getSHA512(final String strText) {
return SHA(strText, "SHA-512");
}
/**
* 傳入字符串界拦,返回 MD5 加密字符串
*
* @param strText
* @return
*/
public String getMD5(final String strText) {
return SHA(strText, "SHA-512");
}
/**
* 字符串 SHA 加密
*
* @param strSourceText
* @return
*/
private String SHA(final String strText, final String strType) {
// 返回值
String strResult = null;
// 是否是有效字符串
if (strText != null && strText.length() > 0) {
try {
// SHA 加密開(kāi)始
// 創(chuàng)建加密對(duì)象吸申,傳入加密類(lèi)型
MessageDigest messageDigest = MessageDigest.getInstance(strType);
// 傳入要加密的字符串
messageDigest.update(strText.getBytes());
// 得到 byte 數(shù)組
byte byteBuffer[] = messageDigest.digest();
// 將 byte 數(shù)組轉(zhuǎn)換 string 類(lèi)型
StringBuffer strHexString = new StringBuffer();
// 遍歷 byte 數(shù)組
for (int i = 0; i < byteBuffer.length; i++) {
// 轉(zhuǎn)換成16進(jìn)制并存儲(chǔ)在字符串中
String hex = Integer.toHexString(0xff & byteBuffer[i]);
if (hex.length() == 1) {
strHexString.append('0');
}
strHexString.append(hex);
}
// 得到返回結(jié)果
strResult = strHexString.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
return strResult;
}
}
加入交易功能
接下來(lái)我們需要實(shí)現(xiàn)一個(gè)交易\記賬功能,所以來(lái)完善newTransactions以及l(fā)astBlock方法:
/**
* @return 得到區(qū)塊鏈中的最后一個(gè)區(qū)塊
*/
public HashMap<String, Object> lastBlock() {
return getChain().get(getChain().size() - 1);
}
/**
* 生成新交易信息享甸,信息將加入到下一個(gè)待挖的區(qū)塊中
*
* @param sender
* 發(fā)送方的地址
* @param recipient
* 接收方的地址
* @param amount
* 交易數(shù)量
* @return 返回存儲(chǔ)該交易事務(wù)的塊的索引
*/
public int newTransactions(String sender, String recipient, long amount) {
Map<String, Object> transaction = new HashMap<String, Object>();
transaction.put("sender", sender);
transaction.put("recipient", recipient);
transaction.put("amount", amount);
getCurrentTransactions().add(transaction);
return (Integer) lastBlock().get("index") + 1;
}
newTransactions方法向列表中添加一個(gè)交易記錄截碴,并返回該記錄將被添加到的區(qū)塊 (下一個(gè)待挖掘的區(qū)塊)的索引,等下在用戶提交交易時(shí)會(huì)有用蛉威。
創(chuàng)建新塊
當(dāng)Blockchain實(shí)例化后日丹,我們需要構(gòu)造一個(gè)創(chuàng)世區(qū)塊(沒(méi)有前區(qū)塊的第一個(gè)區(qū)塊),并且給它加上一個(gè)工作量證明蚯嫌。
每個(gè)區(qū)塊都需要經(jīng)過(guò)工作量證明哲虾,俗稱挖礦,稍后會(huì)繼續(xù)講解择示。
為了構(gòu)造創(chuàng)世塊束凑,我們還需要完善剩下的幾個(gè)方法,并且把該類(lèi)設(shè)計(jì)為單例:
package org.zero01.dao;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.json.JSONObject;
import org.zero01.util.Encrypt;
public class BlockChain {
// 存儲(chǔ)區(qū)塊鏈
private List<Map<String, Object>> chain;
// 該實(shí)例變量用于當(dāng)前的交易信息列表
private List<Map<String, Object>> currentTransactions;
private static BlockChain blockChain = null;
private BlockChain() {
// 初始化區(qū)塊鏈以及當(dāng)前的交易信息列表
chain = new ArrayList<Map<String, Object>>();
currentTransactions = new ArrayList<Map<String, Object>>();
// 創(chuàng)建創(chuàng)世區(qū)塊
newBlock(100, "0");
}
// 創(chuàng)建單例對(duì)象
public static BlockChain getInstance() {
if (blockChain == null) {
synchronized (BlockChain.class) {
if (blockChain == null) {
blockChain = new BlockChain();
}
}
}
return blockChain;
}
public List<Map<String, Object>> getChain() {
return chain;
}
public void setChain(List<Map<String, Object>> chain) {
this.chain = chain;
}
public List<Map<String, Object>> getCurrentTransactions() {
return currentTransactions;
}
public void setCurrentTransactions(List<Map<String, Object>> currentTransactions) {
this.currentTransactions = currentTransactions;
}
/**
* @return 得到區(qū)塊鏈中的最后一個(gè)區(qū)塊
*/
public Map<String, Object> lastBlock() {
return getChain().get(getChain().size() - 1);
}
/**
* 在區(qū)塊鏈上新建一個(gè)區(qū)塊
*
* @param proof
* 新區(qū)塊的工作量證明
* @param previous_hash
* 上一個(gè)區(qū)塊的hash值
* @return 返回新建的區(qū)塊
*/
public Map<String, Object> newBlock(long proof, String previous_hash) {
Map<String, Object> block = new HashMap<String, Object>();
block.put("index", getChain().size() + 1);
block.put("timestamp", System.currentTimeMillis());
block.put("transactions", getCurrentTransactions());
block.put("proof", proof);
// 如果沒(méi)有傳遞上一個(gè)區(qū)塊的hash就計(jì)算出區(qū)塊鏈中最后一個(gè)區(qū)塊的hash
block.put("previous_hash", previous_hash != null ? previous_hash : hash(getChain().get(getChain().size() - 1)));
// 重置當(dāng)前的交易信息列表
setCurrentTransactions(new ArrayList<Map<String, Object>>());
getChain().add(block);
return block;
}
/**
* 生成新交易信息栅盲,信息將加入到下一個(gè)待挖的區(qū)塊中
*
* @param sender
* 發(fā)送方的地址
* @param recipient
* 接收方的地址
* @param amount
* 交易數(shù)量
* @return 返回該交易事務(wù)的塊的索引
*/
public int newTransactions(String sender, String recipient, long amount) {
Map<String, Object> transaction = new HashMap<String, Object>();
transaction.put("sender", sender);
transaction.put("recipient", recipient);
transaction.put("amount", amount);
getCurrentTransactions().add(transaction);
return (Integer) lastBlock().get("index") + 1;
}
/**
* 生成區(qū)塊的 SHA-256格式的 hash值
*
* @param block
* 區(qū)塊
* @return 返回該區(qū)塊的hash
*/
public static Object hash(Map<String, Object> block) {
return new Encrypt().getSHA256(new JSONObject(block).toString());
}
}
通過(guò)上面的代碼和注釋可以對(duì)區(qū)塊鏈有直觀的了解汪诉,接下來(lái)我們來(lái)編寫(xiě)一些簡(jiǎn)單的測(cè)試代碼來(lái)測(cè)試一下這些代碼能否正常工作:
package org.zero01.test;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONObject;
import org.zero01.dao.BlockChain;
public class Test {
public static void main(String[] args) throws Exception {
BlockChain blockChain = BlockChain.getInstance();
// 一個(gè)區(qū)塊中可以不包含任何交易記錄
Map<String, Object> block = blockChain.newBlock(300, null);
System.out.println(new JSONObject(block));
// 一個(gè)區(qū)塊中可以包含一筆交易記錄
blockChain.newTransactions("123", "222", 33);
Map<String, Object> block1 = blockChain.newBlock(500, null);
System.out.println(new JSONObject(block1));
// 一個(gè)區(qū)塊中可以包含多筆交易記錄
blockChain.newTransactions("321", "555", 133);
blockChain.newTransactions("000", "111", 10);
blockChain.newTransactions("789", "369", 65);
Map<String, Object> block2 = blockChain.newBlock(600, null);
System.out.println(new JSONObject(block2));
// 查看整個(gè)區(qū)塊鏈
Map<String, Object> chain = new HashMap<String, Object>();
chain.put("chain", blockChain.getChain());
chain.put("length", blockChain.getChain().size());
System.out.println(new JSONObject(chain));
}
}
運(yùn)行結(jié)果:
// 挖出來(lái)的新區(qū)塊
{
"index": 2,
"transactions": [],
"proof": 300,
"timestamp": 1519478559703,
"previous_hash": "185b62ca1fc31285bce8878acfc970983cb561f19c63b65120d2c95148cf151f"
}
// 包含一筆交易的區(qū)塊
{
"index": 3,
"transactions": [
{
"amount": 33,
"sender": "123",
"recipient": "222"
}
],
"proof": 500,
"timestamp": 1519478559728,
"previous_hash": "bce15693c0a028b1fc6d7d1c1d30494f97ef37b8b3384865559ceed9b5ff798b"
}
// 包含多筆交易的區(qū)塊
{
"index": 4,
"transactions": [
{
"amount": 133,
"sender": "321",
"recipient": "555"
},
{
"amount": 10,
"sender": "000",
"recipient": "111"
},
{
"amount": 65,
"sender": "789",
"recipient": "369"
}
],
"proof": 600,
"timestamp": 1519478656178,
"previous_hash": "b0edde645f76fc3a6cb45b7c91b07b686e8e214cfc1dea4823bf38bda37c909c"
}
// 整個(gè)區(qū)塊鏈,第一個(gè)是創(chuàng)始區(qū)塊
{
"chain": [
{
"index": 1,
"transactions": [],
"proof": 100,
"timestamp": 1519478656153,
"previous_hash": "0"
},
{
"index": 2,
"transactions": [],
"proof": 300,
"timestamp": 1519478656154,
"previous_hash": "7925a01fa8cb67b51ea89b9cfcfa16c5febee008bb559f94c5758418e7acc670"
},
{
"index": 3,
"transactions": [
{
"amount": 33,
"sender": "123",
"recipient": "222"
}
],
"proof": 500,
"timestamp": 1519478656178,
"previous_hash": "40ccc2f4ad97f75cb611ed69a4ecc7438eefd31afca17ca00c2ed7b5163d0831"
},
{
"index": 4,
"transactions": [
{
"amount": 133,
"sender": "321",
"recipient": "555"
},
{
"amount": 10,
"sender": "000",
"recipient": "111"
},
{
"amount": 65,
"sender": "789",
"recipient": "369"
}
],
"proof": 600,
"timestamp": 1519478656178,
"previous_hash": "b0edde645f76fc3a6cb45b7c91b07b686e8e214cfc1dea4823bf38bda37c909c"
}
],
"length": 4
}
通過(guò)以上的測(cè)試谈秫,可以很直觀的看到區(qū)塊鏈的數(shù)據(jù)摩瞎,但是現(xiàn)在只是完成了初步的代碼編寫(xiě)拴签,還有幾件事情還沒(méi)做孝常,接下來(lái)我們看看區(qū)塊是怎么挖出來(lái)的旗们。
理解工作量證明
新的區(qū)塊依賴工作量證明算法(PoW)來(lái)構(gòu)造。PoW的目標(biāo)是找出一個(gè)符合特定條件的數(shù)字构灸,這個(gè)數(shù)字很難計(jì)算出來(lái)上渴,但容易驗(yàn)證。這就是工作量證明的核心思想喜颁。
為了方便理解稠氮,舉個(gè)例子:
假設(shè)一個(gè)整數(shù) x 乘以另一個(gè)整數(shù) y 的積的 Hash 值必須以 0 結(jié)尾,即 hash(x * y) = ac23dc…0半开。設(shè)變量 x = 5隔披,求 y 的值?
用Java實(shí)現(xiàn)如下:
package org.zero01.test;
import org.zero01.util.Encrypt;
public class TestProof {
public static void main(String[] args) {
int x = 5;
int y = 0;
while (!new Encrypt().getSHA256((x * y) + "").endsWith("0")) {
y++;
}
System.out.println("y=" + y);
}
}
結(jié)果是 y=21 寂拆,因?yàn)椋?/p>
hash(5 * 21) = 1253e9373e...5e3600155e860
在比特幣中奢米,使用稱為Hashcash的工作量證明算法,它和上面的問(wèn)題很類(lèi)似纠永。礦工們?yōu)榱藸?zhēng)奪創(chuàng)建區(qū)塊的權(quán)利而爭(zhēng)相計(jì)算結(jié)果鬓长。通常,計(jì)算難度與目標(biāo)字符串需要滿足的特定字符的數(shù)量成正比尝江,礦工算出結(jié)果后涉波,會(huì)獲得比特幣獎(jiǎng)勵(lì)。
當(dāng)然炭序,在網(wǎng)絡(luò)上非常容易驗(yàn)證這個(gè)結(jié)果啤覆。
實(shí)現(xiàn)工作量證明
讓我們來(lái)實(shí)現(xiàn)一個(gè)相似PoW算法,規(guī)則是:尋找一個(gè)數(shù) p惭聂,使得它與前一個(gè)區(qū)塊的 proof 拼接成的字符串的 Hash 值以 4 個(gè)零開(kāi)頭:
...
/**
* 簡(jiǎn)單的工作量證明:
* - 查找一個(gè) p' 使得 hash(pp') 以4個(gè)0開(kāi)頭
* - p 是上一個(gè)塊的證明, p' 是當(dāng)前的證明
*
* @param last_proof
* 上一個(gè)塊的證明
* @return
*/
public long proofOfWork(long last_proof) {
long proof = 0;
while (!validProof(last_proof, proof)) {
proof += 1;
}
return proof;
}
/**
* 驗(yàn)證證明: 是否hash(last_proof, proof)以4個(gè)0開(kāi)頭?
*
* @param last_proof
* 上一個(gè)塊的證明
* @param proof
* 當(dāng)前的證明
* @return 以4個(gè)0開(kāi)頭返回true窗声,否則返回false
*/
public boolean validProof(long last_proof, long proof) {
String guess = last_proof + "" + proof;
String guess_hash = new Encrypt().getSHA256(guess);
return guess_hash.startsWith("0000");
}
衡量算法復(fù)雜度的辦法是修改零開(kāi)頭的個(gè)數(shù)。使用4個(gè)來(lái)用于演示彼妻,你會(huì)發(fā)現(xiàn)多一個(gè)零都會(huì)大大增加計(jì)算出結(jié)果所需的時(shí)間嫌佑。
現(xiàn)在Blockchain類(lèi)基本已經(jīng)完成了,接下來(lái)使用Servlet接收HTTP請(qǐng)求來(lái)進(jìn)行交互侨歉。
Blockchain作為API接口
我們將使用Java Web中的Servlet來(lái)接收用戶的HTTP請(qǐng)求屋摇,通過(guò)Servlet我們可以方便的將網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)映射到相應(yīng)的方法上進(jìn)行處理,現(xiàn)在我們來(lái)讓Blockchain運(yùn)行在基于Java Web上幽邓。
我們將創(chuàng)建三個(gè)接口:
- /transactions/new 創(chuàng)建一個(gè)交易并添加到區(qū)塊
- /mine 告訴服務(wù)器去挖掘新的區(qū)塊
- /chain 返回整個(gè)區(qū)塊鏈
注冊(cè)節(jié)點(diǎn)ID
我們的“Tomcat服務(wù)器”將扮演區(qū)塊鏈網(wǎng)絡(luò)中的一個(gè)節(jié)點(diǎn)炮温,而每個(gè)節(jié)點(diǎn)都需要有一個(gè)唯一的標(biāo)識(shí)符,也就是id牵舵。在這里我們使用UUID來(lái)作為節(jié)點(diǎn)ID柒啤,我們需要在服務(wù)器啟動(dòng)時(shí)倦挂,將UUID設(shè)置到ServletContext屬性中,這樣我們的服務(wù)器就擁有了唯一標(biāo)識(shí)担巩,這一步我們可以配置監(jiān)聽(tīng)類(lèi)來(lái)完成方援,首先配置web.xml文件內(nèi)容如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<listener>
<listener-class>org.zero01.servlet.InitialID</listener-class>
</listener>
</web-app>
然后編寫(xiě)一個(gè)類(lèi)實(shí)現(xiàn)ServletContextListener接口,在初始化方法中把uuid設(shè)置到ServletContext的屬性中:
package org.zero01.servlet;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class InitialID implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
ServletContext servletContext = sce.getServletContext();
String uuid = UUID.randomUUID().toString().replace("-", "");
servletContext.setAttribute("uuid", uuid);
}
public void contextDestroyed(ServletContextEvent sce) {
}
}
創(chuàng)建Servlet類(lèi)
我們這里沒(méi)有使用任何框架涛癌,所以我們需要通過(guò)最基本的Servlet來(lái)接收并處理用戶的HTTP請(qǐng)求:
package org.zero01.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// 該Servlet用于運(yùn)行工作算法的證明來(lái)獲得下一個(gè)證明犯戏,也就是所謂的挖礦
@WebServlet("/mine")
public class Mine extends HttpServlet{
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
package org.zero01.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// 該Servlet用于接收并處理新的交易信息
@WebServlet("/transactions/new")
public class NewTransaction extends HttpServlet{
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
package org.zero01.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// 該Servlet用于輸出整個(gè)區(qū)塊鏈的數(shù)據(jù)
@WebServlet("/chain")
public class FullChain extends HttpServlet{
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
我們先來(lái)完成最簡(jiǎn)單的FullChain的代碼,這個(gè)Servlet用于向客戶端輸出整個(gè)區(qū)塊鏈的數(shù)據(jù)(JSON格式):
package org.zero01.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import org.zero01.core.BlockChain;
// 該Servlet用于輸出整個(gè)區(qū)塊鏈的數(shù)據(jù)
@WebServlet("/chain")
public class FullChain extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BlockChain blockChain = BlockChain.getInstance();
Map<String, Object> response = new HashMap<String, Object>();
response.put("chain", blockChain.getChain());
response.put("length", blockChain.getChain().size());
JSONObject jsonResponse = new JSONObject(response);
resp.setContentType("application/json");
PrintWriter printWriter = resp.getWriter();
printWriter.println(jsonResponse);
printWriter.close();
}
}
發(fā)送交易
然后是記錄交易數(shù)據(jù)的功能拳话,每一個(gè)區(qū)塊都可以記錄交易數(shù)據(jù)先匪,發(fā)送到節(jié)點(diǎn)的交易數(shù)據(jù)結(jié)構(gòu)如下:
{
"sender": "my address",
"recipient": "someone else's address",
"amount": 5
}
實(shí)現(xiàn)代碼如下:
package org.zero01.servlet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import org.zero01.core.BlockChain;
// 該Servlet用于接收并處理新的交易信息
@WebServlet("/transactions/new")
public class NewTransaction extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
// 讀取客戶端傳遞過(guò)來(lái)的數(shù)據(jù)并轉(zhuǎn)換成JSON格式
BufferedReader reader = req.getReader();
String input = null;
StringBuffer requestBody = new StringBuffer();
while ((input = reader.readLine()) != null) {
requestBody.append(input);
}
JSONObject jsonValues = new JSONObject(requestBody.toString());
// 檢查所需要的字段是否位于POST的data中
String[] required = { "sender", "recipient", "amount" };
for (String string : required) {
if (!jsonValues.has(string)) {
// 如果沒(méi)有需要的字段就返回錯(cuò)誤信息
resp.sendError(400, "Missing values");
}
}
// 新建交易信息
BlockChain blockChain = BlockChain.getInstance();
int index = blockChain.newTransactions(jsonValues.getString("sender"), jsonValues.getString("recipient"),
jsonValues.getLong("amount"));
// 返回json格式的數(shù)據(jù)給客戶端
resp.setContentType("application/json");
PrintWriter printWriter = resp.getWriter();
printWriter.println(new JSONObject().append("message", "Transaction will be added to Block " + index));
printWriter.close();
}
}
挖礦
挖礦正是神奇所在,它很簡(jiǎn)單弃衍,只做了以下三件事:
- 計(jì)算工作量證明PoW
- 通過(guò)新增一個(gè)交易授予礦工(自己)一個(gè)幣
- 構(gòu)造新區(qū)塊并將其添加到鏈中
代碼實(shí)現(xiàn)如下:
package org.zero01.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import org.zero01.core.BlockChain;
//該Servlet用于運(yùn)行工作算法的證明來(lái)獲得下一個(gè)證明呀非,也就是所謂的挖礦
@WebServlet("/mine")
public class Mine extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BlockChain blockChain = BlockChain.getInstance();
Map<String, Object> lastBlock = blockChain.lastBlock();
long lastProof = Long.parseLong(lastBlock.get("proof") + "");
long proof = blockChain.proofOfWork(lastProof);
// 給工作量證明的節(jié)點(diǎn)提供獎(jiǎng)勵(lì),發(fā)送者為 "0" 表明是新挖出的幣
String uuid = (String) this.getServletContext().getAttribute("uuid");
blockChain.newTransactions("0", uuid, 1);
// 構(gòu)建新的區(qū)塊
Map<String, Object> newBlock = blockChain.newBlock(proof, null);
Map<String, Object> response = new HashMap<String, Object>();
response.put("message", "New Block Forged");
response.put("index", newBlock.get("index"));
response.put("transactions", newBlock.get("transactions"));
response.put("proof", newBlock.get("proof"));
response.put("previous_hash", newBlock.get("previous_hash"));
// 返回新區(qū)塊的數(shù)據(jù)給客戶端
resp.setContentType("application/json");
PrintWriter printWriter = resp.getWriter();
printWriter.println(new JSONObject(response));
printWriter.close();
}
}
注意交易的接收者是我們自己的服務(wù)器節(jié)點(diǎn)镜盯,我們做的大部分工作都只是圍繞Blockchain類(lèi)的方法進(jìn)行交互岸裙。到此,我們的區(qū)塊鏈就算完成了形耗,我們來(lái)實(shí)際運(yùn)行下哥桥。
運(yùn)行區(qū)塊鏈
由于我們這里也沒(méi)有寫(xiě)前端的web頁(yè)面,只寫(xiě)了后端的API激涤,所以只能使用 Postman 之類(lèi)的軟件去和API進(jìn)行交互拟糕。首先啟動(dòng)Tomcat服務(wù)器,然后通過(guò)post請(qǐng)求 http://localhost:8089/BlockChain_Java/transactions/new 來(lái)添加新的交易信息(注意我這里沒(méi)有使用默認(rèn)的8080端口倦踢,默認(rèn)的情況下是8080端口):
但是這時(shí)候還沒(méi)有新的區(qū)塊可以寫(xiě)入這個(gè)交易信息送滞,所以我們還需要請(qǐng)求 http://localhost:8089/BlockChain_Java/mine 來(lái)進(jìn)行挖礦,挖出一個(gè)新的區(qū)塊來(lái)存儲(chǔ)這筆交易:
在挖了兩次礦之后辱挥,就有3個(gè)塊了犁嗅,通過(guò)請(qǐng)求 http://localhost:8089/BlockChain_Java/chain 可以得到所有的區(qū)塊塊的信息:
{
"chain": [
{
"index": 1,
"proof": 100,
"transactions": [],
"timestamp": 1520928588165,
"previous_hash": "0"
},
{
"index": 2,
"proof": 35293,
"transactions": [
{
"amount": 6,
"sender": "d4ee26eee15148ee92c6cd394edd974e",
"recipient": "someone-other-address"
},
{
"amount": 1,
"sender": "0",
"recipient": "050bbfe4ad644d008545ff490387a889"
}
],
"timestamp": 1520928734580,
"previous_hash": "e5cf7ba38f7f0c3a93fcca5d57b624c8fd255093af4abe3c6999be61bdb81040"
},
{
"index": 3,
"proof": 35089,
"transactions": [
{
"amount": 1,
"sender": "0",
"recipient": "050bbfe4ad644d008545ff490387a889"
}
],
"timestamp": 1520928870963,
"previous_hash": "aa64ab003d15d50a43bd59deb88c939ea43349d00d0b653abd83b42e8fa4417c"
}
],
"length": 3
}
一致性(共識(shí))
我們已經(jīng)有了一個(gè)基本的區(qū)塊鏈可以接受交易和挖礦。但是區(qū)塊鏈系統(tǒng)應(yīng)該是分布式的晤碘。既然是分布式的褂微,那么我們究竟拿什么保證所有節(jié)點(diǎn)有同樣的鏈呢?這就是一致性問(wèn)題园爷,我們要想在網(wǎng)絡(luò)上有多個(gè)節(jié)點(diǎn)宠蚂,就必須實(shí)現(xiàn)一個(gè)一致性的算法。
注冊(cè)節(jié)點(diǎn)
在實(shí)現(xiàn)一致性算法之前童社,我們需要找到一種方式讓一個(gè)節(jié)點(diǎn)知道它相鄰的節(jié)點(diǎn)求厕。每個(gè)節(jié)點(diǎn)都需要保存一份包含網(wǎng)絡(luò)中其它節(jié)點(diǎn)的記錄。因此讓我們新增幾個(gè)接口:
- /nodes/register 接收URL形式的新節(jié)點(diǎn)列表
- /nodes/resolve執(zhí)行一致性算法,解決任何沖突呀癣,確保節(jié)點(diǎn)擁有正確的鏈
我們需要修改下BlockChain的構(gòu)造函數(shù)并提供一個(gè)注冊(cè)節(jié)點(diǎn)方法:
package org.zero01.core;
...
import java.net.URL;
...
private Set<String> nodes;
private BlockChain() {
...
// 用于存儲(chǔ)網(wǎng)絡(luò)中其他節(jié)點(diǎn)的集合
nodes = new HashSet<String>();
...
}
public Set<String> getNodes() {
return nodes;
}
/**
* 注冊(cè)節(jié)點(diǎn)
*
* @param address
* 節(jié)點(diǎn)地址
* @throws MalformedURLException
*/
public void registerNode(String address) throws MalformedURLException {
URL url = new URL(address);
String node = url.getHost() + ":" + (url.getPort() == -1 ? url.getDefaultPort() : url.getPort());
nodes.add(node);
}
...
我們用 HashSet 集合來(lái)儲(chǔ)存節(jié)點(diǎn)美浦,這是一種避免出現(xiàn)重復(fù)添加節(jié)點(diǎn)的簡(jiǎn)單方法。
實(shí)現(xiàn)共識(shí)算法
前面提到项栏,沖突是指不同的節(jié)點(diǎn)擁有不同的鏈浦辨,為了解決這個(gè)問(wèn)題,規(guī)定最長(zhǎng)的忘嫉、有效的鏈才是最終的鏈荤牍,換句話說(shuō),網(wǎng)絡(luò)中有效最長(zhǎng)鏈才是實(shí)際的鏈庆冕。
我們使用以下算法,來(lái)達(dá)到網(wǎng)絡(luò)中的共識(shí):
...
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
...
public class BlockChain {
...
/**
* 檢查是否是有效鏈劈榨,遍歷每個(gè)區(qū)塊驗(yàn)證hash和proof访递,來(lái)確定一個(gè)給定的區(qū)塊鏈?zhǔn)欠裼行? *
* @param chain
* @return
*/
public boolean validChain(List<Map<String, Object>> chain) {
Map<String, Object> lastBlock = chain.get(0);
int currentIndex = 1;
while (currentIndex < chain.size()) {
Map<String, Object> block = chain.get(currentIndex);
System.out.println(lastBlock.toString());
System.out.println(block.toString());
System.out.println("\n-------------------------\n");
// 檢查block的hash是否正確
if (!block.get("previous_hash").equals(hash(lastBlock))) {
return false;
}
lastBlock = block;
currentIndex++;
}
return true;
}
/**
* 共識(shí)算法解決沖突,使用網(wǎng)絡(luò)中最長(zhǎng)的鏈. 遍歷所有的鄰居節(jié)點(diǎn)同辣,并用上一個(gè)方法檢查鏈的有效性拷姿, 如果發(fā)現(xiàn)有效更長(zhǎng)鏈,就替換掉自己的鏈
*
* @return 如果鏈被取代返回true, 否則返回false
* @throws IOException
*/
public boolean resolveConflicts() throws IOException {
Set<String> neighbours = this.nodes;
List<Map<String, Object>> newChain = null;
// 尋找最長(zhǎng)的區(qū)塊鏈
long maxLength = this.chain.size();
// 獲取并驗(yàn)證網(wǎng)絡(luò)中的所有節(jié)點(diǎn)的區(qū)塊鏈
for (String node : neighbours) {
URL url = new URL("http://" + node + "/BlockChain_Java/chain");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
if (connection.getResponseCode() == 200) {
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(connection.getInputStream(), "utf-8"));
StringBuffer responseData = new StringBuffer();
String response = null;
while ((response = bufferedReader.readLine()) != null) {
responseData.append(response);
}
bufferedReader.close();
JSONObject jsonData = new JSONObject(bufferedReader.toString());
long length = jsonData.getLong("length");
List<Map<String, Object>> chain = (List) jsonData.getJSONArray("chain").toList();
// 檢查長(zhǎng)度是否長(zhǎng)旱函,鏈?zhǔn)欠裼行? if (length > maxLength && validChain(chain)) {
maxLength = length;
newChain = chain;
}
}
}
// 如果發(fā)現(xiàn)一個(gè)新的有效鏈比我們的長(zhǎng)响巢,就替換當(dāng)前的鏈
if (newChain != null) {
this.chain = newChain;
return true;
}
return false;
}
...
第一個(gè)方法 validChain() 用來(lái)檢查是否是有效鏈,遍歷每個(gè)塊驗(yàn)證hash和proof.
第2個(gè)方法 resolveConflicts() 用來(lái)解決沖突棒妨,遍歷所有的鄰居節(jié)點(diǎn)踪古,并用上一個(gè)方法檢查鏈的有效性, 如果發(fā)現(xiàn)有效更長(zhǎng)鏈券腔,就替換掉自己的鏈
讓我們添加兩個(gè)Servlet伏穆,一個(gè)用來(lái)注冊(cè)節(jié)點(diǎn),一個(gè)用來(lái)解決沖突:
注冊(cè)節(jié)點(diǎn):
package org.zero01.servlet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import org.zero01.core.BlockChain;
// 用于注冊(cè)節(jié)點(diǎn)的Servlet
@WebServlet("/nodes/register")
public class NodesRegister extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
// 讀取客戶端傳遞過(guò)來(lái)的數(shù)據(jù)并轉(zhuǎn)換成JSON格式
BufferedReader reader = req.getReader();
String input = null;
StringBuffer requestBody = new StringBuffer();
while ((input = reader.readLine()) != null) {
requestBody.append(input);
}
JSONObject jsonValues = new JSONObject(requestBody.toString());
// 獲得節(jié)點(diǎn)集合數(shù)據(jù)纷纫,并進(jìn)行判空
List<String> nodes = (List) jsonValues.getJSONArray("nodes").toList();
if (nodes == null) {
resp.sendError(400, "Error: Please supply a valid list of nodes");
}
// 注冊(cè)節(jié)點(diǎn)
BlockChain blockChain = BlockChain.getInstance();
for (String address : nodes) {
blockChain.registerNode(address);
}
// 向客戶端返回處理結(jié)果
Map<String, Object> response = new HashMap<String, Object>();
response.put("message", "New nodes have been added");
response.put("total_nodes", blockChain.getNodes().toArray());
resp.setContentType("application/json");
PrintWriter printWriter = resp.getWriter();
printWriter.println(new JSONObject(response));
printWriter.close();
}
}
解決沖突:
package org.zero01.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import org.zero01.core.BlockChain;
// 用于解決沖突
@WebServlet("/nodes/resolve")
public class NodesResolve extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BlockChain blockChain = BlockChain.getInstance();
boolean replaced = blockChain.resolveConflicts();
Map<String, Object> response = new HashMap<String, Object>();
if (replaced) {
response.put("message", "Our chain was replaced");
response.put("new_chain", blockChain.getChain());
} else {
response.put("message", "Our chain is authoritative");
response.put("chain", blockChain.getChain());
}
resp.setContentType("application/json");
PrintWriter printWriter = resp.getWriter();
printWriter.println(new JSONObject(response));
printWriter.close();
}
}
我們可以在不同的機(jī)器運(yùn)行節(jié)點(diǎn)枕扫,或在一臺(tái)機(jī)機(jī)開(kāi)啟不同的網(wǎng)絡(luò)端口來(lái)模擬多節(jié)點(diǎn)的網(wǎng)絡(luò),這里在同一臺(tái)機(jī)器開(kāi)啟不同的端口演示辱魁,配置兩個(gè)不同端口的服務(wù)器即可烟瞧,我這里啟動(dòng)了兩個(gè)節(jié)點(diǎn):http://localhost:8089 和 http://localhost:8066。
兩個(gè)節(jié)點(diǎn)互相進(jìn)行注冊(cè):
然后在8066節(jié)點(diǎn)上挖兩個(gè)塊染簇,確保是更長(zhǎng)的鏈:
接著在8089節(jié)點(diǎn)上訪問(wèn)接口/nodes/resolve 参滴,這時(shí)8089節(jié)點(diǎn)的鏈會(huì)通過(guò)共識(shí)算法被8066節(jié)點(diǎn)的鏈取代:
通過(guò)共識(shí)算法保持一致性后,兩個(gè)節(jié)點(diǎn)的區(qū)塊鏈數(shù)據(jù)就都是一致的了:
到此為止我們就完成了一個(gè)區(qū)塊鏈的開(kāi)發(fā)剖笙,雖然這只是一個(gè)最基本的區(qū)塊鏈卵洗,而且在開(kāi)發(fā)的過(guò)程中也沒(méi)有考慮太多的程序設(shè)計(jì)方面的問(wèn)題,而是以最基本、原始的方式進(jìn)行開(kāi)發(fā)的过蹂。但是我們不妨以這個(gè)簡(jiǎn)單的區(qū)塊鏈為基礎(chǔ)十绑,發(fā)揮自己的能力動(dòng)手去重構(gòu)、擴(kuò)展酷勺、完善這個(gè)區(qū)塊鏈程序本橙,直至成為自己的一個(gè)小項(xiàng)目。
本文項(xiàng)目代碼地址如下: