使用Java从零开始创建区块链
目前网络上关于区块链入门、科普的文章不少,本文就不再赘述区块链的基本概念了,如果对区块链不是很了解的话,可以看一下我之前收集的一些入门学习资源:
对区块链技术感到新奇的我们,都想知道区块链在代码上是怎么实现的,所以本文是实战向的,毕竟理论我们都看了不少,但是对于区块链具体的实现还不是很清楚,本文就使用Java语言来实现一个简单的区块链。
但是要完全搞懂区块链并非易事,对于一门较为陌生的技术,我们需要在理论+实践中学习,通过写代码来学习技术会掌握得更牢固,构建一个区块链可以加深对区块链的理解。
准备工作
掌握基本的JavaSE以及JavaWeb开发,能够使用Java开发简单的项目,并且需要了解HTTP协议。
我们知道区块链是由区块的记录构成的不可变、有序的链结构,记录可以是交易、文件或任何你想要的数据,重要的是它们是通过哈希值(hashes)链接起来的。
如果你还不是很了解哈希是什么,可以查看这篇文章
环境描述
- JDK1.8
- Tomcat 9.0
- Maven 3.5
- JSON 20160810
- javaee-api 7.0
pom.xml文件配置内容:
<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>
然后还需要一个HTTP客户端,比如Postman,Linux命令行下的curl或其它客户端,我这里使用的是Postman。
Blockchain类
首先创建一个Blockchain类,在构造器中创建了两个主要的集合,一个用于储存区块链,一个用于储存交易列表,本文中所有核心的主要代码都写在这个类里,方便随时查看,在实际开发则不宜这么做,应该把代码拆分仔细降低耦合度。
以下是Blockchain类的框架代码:
package org.zero01.dao;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class BlockChain {
// 存储区块链
private List<Object> chain;
// 该实例变量用于当前的交易信息列表
private List<Object> currentTransactions;
public BlockChain() {
// 初始化区块链以及当前的交易信息列表
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类用来管理区块链,它能存储交易,加入新块等,下面我们来进一步完善这些方法。
区块的结构
首先需要说明一下区块的结构,每个区块包含属性:索引(index),时间戳(timestamp),交易列表(transactions),工作量证明(稍后解释)以及前一个区块的Hash值。
以下是一个区块的结构:
block = {
'index': 1,
'timestamp': 1506057125.900785,
'transactions': [
{
'sender': "8527147fe1f5426f9dd545de4b27ee00",
'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
'amount': 5,
}
],
'proof': 324984774000,
'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}
到这里,区块链的概念就清楚了,每个新的区块都包含上一个区块的Hash,这是关键的一点,它保障了区块链不可变性。如果攻击者破坏了前面的某个区块,那么后面所有区块的Hash都会变得不正确。不理解的话,慢慢消化,可以参考区块链记账原理。
由于需要计算区块的hash,所以我们得先编写一个用于计算hash值的工具类:
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 加密开始
// 创建加密对象,传入加密类型
MessageDigest messageDigest = MessageDigest.getInstance(strType);
// 传入要加密的字符串
messageDigest.update(strText.getBytes());
// 得到 byte 数组
byte byteBuffer[] = messageDigest.digest();
// 將 byte 数组转换 string 类型
StringBuffer strHexString = new StringBuffer();
// 遍历 byte 数组
for (int i = 0; i < byteBuffer.length; i++) {
// 转换成16进制并存储在字符串中
String hex = Integer.toHexString(0xff & byteBuffer[i]);
if (hex.length() == 1) {
strHexString.append('0');
}
strHexString.append(hex);
}
// 得到返回結果
strResult = strHexString.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
return strResult;
}
}
加入交易功能
接下来我们需要实现一个交易\记账功能,所以来完善newTransactions以及lastBlock方法:
/**
* @return 得到区块链中的最后一个区块
*/
public HashMap<String, Object> lastBlock() {
return getChain().get(getChain().size() - 1);
}
/**
* 生成新交易信息,信息将加入到下一个待挖的区块中
*
* @param sender
* 发送方的地址
* @param recipient
* 接收方的地址
* @param amount
* 交易数量
* @return 返回存储该交易事务的块的索引
*/
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方法向列表中添加一个交易记录,并返回该记录将被添加到的区块 (下一个待挖掘的区块)的索引,等下在用户提交交易时会有用。
创建新块
当Blockchain实例化后,我们需要构造一个创世区块(没有前区块的第一个区块),并且给它加上一个工作量证明。
每个区块都需要经过工作量证明,俗称挖矿,稍后会继续讲解。
为了构造创世块,我们还需要完善剩下的几个方法,并且把该类设计为单例:
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 {
// 存储区块链
private List<Map<String, Object>> chain;
// 该实例变量用于当前的交易信息列表
private List<Map<String, Object>> currentTransactions;
private static BlockChain blockChain = null;
private BlockChain() {
// 初始化区块链以及当前的交易信息列表
chain = new ArrayList<Map<String, Object>>();
currentTransactions = new ArrayList<Map<String, Object>>();
// 创建创世区块
newBlock(100, "0");
}
// 创建单例对象
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 得到区块链中的最后一个区块
*/
public Map<String, Object> lastBlock() {
return getChain().get(getChain().size() - 1);
}
/**
* 在区块链上新建一个区块
*
* @param proof
* 新区块的工作量证明
* @param previous_hash
* 上一个区块的hash值
* @return 返回新建的区块
*/
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);
// 如果没有传递上一个区块的hash就计算出区块链中最后一个区块的hash
block.put("previous_hash", previous_hash != null ? previous_hash : hash(getChain().get(getChain().size() - 1)));
// 重置当前的交易信息列表
setCurrentTransactions(new ArrayList<Map<String, Object>>());
getChain().add(block);
return block;
}
/**
* 生成新交易信息,信息将加入到下一个待挖的区块中
*
* @param sender
* 发送方的地址
* @param recipient
* 接收方的地址
* @param amount
* 交易数量
* @return 返回该交易事务的块的索引
*/
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;
}
/**
* 生成区块的 SHA-256格式的 hash值
*
* @param block
* 区块
* @return 返回该区块的hash
*/
public static Object hash(Map<String, Object> block) {
return new Encrypt().getSHA256(new JSONObject(block).toString());
}
}
通过上面的代码和注释可以对区块链有直观的了解,接下来我们来编写一些简单的测试代码来测试一下这些代码能否正常工作:
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();
// 一个区块中可以不包含任何交易记录
Map<String, Object> block = blockChain.newBlock(300, null);
System.out.println(new JSONObject(block));
// 一个区块中可以包含一笔交易记录
blockChain.newTransactions("123", "222", 33);
Map<String, Object> block1 = blockChain.newBlock(500, null);
System.out.println(new JSONObject(block1));
// 一个区块中可以包含多笔交易记录
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));
// 查看整个区块链
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));
}
}
运行结果:
// 挖出来的新区块
{
"index": 2,
"transactions": [],
"proof": 300,
"timestamp": 1519478559703,
"previous_hash": "185b62ca1fc31285bce8878acfc970983cb561f19c63b65120d2c95148cf151f"
}
// 包含一笔交易的区块
{
"index": 3,
"transactions": [
{
"amount": 33,
"sender": "123",
"recipient": "222"
}
],
"proof": 500,
"timestamp": 1519478559728,
"previous_hash": "bce15693c0a028b1fc6d7d1c1d30494f97ef37b8b3384865559ceed9b5ff798b"
}
// 包含多笔交易的区块
{
"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"
}
// 整个区块链,第一个是创始区块
{
"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
}
通过以上的测试,可以很直观的看到区块链的数据,但是现在只是完成了初步的代码编写,还有几件事情还没做,接下来我们看看区块是怎么挖出来的。
理解工作量证明
新的区块依赖工作量证明算法(PoW)来构造。PoW的目标是找出一个符合特定条件的数字,这个数字很难计算出来,但容易验证。这就是工作量证明的核心思想。
为了方便理解,举个例子:
假设一个整数 x 乘以另一个整数 y 的积的 Hash 值必须以 0 结尾,即 hash(x * y) = ac23dc…0。设变量 x = 5,求 y 的值?
用Java实现如下:
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);
}
}
结果是 y=21 ,因为:
hash(5 * 21) = 1253e9373e...5e3600155e860
在比特币中,使用称为Hashcash的工作量证明算法,它和上面的问题很类似。矿工们为了争夺创建区块的权利而争相计算结果。通常,计算难度与目标字符串需要满足的特定字符的数量成正比,矿工算出结果后,会获得比特币奖励。
当然,在网络上非常容易验证这个结果。
实现工作量证明
让我们来实现一个相似PoW算法,规则是:寻找一个数 p,使得它与前一个区块的 proof 拼接成的字符串的 Hash 值以 4 个零开头:
...
/**
* 简单的工作量证明:
* - 查找一个 p' 使得 hash(pp') 以4个0开头
* - p 是上一个块的证明, p' 是当前的证明
*
* @param last_proof
* 上一个块的证明
* @return
*/
public long proofOfWork(long last_proof) {
long proof = 0;
while (!validProof(last_proof, proof)) {
proof += 1;
}
return proof;
}
/**
* 验证证明: 是否hash(last_proof, proof)以4个0开头?
*
* @param last_proof
* 上一个块的证明
* @param proof
* 当前的证明
* @return 以4个0开头返回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");
}
衡量算法复杂度的办法是修改零开头的个数。使用4个来用于演示,你会发现多一个零都会大大增加计算出结果所需的时间。
现在Blockchain类基本已经完成了,接下来使用Servlet接收HTTP请求来进行交互。
Blockchain作为API接口
我们将使用Java Web中的Servlet来接收用户的HTTP请求,通过Servlet我们可以方便的将网络请求的数据映射到相应的方法上进行处理,现在我们来让Blockchain运行在基于Java Web上。
我们将创建三个接口:
- /transactions/new 创建一个交易并添加到区块
- /mine 告诉服务器去挖掘新的区块
- /chain 返回整个区块链
注册节点ID
我们的“Tomcat服务器”将扮演区块链网络中的一个节点,而每个节点都需要有一个唯一的标识符,也就是id。在这里我们使用UUID来作为节点ID,我们需要在服务器启动时,将UUID设置到ServletContext属性中,这样我们的服务器就拥有了唯一标识,这一步我们可以配置监听类来完成,首先配置web.xml文件内容如下:
<?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>
然后编写一个类实现ServletContextListener接口,在初始化方法中把uuid设置到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) {
}
}
创建Servlet类
我们这里没有使用任何框架,所以我们需要通过最基本的Servlet来接收并处理用户的HTTP请求:
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("/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用于输出整个区块链的数据
@WebServlet("/chain")
public class FullChain extends HttpServlet{
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
我们先来完成最简单的FullChain的代码,这个Servlet用于向客户端输出整个区块链的数据(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用于输出整个区块链的数据
@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();
}
}
发送交易
然后是记录交易数据的功能,每一个区块都可以记录交易数据,发送到节点的交易数据结构如下:
{
"sender": "my address",
"recipient": "someone else's address",
"amount": 5
}
实现代码如下:
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");
// 读取客户端传递过来的数据并转换成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)) {
// 如果没有需要的字段就返回错误信息
resp.sendError(400, "Missing values");
}
}
// 新建交易信息
BlockChain blockChain = BlockChain.getInstance();
int index = blockChain.newTransactions(jsonValues.getString("sender"), jsonValues.getString("recipient"),
jsonValues.getLong("amount"));
// 返回json格式的数据给客户端
resp.setContentType("application/json");
PrintWriter printWriter = resp.getWriter();
printWriter.println(new JSONObject().append("message", "Transaction will be added to Block " + index));
printWriter.close();
}
}
挖矿
挖矿正是神奇所在,它很简单,只做了以下三件事:
- 计算工作量证明PoW
- 通过新增一个交易授予矿工(自己)一个币
- 构造新区块并将其添加到链中
代码实现如下:
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用于运行工作算法的证明来获得下一个证明,也就是所谓的挖矿
@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);
// 给工作量证明的节点提供奖励,发送者为 "0" 表明是新挖出的币
String uuid = (String) this.getServletContext().getAttribute("uuid");
blockChain.newTransactions("0", uuid, 1);
// 构建新的区块
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"));
// 返回新区块的数据给客户端
resp.setContentType("application/json");
PrintWriter printWriter = resp.getWriter();
printWriter.println(new JSONObject(response));
printWriter.close();
}
}
注意交易的接收者是我们自己的服务器节点,我们做的大部分工作都只是围绕Blockchain类的方法进行交互。到此,我们的区块链就算完成了,我们来实际运行下。
运行区块链
由于我们这里也没有写前端的web页面,只写了后端的API,所以只能使用 Postman 之类的软件去和API进行交互。首先启动Tomcat服务器,然后通过post请求 http://localhost:8089/BlockChain_Java/transactions/new 来添加新的交易信息(注意我这里没有使用默认的8080端口,默认的情况下是8080端口):
但是这时候还没有新的区块可以写入这个交易信息,所以我们还需要请求 http://localhost:8089/BlockChain_Java/mine 来进行挖矿,挖出一个新的区块来存储这笔交易:
在挖了两次矿之后,就有3个块了,通过请求 http://localhost:8089/BlockChain_Java/chain 可以得到所有的区块块的信息:
{
"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
}
一致性(共识)
我们已经有了一个基本的区块链可以接受交易和挖矿。但是区块链系统应该是分布式的。既然是分布式的,那么我们究竟拿什么保证所有节点有同样的链呢?这就是一致性问题,我们要想在网络上有多个节点,就必须实现一个一致性的算法。
注册节点
在实现一致性算法之前,我们需要找到一种方式让一个节点知道它相邻的节点。每个节点都需要保存一份包含网络中其它节点的记录。因此让我们新增几个接口:
- /nodes/register 接收URL形式的新节点列表
- /nodes/resolve执行一致性算法,解决任何冲突,确保节点拥有正确的链
我们需要修改下BlockChain的构造函数并提供一个注册节点方法:
package org.zero01.core;
...
import java.net.URL;
...
private Set<String> nodes;
private BlockChain() {
...
// 用于存储网络中其他节点的集合
nodes = new HashSet<String>();
...
}
public Set<String> getNodes() {
return nodes;
}
/**
* 注册节点
*
* @param address
* 节点地址
* @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 集合来储存节点,这是一种避免出现重复添加节点的简单方法。
实现共识算法
前面提到,冲突是指不同的节点拥有不同的链,为了解决这个问题,规定最长的、有效的链才是最终的链,换句话说,网络中有效最长链才是实际的链。
我们使用以下算法,来达到网络中的共识:
...
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
...
public class BlockChain {
...
/**
* 检查是否是有效链,遍历每个区块验证hash和proof,来确定一个给定的区块链是否有效
*
* @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;
}
/**
* 共识算法解决冲突,使用网络中最长的链. 遍历所有的邻居节点,并用上一个方法检查链的有效性, 如果发现有效更长链,就替换掉自己的链
*
* @return 如果链被取代返回true, 否则返回false
* @throws IOException
*/
public boolean resolveConflicts() throws IOException {
Set<String> neighbours = this.nodes;
List<Map<String, Object>> newChain = null;
// 寻找最长的区块链
long maxLength = this.chain.size();
// 获取并验证网络中的所有节点的区块链
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();
// 检查长度是否长,链是否有效
if (length > maxLength && validChain(chain)) {
maxLength = length;
newChain = chain;
}
}
}
// 如果发现一个新的有效链比我们的长,就替换当前的链
if (newChain != null) {
this.chain = newChain;
return true;
}
return false;
}
...
第一个方法 validChain() 用来检查是否是有效链,遍历每个块验证hash和proof.
第2个方法 resolveConflicts() 用来解决冲突,遍历所有的邻居节点,并用上一个方法检查链的有效性, 如果发现有效更长链,就替换掉自己的链
让我们添加两个Servlet,一个用来注册节点,一个用来解决冲突:
注册节点:
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;
// 用于注册节点的Servlet
@WebServlet("/nodes/register")
public class NodesRegister extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
// 读取客户端传递过来的数据并转换成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());
// 获得节点集合数据,并进行判空
List<String> nodes = (List) jsonValues.getJSONArray("nodes").toList();
if (nodes == null) {
resp.sendError(400, "Error: Please supply a valid list of nodes");
}
// 注册节点
BlockChain blockChain = BlockChain.getInstance();
for (String address : nodes) {
blockChain.registerNode(address);
}
// 向客户端返回处理结果
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();
}
}
我们可以在不同的机器运行节点,或在一台机机开启不同的网络端口来模拟多节点的网络,这里在同一台机器开启不同的端口演示,配置两个不同端口的服务器即可,我这里启动了两个节点:http://localhost:8089 和 http://localhost:8066。
两个节点互相进行注册:
然后在8066节点上挖两个块,确保是更长的链:
接着在8089节点上访问接口/nodes/resolve ,这时8089节点的链会通过共识算法被8066节点的链取代:
通过共识算法保持一致性后,两个节点的区块链数据就都是一致的了:
到此为止我们就完成了一个区块链的开发,虽然这只是一个最基本的区块链,而且在开发的过程中也没有考虑太多的程序设计方面的问题,而是以最基本、原始的方式进行开发的。但是我们不妨以这个简单的区块链为基础,发挥自己的能力动手去重构、扩展、完善这个区块链程序,直至成为自己的一个小项目。
本文项目代码地址如下: