使用Java编写自己的区块链

  • 准备工作
  • 开发环境
  • 开始开发
    • Transaction类
    • Block类
    • BlockChain类
    • 实现交易功能
    • 实现创建新块功能
    • 工作量证明
    • Blockchain作为API接口
    • 绑定节点ID
    • 创建Controller类
    • 运行区块链
    • 一致性(共识)
    • 注册节点
    • 实现共识算法
    • 创建NodesController类


关于区块链技术,网络上有很多入门、科普的文章,如果大家对于区块链感兴趣,应该已经通过网络了解区块链的基本概念了,这里就不再赘述基本概念了。

相信阅读本文章的朋友们应该都和我一样对于区块链技术感到新奇,都想知道区块链在代码上怎么实现的,所以本文是实战为主,理论为辅的,毕竟大家应该都看过不少的理论文章了,但是对于区块链具体实现还不是很清楚,本文就是用Java语言来实现一个简易的区块链。

准备工作

使用Java语言编写区块链程序,需要掌握基本的JavaSE以及JavaWeb开发,能够使用Java开发简单的项目,并且对于HTTP协议有一定的了解。

相信大家都听说过区块链的记录构成是不可变、有序的链结构,记录可以是交易、文件或任何你想要的数据,重要的是它们是通过哈希值(Hash)连接起来的。

如果你还不知道什么是哈希,可以查看 这篇文章 。

开发环境

  • JDK 1.8
  • Tomcat 9.0
  • Maven 3.6
  • IntelliJ IDEA 2018及以上版本
  • Springboot 2.3.7.RELEASE
  • alibaba fastjson 1.2.47
  • Postman

pom.xml文件配置内容:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version></dependency>
</dependencies>

开始开发

Transaction类

首先创建一个Transaction类,主要有三个参数分别是:sender(发送者)、recipient(接收者)、amount(金额)。

以下是Transaction类的代码:

package com.feonix.blockchain.pojo;import java.io.Serializable;/*** 交易类*/
public class Transaction implements Serializable {/*** 发送者*/private String sender;/*** 接收者*/private String recipient;/*** 交易金额*/private long amount;public Transaction() {}public Transaction(String sender, String recipient, long amount) {this.sender = sender;this.recipient = recipient;this.amount = amount;}public String getSender() {return sender;}public void setSender(String sender) {this.sender = sender;}public String getRecipient() {return recipient;}public void setRecipient(String recipient) {this.recipient = recipient;}public long getAmount() {return amount;}public void setAmount(long amount) {this.amount = amount;}@Overridepublic String toString() {return "Transaction{" +"sender='" + sender + '\'' +", recipient='" + recipient + '\'' +", amount=" + amount +'}';}
}

Transaction类是用来表示交易的实体类,交易中所涉及的要素都在类中体现出来。

Block类

再创建一个Block区块类,每个区块包含属性:index(索引)、timestamp(时间戳)、transactions(交易列表)、proof(工作量证明)、previous_hash(前一个区块的哈希值)。

以下是一个区块的结构:

block = {"index": 2,"previous_hash": "d86a71b5af281c5b32cf50323114975ae0394ca2754b0a590390e65e5bd6cc68","proof": 35293,"timestamp": 1608699216469,"transactions": [{"amount": 5,"recipient": "327598e7426e4c6593e167444a13efvc","sender": "672798e7426e4c6593e167444a13fcad"}]
}

以下是Block类的具体实现:

package com.feonix.blockchain.pojo;import java.io.Serializable;
import java.util.List;/*** 区块类*/
public class Block implements Serializable {/*** 索引*/private int index;/*** 时间戳*/private long timestamp;/*** 交易列表*/private List<Transaction> transactions;/*** 工作量证明*/private long proof;/*** 前一个区块的哈希值*/private String previous_hash;public Block() {}public Block(int index, long timestamp, List<Transaction> transactions, long proof, String previous_hash) {this.index = index;this.timestamp = timestamp;this.transactions = transactions;this.proof = proof;this.previous_hash = previous_hash;}public int getIndex() {return index;}public void setIndex(int index) {this.index = index;}public long getTimestamp() {return timestamp;}public void setTimestamp(long timestamp) {this.timestamp = timestamp;}public List<Transaction> getTransactions() {return transactions;}public void setTransactions(List<Transaction> transactions) {this.transactions = transactions;}public long getProof() {return proof;}public void setProof(long proof) {this.proof = proof;}public String getPrevious_hash() {return previous_hash;}public void setPrevious_hash(String previous_hash) {this.previous_hash = previous_hash;}@Overridepublic String toString() {return "Block{" +"index=" + index +", timestamp=" + timestamp +", transactions=" + transactions +", proof=" + proof +", previous_hash='" + previous_hash + '\'' +'}';}
}

