作者:ZeroOne01
来自:http://blog.51cto.com/zero01/2086195
目前网络上关于区块链入门、科普的文章不少,本文就不再赘述区块链的基本概念了,如果对区块链不是很了解的话,可以看一下我之前收集的一些入门学习资源:
http://blog.51cto.com/zero01/2066321
对区块链技术感到新奇的我们,都想知道区块链在代码上是怎么实现的,所以本文是实战向的,毕竟理论我们都看了不少,但是对于区块链具体的实现还不是很清楚,本文就使用Java语言来实现一个简单的区块链。
但是要完全搞懂区块链并非易事,对于一门较为陌生的技术,我们需要在理论+实践中学习,通过写代码来学习技术会掌握得更牢固,构建一个区块链可以加深对区块链的理解。
准备工作
掌握基本的JavaSE以及JavaWeb开发,能够使用Java开发简单的项目,并且需要了解HTTP协议。
我们知道区块链是由区块的记录构成的不可变、有序的链结构,记录可以是交易、文件或任何你想要的数据,重要的是它们是通过哈希值(hashes)链接起来的。
如果你还不是很了解哈希是什么,可以查看这篇文章:
https://learncryptography.com/hash-functions/what-are-hash-functions
环境描述
-
JDK1.8
-
Tomcat 9.0
-
Maven 3.5
-
JSON 20160810
-
javaee-api 7.0
pom.xml文件配置内容:
<dependencies>
<dependency>
<groupId>javaxgroupId>
<artifactId>javaee-apiartifactId>
<version>7.0version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.jsongroupId>
<artifactId>jsonartifactId>
<version>20160810version>
dependency>
dependencies>
然后还需要一个HTTP客户端,比如Postman,Linux命令行下的curl或其它客户端,我这里使用的是Postman。
Blockchain类
首先创建一个Blockchain类,在构造器中创建了两个主要的集合,一个用于储存区块链,一个用于储存交易列表,本文中所有核心的主要代码都写在这个类里,方便随时查看,在实际开发则不宜这么做,应该把代码拆分仔细降低耦合度。
以下是Blockchain类的框架代码:
package org.zero01.core;
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都会变得不正确。不理解的话,慢慢消化,可以参考区块链记账原理(
https://learnblockchain.cn/2017/10/25/whatbc/
)。
由于需要计算区块的hash,所以我们得先编写一个用于计算hash值的工具类:
package org.zero01.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Encrypt {
public String getSHA256(final String strText) {
return SHA(strText, "SHA-256");
}
public String getSHA512(final String strText) {
return SHA(strText, "SHA-512");
}
public String getMD5(final String strText) {
return SHA(strText, "SHA-512");
}
private String SHA(final String strText, final String strType) {
String strResult = null;
if (strText != null && strText.length() > 0) {
try {
MessageDigest messageDigest = MessageDigest.getInstance(strType);
messageDigest.update(strText.getBytes());
byte byteBuffer[] = messageDigest.digest();
StringBuffer strHexString = new StringBuffer();
for (int i = 0; i < byteBuffer.length; i++) {
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方法:
public HashMap<String, Object> lastBlock() {
return getChain().get(getChain().size() - 1);
}
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;
}
public Map<String, Object> lastBlock() {
return getChain().get(getChain().size() - 1);
}
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);
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;
}
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;
}
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 个零开头:
...
public long proofOfWork(long last_proof) {
long proof = 0;
while (!validProof(last_proof, proof)) {
proof += 1;
}
return proof;
}
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上。
我们将创建三个接口:
注册节点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.InitialIDlistener-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;
@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;
@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;
@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;
@WebServlet("/chain")
public class FullChain extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BlockChain blockChain = BlockChain.getInstance();
Map response = new HashMap();
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;
@WebServlet("/transactions/new")
public class NewTransaction extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
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());
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"