关于区块链技术,网络上有很多入门、科普的文章,如果大家对于区块链感兴趣,应该已经通过网络了解区块链的基本概念了,这里就不再赘述基本概念了。
相信阅读本文章的朋友们应该都和我一样对于区块链技术感到新奇,都想知道区块链在代码上怎么实现的,所以本文是实战为主,理论为辅的,毕竟大家应该都看过不少的理论文章了,但是对于区块链具体实现还不是很清楚,本文就是用Java语言来实现一个简易的区块链。
使用Java语言编写区块链程序,需要掌握基本的JavaSE以及JavaWeb开发,能够使用Java开发简单的项目,并且对于HTTP协议有一定的了解。
相信大家都听说过区块链的记录构成是不可变、有序的链结构,记录可以是交易、文件或任何你想要的数据,重要的是它们是通过哈希值(Hash)连接起来的。
如果你还不知道什么是哈希,可以查看 这篇文章 。
pom.xml文件配置内容:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.47version>
dependency>
dependencies>
首先创建一个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;
}
@Override
public String toString() {
return "Transaction{" +
"sender='" + sender + '\'' +
", recipient='" + recipient + '\'' +
", amount=" + amount +
'}';
}
}
Transaction类是用来表示交易的实体类,交易中所涉及的要素都在类中体现出来。
再创建一个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;
}
@Override
public String toString() {
return "Block{" +
"index=" + index +
", timestamp=" + timestamp +
", transactions=" + transactions +
", proof=" + proof +
", previous_hash='" + previous_hash + '\'' +
'}';
}
}
接下来创建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 {
@Test
public 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 {
@Test
public 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请求来进行交互。
我们将使用Springboot提供的web编程能力编写接收HTTP请求的网络服务,可以很方便的将请求数据映射到相应的方法上进行处理,现在我们来让BlockChain运行基于Java Web的网络服务上。
我们先来创建三个服务接口:
我们的“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> {
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
// 将 ApplicationContext 转化为 WebApplicationContext
WebApplicationContext webApplicationContext =
(WebApplicationContext) contextRefreshedEvent.getApplicationContext();
// 从 webApplicationContext 中获取 servletContext
ServletContext servletContext = webApplicationContext.getServletContext();
String uuid = UUID.randomUUID().toString().replace("-", "");
// servletContext设置值,把UUID绑定到servletContext
servletContext.setAttribute("uuid", uuid);
}
}
接下来创建一个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 // 这里直接注入servletContext
private 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个接口:
我们需要修改下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是用来进行节点注册和解决冲突的服务控制器,包含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