目前网上有很多关于区块链介绍和科普链的文章,在此我就不重复区块链的基本概念了。如果你不太了解区块链,可以看看我之前收集的一些入门学习资源:
https://blog.51cto.com/zero01/2066321
我们是区块链技术的新手,都想知道区块链是如何用代码实现的,所以这篇文章很实用。毕竟,我们读过很多理论,但对区块链的具体实施并不十分清楚。本文使用Java语言实现了一个简单的区块链。
然而,要完全理解区块链并不容易。对于一个相对陌生的技术,需要在理论和实践中学习。通过编写代码来学习技术会让我们更加安全,建造区块链可以加深我们对区块链的了解。
准备工作/即将开始工作
掌握基本的JavaSE和JavaWeb开发,能够使用Java开发简单的项目,需要了解HTTP协议。
我们知道,区块链是由块记录组成的不可变的有序链结构。记录可以是交易、文件或任何你想要的数据。重要的是,它们是通过哈希链接在一起的。
如果你还不知道hash是什么,可以查一下这篇文章。
环境描述
JDK 1.8 Tomcat 9.0 Maven 3.5 JSON 2016 08 10 javaee-API 7.0 POM . XML文件配置内容:
属国
属国
groupIdjavax/groupId
artifactId javaee-API/artifactId
7.0版/版本
范围提供/范围
/依赖关系
属国
groupIdorg.json/groupId
artifactIdjson/artifactId
版本2016 08 10/版本
/依赖关系
/dependencies那么你需要一个HTTP客户端,比如Linux命令行下的Postman、curl或者其他客户端。我在这里用邮递员。
区块链类首先创建一个区块链类,在构造函数中创建两个主集合,一个用于存储区块链,另一个用于存储交易列表。本文所有核心的主要代码都是用这个类写的,方便随时查看,但在实际开发中这样做并不合适。应该小心地拆分代码,以降低耦合度。
以下是区块链类的框架代码:
包org . zero 01 . core;
导入Java . util . ArrayList;
导入Java . util . hashmap;
导入Java . util . list;
公共类区块链{
//存储区块链
私有ListObject链;
//该实例变量用于当前交易信息列表。
私有ListObject currentTransactions
公共区块链(){
//初始化区块链和当前交易信息列表
this . chain=new ArrayList object();
this . currenttransactions=new ArrayListObject();
}
公共ListObject getChain() {
返回链;
}
public void set chain(ListObject chain){
this.chain=chain
}
公共ListObject getCurrentTransactions(){
返回当前交易;
}
public void setCurrentTransactions(ListObject currentTransactions){
this . current transactions=current transactions;
}
公共对象lastBlock() {
返回null
}
public HashMapString,Object newBlock() {
返回null
}
public int newTransactions() {
返回0;
}
公共静态对象哈希(HashMapString,Object block) {
返回null
}
}区块链类用于管理区块链。它可以存储事务、添加新块等。让我们进一步改进这些方法。
方块图
首先,我们需要解释块的结构。每个块包含属性:索引、时间戳、事务、工作负载证书(稍后解释)和前一个块的哈希值。
以下是块的结构:
block={
索引\' : 1,
时间戳\' : 1506057125.900785,
交易\' : [
{
发件人\' : \' 8527147 fe1f 5426 f9dd 545 de 4b 27 ee 00 \',
收件人\' : \' a 77 F5 cdfa 2934 df 3954 a5 C7 c 7 da 5d f1f \':
金额\' : 5,
}
],
证明\' : 324984774000,
previous _ hash \' : \' 2 cf 24 DBA 5 FB 0 a 30 e 26 e 83 B2 AC 5b 9 e 29 E1 e 1b 161 E5 C1 fa 7425 e 73043362938 b 9824 \'
}到这里,区块链的概念就清楚了,每个新的区块都包含上一个区块的哈希,这是关键的一点,它保障了区块链不可变性。如果***者破坏了前面的某个区块,那么后面所有区块的混杂都会变得不正确。不理解的话,慢慢消化,可以参考区块链记账原理。
由于需要计算区块的哈希,所以我们得先编写一个用于计算混杂值的工具类:
包org。零零一。util
导入Java。安全。消息摘要;
导入Java。安全。nosuchalgorithm异常;
公共类加密{
/**
* 传入字符串,返回SHA-256加密字符串
*
* @param strText
* @返回
*/
公共字符串getSHA256(最终字符串strText) {
返回沙(strText,“SHA-256”);
}
/**
* 传入字符串,返回SHA-512加密字符串
*
* @param strText
* @返回
*/
公共字符串getSHA512(最终字符串strText) {
返回沙(strText,“SHA-512”);
}
/**
* 传入字符串,返回讯息摘要5加密字符串
*
* @param strText
* @返回
*/
公共字符串getMD5(最终字符串strText) {
返回沙(strText,“SHA-512”);
}
/**
* 字符串恒星时角加密
*
* @param strSourceText
* @返回
*/
私有字符串沙(最终字符串strText,最终字符串strType) {
//返回值
String strResult=null
//是否是有效字符串
if (strText!=null strText.length() 0) {
尝试{
//SHA加密开始
//创建加密对象,传入加密类型
消息摘要=消息摘要。getinstance(strType);
//传入要加密的字符串
消息摘要。更新(strtext。getbytes());
byte byte buffer[]=消息摘要。digest();
//將字节数组转换线类型
string buffer strHexString=new string buffer();
//遍历字节数组
for(int I=0;i byteBuffer.lengthi ) {
//转换成16进制并存储在字符串中
字符串十六进制=整数。tohexstring(0x ff字节缓冲区[I]);
if (hex.length()==1) {
strhexstring。追加(“0”);
}
strHexString.append(十六进制);
}
//得到返回結果
strResult=strhexstring。tostring();
} catch(nosuch算法异常e){
e。printstacktrace();
}
}
返回str结果
}
}加入交易功能
接下来我们需要实现一个交易\\记账功能,所以来完善新交易记录以及上一块方法:
/**
* @返回得到区块链中的最后一个区块
*/
public HashMapString,Object lastBlock() {
返回getChain().get(getChain().size()-1);
}
/**
* 生成新交易信息,信息将加入到下一个待挖的区块中
*
* @param发送者
* 发送方的地址
* @param收件人
* 接收方的地址
* @param金额
* 交易数量
* @返回返回存储该交易事务的块的索引
*/
public int newTransactions(字符串发送方,字符串接收方,长金额){
MapString,Object transaction=new hashmap string,Object();
transaction.put(\'sender \'),发送方);
transaction.put(\'收件人\'),收件人);
transaction.put(\'金额\'),金额);
getCurrentTransactions().添加(交易);
return(整数)lastBlock().get(\' index \')1;
}新交易方法向列表中添加一个交易记录,并返回该记录将被添加到的区块(下一个待挖掘的区块)的索引,等下载用户提交交易时会有用。
创建新块
当区块链实例化后,我们需要构造一个创世区块(没有前区块的第一个区块),并且给它加上一个工作量证明。每个区块都需要经过工作量证明,俗称挖矿,稍后会继续讲解。
为了构造创世块,我们还需要完善剩下的几个方法,并且把该类设计为单例:
包org。零零一。道;
导入Java。util。ArrayList
导入Java。util。hashmap
导入Java。util。列表;
导入Java。util。地图;
导入org。JSON。JSON对象;
导入org。零零一。util。加密;
公共类区块链{
//存储区块链
private ListMapString,对象链;
//该实例变量用于当前的交易信息列表
private ListMapString,Object currentTransactions .
私有静态区块链区块链=空;
二等兵区块链(){
//初始化区块链以及当前的交易信息列表
chain=new ArrayListMapString,Object();
current transactions=new ArrayList mapstring,Object();
//创建创世区块
newBlock(100,\' 0 \');
}
//创建单例对象
公共静态区块链getInstance() {
如果(区块链==空){
同步(区块链。类){
如果(区块链==空){
区块链=新区块链();
}
}
}
归还区块链;
}
public ListMapString,Object getChain() {
返回链;
}
公共空集链(列表映射字符串,对象链){
this.chain=链条
}
public ListMapString,Object getCurrentTransactions(){
返回当前交易;
}
public void setCurrentTransactions(ListMapString,Object currentTransactions) {
这个。当前交易=当前交易;
}
/**
* @返回得到区块链中的最后一个区块
*/
公共映射字符串,对象lastBlock() {
返回getChain().get(getChain().size()-1);
}
/**
* 在区块链上新建一个区块
*
* @param证明
* 新区块的工作量证明
* @param previous_hash
* 上一个区块的混杂值
* @返回返回新建的区块
*/
public MapString,Object newBlock(long proof,String previous_hash) {
MapString,Object block=new HashMapString,Object();
block.put(\'index \',getChain().size()1);
block.put(\'timestamp \',system。当前时间毫秒());
block.put(\'transactions \',getCurrentTransactions());
block.put(\'proof \',proof);
//如果没有传递上一个区块的混杂就计算出区块链中最后一个区块的混杂
block.put(\'previous_hash \',previous_hash!=null?previous _ hash :哈希(get chain().get(getChain().size()-1)));
//重置当前的交易信息列表
setCurrentTransactions(new ArrayList mapstring,Object());
getChain().添加(块);
返回块;
}
/**
* 生成新交易信息,信息将加入到下一个待挖的区块中
*
* @param发送者
* 发送方的地址
* @param收件人
* 接收方的地址
* @param金额
* 交易数量
* @返回返回该交易事务的块的索引
*/
public int newTransactions(字符串发送方,字符串接收方,长金额){
MapString,Object transaction=new hashmap string,Object();
transaction.put(\'sender \'),发送方);
transaction.put(\'收件人\'),收件人);
transaction.put(\'金额\'),金额);
getCurrentTransactions().添加(交易);
return(整数)lastBlock().get(\' index \')1;
}
/**
* 生成区块的SHA-256格式的混杂值
*
* @param块
* 区块
* @返回返回该区块的混杂
*/
公共静态对象哈希(映射字符串,对象块){
返回new Encrypt()getsha 256(新的JSON对象(块)).toString());
}
}通过上面的代码和注释可以对区块链有直观的了解,接下来我们来编写一些简单的测试代码来测试一下这些代码能否正常工作:
包org。零零一。测试;
导入Java。util。hashmap
导入Java。util。地图;
导入org。JSON。JSON对象;
导入org。零零一。道。区块链;
公共类测试{
公共静态void main(String[] args)引发异常{
区块链区块链=区块链。getinstance();
//一个区块中可以不包含任何交易记录
MapString,对象块=区块链。新块(300,空);
系统。出去。println(新JSON对象(块));
//一个区块中可以包含一笔交易记录
区块链。新交易(\' 123 \',\' 222 \',33);
MapString,对象块1=区块链。新块(500,空);
System.out.println(新JSON对象(块1));
//一个区块中可以包含多笔交易记录
区块链。新交易(“321”、“555”、“133”);
区块链。新交易(“000”,“111”,10);
区块链。新交易(\' 789 \',\' 369 \',65);
MapString,对象块2=区块链。新块(600,空);
System.out.println(新JSON对象(block 2));
//查看整个区块链
MapString,Object chain=new HashMapString,Object();
chain.put(“链条”,区块链。get chain());
chain.put(\'length \',blockChain.getChain().size());
System.out.println(新JSON对象(链));
}
}运行结果:
//挖出来的新区块
{
索引\' : 2,
交易\' : [],
证明\' : 300,
时间戳\' : 1519478559703,
previous _ hash \' : \' 185 b 62 ca 1 fc 31285 BCE 8878 acfc 970983 CB 561 f19 c 63 b 65120 d2c 95148 cf 151 f \'
}
//包含一笔交易的区块
{
索引\' : 3,
交易\' : [
{
金额\' : 33,
发件人\' : \'123 \':
收件人\' : \'222 \'
}
],
证明\' : 500,
时间戳\' : 1519478559728,
\' previous _ hash \' : \' BCE 15693 c0a 028 B1 fc 6d 7 D1 C1 d 30494 f 97 ef 37 b8b 3384865559 ceed 9 b5 ff 798 b \'
}
//包含多笔交易的区块
{
索引\' : 4,
交易\' : [
{
金额\' : 133,
发件人\' : \'321 \':
收件人\' : \'555 \'
},
{
金额\' : 10,
发件人\' : \'000 \':
收件人\' : \'111 \'
},
{
金额\' : 65,
发件人\' : \'789 \':
收件人\' : \'369 \'
}
],
证明\' : 600,
时间戳\' : 1519478656178,
previous _ hash \' : \' b0edde 645 f 76 fc 3a 6 CB 45 b 7 c 91 b 07 b 686 e 8 e 214 CFC 1 DEA 4823 BF 38 BDA 37 c 909 c \'
}
//整个区块链,第一个是创始区块
{
链条\' : [
{
索引\' : 1,
交易\' : [],
证明\' : 100,
时间戳\' : 1519478656153,
previous_hash\': \'0 \'
},
{
索引\' : 2,
交易\' : [],
证明\' : 300,
时间戳\' : 1519478656154,
previous _ hash \' : \' 7925 a 01 fa 8 CB 67 b 51 ea 89 b 9 CFC fa 16 C5 Fe bee 008 bb 559 f94 c 5758418 E7 ACC 670 \'
},
{
索引\' : 3,
交易\' : [
{
金额\' : 33,
发件人\' : \'123 \':
收件人\' : \'222 \'
}
],
证明\' : 500,
时间戳\' : 1519478656178,
previous _ hash \' : \' 40 CCC 2 f 4 ad 97 f 75 CB 611 ed 69 a 4 ECC 7438 eefd 31 afca 17 ca 00 C2 ed 7 b 5163d 0831 \'
},
{
索引\' : 4,
交易\' : [
{
金额\' : 133,
发件人\' : \'321 \':
收件人\' : \'555 \'
},
{
金额\' : 10,
发件人\' : \'000 \':
收件人\' : \'111 \'
},
{
金额\' : 65,
发件人\' : \'789 \':
收件人\' : \'369 \'
}
],
证明\' : 600,
时间戳\' : 1519478656178,
previous _ hash \' : \' b0edde 645 f 76 fc 3a 6 CB 45 b 7 c 91 b 07 b 686 e 8 e 214 CFC 1 DEA 4823 BF 38 BDA 37 c 909 c \'
}
],
长度\' : 4
}通过上面的测试,我们可以直观的看到区块链的数据,但是现在我们刚刚完成了前期的代码编写,还有几件事情没有做。接下来,我们来看看积木是怎么挖出来的。
了解工作量证明。新块由工作负载证明算法(PoW)构建。PoW的目标是找到满足特定条件的数字。这个数字很难计算,但很容易核实。这是工作量证明的核心思想。
为了便于理解,举个例子:
假设一个整数x乘以另一个整数y的乘积的哈希值必须以0结尾,即hash(x * y)=ac23dc…0。设变量x=5,求y的值?
用Java实现如下:
包org . zero 01 . test;
导入org . zero 01 . util . encrypt;
公共类TestProof {
公共静态void main(String[] args) {
int x=5;
int y=0;
而(!new Encrypt()getsha 256((x * y)\' \')。endsWith(\' 0 \'){
y;
}
system . out . println(\' y=\' y);
}
}结果是y=21,因为:
哈希(5 * 21)=1253E9373E.5E3600155E860在比特币中,使用了名为Hashcash的工作量证明算法,与上述问题类似。矿工们正在争夺创建区块来计算结果的权利。通常情况下,计算的难度与目标字符串需要满足的特定字符数成正比。矿工计算结果后会获得比特币奖励。当然,在网络上验证这个结果是非常容易的。
实现工作负载证明让我们实现一个类似的PoW算法。规则是:寻找一个数p,使与前一个块的证明拼接的字符串的哈希值以4个零开始:
.
/**
*简单工作量证书3360
*-找到一个p \'使得散列(pp \')以4个0开始。
*-p是前一个块的证明,P’是当前的证明
*
* @param last_proof
*前一块的证明
* @返回
*/
public long proof for work(long last _ proof){
长证明=0;
而(!validProof(last_proof,proof)) {
证明=1;
}
退货证明;
}
/**
*验证证书3360的hash(last_proof,proof)是否以4个0开头?
*
* @param last_proof
*前一块的证明
* @param证明
*当前认证
* @return返回带有4个零的true,否则返回false。
*/
public boolean valid proof(long last _ proof,long proof) {
String guess=last _ proof \' \' proof
string guess _ hash=new Encrypt()getsha 256(guess);
返回guess _ hash . starts with(\' 0000 \');
}衡量算法复杂度的方法是修改零的个数。用4来做演示,你会发现多一个零会大大增加计算结果所需的时间。
现在区块链类已经基本完成,使用Servlet接收HTTP请求进行交互。
区块链作为API接口。我们将使用Java Web中的Servlet来接收用户的HTTP请求。通过Servlet,我们可以方便地将网络请求的数据映射到相应的方法进行处理。现在,让我们让区块链在Java Web上运行。
我们将创建三个接口:
/交易/新建创建一个交易并添加到区块/我的告诉服务器去挖掘新的区块/链返回整个区块链注册节点身份我们的\"雄猫服务器\"将扮演区块链网络中的一个节点,而每个节点都需要有一个唯一的标识符,也就是身份证。在这里我们使用全球唯一标识符来作为节点ID,我们需要在服务器启动时,将全球唯一标识符设置到小型应用程序上下文属性中,这样我们的服务器就拥有了唯一标识,这一步我们可以配置监听类来完成,首先配置web.xml文件内容如下:
?可扩展标记语言版本=\'1.0 \'编码=\'UTF八号\'?
我们B- app版本=\' 3.0 \' xmlns=\' http://Java。星期日\' com/XML/ns/javaee \'
xmlns : xsi=\' http://www。w3。\' org/2001/XML架构-实例\'
xsi :架构位置=\' http://Java。星期日http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd\'
听众
listener-classorg.zero01.servlet.InitialID/listener-class
/听众
/网络应用然后编写一个类实现监听器接口,在初始化方法中把全局唯一识别设置到小型应用程序上下文的属性中:
包org。零零一。servlet
导入Java。util。uuid
导入javax。servlet。servlet上下文;
导入javax。servlet。servletcontextevent
导入javax。servlet。servletcontextlistener
公共类InitialID实现ServletContextListener {
公共void上下文已初始化(ServletContextEvent SCE){
servlet上下文servlet上下文=SCE。get servlet context();
String uuid=UUID.randomUUID().toString().替换(\'-\',\' \');
servlet上下文。设置属性(\' uuid \',uuid);
}
公共void上下文已销毁(ServletContextEvent SCE){
}
}创建小型应用程序类我们这里没有使用任何框架,所以我们需要通过最基本的小型应用程序来接收并处理用户的超文本传送协议请求:
包org。零零一。servlet
导入Java。io。io异常;
导入javax。servlet。servlet异常;
导入javax。servlet。注释。web servlet
导入javax。servlet。http。http servlet
导入javax。servlet。http。http servlet请求;
导入javax。servlet。http。http servlet响应;
//该小型应用程序用于运行工作算法的证明来获得下一个证明,也就是所谓的挖矿
@WebServlet(\'/mine \')
公共类我的扩展HttpServlet{
受保护的void doGet(http servlet请求req,HttpServletResponse resp)抛出ServletException,IOException {
}
}
包org。零零一。servlet
导入Java。io。io异常;
导入javax。servlet。servlet异常;
导入javax。servlet。注释。web servlet
导入javax。servlet。http。http servlet
导入javax。servlet。http。http servlet请求;
导入javax。servlet。http。http servlet响应;
//该小型应用程序用于接收并处理新的交易信息
@WebServlet(\'/transactions/new \')
公共类新交易扩展HttpServlet{
受保护的void doPost(http servlet请求req,HttpServletResponse resp)抛出ServletException,IOException {
}
}
包org。零零一。servlet
导入Java。io。io异常;
导入javax。servlet。servlet异常;
导入javax。servlet。注释。web servlet
导入javax。servlet。http。http servlet
导入javax。servlet。http。http servlet请求;
导入javax。servlet。http。http servlet响应;
//该小型应用程序用于输出整个区块链的数据
@WebServlet(\'/chain \')
公共类全链扩展HttpServlet{
受保护的void doGet(http servlet请求req,HttpServletResponse resp)抛出ServletException,IOException {
}
}我们先来完成最简单的全链的代码,这个小型应用程序用于向客户端输出整个区块链的数据(JSON格式):
包org。零零一。servlet
导入Java。io。io异常;
导入Java。io。版画家;
导入Java。util。hashmap
导入Java。util。地图;
导入javax。servlet。servlet异常;
导入javax。servlet。注释。web servlet
导入javax。servlet。http。http servlet
导入javax。servlet。http。http servlet请求;
导入javax。servlet。http。http servlet响应;
导入org。JSON。JSON对象;
导入org。零零一。核心。区块链;
//该小型应用程序用于输出整个区块链的数据
@WebServlet(\'/chain \')
公共类全链扩展HttpServlet {
受保护的void doGet(http servlet请求req,HttpServletResponse resp)抛出ServletException,IOException {
区块链区块链=区块链。getinstance();
MapString,Object response=new hashmap string,Object();
response.put(\'链\',区块链。getchain());
response.put(\'length \',blockChain.getChain().size());
JSON对象JSON响应=新JSON对象(响应);
resp。设置内容类型(“应用程序/JSON”);
PrintWriter。getwriter();
版画家。println(JSON响应);
版画家。close();
}
}发送交易然后是记录交易数据的功能,每一个区块都可以记录交易数据,发送到节点的交易数据结构如下:
{
发件人\' : \'我地址:
收件人\' : \'别人地址,
金额\' : 5
}实现代码如下:
包org。零零一。servlet
导入Java。io。缓冲阅读器;
导入Java。io。io异常;
导入Java。io。版画家;
导入javax。servlet。servlet异常;
导入javax。servlet。注释。web servlet
导入javax。servlet。http。http servlet
导入javax.servlet.http.HttpServl
etRequest;
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节点的链取代:
通过共识算法保持一致性后,两个节点的区块链数据就都是一致的了:
到此为止我们就完成了一个区块链的开发,虽然这只是一个最基本的区块链,而且在开发的过程中也没有考虑太多的程序设计方面的问题,而是以最基本、原始的方式进行开发的。但是我们不妨以这个简单的区块链为基础,发挥自己的能力动手去重构、扩展、完善这个区块链程序,直至成为自己的一个小项目。
原文链接:https://blog.51cto.com/zero01/2086195