用spring boot 2从零开始创建区块链
区块链这么火的技术,大java怎能落后,所以有了本文,主要代码参考自 Learn Blockchains by Building One , 中文翻译:用Python从零开始创建区块链 。
一、区块链对象模型的基础属性(BlockChain)
区块链的基本数据模型参考:最基本的区块链hello world(python3实现) 。主要属性如下:
@ApiModelProperty(value = "当前交易列表", dataType = "List<Transaction>")
@JSONField(serialize = false)
@JsonIgnore
private List<Transaction> currentTransactions; @ApiModelProperty(value = "所有交易列表", dataType = "List<Transaction>")
private List<Transaction> transactions; @ApiModelProperty(value = "区块列表", dataType = "List<BlockChain>")
@JSONField(serialize = false)
@JsonIgnore
private List<BlockChain> chain; @ApiModelProperty(value = "集群的节点列表", dataType = "Set<String>")
@JSONField(serialize = false)
@JsonIgnore
private Set<String> nodes; @ApiModelProperty(value = "上一个区块的哈希值", dataType = "String", example = "f461ac428043f328309da7cac33803206cea9912f0d4e8d8cf2786d21e5ff403")
private String previousHash = ""; @ApiModelProperty(value = "工作量证明", dataType = "Integer", example = "100")
private Integer proof = 0; @ApiModelProperty(value = "当前区块的索引序号", dataType = "Long", example = "2")
private Long index = 0L; @ApiModelProperty(value = "当前区块的时间戳", dataType = "Long", example = "1526458171000")
private Long timestamp = 0L; @ApiModelProperty(value = "当前区块的哈希值", dataType = "String", example = "g451ac428043f328309da7cac33803206cea9912f0d4e8d8cf2786d21e5ff401")
private String hash;
注:上面有些注解来自swagger,主要为了方便生成在线文档以及直接调试rest接口。相对之前最基本的区块链hello world(python3实现)一文,每个区块中的data,在这里细分为transactions、currentTransactions。另外区块“链”本质上可以理解为链表,所以得有一个List<?> chain;此外这里引入了所谓“工作量证明”,用于验证每个区域的hash值不是随便来的,而是要达到一定规则的运算量才能获取,可以理解为控制挖矿速度的难度系数。
二、BlockChain的常规操作
2.1 生成新块newBlock
public BlockChain newBlock(Integer proof, String previousHash) {
BlockChain block = new BlockChain();
block.index = chain.size() + 1L;
block.timestamp = System.currentTimeMillis();
block.transactions.addAll(currentTransactions);
block.proof = proof;
block.previousHash = previousHash;
currentTransactions.clear();
chain.add(block);
return block;
}
2.2 生成第1个"创世"块
链表总归要有一个Head节点,区块链也不例外
public void newSeedBlock() {
newBlock(100, "1");
}
约定previousHash=1的,即为所谓的"创世"块
2.3 生成hash值
public String getHash() {
String json = jsonUtil.toJson(this.getCurrentTransactions()) +
jsonUtil.toJson(this.getTransactions()) +
jsonUtil.toJson(this.getChain()) +
this.getPreviousHash() + this.getProof() + this.getIndex() + this.getTimestamp();
hash = SHAUtils.getSHA256Str(json);
return hash;
}
这里把区块的主要属性:交易数据、链表中所有元素、工作量证明、区块索引号、时间戳 拼在一起,然后计算sha256。总之,这些主要属性中的任何一个属性发生变化,整个hash值就变了。
2.4 工作量证明
相信对区块链有了解的同学,都知道“挖矿”。为了控制挖矿的难度,得有一个规则来约束下,所以就有了这个工作量证明,这里我们模拟一个简单的策略:
public Boolean validProof(Integer lastProof, Integer proof) {
System.out.println("validProof==>lastProof:" + lastProof + ",proof:" + proof);
String guessHash = SHAUtils.getSHA256Str(String.format("{%d}{%d}", lastProof, proof));
return guessHash.startsWith("00");
}
把上一块的proof值与本区块的proof在一起,算sha256值,如果正好前2位是00,表示证明通过。(注:0的个数越多,挖矿难度越大,有兴趣的同学可以自己调整试下)
2.5 区块链验证数据是否正确
为了防止区块链的节点中混入非法脏数据(或被篡改),需要一个检测数据完整性的方法
public boolean validChain(List<BlockChain> chain) {
if (CollectionUtils.isEmpty(chain)) {
return false;
}
BlockChain previousBlock = chain.get(0);
int currentIndex = 1;
while (currentIndex < chain.size()) {
BlockChain block = chain.get(currentIndex);
if (!block.getPreviousHash().equals(previousBlock.getHash())) {
return false;
}
if (!validProof(previousBlock.getProof(), block.getProof())) {
return false;
}
previousBlock = block;
currentIndex += 1;
}
return true;
}
规则很简单:
a)每个区块的previousHash值,必须等于前一个块的hash值
b) 验证每个块上的proof值是否有效
2.6 集群中的分叉校验
区块链是一个去中心化的分布式体系,每个节点都能挖矿,挖出来的“新区块”都能加入链中,如果出现节点之间的区块链数据不一致,需要一个策略来做仲裁,可以定一个简单的规则:链最长的节点认定为有效的,其它节点都以此为准。
为了模拟这种情况,在BlockChain类的属性中,特地留了一个nodes节点列表,用于登记集群中的其它节点信息。
public void registerNode(String address) {
nodes.add(address);
}
上面的方法,将把其它节点的实例(类似http://localhost:8081/),登记到节点列表中。知道了集群中所有其它节点,就可以一一检查谁的链条最长,代码如下:
public boolean resolveConflicts() {
int maxLength = getChain().size();
List<BlockChain> newChain = new ArrayList<>();
for (String node : getNodes()) {
RestTemplate template = new RestTemplate();
Map map = template.getForObject(node + "chain", Map.class);
int length = MapUtils.getInteger(map, "length");
String json = jsonUtil.toJson(MapUtils.getObject(map, "chain"));
List<BlockChain> chain = jsonUtil.fromJson(json, new TypeReference<List<BlockChain>>() {
});
if (length > maxLength && validChain(chain)) {
maxLength = length;
newChain = chain;
}
}
if (!CollectionUtils.isEmpty(newChain)) {
this.chain = newChain;
return true;
}
return false;
}
大意是遍历整个节点,逐一请求其它节点的rest接口,获取其完整的链表,然后跟自己对比,如果比自己长的,就把自己给换掉。这样轮一圈后,自身的链表,就被替换为整个集群中最长的那个。
三、调试运行
为了方便调试,本文引入了swagger(不熟悉的同学可以参考spring cloud 学习(10) - 利用springfox集成swagger一文),然后加一堆rest api,跑起来,就可以直接测了:

3.1 调用/chain查看下初始值:
{
"chain": [
{
"transactions": [],
"previousHash": "1",
"proof": 100,
"index": 1,
"timestamp": 1527427873298,
"hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479"
}
],
"length": 1
}
可以看到就只有一个“创世”块,其previousHash为特定值1
3.2 调用/mine挖一块矿
{
"previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479",
"index": 2,
"proof": 172,
"message": "New Block Forged",
"transactions": [
{
"sender": "0",
"recepient": "50130c5283e640779b4e5e7a5afd2e6b",
"amount": 1
}
]
}
挖到矿(即:产生一个新的区块block),系统自动奖励本节点1个币(从transaction可以看出这一点),同时这笔奖励的交易被写入新块中。这时再来看下/chain
{
"chain": [
{
"transactions": [],
"previousHash": "1",
"proof": 100,
"index": 1,
"timestamp": 1527427873298,
"hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479"
},
{
"transactions": [
{
"sender": "0",
"recepient": "50130c5283e640779b4e5e7a5afd2e6b",
"amount": 1
}
],
"previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479",
"proof": 172,
"index": 2,
"timestamp": 1527427956435,
"hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93"
}
],
"length": 2
}
可以看到,有二个区块加入"链表"中了,可以继续再挖一块,最终/chain可能长成这样:
{
"chain": [
{
"transactions": [],
"previousHash": "1",
"proof": 100,
"index": 1,
"timestamp": 1527427873298,
"hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479"
},
{
"transactions": [
{
"sender": "0",
"recepient": "50130c5283e640779b4e5e7a5afd2e6b",
"amount": 1
}
],
"previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479",
"proof": 172,
"index": 2,
"timestamp": 1527427956435,
"hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93"
},
{
"transactions": [
{
"sender": "0",
"recepient": "50130c5283e640779b4e5e7a5afd2e6b",
"amount": 1
}
],
"previousHash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93",
"proof": 153,
"index": 3,
"timestamp": 1527428128077,
"hash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1"
}
],
"length": 3
}
3.3 调用/transactions/new 发起一笔新交易
参数如下:
{
"amount": 1.0,
"recepient": "block-on-other-node",
"sender": "50130c5283e640779b4e5e7a5afd2e6b"
}
注:sender一般取为当前矿机的标识,即本节点的nodeId,接收方一般指其它节点(这里我们随便输入点内容,当作演示),然后交易的金额为“1”个币,成功后,将返回
{
"message": "Transaction will be added to Block 4"
}
但这时,如果调用/chain查看整个链的数据,会发现没有变化,因为这笔交易数据,只是放在本区块的currentTransactions列表中(注:该属性并未json序列化输出,忘记的同学,可以拉到本文最开头,复习下几个重要的属性)。只有下一个可用区块产生时,这笔交易才会写入新的区块中,so,我们再继续挖一块新矿,调用/mine,然后再查看/chain
{
"chain": [
{
"transactions": [],
"previousHash": "1",
"proof": 100,
"index": 1,
"timestamp": 1527427873298,
"hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479"
},
{
"transactions": [
{
"sender": "0",
"recepient": "50130c5283e640779b4e5e7a5afd2e6b",
"amount": 1
}
],
"previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479",
"proof": 172,
"index": 2,
"timestamp": 1527427956435,
"hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93"
},
{
"transactions": [
{
"sender": "0",
"recepient": "50130c5283e640779b4e5e7a5afd2e6b",
"amount": 1
}
],
"previousHash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93",
"proof": 153,
"index": 3,
"timestamp": 1527428128077,
"hash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1"
},
{
"transactions": [
{
"sender": "50130c5283e640779b4e5e7a5afd2e6b",
"recepient": "block-on-other-node",
"amount": 1
},
{
"sender": "0",
"recepient": "50130c5283e640779b4e5e7a5afd2e6b",
"amount": 1
}
],
"previousHash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1",
"proof": 86,
"index": 4,
"timestamp": 1527428477991,
"hash": "4b3c261d2f878cebbdc1ade1a09809bb647abbd990353e529f630220a53d60ed"
}
],
"length": 4
}
刚才的交易,已经被写入最后一个刚挖出的Block中。
3.4 模拟多节点数据不一致,使用/resolve仲裁解决
a) 再启动一个新端口的运行实例
方法一:参考下图,idea中设置运行时的环境变量,填上server.port=8081,就可以在另一个端口上启动

