用Java搭建一条区块链

前言

为了更好的理解区块链的底层实现原理,决定自己动手模拟实现一条区块链。

思路分析

通过之前的学习,从文本知识的角度,我们知道,创世区块、记账原理、挖矿原理、工作量证明、共识机制等等区块链的相关知识。

创建一条区块链,首先默认构造创世区块。在此基础上,我们可以发布交易,并进行挖矿,计算出工作量证明,将交易记录到区块中,每成功的挖一次矿,块高就+1。当然在此过程中,可能会出现“造假”的问题。也就是说,每一个新注册的节点,都可以有自己的链。这些链长短不一,为了保证账本的一致性,需要通过一种一致性共识算法来找到最长的链,作为样本,同步数据,保证每个节点上的账本信息都是一致的。

数据结构

  • 区块链
    这里写图片描述
    如图所示,索引为1的区块即为创始区块。可想而知,可以用List<区块>来表示区块链。其中,区块链的高度即为链上区块的块数,上图区块高度为4。
  • 区块
    这里写图片描述
    单个区块的数据结构有索引、交易列表、时间戳、工作量证明、上一个区块的hash组成。
  • 交易列表
    这里写图片描述
    整个区块链就是一个超级大的分布式账本,当发生交易时,矿工们通过计算工作量证明的方法来进行挖矿(本文中挖到矿将得到1个币的奖励),将发生的交易记录到账本之中。

Web API

我们将通过Postman来模拟请求。请求API如下:

/nodes/register 注册网络节点
/nodes/resolve 一致性共识算法
/transactions/new 新建交易
/mine 挖矿
/chain 输出整条链的数据

项目目录结构

Gradle Web 项目
这里写图片描述

dependencies {
    compile('javax:javaee-api:7.0')
    compile('org.json:json:20160810')

    testCompile('junit:junit:4.12')
}

实现代码

注释写的很详细,如果遇到不懂的地方,欢迎大家一同讨论。

  • BlockChain类 ,所有的核心代码都在其中。
    // 存储区块链
    private List<Map<String, Object>> chain;
    // 该实例变量用于当前的交易信息列表
    private List<Map<String, Object>> currentTransactions;
    // 网络中所有节点的集合
    private Set<String> nodes;


    private static BlockChain blockChain = null;

    private BlockChain() {
        // 初始化区块链以及当前的交易信息列表
        chain = new ArrayList<Map<String, Object>>();
        currentTransactions = new ArrayList<Map<String, Object>>();
        // 初始化存储网络中其他节点的集合
        nodes = new HashSet<String>();

        // 创建创世区块
        newBlock(100, "0");
    }

    /**
     * 在区块链上新建一个区块
     * @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;
    }

    // 创建单例对象
    public static BlockChain getInstance() {
        if (blockChain == null) {
            synchronized (BlockChain.class) {
                if (blockChain == null) {
                    blockChain = new BlockChain();
                }
            }
        }
        return blockChain;
    }

    /**
     * @return 得到区块链中的最后一个区块
     */
    public Map<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;
    }

    /**
     * 生成区块的 SHA-256格式的 hash值
     * @param block 区块
     * @return 返回该区块的hash
     */
    public static Object hash(Map<String, Object> block) {
        return new Encrypt().Hash(new JSONObject(block).toString());
    }

    /**
     * 注册节点
     * @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);
    }

    /**
     * 验证是否为有效链,遍历每个区块验证hash和proof,来确定一个给定的区块链是否有效
     * @param chain
     * @return
     */
    public boolean vaildChain(List<Map<String,Object>> chain) {
        Map<String,Object> lastBlock = chain.get(0);
        int currentBlockIndex = 1;
        while (currentBlockIndex < lastBlock.size()) {
            Map<String,Object> currentBlock = chain.get(currentBlockIndex);
            //检查区块的hash是否正确
            if (!currentBlock.get("previous_hash").equals(hash(lastBlock))) {
                return false;
            }
            lastBlock = currentBlock;
            currentBlockIndex ++;
        }
        return true;
    }

    /**
     * 使用网络中最长的链. 遍历所有的邻居节点,并用上一个方法检查链的有效性,
     * 如果发现有效更长链,就替换掉自己的链
     * @return 如果链被取代返回true, 否则返回false
     * @throws IOException
     */
    public boolean resolveConflicts() throws IOException {
        //获得当前网络上所有的邻居节点
        Set<String> neighbours = this.nodes;

        List<Map<String, Object>> newChain = null;

        // 寻找最长的区块链0
        long maxLength = this.chain.size();

        // 获取并验证网络中的所有节点的区块链
        for (String node : neighbours) {

            URL url = new URL("http://" + node + "/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(responseData.toString());
                long length = jsonData.getLong("blockLength");
                List<Map<String, Object>> chain = (List) jsonData.getJSONArray("chain").toList();

                // 检查长度是否长,链是否有效
                if (length > maxLength && vaildChain(chain)) {
                    maxLength = length;
                    newChain = chain;
                }
            }

        }
        // 如果发现一个新的有效链比我们的长,就替换当前的链
        if (newChain != null) {
            this.chain = newChain;
            return true;
        }
        return false;
    }
  • Proof 类 ,计算工作量证明