BlockChain类

接下来创建BlockChain类,在构造器中创建了两个主要的集合,一个用于储存区块链,一个用于储存交易列表,这是本文中所用到的核心类,关于区块链的操作都封装在这个类中。

下面是BlockChain类的基础代码框架:

package com.feonix.blockchain.dao;import com.feonix.blockchain.pojo.Block;
import com.feonix.blockchain.pojo.Transaction;public class BlockChain {/*** 存储区块链*/private List<Block> chain;/*** 当前交易信息列表*/private List<Transaction> currentTransactions;private BlockChain() {// 初始化区块链this.chain = new ArrayList<Block>();// 初始化当前的交易信息列表this.currentTransactions = new ArrayList<Transaction>();}public List<Block> getChain() {return chain;}public void setChain(List<Block> chain) {this.chain = chain;}public List<Transaction> getCurrentTransactions() {return currentTransactions;}public void setCurrentTransactions(List<Transaction> currentTransactions) {this.currentTransactions = currentTransactions;}public Block getLastBlock() {return null;}public Block newBlock(long proof, String previous_hash) {return null;}public static String hash(Block block) {return null;}
}

BlockChain类用来管理区块链,它能存储交易,加入新块等,到这里,区块链的概念就清楚了,每个新的区块都包含上一个区块的Hash,这是关键的一点,它保障了区块链不可变性。如果攻击者破坏了前面的某个区块,那么后面所有区块的Hash都会变得不正确。不理解的话,慢慢消化,可以参考 区块链记账原理。

接下来让我们进一步完善这个区块链程序,由于需要计算区块的hash,我们需要先编写一个计算hash值的工具类:

package com.feonix.blockchain.util;import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;/*** 加密*/
public class Encrypt {/*** 传入字符串,返回 SHA-256 加密字符串** @param strText* @return*/public static String getSHA256(final String strText) {return SHA(strText, "SHA-256");}/*** 传入字符串,返回 SHA-512 加密字符串** @param strText* @return*/public static String getSHA512(final String strText) {return SHA(strText, "SHA-512");}/*** 传入字符串,返回 MD5 加密字符串** @param strText* @return*/public static String getMD5(final String strText) {return SHA(strText, "SHA-512");}/*** 字符串 SHA 加密** @param strText* @param strType* @return*/private static 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方法,并完善getLastBlock方法:

 /*** 获取到区块链中最后一个区块** @return*/public Block getLastBlock() {return getChain().get(getChain().size() - 1);}/*** 生成新交易信息,信息将加入到下一个待挖的区块中** @param sender    发送方的地址* @param recipient 接收方的地址* @param amount    交易数量* @return 返回该交易事务的块的索引*/public int newTransactions(String sender, String recipient, long amount) {Transaction transaction = new Transaction();transaction.setSender(sender);transaction.setRecipient(recipient);transaction.setAmount(amount);getCurrentTransactions().add(transaction);return getLastBlock().getIndex() + 1;}

newTransactions方法向列表中添加一个交易记录,并返回该记录将被添加到的区块 (下一个待挖掘的区块)的索引,等下在用户提交交易时会有用。

实现创建新块功能

当Blockchain实例化后,我们需要构造一个创世区块(没有前区块的第一个区块),并且给它加上一个工作量证明。
每个区块都需要经过工作量证明,俗称挖矿,稍后会继续讲解。

为了构造创世块,我们还需要完善剩下的几个方法,并且把该类设计为单例:

package com.feonix.blockchain.dao;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.feonix.blockchain.pojo.Block;
import com.feonix.blockchain.pojo.Transaction;
import com.feonix.blockchain.util.Encrypt;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;public class BlockChain {/*** 存储区块链*/private List<Block> chain;/*** 当前交易信息列表*/private List<Transaction> currentTransactions;private static BlockChain blockChain = null;private BlockChain() {// 初始化区块链this.chain = new ArrayList<Block>();// 初始化当前的交易信息列表this.currentTransactions = new ArrayList<Transaction>();// 创建创世区块newBlock(100, "0");}// 创建单例对象public static BlockChain getInstance() {if (blockChain == null) {synchronized (BlockChain.class) {if (blockChain == null) {blockChain = new BlockChain();}}}return blockChain;}public List<Block> getChain() {return chain;}public void setChain(List<Block> chain) {this.chain = chain;}public List<Transaction> getCurrentTransactions() {return currentTransactions;}public void setCurrentTransactions(List<Transaction> currentTransactions) {this.currentTransactions = currentTransactions;}/*** 获取到区块链中最后一个区块** @return*/public Block getLastBlock() {return getChain().get(getChain().size() - 1);}/*** 在区块链上新建一个区块** @param proof         新区块的工作量证明* @param previous_hash 上一个区块的hash值* @return 返回新建的区块*/public Block newBlock(long proof, String previous_hash) {Block block = new Block();block.setIndex(getChain().size() + 1);block.setTimestamp(System.currentTimeMillis());block.setTransactions(getCurrentTransactions());block.setProof(proof);block.setPrevious_hash(previous_hash != null ? previous_hash : hash(getLastBlock()));// 重置当前的交易信息列表setCurrentTransactions(new ArrayList<Transaction>());getChain().add(block);return block;}/*** 生成新交易信息,信息将加入到下一个待挖的区块中** @param sender    发送方的地址* @param recipient 接收方的地址* @param amount    交易数量* @return 返回该交易事务的块的索引*/public int newTransactions(String sender, String recipient, long amount) {Transaction transaction = new Transaction();transaction.setSender(sender);transaction.setRecipient(recipient);transaction.setAmount(amount);getCurrentTransactions().add(transaction);return getLastBlock().getIndex() + 1;}/*** 生成区块的 SHA-256格式的 hash值** @param block 区块* @return 返回该区块的hash*/public static String hash(Block block) {return Encrypt.getSHA256(JSON.toJSONString(block));}
}

从上面的代码和注释可以对区块链有了直观的了解,接下来编写一些简单的测试代码来验证一下这些代码是否正常工作:

package com.feonix.blockchain;import com.alibaba.fastjson.JSON;
import com.feonix.blockchain.dao.BlockChain;
import com.feonix.blockchain.pojo.Block;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import java.util.HashMap;
import java.util.Map;@SpringBootTest
class BlockChainApplicationTests {@Testpublic void blockChainTest() {BlockChain blockChain = BlockChain.getInstance();Block block = blockChain.newBlock(300, null);System.out.println(JSON.toJSONString(block));// 一个区块中可以包含一笔交易记录blockChain.newTransactions("123", "222", 33);Block block1 = blockChain.newBlock(500, null);System.out.println(JSON.toJSONString(block1));// 一个区块中可以包含多笔交易记录blockChain.newTransactions("321", "555", 133);blockChain.newTransactions("000", "111", 10);blockChain.newTransactions("789", "369", 65);Block block2 = blockChain.newBlock(600, null);System.out.println(JSON.toJSONString(block2));// 查看整个区块链Map<String, Object> chain = new HashMap<String, Object>();chain.put("chain", blockChain.getChain());chain.put("length", blockChain.getChain().size());System.out.println(JSON.toJSONString(chain));}
}

运行结果:

// 挖出来的新区块
{"index": 2,"previous_hash": "c1d0e4bcdee5d2364031aab5d1b7aa71b6ee7843dc0b0a1adbb18e9c3ab80ecf","proof": 300,"timestamp": 1608706922412,"transactions": []
}// 包含一笔交易的区块
{"index": 3,"previous_hash": "a496d448bd27b9f11aea0a50c036bb6c95f40f2218d30d93a1c5c4729daae618","proof": 500,"timestamp": 1608706922522,"transactions": [{"amount": 33,"recipient": "222","sender": "123"}]
}// 包含多笔交易的区块
{"index": 4,"previous_hash": "0ac6ca410468f1cce92c895a5e39c5b125116877ecee8941a518d904433ff4ce","proof": 600,"timestamp": 1608706922525,"transactions": [{"amount": 133,"recipient": "555","sender": "321"},{"amount": 10,"recipient": "111","sender": "000"},{"amount": 65,"recipient": "369","sender": "789"}]
}// 整个区块链,索引为1的是创世区块
{"chain": [{"index": 1,"previous_hash": "0","proof": 100,"timestamp": 1608706922412,"transactions": []},{"index": 2,"previous_hash": "c1d0e4bcdee5d2364031aab5d1b7aa71b6ee7843dc0b0a1adbb18e9c3ab80ecf","proof": 300,"timestamp": 1608706922412,"transactions": []},{"index": 3,"previous_hash": "a496d448bd27b9f11aea0a50c036bb6c95f40f2218d30d93a1c5c4729daae618","proof": 500,"timestamp": 1608706922522,"transactions": [{"amount": 33,"recipient": "222","sender": "123"}]},{"index": 4,"previous_hash": "0ac6ca410468f1cce92c895a5e39c5b125116877ecee8941a518d904433ff4ce","proof": 600,"timestamp": 1608706922525,"transactions": [{"amount": 133,"recipient": "555","sender": "321"},{"amount": 10,"recipient": "111","sender": "000"},{"amount": 65,"recipient": "369","sender": "789"}]}],"length": 4
}

通过以上的测试,可以看出区块链的直观数据,一个初步的区块链代码已经完成了,但是还有很多事情没有做,接下来我们看看区块是怎么挖出来的。

工作量证明

首先理解一下工作量证明,新的区块依赖工作量证明算法(PoW)来构造。PoW的目标是找出一个符合特定条件的数字,这个数字很难计算出来,但是很好验证。这就是工作量证明的核心思想。

为了方便理解,举个例子:

假设一个整数 x 加上另一个整数 y 的积的 Hash 值必须以 0 开头,即 hash(x + y) = 0b918943…e3f9。设变量 x = 10,求 y 的值?

Java代码实现如下:

package com.feonix.blockchain;import com.feonix.blockchain.util.Encrypt;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class ProofTest {@Testpublic void testProof() {int x = 10;int y = 0;while (!Encrypt.getSHA256((x + y) + "").startsWith("0")) {y++;}System.out.println("y=" + y);System.out.println("hash=" + Encrypt.getSHA256((x + y) + ""));}
}

运行结果:

y=29
hash=0b918943df0962bc7a1824c0555a389347b4febdc7cf9d1254406d80ce44e3f9

在比特币中,使用称为Hashcash的工作量证明算法,它和上面的问题很类似。矿工们为了争夺创建区块的权利而争相计算结果。通常,计算难度与目标字符串需要满足的特定字符的数量成正比,矿工算出结果后,会获得比特币奖励。

当然,在网络上非常容易验证这个结果。

接下来让我们来实现一个相似的PoW算法,规则是寻找一个数p,使其与前一个区块的proof拼接起来的字符串的Hash值以4个“0”开头:

 /*** 简单的工作量证明:* - 查找一个 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 = Encrypt.getSHA256(guess);return guess_hash.startsWith("0000");}

使用4个“0”开头用来演示比较合适,修改开头0的个数可以改变算法复杂度,增加一个0都会大大增加计算出结果的时间。

现在Blockchain类基本已经完成了,接下来使用Springboot提供的web服务能力接收HTTP请求来进行交互。

Blockchain作为API接口

我们将使用Springboot提供的web编程能力编写接收HTTP请求的网络服务,可以很方便的将请求数据映射到相应的方法上进行处理,现在我们来让BlockChain运行基于Java Web的网络服务上。

我们先来创建三个服务接口:

  • /wallet/trans 创建一个交易并添加到区块
  • /wallet/mine 告诉服务器去挖掘新的区块
  • /wallet/chain 查看整个区块链

绑定节点ID

我们的“Tomcat服务器”将扮演区块链网络中的一个节点,而每个节点都需要有一个唯一的标识符,也就是id。在这里我们使用UUID来作为节点ID,我们需要在服务器启动时,将UUID设置到ServletContext属性中,这样我们的服务器就拥有了唯一标识,这一步我们可以配置监听类来完成,我们来编写一个监听类:

package com.feonix.blockchain.config;import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;import javax.servlet.ServletContext;
import java.util.UUID;
/*** 这里利用Springboot的事件监听机制,当Springboot初始化完成时,就会扫描这些监听类,* 进行相应的数据初始化*/
@Component
public class InitIDListener implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {// 将 ApplicationContext 转化为 WebApplicationContextWebApplicationContext webApplicationContext =(WebApplicationContext) contextRefreshedEvent.getApplicationContext();// 从 webApplicationContext 中获取  servletContextServletContext servletContext = webApplicationContext.getServletContext();String uuid = UUID.randomUUID().toString().replace("-", "");// servletContext设置值,把UUID绑定到servletContextservletContext.setAttribute("uuid", uuid);}
}

创建Controller类

接下来创建一个WalletController类,用来对外提供HTTP请求服务接口:

package com.feonix.blockchain.controller;import com.alibaba.fastjson.JSON;
import com.feonix.blockchain.dao.BlockChain;
import com.feonix.blockchain.pojo.Block;
import com.feonix.blockchain.pojo.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.ServletContext;
import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/wallet")
public class WalletController {@Autowired  // 这里直接注入servletContextprivate ServletContext servletContext;// 发起新交易@RequestMapping("/trans")public String trans(Transaction trans) {return null;}// 挖矿@RequestMapping("/mine")public String mine() {return null;}// 查看整个区块链数据@RequestMapping("/chain")public String fullChain() {return null;}
}

我们先来完善最简单的fullChain的代码,这个接口用于向客户端输出整个区块链的数据(JSON格式):

 // 查看整个区块链数据@RequestMapping("/chain")public String fullChain() {BlockChain blockChain = BlockChain.getInstance();Map<String, Object> resp = new HashMap<String, Object>();resp.put("chain", blockChain.getChain());resp.put("length", blockChain.getChain().size());return JSON.toJSONString(resp);}

然后是发起交易功能,每一个区块都可以记录交易数据,具体实现代码如下:

 // 发起新交易@RequestMapping("/trans")public String trans(Transaction trans) {Map<String, Object> resp = new HashMap<String, Object>();// 判断获取到的交易参数是否完整if (trans == null || trans.getSender() == null || trans.getRecipient() == null || trans.getAmount() <= 0) {resp.put("message", "Error: Missing values");return JSON.toJSONString(resp);}// 发送交易BlockChain blockChain = BlockChain.getInstance();int index = blockChain.newTransactions(trans.getSender(), trans.getRecipient(), trans.getAmount());// 向客户端返回处理结果resp.put("message", "Transaction will be added to Block " + index);return JSON.toJSONString(resp);}

接下来是挖矿,这正是神奇所在。它很简单,只做了以下三件事:

  • 计算工作量证明
  • 通过新增一个交易授予矿工(自己)一个币
  • 构造新的区块并将其添加到链中

代码实现如下:

 // 挖矿@RequestMapping("/mine")public String mine() {BlockChain blockChain = BlockChain.getInstance();Block lastBlock = blockChain.getLastBlock();long lastProof = Long.parseLong(lastBlock.getProof() + "");long proof = blockChain.proofOfWork(lastProof);// 给工作量证明的节点提供奖励,发送者为 "0" 表明是新挖出的币String uuid = (String) servletContext.getAttribute("uuid");blockChain.newTransactions("0", uuid, 1);// 构建新的区块Block newBlock = blockChain.newBlock(proof, null);Map<String, Object> resp = new HashMap<String, Object>();resp.put("message", "New Block Forged");resp.put("block", newBlock);return JSON.toJSONString(resp);}

注意挖矿的交易接收者是我们自己的服务器节点,我们做的大部分工作都只是围绕Blockchain类的方法进行交互。到此,我们的区块链就算完成了,我们来实际运行下。

运行区块链

由于我们这里也没有写前端的web页面,只写了后端的API,所以只能使用 Postman 之类的软件去和API进行交互。首先启动Tomcat服务器,然后通过post请求http://localhost:8080/wallet/trans 来添加新的交易信息:

但是这时候还没有新的区块可以写入这个交易信息,所以我们还需要请求 http://localhost:8080/wallet/mine 来进行挖矿,挖出一个新的区块来存储这笔交易:

经过多次挖矿和交易之后,来看一下完整的区块链,通过请求http://localhost:8080/wallet/chain 来查看所有的区块信息:

{"chain": [{"index": 1,"previous_hash": "0","proof": 100,"timestamp": 1608699205833,"transactions": []},{"index": 2,"previous_hash": "d86a71b5af281c5b32cf50323114975ae0394ca2754b0a590390e65e5bd6cc68","proof": 35293,"timestamp": 1608699216469,"transactions": [{"amount": 1,"recipient": "4fd33a6e70b84cdc9130348ce1d60fb8","sender": "0"}]},{"index": 3,"previous_hash": "9233f72c4122fe55211e9668b75376dadc8115a3faea3cd5404abff967085436","proof": 35089,"timestamp": 1608699223812,"transactions": [{"amount": 6,"recipient": "327598e7426e4c6593e167444a13efvc","sender": "672798e7426e4c6593e167444a13fcad"},{"amount": 6,"recipient": "327598e7426e4c6593e167444a13efvc","sender": "672798e7426e4c6593e167444a13fcad"},{"amount": 1,"recipient": "4fd33a6e70b84cdc9130348ce1d60fb8","sender": "0"}]},{"index": 4,"previous_hash": "5324f91fa12fdcdaa7dd8fe91492c2ed2d6c7573d3d9c163b373c9127b2b3432","proof": 119678,"timestamp": 1608699226009,"transactions": [{"amount": 1,"recipient": "4fd33a6e70b84cdc9130348ce1d60fb8","sender": "0"}]},{"index": 5,"previous_hash": "190d443153a7a846ccc9ce8abfdd373b7807e192c1fe19b779dfe25fd735a906","proof": 146502,"timestamp": 1608712131192,"transactions": [{"amount": 6,"recipient": "327598e7426e4c6593e167444a13efvc","sender": "672798e7426e4c6593e167444a13fcad"},{"amount": 1,"recipient": "4fd33a6e70b84cdc9130348ce1d60fb8","sender": "0"}]}],"length": 5
}

一致性(共识)

我们已经有了一个基本的区块链可以接受交易和挖矿。但是区块链系统应该是分布式的。既然是分布式的,那么我们究竟拿什么保证所有节点有同样的链呢?这就是一致性问题,我们要想在网络上有多个节点,就必须实现一个一致性的算法。

注册节点

在实现一致性算法之前,我们需要找到一种方式让一个节点知道它相邻的节点。每个节点都需要保存一份包含网络中其它节点的记录。因此让我们新增2个接口:

  • /nodes/register 接收URL形式的新节点列表
  • /nodes/resolve执行一致性算法,解决任何冲突,确保节点拥有正确的链

我们需要修改下BlockChain的构造函数并提供一个注册节点方法:

package com.feonix.blockchain.dao;
...
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 = String.format("%s:%s", 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;
.../*** 检查是否是有效链,遍历每个区块验证hash和proof,来确定一个给定的区块链是否有效** @param chain* @return*/public boolean validChain(List<Block> chain) {Block lastBlock = chain.get(0);int currentIndex = 1;while (currentIndex < chain.size()) {Block block = chain.get(currentIndex);// 检查block的hash是否正确if (block.getPrevious_hash() == null || !block.getPrevious_hash().equals(hash(lastBlock))) {return false;}lastBlock = block;currentIndex++;}return true;}/*** 共识算法解决冲突,使用网络中最长的链.* 遍历所有的邻居节点,并用上一个方法检查链的有效性,* 如果发现有效更长链,就替换掉自己的链** @return 如果链被取代返回true, 否则返回false* @throws IOException*/public boolean resolveConflicts() throws IOException {List<Block> newChain = null;// 寻找最长的区块链long maxLength = this.chain.size();// 获取并验证网络中的所有节点的区块链for (String node : nodes) {URL url = new URL("http://" + node + "/wallet/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();// System.out.println("responseData ------> " + responseData.toString());JSONObject jsonData = JSON.parseObject(responseData.toString());long length = jsonData.getLong("length");List<Block> chain = jsonData.getJSONArray("chain").toJavaList(Block.class);// 检查长度是否长,链是否有效if (length > maxLength && validChain(chain)) {maxLength = length;newChain = chain;}}}// 如果发现一个新的有效链比我们的长,就替换当前的链if (newChain != null) {this.chain = newChain;return true;}return false;}...

第一个方法 validChain() 用来检查是否是有效链,遍历每个块验证hash和proof.

第2个方法 resolveConflicts() 用来解决冲突,遍历所有的邻居节点,并用上一个方法检查链的有效性, 如果发现有效更长链,就替换掉自己的链

创建NodesController类

NodesController是用来进行节点注册和解决冲突的服务控制器,包含register和resolve两个网络服务接口方法

package com.feonix.blockchain.controller;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.feonix.blockchain.dao.BlockChain;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import java.io.IOException;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@RestController
@RequestMapping("/nodes")
public class NodesController {// 注册节点@RequestMapping(value = "/register", method = RequestMethod.POST, produces = "application/json")public String register(@RequestBody String jsonParam) throws MalformedURLException {Map<String, Object> result = new HashMap<String, Object>();JSONObject jsonObject = JSON.parseObject(jsonParam);List<String> addresses = jsonObject.getJSONArray("addresses").toJavaList(String.class);// 对获取到的节点地址集合判断是否为空if (addresses == null) {result.put("message", "Error: Please supply a valid list of nodes");return JSON.toJSONString(result);}// 注册节点BlockChain blockChain = BlockChain.getInstance();for (String address : addresses) {blockChain.registerNode(address);}// 向客户端返回处理结果result.put("message", "New nodes have been added");result.put("registered_nodes", blockChain.getNodes().toArray());return JSON.toJSONString(result);}// 解决冲突@RequestMapping("/resolve")public String resolve() throws IOException {BlockChain blockChain = BlockChain.getInstance();boolean resolved = blockChain.resolveConflicts();Map<String, Object> result = new HashMap<String, Object>();if (resolved) {result.put("message", "Our chain was replaced");result.put("new_chain", blockChain.getChain());} else {result.put("message", "Our chain is authoritative");result.put("chain", blockChain.getChain());}return JSON.toJSONString(result);}
}

我们可以在不同的机器运行节点,或在一台机机开启不同的网络端口来模拟多节点的网络,这里在同一台机器开启不同的端口演示,配置两个不同端口的服务器即可,我这里启动了两个节点:http://localhost:8080 和 http://localhost:8081。

两个节点相互注册:


然后在8080节点上挖两个块,确保8080节点上的链最长:

接着在8081节点上访问接口/nodes/resolve ,这时8081节点的链会通过共识算法被8080节点的链取代:

{"new_chain": [{"index": 1,"previous_hash": "0","proof": 100,"timestamp": 1608713986647,"transactions": []},{"index": 2,"previous_hash": "0977d2825796c2f313abad35f664c3e60623bbf84ec8a9ee21e6b6bef30c5b9f","proof": 35293,"timestamp": 1608714234444,"transactions": [{"amount": 1,"recipient": "ece1a32593ea497c80ef9853f415b97e","sender": "0"}]},{"index": 3,"previous_hash": "13f96d1d571f02df0e7a4c922ae3f3cf5fdc2ecac56f5fc252cbeda05daf3e02","proof": 35089,"timestamp": 1608714236413,"transactions": [{"amount": 1,"recipient": "ece1a32593ea497c80ef9853f415b97e","sender": "0"}]}],"message": "Our chain was replaced"
}

到此为止我们就完成了一个区块链的开发,虽然这只是一个最基本的区块链,而且开发过程中也有很多细节方面没有考虑到。但是我们不妨以这个简单的区块链为基础,发挥自己的能力去重构、拓展、完善这个区块链程序,直至成为自己的一个小项目。


本文代码地址如下:

https://gitee.com/demo./BlockChain.git

使用Java编写自己的区块链相关推荐

  1. 用Java编写第一个区块链(二)

    用Java编写第一个区块链(二) 这篇文章将去介绍如何使用区块链进行交易. [本文禁止任何形式的全文粘贴式转载,本文来自 zacky31 的随笔] 目标: 在上一篇文章中,我们已经创建了一个可信任的区 ...

  2. 如何用 Java 实现简单的区块链

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 来源 | 公众号「锅外的大佬」 1.  概述 本文中,我 ...

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

    使用Java语言从零开始创建区块链 2018年04月01日 17:08:12 大侠区块链 阅读数:1312 标签: java区块链java区块链 更多 个人分类: 区块链 Java区块链开发与交流群: ...

  4. 大数据、java、python、区块链、人工智能发展前景

    在这个信息时代高速发展的情况下,很多人会对自己该往哪个方向发展感到迷茫,下面我就浅显的给大家介绍一下五大流行区域的发展前景. 一.大数据的发展前景 当前大数据行业真的是人才稀缺吗? 学了几年后,大数据 ...

  5. 为什么选择Java进行以太坊区块链开发

    当有人说Java时,你会想到什么?如果没有,你可以试试old.虽然,其他人可能会说它坚固,可靠和安全.而对于Oodles Blockchain来说,Java意味着"企业级". 毫无 ...

  6. java 区块链使用_使用Java创建第一个区块链

    本系列教程的目的是帮助你了解如何开发区块链技术.在本教程中,我们将: 创建你的第一个非常基础的区块链. 实施简单的工作量证明系统(采矿). 探讨任何的可能性. 我假设你对面向对象编程有基本的了解.值得 ...

  7. 大数据、java、python、区块链、人工智能哪个发展前景更好?

    在这个信息时代高速发展的情况下,很多人会对自己该往哪个方向发展感到迷茫,下面我就浅显的给大家介绍一下五大流行区域的发展前景. 大数据的发展前景: 当前大数据行业真的是人才稀缺吗? 学了几年后,大数据行 ...

  8. 挖矿区块链_使用Java语言从零开始创建区块链

    目前网络上关于区块链入门.科普的文章不少,本文就不再赘述区块链的基本概念了,如果对区块链不是很了解的话,可以看一下我之前收集的一些入门学习资源:http://blog.51cto.com/zero01 ...

  9. java开发区块链_使用Java语言从零开始创建区块链

    目前网络上关于区块链入门.科普的文章不少,本文就不再赘述区块链的基本概念了,如果对区块链不是很了解的话,可以看一下我之前收集的一些入门学习资源: 对区块链技术感到新奇的我们,都想知道区块链在代码上是怎 ...

最新文章

  1. WSL2问题汇总:转换为WSL2、WSL2代理、安装MySQL等
  2. 观点 | 哈哈,TensorFlow被吐槽了吧
  3. Python函数作为参数传递给函数
  4. 【数据库系统概论】考研第二部分重点分析【2.2】
  5. J - 数塔 HDU - 2084(深搜,记忆化搜索+)
  6. ux设计中的各种地图_如何在UX设计中使用颜色
  7. ldap java_使用LDAP保护Java EE6中的Web应用程序
  8. perl 安装html,centos perl 安装HTML-Parser时报错
  9. 关于PHP的错误机制总结
  10. 人工智能重点汇总(搜索策略、博弈、贝叶斯、SVM、神经网络、弧相容、SVM、决策树、反向传播、卷积神经网络)
  11. css sprite 介绍和实例
  12. 多目标进化优化 郑金华pdf_简化审批流程 金华首张以“告知承诺制”审批的医疗器械经营许可证发放...
  13. 分享400多道算法题,来挑战吧
  14. 【安全资讯】安卓设备容易受到僵尸网络的DDoS攻击
  15. java 观察者模式类图_设计模式——观察者模式
  16. reg文件导入注册表后出现中文乱码的解决方法
  17. 2020-12-09
  18. STM32学习笔记之IIC(1) ADS1115
  19. HDU 5855 Less Time, More profit(最大权闭合子图)
  20. sqoop -D 指定资源池( mapred.job.queue.name=root.myqueue)或者( mapred.job.queuename=root.myqueue)

热门文章

  1. Unity2DRelative Joint 2D详解
  2. echarts自传作品入口
  3. Shell 8种字符串截取方法
  4. 如何快速搭建企业内部社区知识库?来说一说搭建企业内部知识库的作用!
  5. Axure 9 元件学习——中继器原理/中继器添加、删除行
  6. 【渝粤教育】 国家开放大学2020年春季 2585城市轨道交通概论 参考试题
  7. 文本处理之贝叶斯垃圾邮件分类
  8. 人工智能企业全球一百强,中国6家上榜,5家在北京
  9. linux 虚拟机桥接上网方式
  10. EMVTag系列8《IC卡公钥证书》