专栏名称: 区块链技术学习
致力于区块链技术的学习和普及,对区块链技术和相关企业事件进行深度分析和研判,探索去中心化账本技术应用领域。
目录
相关文章推荐
新华社  ·  这些下饭菜真的可能致癌,建议少吃! ·  20 小时前  
半岛晨报  ·  闭店!刚刚,大连一知名商场发布公告 ·  昨天  
新华社  ·  全红婵,拟被保送! ·  昨天  
51好读  ›  专栏  ›  区块链技术学习

使用Java语言从零开始创建区块链

区块链技术学习  · 公众号  ·  · 2018-08-28 09:16

正文

作者: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 {

   /**
    * 传入字符串,返回 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.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;

// 该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 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;

// 该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"






请到「今天看啥」查看全文