/**
     * 计算当前区块的工作量证明
     * @param last_proof 上一个区块的工作量证明
     * @return
     */
    public long ProofOfWork(long last_proof){
        long proof = 0;
        while (!(vaildProof(last_proof,proof))) {
            proof ++;
        }
        return proof;
    }

    /**
     * 验证证明,是否拼接后的Hash值以4个0开头
     * @param last_proof 上一个区块工作量证明
     * @param proof 当前区块的工作量证明
     * @return
     */
    public boolean vaildProof(long last_proof, long proof) {
        String guess = last_proof + "" + proof;
        String guess_hash = new Encrypt().Hash(guess);
        boolean flag = guess_hash.startsWith("0000");
        return  flag;
    }
  • Encrypt 类 ,Hash计算工具类
public class Encrypt {
     /**
      * 传入字符串,返回 SHA-256 加密字符串
      * @param strText
      * @return
      */
     public String Hash(final String strText) {
         // 返回值
         String strResult = null;
         // 是否是有效字符串
         if (strText != null && strText.length() > 0) {
             try {
                 // 创建加密对象,传入要加密类型
                 MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
                 // 传入要加密的字符串
                 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;
     }
 }
  • FullChain 类,输出整条链的信息。
/**
 * @Author: cfx
 * @Description: 该Servlet用于输出整个区块链的数据(Json)
 * @Date: Created in 2018/5/9 17:24
 */
@WebServlet("/chain")
public class FullChain extends HttpServlet{
    @Override
    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("blockLength",blockChain.getChain().size());

        JSONObject jsonObject = new JSONObject(response);
        resp.setContentType("application/json");
        PrintWriter printWriter = resp.getWriter();
        printWriter.println(jsonObject);
        printWriter.close();
    }
}
  • InitialID 类 ,初始化时执行,随机的uuid作为矿工的账户地址。
/**
 * @Author: cfx
 * @Description: 初始化时,使用UUID来作为节点ID
 * @Date: Created in 2018/5/9 17:17
 */
@WebListener
public class InitialID implements ServletContextListener {

    public void contextInitialized(ServletContextEvent sce) {
        ServletContext servletContext = sce.getServletContext();
        String uuid = UUID.randomUUID().toString().replace("-", "");
        servletContext.setAttribute("uuid", uuid);
        System.out.println("uuid is : "+servletContext.getAttribute("uuid"));
    }

    public void contextDestroyed(ServletContextEvent sce) {
    }
}
  • Register 类 ,节点注册类,记录网络上所有的节点,用户共识算法,保证所有的节点上的账本都是一致的。
/**
 * @Author: cfx
 * @Description: 注册网络节点
 * @Date: Created in 2018/5/10 11:26
 */
@WebServlet("/nodes/register")
public class Register extends HttpServlet {
    @Override
    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 jsonValue = new JSONObject(requestBody.toString());
        BlockChain blockChain = BlockChain.getInstance();
        blockChain.registerNode(jsonValue.getString("nodes"));

        PrintWriter printWriter = resp.getWriter();
        printWriter.println(new JSONObject().append("message","The Nodes is : " + blockChain.getNodes()));
        printWriter.close();

    }
}
  • NewTransaction 类,新建交易类。
/**
 * @Author: cfx
 * @Description: 该Servlet用于接收并处理新的交易信息
 * @Date: Created in 2018/5/9 17:22
 */
@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();
    }
}
  • Mine , 挖矿类。
/**
 * @Author: cfx
 * @Description: 该Servlet用于运行工作算法的证明来获得下一个证明,也就是所谓的挖矿
 * @Date: Created in 2018/5/9 17:21
 */
@WebServlet("/mine")
public class Mine extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        BlockChain blockChain = BlockChain.getInstance();

        //计算出工作量证明
        Map<String,Object> lastBlock = blockChain.lastBlock();
        Long last_proof = Long.parseLong(lastBlock.get("proof") + "");
        Long proof = new Proof().ProofOfWork(last_proof);

        //奖励计算出工作量证明的矿工1个币的奖励,发送者为"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();
    }
}
  • Consensus 类 ,通过判断不同节点上链的长度,来找出最长链,这就是一致性共识算法。
/**
 * @Author: cfx
 * @Description: 一致性共识算法,解决共识冲突,保证所有的节点都在同一条链上(最长链)
 * @Date: Created in 2018/5/10 11:38
 */