方法二:在build.gradle里加一个task
task 8081 << {
bootRun.systemProperty 'server.port', '8081'
}
然后就可以命令行下,直接gradle 8081 bootRun
方法三:java -jar xxx.jar --name="Spring" --server.port=8081 直接在运行jar的时候指定端口
b) 调用/register 将新节点实例(即:8081端口的节点),注册到8080的节点上
参数如下:
{
"nodes": [
"http://localhost:8081/"
]
}
反过来,把8080老节点也注册到新节点上(即:相当于两两相互注册)。注册成功后,这时调用8081新节点上的/chain ,因为这是个新节点,里面只有一个创世块,显然跟8080老节点上的数据不一致
c) 新8081节点上调用/resolve
输出如下:
{
"newChain": [
{
"transactions": [],
"previousHash": "1",
"proof": 100,
"index": 1,
"timestamp": 1527427873298,
"hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479"
},
{
"transactions": [
{
"sender": "0",
"recepient": "50130c5283e640779b4e5e7a5afd2e6b",
"amount": 1
}
],
"previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479",
"proof": 172,
"index": 2,
"timestamp": 1527427956435,
"hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93"
},
{
"transactions": [
{
"sender": "0",
"recepient": "50130c5283e640779b4e5e7a5afd2e6b",
"amount": 1
}
],
"previousHash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93",
"proof": 153,
"index": 3,
"timestamp": 1527428128077,
"hash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1"
},
{
"transactions": [
{
"sender": "50130c5283e640779b4e5e7a5afd2e6b",
"recepient": "block-on-other-node",
"amount": 1
},
{
"sender": "0",
"recepient": "50130c5283e640779b4e5e7a5afd2e6b",
"amount": 1
}
],
"previousHash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1",
"proof": 86,
"index": 4,
"timestamp": 1527428477991,
"hash": "4b3c261d2f878cebbdc1ade1a09809bb647abbd990353e529f630220a53d60ed"
}
],
"message": "Our chain was replaced"
}
最后一行的message: Our chain was replaced 表示,本节点的区块链已经被集群其它节点中最长的那个替换掉了。
最后,文中演示的所有代码,已经托管在github上,地址:https://github.com/yjmyzz/springboot-blockchain-helloworld 欢迎大家Fork.
用spring boot 2从零开始创建区块链的更多相关文章
- 用Python从零开始创建区块链
本文主要内容翻译自Learn Blockchains by Building One 本文原始链接,转载请注明出处. 作者认为最快的学习区块链的方式是自己创建一个,本文就跟随作者用Python来创建一 ...
- [转] 使用Spring Boot和Gradle创建项目
Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的 ...
- 使用Spring Boot和Gradle创建AngularJS项目
Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的 ...
- spring boot之从零开始开发自己的网站
概述 首先要感谢两位大神,该项目的想法来源自tale和MyBlog,本项目的想法. 做了一些改造,增加了一些功能和一些代码的重构,并且更换了博客主题. 关于项目,对于开发的练手项目,能够工程化,严谨一 ...
- [转]通过Spring Boot三分钟创建Spring Web项目
来源:https://www.tianmaying.com/tutorial/project-based-on-spring-boot Spring Boot简介 接下来我们所有的Spring代码实例 ...
- Spring Boot的快速创建
一.利用向导快速搭建Spring Boot应用 创建一个controller package com.hoje.springboot.Controller; import org.springfram ...
- spring boot 之 如何创建spring boot项目
创建spring boot的方式有非常多,今天我们使用maven来进行创建spring boot项目,因为maven使用的非常广泛,也很好用,很多IDE也都支持maven. 1 创建maven项目 1 ...
- spring boot 项目的创建
一. 进入https://start.spring.io 快速创建项目 二. 利用eclipse sts插件创建项目 1. 安装sts插件 进入https://spring.io/tools3/sts ...
- idea 社区版+spring boot+ssm+swagger创建rest api
新手上路,出了好多错,记录一下 1.创建数据库:springBootSsmTest 2.打开IDEA创建 Spring boot项目:File——New——Project——Spring Assist ...
随机推荐
- 关于ftp上传changeWorkingDirectory()方法的路径切换问题
在上传时 FTPClient提供了upload方法,对于upload(file,path)的第二个参数path ,上传到哪里的这个路径, ftp是利用changeWorkingDirectory()方 ...
- Go语言规格说明书 之 内建函数(Built-in functions)
go version go1.11 windows/amd64 本文为阅读Go语言中文官网的规则说明书(https://golang.google.cn/ref/spec)而做的笔记,介绍Go语言的 ...
- git命令行提交并且同步到远程代码库
远程代码库以github为例 1.打开 git bash 2.进入项目目录 cd /e/myGitProjects/test 3.提交到本地git仓库 git add -Agit commit -m ...
- 让Linux任务在后台可靠运行的几种方法
我们经常会碰到这样的问题,用 telnet/ssh 登录了远程的 Linux 服务器,运行了一些耗时较长的任务, 结果却由于网络的不稳定导致任务中途失败.如何让命令提交后不受本地关闭终端窗口/网络 ...
- 温故而知新--JavaScript书摘(二)
前言 毕业到入职腾讯已经差不多一年的时光了,接触了很多项目,也积累了很多实践经验,在处理问题的方式方法上有很大的提升.随着时间的增加,愈加发现基础知识的重要性,很多开发过程中遇到的问题都是由最基础的知 ...
- 使用@font-family时各浏览器对字体格式(format)的支持情况
说到浏览器对@font-face的兼容问题,这里涉及到一个字体format的问题,因为不同的浏览器对字体格式支持是不一致的,这样大家有必要了解一下,各种版本的浏览器支持什么样的字体,前面也简单带到了有 ...
- js计时器 setInterval与clearInterval
var timer = setInterval(函数, 毫秒数) 功能:每隔对应的毫秒数执行一次函数. 返回值:系统没启动一个定时器,就会给一个标识,返回值就是这个定时器的编号. clearInter ...
- Zookeeper集群部署与配置(三)
在上一篇博客中我们讲解了<Zookeeper的单机配置>,此篇博客将继续介绍Zookeeper的集群部署与配置. 环境 集群配置的环境与单机配置的环境相同,唯一不同的就是集群是在多台服务器 ...
- DOM事件监听器
DOM事件监听器,允许一个事件触发多个方法.在实际工作中应用比较多. 它的调用形式如下: <body> <div> DOM事件监听器,允许一个事件触发多个方法. </di ...
- 2018-2019 2 20165203 《网络对抗技术》 Exp2 后门原理与实践
2018-2019 2 20165203 <网络对抗技术> Exp2 后门原理与实践 实验内容 1.使用netcat获取主机操作Shell,cron启动 (0.5分) 2.使用socat获 ...