@WebServlet("/nodes/resolve")
public class Consensus extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        BlockChain blockChain = BlockChain.getInstance();
        boolean flag = blockChain.resolveConflicts();
        System.out.println("是否解决一致性共识冲突:" + flag);
    }
}

运行结果

以下是本人之前的测试记录:

首次请求/chain:
    初始化Blockchain
    {
        "chain": [
            {
                "index": 1,
                "proof": 100,
                "transactions": [],
                "timestamp": 1526284543591,
                "previous_hash": "0"
            }
        ],
        "chainLenth": 1
    }

请求/nodes/register,进行网络节点的注册。
request:
    {
      "nodes": "http://lcoalhost:8080"
    }
response:
    {"message":["All Nodes are:[lcoalhost:8080]"]}

请求/mine,进行挖矿。
{
    "index": 2,
    "proof": 35293,
    "message": "New Block Forged",
    "transactions": [
        {
            "amount": 1,
            "sender": "0",
            "recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
        }
    ],
    "previous_hash": "c4b2bb2f6e042680aed249309791cac96da6c1f65b811c306088723ae3c73f66"
}
请求/chain,查看链上所有区块的数据
{
    "chain": [
        {
            "index": 1,
            "proof": 100,
            "transactions": [],
            "timestamp": 1526284543591,
            "previous_hash": "0"
        },
        {
            "index": 2,
            "proof": 35293,
            "transactions": [
                {
                    "amount": 1,
                    "sender": "0",
                    "recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
                }
            ],
            "timestamp": 1526284661678,
            "previous_hash": "c4b2bb2f6e042680aed249309791cac96da6c1f65b811c306088723ae3c73f66"
        }
    ],
    "chainLenth": 2
}

请求/transactions/new,新建交易。
request: 
    {
     "sender": "d4ee26eee15148ee92c6cd394edd974e",
     "recipient": "someone-other-address",
     "amount": 6
    }
response:
    {
        "message": [
            "Transaction will be added to Block 3"
        ]
    }
请求/mine,计算出工作量证明。将上面的交易记录到账本之中。
{
    "index": 3,
    "proof": 35089,
    "message": "New Block Forged",
    "transactions": [
        {
            "amount": 6,
            "sender": "d4ee26eee15148ee92c6cd394edd974e",
            "recipient": "someone-other-address"
        },
        {
            "amount": 1,
            "sender": "0",
            "recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
        }
    ],
    "previous_hash": "a12748a35d57a4a371cefc4a8c294236d69c762d28b889abb2ae34a31d2b7597"
}

请求/chain,查看链上所有区块的数据
{
    "chain": [
        {
            "index": 1,
            "proof": 100,
            "transactions": [],
            "timestamp": 1526284543591,
            "previous_hash": "0"
        },
        {
            "index": 2,
            "proof": 35293,
            "transactions": [
                {
                    "amount": 1,
                    "sender": "0",
                    "recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
                }
            ],
            "timestamp": 1526284661678,
            "previous_hash": "c4b2bb2f6e042680aed249309791cac96da6c1f65b811c306088723ae3c73f66"
        },
        {
            "index": 3,
            "proof": 35089,
            "transactions": [
                {
                    "amount": 6,
                    "sender": "d4ee26eee15148ee92c6cd394edd974e",
                    "recipient": "someone-other-address"
                },
                {
                    "amount": 1,
                    "sender": "0",
                    "recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
                }
            ],
            "timestamp": 1526284774452,
            "previous_hash": "a12748a35d57a4a371cefc4a8c294236d69c762d28b889abb2ae34a31d2b7597"
        }
    ],
    "chainLenth": 3
}

存在的问题

有一个问题没有解决,就是我们启动多实例来模拟不同的网络节点时,并不能解决节点加入同一个Set的问题,也就是说根本无法通过节点本身来获得其他网络节点,进而判断最长链。所以/nodes/resolve请求暂时时无用的。期间也有想方法解决,比如通过所谓的“第三方”–数据库,当一个节点注册时,保存到数据库中;当第二个节点加入时,也加入到数据库中…当需要请求解决一致性算法时,去数据库中读取节点信息遍历即可。但是,自己没有去实现。这是我的想法,毕竟是两个不相干的实例。如果有朋友有其他的解决方案,请一定要告诉我!谢谢。

总结

通过简单的Demo实现区块链,当然其中简化了大量的实现细节,所以说其实并没有多少实际参考价值。但是意义在于,能帮助我们更容易的理解区块链,为之后的学习打下夯实的基础。

项目源码

Java从零开始创建区块链Demo

参考文章

https://learnblockchain.cn/2017/11/04/bitcoin-pow/
http://blog.51cto.com/zero01/2086195等。

猜你喜欢

转载自blog.csdn.net/qq_33764491/article/details/80330542